From 1aea0b6662a9db3db493f2c57edff53bec440b4b Mon Sep 17 00:00:00 2001 From: Madhumita Date: Tue, 1 Feb 2022 21:54:13 +0530 Subject: [PATCH] feat: update to MDS version 3.0 #16 --- .../fido2/service/app/AppInitializer.java | 4 + .../fido2/service/app/MDS3UpdateEvent.java | 5 + .../fido2/service/app/MDS3UpdateTimer.java | 73 +++++++++ .../gluu/fido2/service/mds/MdsService.java | 22 +-- .../gluu/fido2/service/mds/TocService.java | 151 ++++++++++++++++-- 5 files changed, 222 insertions(+), 33 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..9122385 --- /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 class 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..f4dd6fe --- /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(); + log.debug("MDS3 nextUpdate: " + nextUpdate.toString()); + 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); + } + tocService.refresh(); + } else { + log.info(LocalDate.now().until(nextUpdate, ChronoUnit.DAYS) + " more days for MDS3 Update"); + } + } + +} \ 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..9f7af81 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 @@ -84,10 +84,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,23 +99,10 @@ 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; } 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..63a76bf 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) { @@ -137,6 +158,36 @@ private Pair> parseTOC(String mdsTocRootCertsFo .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.size() > 0) { + // The FIDO Server SHOULD ignore the file if the chain cannot be verified or if + // one of the chain certificates is revoked. + log.info("x5c"); + } 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)) { @@ -151,20 +202,44 @@ private Pair> parseTOC(String mdsTocRootCertsFo 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 numberOfEntries = toc.get("no").asInt(); - log.debug("Property 'no' value: {}. Number of entries: {}", numberOfEntries, entries.size()); + 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 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); + JsonNode metadataEntry = iter.next(); + if (metadataEntry.hasNonNull("aaguid")) { + String aaguid = metadataEntry.get("aaguid").asText(); + + JsonNode metaDataStatement = null; + try { + metaDataStatement = dataMapperService.readTree(metadataEntry.get("metadataStatement").toPrettyString()); + } catch (IOException e) { + log.error("Error parsing the metadata statement",e); + } + + log.info("Added TOC entry {} from {} with status {} and timeOfLastStatusChange {} ", aaguid, path,metaDataStatement.get("statusReports") != null ? metaDataStatement.get("statusReports").findValue("status"):"No Status reports", metaDataStatement.get("timeOfLastStatusChange") != null ? metaDataStatement.get("timeOfLastStatusChange") : "Not mentioned"); + tocEntries.put(aaguid, metaDataStatement); } + 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(); @@ -182,22 +257,33 @@ private JWSVerifier resolveVerifier(JWSAlgorithm algorithm, String mdsTocRootCer List x509TrustedCertificates = certificateService.getCertificates(mdsTocRootCertsFolder); X509Certificate verifiedCert = certificateVerifier.verifyAttestationCertificates(x509CertificateChain, x509TrustedCertificates); - + //possible set of algos are : ES256, RS256, PS256, ED256 + // TODO: 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 +315,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()) { + log.info(path + "/tempfile"); + Path tempFile = Files.createTempFile(null,null); + + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + + // Actual copy. + Files.copy(tempFile, filePath, StandardCopyOption.REPLACE_EXISTING); + + // Cleanup. + Files.delete(tempFile); + + //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; + } }