From 0dacb0c92a9d7e5c0fce436d65754f13e40fbad1 Mon Sep 17 00:00:00 2001 From: maduvena Date: Wed, 2 Feb 2022 17:19:40 +0530 Subject: [PATCH] feat: update to MDS version 3.0 #16 (#19) * feat: update to MDS version 3.0 #16 * fix: sonar code issues * fix: refactoring * fix: some more refactoring --- .../fido2/service/app/AppInitializer.java | 4 + .../fido2/service/app/MDS3UpdateEvent.java | 5 + .../fido2/service/app/MDS3UpdateTimer.java | 73 ++++++ .../gluu/fido2/service/mds/MdsService.java | 62 +---- .../gluu/fido2/service/mds/TocService.java | 226 ++++++++++++++---- 5 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateEvent.java create mode 100644 server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateTimer.java diff --git a/server/src/main/java/org/gluu/fido2/service/app/AppInitializer.java b/server/src/main/java/org/gluu/fido2/service/app/AppInitializer.java index e10188f..d9e3b93 100644 --- a/server/src/main/java/org/gluu/fido2/service/app/AppInitializer.java +++ b/server/src/main/java/org/gluu/fido2/service/app/AppInitializer.java @@ -93,6 +93,9 @@ public class AppInitializer { @Inject private CleanerTimer cleanerTimer; + + @Inject + private MDS3UpdateTimer mds3UpdateTimer; @Inject private QuartzSchedulerManager quartzSchedulerManager; @@ -130,6 +133,7 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas configurationFactory.initTimer(); loggerService.initTimer(); cleanerTimer.initTimer(); + mds3UpdateTimer.initTimer(); customScriptManager.initTimer(supportedCustomScriptTypes); // Notify plugins about finish application initialization diff --git a/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateEvent.java b/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateEvent.java new file mode 100644 index 0000000..e6f19c3 --- /dev/null +++ b/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateEvent.java @@ -0,0 +1,5 @@ +package org.gluu.fido2.service.app; + +public interface MDS3UpdateEvent { + +} diff --git a/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateTimer.java b/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateTimer.java new file mode 100644 index 0000000..2ece9f9 --- /dev/null +++ b/server/src/main/java/org/gluu/fido2/service/app/MDS3UpdateTimer.java @@ -0,0 +1,73 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.fido2.service.app; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.fido2.service.mds.TocService; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +/** + * @author madhumitas + * + */ +@ApplicationScoped +@Named +public class MDS3UpdateTimer { + + private static final int DEFAULT_INTERVAL = 60 *60*24; // every 24 hours + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private TocService tocService; + + public void initTimer() { + log.info("Initializing MDS3 Update Timer"); + + timerEvent.fire(new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new MDS3UpdateEvent() {}, + Scheduled.Literal.INSTANCE)); + + log.info("Initialized MDS3 Update Timer"); + } + + @Asynchronous + public void process(@Observes @Scheduled MDS3UpdateEvent mds3UpdateEvent) { + LocalDate nextUpdate = tocService.getNextUpdateDate(); + if (nextUpdate.equals(LocalDate.now()) || nextUpdate.isBefore(LocalDate.now())) { + log.info("Downloading the latest TOC from https://mds.fidoalliance.org/"); + try { + tocService.downloadMdsFromServer(new URL("https://mds.fidoalliance.org/")); + + } catch (MalformedURLException e) { + log.error("Error while parsing the FIDO alliance URL :", e); + return; + } + tocService.refresh(); + } else { + log.info( "{} more days for MDS3 Update",LocalDate.now().until(nextUpdate, ChronoUnit.DAYS) ); + } + } + +} \ No newline at end of file diff --git a/server/src/main/java/org/gluu/fido2/service/mds/MdsService.java b/server/src/main/java/org/gluu/fido2/service/mds/MdsService.java index c528dd4..e6b90ae 100644 --- a/server/src/main/java/org/gluu/fido2/service/mds/MdsService.java +++ b/server/src/main/java/org/gluu/fido2/service/mds/MdsService.java @@ -13,10 +13,6 @@ package org.gluu.fido2.service.mds; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -27,10 +23,6 @@ import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.Response.StatusType; import org.apache.commons.codec.binary.Hex; import org.gluu.fido2.exception.Fido2RuntimeException; @@ -42,8 +34,6 @@ import org.gluu.fido2.service.client.ResteasyClientFactory; import org.gluu.fido2.service.verifier.CommonVerifiers; import org.gluu.service.cdi.event.ApplicationInitialized; -import org.gluu.util.StringHelper; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.slf4j.Logger; import com.fasterxml.jackson.databind.JsonNode; @@ -84,10 +74,7 @@ public JsonNode fetchMetadata(byte[] aaguidBuffer) { throw new Fido2RuntimeException("Fido2 configuration not exists"); } - String mdsAccessToken = fido2Configuration.getMdsAccessToken(); - if (StringHelper.isEmpty(mdsAccessToken)) { - throw new Fido2RuntimeException("Fido2 MDS access token should be set"); - } + String aaguid = deconvert(aaguidBuffer); @@ -102,57 +89,12 @@ public JsonNode fetchMetadata(byte[] aaguidBuffer) { throw new Fido2RuntimeException("Authenticator not in TOC aaguid " + aaguid); } - String tocEntryUrl = tocEntry.get("url").asText(); - URI metadataUrl; - try { - metadataUrl = new URI(String.format("%s/?token=%s", tocEntryUrl, mdsAccessToken)); - log.debug("Authenticator AAGUI {} url metadataUrl {} downloaded", aaguid, metadataUrl); - } catch (URISyntaxException e) { - throw new Fido2RuntimeException("Invalid URI in TOC aaguid " + aaguid); - } - verifyTocEntryStatus(aaguid, tocEntry); - String metadataHash = commonVerifiers.verifyThatFieldString(tocEntry, "hash"); - - log.debug("Reaching MDS at {}", tocEntryUrl); - - mdsEntry = downloadMdsFromServer(aaguid, metadataUrl, metadataHash); - - mdsEntries.put(aaguid, mdsEntry); return mdsEntry; } - private JsonNode downloadMdsFromServer(String aaguid, URI metadataUrl, String metadataHash) { - ResteasyClient resteasyClient = resteasyClientFactory.buildResteasyClient(); - Response response = resteasyClient.target(metadataUrl).request().header("Content-Type", MediaType.APPLICATION_JSON).get(); - String body = response.readEntity(String.class); - - StatusType status = response.getStatusInfo(); - log.debug("Response from resource server {}", status); - if (status.getFamily() == Status.Family.SUCCESSFUL) { - byte[] bodyBuffer; - try { - bodyBuffer = body.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new Fido2RuntimeException("Unable to verify metadata hash for aaguid " + aaguid); - } - - byte[] digest = tocService.getDigester().digest(bodyBuffer); - if (!Arrays.equals(digest, base64Service.urlDecode(metadataHash))) { - throw new Fido2RuntimeException("Unable to verify metadata hash for aaguid " + aaguid); - } - - try { - return dataMapperService.readTree(base64Service.urlDecode(body)); - } catch (IOException e) { - log.error("Can't parse payload from the server"); - throw new Fido2RuntimeException("Unable to parse payload from server for aaguid " + aaguid); - } - } else { - throw new Fido2RuntimeException("Unable to retrieve metadata for aaguid " + aaguid + " status " + status); - } - } + private void verifyTocEntryStatus(String aaguid, JsonNode tocEntry) { JsonNode statusReports = tocEntry.get("statusReports"); diff --git a/server/src/main/java/org/gluu/fido2/service/mds/TocService.java b/server/src/main/java/org/gluu/fido2/service/mds/TocService.java index d5399de..638534f 100644 --- a/server/src/main/java/org/gluu/fido2/service/mds/TocService.java +++ b/server/src/main/java/org/gluu/fido2/service/mds/TocService.java @@ -4,13 +4,17 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.cert.X509Certificate; import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.time.LocalDate; import java.util.ArrayList; @@ -33,6 +37,7 @@ import org.gluu.fido2.service.Base64Service; import org.gluu.fido2.service.CertificateService; import org.gluu.fido2.service.DataMapperService; +import org.gluu.fido2.service.client.ResteasyClientFactory; import org.gluu.fido2.service.verifier.CertificateVerifier; import org.gluu.service.cdi.event.ApplicationInitialized; import org.gluu.util.Pair; @@ -46,6 +51,7 @@ import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; /** * @author Yuriy Movchan @@ -71,15 +77,30 @@ public class TocService { @Inject private AppConfiguration appConfiguration; + + @Inject + private ResteasyClientFactory resteasyClientFactory; private Map tocEntries; + + private LocalDate nextUpdate; private MessageDigest digester; + + public LocalDate getNextUpdateDate() + { + return nextUpdate; + } public void init(@Observes @ApplicationInitialized(ApplicationScoped.class) Object init) { - this.tocEntries = Collections.synchronizedMap(new HashMap()); - tocEntries.putAll(parseTOCs()); + refresh(); } + public void refresh() + { + this.tocEntries = Collections.synchronizedMap(new HashMap()); + tocEntries.putAll(parseTOCs()); + } + private Map parseTOCs() { Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration(); if (fido2Configuration == null) { @@ -129,75 +150,147 @@ private Map parseTOC(String mdsTocRootCertFile, String mdsTocF } } - private Pair> parseTOC(String mdsTocRootCertsFolder, Path path) throws IOException, ParseException { - try (BufferedReader reader = Files.newBufferedReader(path)) { - JWSObject jwsObject = JWSObject.parse(reader.readLine()); - - List certificateChain = jwsObject.getHeader().getX509CertChain().stream().map(c -> base64Service.encodeToString(c.decode())) - .collect(Collectors.toList()); - JWSAlgorithm algorithm = jwsObject.getHeader().getAlgorithm(); - - try { - JWSVerifier verifier = resolveVerifier(algorithm, mdsTocRootCertsFolder, certificateChain); - if (!jwsObject.verify(verifier)) { - log.warn("Unable to verify JWS object using algorithm {} for file {}", algorithm, path); - return new Pair>(null, Collections.emptyMap()); - } - } catch (Exception e) { - log.warn("Unable to verify JWS object using algorithm {} for file {} {}", algorithm, path, e); - return new Pair>(null, Collections.emptyMap()); - } - - String jwtPayload = jwsObject.getPayload().toString(); - JsonNode toc = dataMapperService.readTree(jwtPayload); - log.debug("Legal header {}", toc.get("legalHeader")); - - ArrayNode entries = (ArrayNode) toc.get("entries"); - int numberOfEntries = toc.get("no").asInt(); - log.debug("Property 'no' value: {}. Number of entries: {}", numberOfEntries, entries.size()); - - Iterator iter = entries.elements(); - Map tocEntries = new HashMap<>(); - while (iter.hasNext()) { - JsonNode tocEntry = iter.next(); - if (tocEntry.hasNonNull("aaguid")) { - String aaguid = tocEntry.get("aaguid").asText(); - log.info("Added TOC entry {} from {} with status {}", aaguid, path, tocEntry.get("statusReports").findValue("status")); - tocEntries.put(aaguid, tocEntry); - } - } - - String nextUpdateText = toc.get("nextUpdate").asText(); - - LocalDate nextUpdateDate = LocalDate.parse(nextUpdateText); - - this.digester = resolveDigester(algorithm); - - return new Pair>(nextUpdateDate, tocEntries); - } - } + private Pair> parseTOC(String mdsTocRootCertsFolder, Path path) + throws IOException, ParseException { + try (BufferedReader reader = Files.newBufferedReader(path)) { + JWSObject jwsObject = JWSObject.parse(reader.readLine()); + + List certificateChain = jwsObject.getHeader().getX509CertChain().stream() + .map(c -> base64Service.encodeToString(c.decode())).collect(Collectors.toList()); + JWSAlgorithm algorithm = jwsObject.getHeader().getAlgorithm(); + + // If the x5u attribute is present in the JWT Header then + // if (jwsObject.getHeader().getX509CertURL() != null) { + // 1. The FIDO Server MUST verify that the URL specified by the x5u attribute + // has the same web-origin as the URL used to download the metadata BLOB from. + // The FIDO Server SHOULD ignore the file if the web-origin differs (in order to + // prevent loading objects from arbitrary sites). + // 2. The FIDO Server MUST download the certificate (chain) from the URL + // specified by the x5u attribute [JWS]. The certificate chain MUST be verified + // to properly chain to the metadata BLOB signing trust anchor according to + // [RFC5280]. All certificates in the chain MUST be checked for revocation + // according to [RFC5280]. + // 3. The FIDO Server SHOULD ignore the file if the chain cannot be verified or + // if one of the chain certificates is revoked. + + // the chain should be retrieved from the x5c attribute. + // else if (certificateChain.isEmpty()) { + // The FIDO Server SHOULD ignore the file if the chain cannot be verified or if + // one of the chain certificates is revoked. + // } else { + log.info("Metadata BLOB signing trust anchor is considered the BLOB signing certificate chain"); + // Metadata BLOB signing trust anchor is considered the BLOB signing certificate + // chain. + // Verify the signature of the Metadata BLOB object using the BLOB signing + // certificate chain (as determined by the steps above). The FIDO Server SHOULD + // ignore the file if the signature is invalid. It SHOULD also ignore the file + // if its number (no) is less or equal to the number of the last Metadata BLOB + // object cached locally. + // } + + try { + JWSVerifier verifier = resolveVerifier(algorithm, mdsTocRootCertsFolder, certificateChain); + if (!jwsObject.verify(verifier)) { + log.warn("Unable to verify JWS object using algorithm {} for file {}", algorithm, path); + return new Pair>(null, Collections.emptyMap()); + } + } catch (Exception e) { + log.warn("Unable to verify JWS object using algorithm {} for file {} {}", algorithm, path, e); + return new Pair>(null, Collections.emptyMap()); + } + + String jwtPayload = jwsObject.getPayload().toString(); + JsonNode toc = dataMapperService.readTree(jwtPayload); + log.debug("Legal header {}", toc.get("legalHeader")); + nextUpdate = LocalDate.parse(toc.get("nextUpdate").asText(), ISO_DATE); + + ArrayNode entries = (ArrayNode) toc.get("entries"); + int serialNo = toc.get("no").asInt(); + // The serial number of this UAF Metadata BLOB Payload. Serial numbers MUST be + // consecutive and strictly monotonic, i.e. the successor BLOB will have a no + // value exactly incremented by one. + + log.debug("Property 'no' value: {}. serialNo: {}", serialNo, entries.size()); + + Iterator iter = entries.elements(); + Map tocEntries = new HashMap<>(); + while (iter.hasNext()) { + JsonNode metadataEntry = iter.next(); + if (metadataEntry.hasNonNull("aaguid")) { + String aaguid = metadataEntry.get("aaguid").asText(); + + try { + JsonNode metaDataStatement = dataMapperService + .readTree(metadataEntry.get("metadataStatement").toPrettyString()); + if (metaDataStatement != null) { + + log.info("Added TOC entry {} ", aaguid); + tocEntries.put(aaguid, metaDataStatement); + } + + } catch (IOException e) { + log.error("Error parsing the metadata statement", e); + } + + } else if (metadataEntry.hasNonNull("aaid")) { + String aaid = metadataEntry.get("aaid").asText(); + log.info("TODO: handle aaid addition to tocEntries {}", aaid); + } else if (metadataEntry.hasNonNull("attestationCertificateKeyIdentifiers")) { + // FIDO U2F authenticators do not support AAID nor AAGUID, but they use + // attestation certificates dedicated to a single authenticator model. + String attestationCertificateKeyIdentifiers = metadataEntry + .get("attestationCertificateKeyIdentifiers").asText(); + log.info("TODO: handle attestationCertificateKeyIdentifiers addition to tocEntries {}", + attestationCertificateKeyIdentifiers); + } else { + log.info( + "Null - aaguid , aaid, attestationCertificateKeyIdentifiers - Added TOC entry from {} with status {}", + path, metadataEntry.get("statusReports").findValue("status")); + } + } + + String nextUpdateText = toc.get("nextUpdate").asText(); + + LocalDate nextUpdateDate = LocalDate.parse(nextUpdateText); + + this.digester = resolveDigester(algorithm); + + return new Pair>(nextUpdateDate, tocEntries); + } + } private JWSVerifier resolveVerifier(JWSAlgorithm algorithm, String mdsTocRootCertsFolder, List certificateChain) { List x509CertificateChain = certificateService.getCertificates(certificateChain); List x509TrustedCertificates = certificateService.getCertificates(mdsTocRootCertsFolder); X509Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(x509CertificateChain, x509TrustedCertificates); - + //possible set of algos are : ES256, RS256, PS256, ED256 + // no support for ED256 in JOSE library + if (JWSAlgorithm.ES256.equals(algorithm)) { + log.debug("resolveVerifier : ES256"); try { return new ECDSAVerifier((ECPublicKey) verifiedCert.getPublicKey()); } catch (JOSEException e) { throw new Fido2RuntimeException("Unable to create verifier for algorithm " + algorithm, e); } - } else { + } + else if (JWSAlgorithm.RS256.equals(algorithm) || JWSAlgorithm.PS256.equals(algorithm)) { + log.debug("resolveVerifier : RS256"); + return new RSASSAVerifier((RSAPublicKey) verifiedCert.getPublicKey()); + + } + else { throw new Fido2RuntimeException("Don't know what to do with " + algorithm); } } private MessageDigest resolveDigester(JWSAlgorithm algorithm) { - if (JWSAlgorithm.ES256.equals(algorithm)) { + // fix: algorithm RS256 added for https://github.com/GluuFederation/fido2/issues/16 + if (JWSAlgorithm.ES256.equals(algorithm) || JWSAlgorithm.RS256.equals(algorithm) ) { return DigestUtils.getSha256Digest(); - } else { + } + else { throw new Fido2RuntimeException("Don't know what to do with " + algorithm); } } @@ -229,6 +322,7 @@ private Map mergeAndResolveDuplicateEntries(List directoryStream = Files.newDirectoryStream(path)) { + Iterator iter = directoryStream.iterator(); + while (iter.hasNext()) { + Path filePath = iter.next(); + try (InputStream in = metadataUrl.openStream()) { + + Files.copy(in, filePath, StandardCopyOption.REPLACE_EXISTING); + + log.info("TOC file updated."); + return true; + } + } + } catch (IOException e) { + log.warn("Can't access or open path: {}", path, e); + } + return false; + } }