Skip to content

Commit

Permalink
feat: update to MDS version 3.0 #16
Browse files Browse the repository at this point in the history
  • Loading branch information
maduvena committed Feb 1, 2022
1 parent 377b4c9 commit 1aea0b6
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public class AppInitializer {

@Inject
private CleanerTimer cleanerTimer;

@Inject
private MDS3UpdateTimer mds3UpdateTimer;

@Inject
private QuartzSchedulerManager quartzSchedulerManager;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.gluu.fido2.service.app;

public class MDS3UpdateEvent {

}
Original file line number Diff line number Diff line change
@@ -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> 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");
}
}

}
22 changes: 3 additions & 19 deletions server/src/main/java/org/gluu/fido2/service/mds/MdsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
}
Expand Down
151 changes: 137 additions & 14 deletions server/src/main/java/org/gluu/fido2/service/mds/TocService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -71,15 +77,30 @@ public class TocService {

@Inject
private AppConfiguration appConfiguration;

@Inject
private ResteasyClientFactory resteasyClientFactory;

private Map<String, JsonNode> 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<String, JsonNode>());
tocEntries.putAll(parseTOCs());
refresh();
}

public void refresh()
{
this.tocEntries = Collections.synchronizedMap(new HashMap<String, JsonNode>());
tocEntries.putAll(parseTOCs());
}

private Map<String, JsonNode> parseTOCs() {
Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration();
if (fido2Configuration == null) {
Expand Down Expand Up @@ -137,6 +158,36 @@ private Pair<LocalDate, Map<String, JsonNode>> 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)) {
Expand All @@ -151,20 +202,44 @@ private Pair<LocalDate, Map<String, JsonNode>> 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<JsonNode> iter = entries.elements();
Map<String, JsonNode> 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();
Expand All @@ -182,22 +257,33 @@ private JWSVerifier resolveVerifier(JWSAlgorithm algorithm, String mdsTocRootCer
List<X509Certificate> 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);
}
}
Expand Down Expand Up @@ -229,6 +315,7 @@ private Map<String, JsonNode> mergeAndResolveDuplicateEntries(List<Map<String, J
return allEntries;
}


private LocalDate getDate(JsonNode node) {
JsonNode dateNode = node.get("timeOfLastStatusChange");
LocalDate date;
Expand All @@ -242,11 +329,47 @@ private LocalDate getDate(JsonNode node) {
}

public JsonNode getAuthenticatorsMetadata(String aaguid) {

return tocEntries.get(aaguid);
}

public MessageDigest getDigester() {
return digester;
}

public boolean downloadMdsFromServer(URL metadataUrl) {

Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration();

String mdsTocFilesFolder = fido2Configuration.getMdsTocsFolder();

Path path = FileSystems.getDefault().getPath(mdsTocFilesFolder);
log.info("folder "+path+" : isWritable - "+Files.isWritable(path));
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) {
Iterator<Path> 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;
}

}

0 comments on commit 1aea0b6

Please sign in to comment.