Skip to content

Commit 6ac5333

Browse files
committed
Enable using FidoMetadataService in demo
1 parent 793053d commit 6ac5333

File tree

11 files changed

+246
-56
lines changed

11 files changed

+246
-56
lines changed

webauthn-server-demo/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# FidoMetadataDownloader cache files
2+
webauthn-server-demo-fido-mds-blob-cache.bin
3+
webauthn-server-demo-fido-mds-trust-root-cache.bin

webauthn-server-demo/README

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ class which provides the REST API on top of it.
1818
$ ./gradlew run
1919
$ $BROWSER https://localhost:8443/
2020

21+
Or to run with the https://fidoalliance.org/metadata/[FIDO Metadata Service] as
22+
a source of attestation metadata:
23+
24+
$ YUBICO_WEBAUTHN_USE_FIDO_MDS=true ./gradlew run
25+
$ $BROWSER https://localhost:8443/
26+
2127

2228
== Architecture
2329

@@ -150,3 +156,11 @@ correct environment.
150156
https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-name[RP name]
151157
the server will report. Example: `YUBICO_WEBAUTHN_RP_ID='Yubico Web
152158
Authentication demo'`
159+
160+
- `YUBICO_WEBAUTHN_USE_FIDO_MDS`: If set to `true` (case-insensitive), use
161+
https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-attestation/2.4.1/com/yubico/fido/metadata/FidoMetadataService.html[`FidoMetadataService`]
162+
from the link:../webauthn-server-attestation[`webauthn-server-attestation`]
163+
module as a source of attestation data in addition to the static JSON file
164+
bundled with the demo. This will write cache files to the
165+
`webauthn-server-demo` project root directory; see the patterns in the
166+
`.gitignore` file.

webauthn-server-demo/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ dependencies {
5555

5656
application {
5757
mainClass.set("demo.webauthn.EmbeddedServer")
58+
59+
// Required for processing CRL distribution points extension
60+
applicationDefaultJvmArgs = listOf("-Dcom.sun.security.enableCRLDP=true")
5861
}
5962

6063
for (task in listOf(tasks.installDist, tasks.distZip, tasks.distTar)) {

webauthn-server-demo/src/main/java/com/yubico/webauthn/attestation/YubicoJsonMetadataService.java

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
import com.yubico.internal.util.CertificateParser;
3232
import com.yubico.internal.util.CollectionUtil;
3333
import com.yubico.internal.util.OptionalUtil;
34+
import com.yubico.webauthn.RegistrationResult;
3435
import com.yubico.webauthn.attestation.matcher.ExtensionMatcher;
3536
import com.yubico.webauthn.data.ByteArray;
37+
import demo.webauthn.MetadataService;
3638
import java.security.cert.CertificateException;
3739
import java.security.cert.X509Certificate;
3840
import java.util.Collection;
@@ -48,7 +50,7 @@
4850
import lombok.extern.slf4j.Slf4j;
4951

5052
@Slf4j
51-
public final class YubicoJsonMetadataService implements AttestationTrustSource {
53+
public final class YubicoJsonMetadataService implements AttestationTrustSource, MetadataService {
5254

5355
private static final String SELECTORS = "selectors";
5456
private static final String SELECTOR_TYPE = "type";
@@ -90,43 +92,53 @@ public YubicoJsonMetadataService() {
9092
DEFAULT_DEVICE_MATCHERS);
9193
}
9294

93-
public Optional<Attestation> findMetadata(X509Certificate attestationCertificate) {
94-
return metadataObjects.stream()
95+
@Override
96+
public Set<Object> findEntries(@NonNull RegistrationResult registrationResult) {
97+
return registrationResult
98+
.getAttestationTrustPath()
9599
.map(
96-
metadata -> {
97-
Map<String, String> vendorProperties;
98-
Map<String, String> deviceProperties = null;
99-
String identifier;
100+
certs -> {
101+
X509Certificate attestationCertificate = certs.get(0);
102+
return metadataObjects.stream()
103+
.map(
104+
metadata -> {
105+
Map<String, String> vendorProperties;
106+
Map<String, String> deviceProperties = null;
107+
String identifier;
100108

101-
identifier = metadata.getIdentifier();
102-
vendorProperties = Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull);
103-
for (JsonNode device : metadata.getDevices()) {
104-
if (deviceMatches(device.get(SELECTORS), attestationCertificate)) {
105-
ImmutableMap.Builder<String, String> devicePropertiesBuilder =
106-
ImmutableMap.builder();
107-
for (Map.Entry<String, JsonNode> deviceEntry :
108-
Lists.newArrayList(device.fields())) {
109-
JsonNode value = deviceEntry.getValue();
110-
if (value.isTextual()) {
111-
devicePropertiesBuilder.put(deviceEntry.getKey(), value.asText());
112-
}
113-
}
114-
deviceProperties = devicePropertiesBuilder.build();
115-
break;
116-
}
117-
}
109+
identifier = metadata.getIdentifier();
110+
vendorProperties =
111+
Maps.filterValues(metadata.getVendorInfo(), Objects::nonNull);
112+
for (JsonNode device : metadata.getDevices()) {
113+
if (deviceMatches(device.get(SELECTORS), attestationCertificate)) {
114+
ImmutableMap.Builder<String, String> devicePropertiesBuilder =
115+
ImmutableMap.builder();
116+
for (Map.Entry<String, JsonNode> deviceEntry :
117+
Lists.newArrayList(device.fields())) {
118+
JsonNode value = deviceEntry.getValue();
119+
if (value.isTextual()) {
120+
devicePropertiesBuilder.put(deviceEntry.getKey(), value.asText());
121+
}
122+
}
123+
deviceProperties = devicePropertiesBuilder.build();
124+
break;
125+
}
126+
}
118127

119-
return Optional.ofNullable(deviceProperties)
120-
.map(
121-
deviceProps ->
122-
Attestation.builder()
123-
.metadataIdentifier(Optional.ofNullable(identifier))
124-
.vendorProperties(Optional.of(vendorProperties))
125-
.deviceProperties(deviceProps)
126-
.build());
128+
return Optional.ofNullable(deviceProperties)
129+
.map(
130+
deviceProps ->
131+
Attestation.builder()
132+
.metadataIdentifier(Optional.ofNullable(identifier))
133+
.vendorProperties(Optional.of(vendorProperties))
134+
.deviceProperties(deviceProps)
135+
.build());
136+
})
137+
.flatMap(OptionalUtil::stream)
138+
.map(attestation -> (Object) attestation)
139+
.collect(Collectors.toSet());
127140
})
128-
.flatMap(OptionalUtil::stream)
129-
.findAny();
141+
.orElseGet(Collections::emptySet);
130142
}
131143

132144
private boolean deviceMatches(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package demo.webauthn;
2+
3+
import com.yubico.webauthn.RegistrationResult;
4+
import com.yubico.webauthn.attestation.AttestationTrustSource;
5+
import com.yubico.webauthn.data.ByteArray;
6+
import java.security.cert.X509Certificate;
7+
import java.util.*;
8+
import lombok.NonNull;
9+
10+
/**
11+
* Combines several attestation metadata sources into one, which delegates to each sub-service in
12+
* order until one returns a non-empty result.
13+
*/
14+
public class CompositeMetadataService implements AttestationTrustSource, MetadataService {
15+
16+
private final List<MetadataService> delegates;
17+
18+
public CompositeMetadataService(MetadataService... delegates) {
19+
this.delegates = Collections.unmodifiableList(Arrays.asList(delegates));
20+
}
21+
22+
@Override
23+
public TrustRootsResult findTrustRoots(
24+
List<X509Certificate> attestationCertificateChain, Optional<ByteArray> aaguid) {
25+
for (MetadataService delegate : delegates) {
26+
TrustRootsResult res = delegate.findTrustRoots(attestationCertificateChain, aaguid);
27+
if (!res.getTrustRoots().isEmpty()) {
28+
return res;
29+
}
30+
}
31+
32+
return TrustRootsResult.builder().trustRoots(Collections.emptySet()).build();
33+
}
34+
35+
@Override
36+
public Set<Object> findEntries(@NonNull RegistrationResult registrationResult) {
37+
for (MetadataService delegate : delegates) {
38+
Set<Object> res = delegate.findEntries(registrationResult);
39+
if (!res.isEmpty()) {
40+
return res;
41+
}
42+
}
43+
44+
return Collections.emptySet();
45+
}
46+
}

webauthn-server-demo/src/main/java/demo/webauthn/Config.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public static RelyingPartyIdentity getRpIdentity() {
7878
return getInstance().rpIdentity;
7979
}
8080

81+
public static boolean useFidoMds() {
82+
return "true".equalsIgnoreCase(System.getenv("YUBICO_WEBAUTHN_USE_FIDO_MDS"));
83+
}
84+
8185
private static Set<String> computeOrigins() {
8286
final String origins = System.getenv("YUBICO_WEBAUTHN_ALLOWED_ORIGINS");
8387

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package demo.webauthn;
2+
3+
import com.yubico.fido.metadata.FidoMetadataService;
4+
import com.yubico.webauthn.RegistrationResult;
5+
import com.yubico.webauthn.data.ByteArray;
6+
import java.security.cert.X509Certificate;
7+
import java.util.List;
8+
import java.util.Optional;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
import lombok.AllArgsConstructor;
12+
import lombok.NonNull;
13+
14+
@AllArgsConstructor
15+
public class FidoMetadataServiceAdapter implements MetadataService {
16+
private final FidoMetadataService fido;
17+
18+
@Override
19+
public TrustRootsResult findTrustRoots(
20+
List<X509Certificate> attestationCertificateChain, Optional<ByteArray> aaguid) {
21+
return fido.findTrustRoots(attestationCertificateChain, aaguid);
22+
}
23+
24+
@Override
25+
public Set<Object> findEntries(@NonNull RegistrationResult registrationResult) {
26+
return fido.findEntries(registrationResult).stream()
27+
.map(metadataBLOBPayloadEntry -> (Object) metadataBLOBPayloadEntry)
28+
.collect(Collectors.toSet());
29+
}
30+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package demo.webauthn;
2+
3+
import com.yubico.webauthn.RegistrationResult;
4+
import com.yubico.webauthn.attestation.AttestationTrustSource;
5+
import java.util.Set;
6+
import lombok.NonNull;
7+
8+
public interface MetadataService extends AttestationTrustSource {
9+
Set<Object> findEntries(@NonNull RegistrationResult registrationResult);
10+
}

webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
3333
import com.google.common.cache.Cache;
3434
import com.google.common.cache.CacheBuilder;
35+
import com.yubico.fido.metadata.FidoMetadataDownloader;
36+
import com.yubico.fido.metadata.FidoMetadataDownloaderException;
37+
import com.yubico.fido.metadata.FidoMetadataService;
38+
import com.yubico.fido.metadata.UnexpectedLegalHeader;
3539
import com.yubico.internal.util.CertificateParser;
3640
import com.yubico.internal.util.JacksonCodecs;
3741
import com.yubico.util.Either;
@@ -43,7 +47,6 @@
4347
import com.yubico.webauthn.RelyingParty;
4448
import com.yubico.webauthn.StartAssertionOptions;
4549
import com.yubico.webauthn.StartRegistrationOptions;
46-
import com.yubico.webauthn.attestation.Attestation;
4750
import com.yubico.webauthn.attestation.YubicoJsonMetadataService;
4851
import com.yubico.webauthn.data.AttestationConveyancePreference;
4952
import com.yubico.webauthn.data.AuthenticatorData;
@@ -53,15 +56,23 @@
5356
import com.yubico.webauthn.data.RelyingPartyIdentity;
5457
import com.yubico.webauthn.data.ResidentKeyRequirement;
5558
import com.yubico.webauthn.data.UserIdentity;
59+
import com.yubico.webauthn.data.exception.Base64UrlException;
5660
import com.yubico.webauthn.exception.AssertionFailedException;
5761
import com.yubico.webauthn.exception.RegistrationFailedException;
5862
import demo.webauthn.data.AssertionRequestWrapper;
5963
import demo.webauthn.data.AssertionResponse;
6064
import demo.webauthn.data.CredentialRegistration;
6165
import demo.webauthn.data.RegistrationRequest;
6266
import demo.webauthn.data.RegistrationResponse;
67+
import java.io.File;
6368
import java.io.IOException;
69+
import java.security.DigestException;
70+
import java.security.InvalidAlgorithmParameterException;
71+
import java.security.InvalidKeyException;
72+
import java.security.NoSuchAlgorithmException;
6473
import java.security.SecureRandom;
74+
import java.security.SignatureException;
75+
import java.security.cert.CertPathValidatorException;
6576
import java.security.cert.CertificateException;
6677
import java.security.cert.X509Certificate;
6778
import java.time.Clock;
@@ -91,14 +102,48 @@ public class WebAuthnServer {
91102
private final InMemoryRegistrationStorage userStorage;
92103
private final SessionManager sessions = new SessionManager();
93104

94-
private final YubicoJsonMetadataService metadataService = new YubicoJsonMetadataService();
105+
private final MetadataService metadataService = getMetadataService();
106+
107+
private static MetadataService getMetadataService()
108+
throws CertPathValidatorException, InvalidAlgorithmParameterException, Base64UrlException,
109+
DigestException, FidoMetadataDownloaderException, CertificateException,
110+
UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException,
111+
InvalidKeyException {
112+
if (Config.useFidoMds()) {
113+
logger.info("Using combination of Yubico JSON file and FIDO MDS for attestation metadata.");
114+
return new CompositeMetadataService(
115+
new YubicoJsonMetadataService(),
116+
new FidoMetadataServiceAdapter(
117+
FidoMetadataService.builder()
118+
.useBlob(
119+
FidoMetadataDownloader.builder()
120+
.expectLegalHeader(
121+
"Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/")
122+
.useDefaultTrustRoot()
123+
.useTrustRootCacheFile(
124+
new File("webauthn-server-demo-fido-mds-trust-root-cache.bin"))
125+
.useDefaultBlob()
126+
.useBlobCacheFile(
127+
new File("webauthn-server-demo-fido-mds-blob-cache.bin"))
128+
.build()
129+
.loadCachedBlob())
130+
.build()));
131+
} else {
132+
logger.info("Using only Yubico JSON file for attestation metadata.");
133+
return new YubicoJsonMetadataService();
134+
}
135+
}
95136

96137
private final Clock clock = Clock.systemDefaultZone();
97138
private final ObjectMapper jsonMapper = JacksonCodecs.json();
98139

99140
private final RelyingParty rp;
100141

101-
public WebAuthnServer() {
142+
public WebAuthnServer()
143+
throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException,
144+
Base64UrlException, DigestException, FidoMetadataDownloaderException,
145+
UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException,
146+
InvalidKeyException {
102147
this(
103148
new InMemoryRegistrationStorage(),
104149
newCache(),
@@ -112,7 +157,11 @@ public WebAuthnServer(
112157
Cache<ByteArray, RegistrationRequest> registerRequestStorage,
113158
Cache<ByteArray, AssertionRequestWrapper> assertRequestStorage,
114159
RelyingPartyIdentity rpIdentity,
115-
Set<String> origins) {
160+
Set<String> origins)
161+
throws CertificateException, CertPathValidatorException, InvalidAlgorithmParameterException,
162+
Base64UrlException, DigestException, FidoMetadataDownloaderException,
163+
UnexpectedLegalHeader, IOException, NoSuchAlgorithmException, SignatureException,
164+
InvalidKeyException {
116165
this.userStorage = userStorage;
117166
this.registerRequestStorage = registerRequestStorage;
118167
this.assertRequestStorage = assertRequestStorage;
@@ -526,18 +575,15 @@ private CredentialRegistration addRegistration(
526575
.signatureCount(result.getSignatureCount())
527576
.build(),
528577
result.getKeyId().getTransports().orElseGet(TreeSet::new),
529-
result
530-
.getAttestationTrustPath()
531-
.flatMap(x5c -> x5c.stream().findFirst())
532-
.flatMap(metadataService::findMetadata));
578+
metadataService.findEntries(result).stream().findAny());
533579
}
534580

535581
private CredentialRegistration addRegistration(
536582
UserIdentity userIdentity,
537583
Optional<String> nickname,
538584
RegisteredCredential credential,
539585
SortedSet<AuthenticatorTransport> transports,
540-
Optional<Attestation> attestationMetadata) {
586+
Optional<Object> attestationMetadata) {
541587
CredentialRegistration reg =
542588
CredentialRegistration.builder()
543589
.userIdentity(userIdentity)

webauthn-server-demo/src/main/java/demo/webauthn/data/CredentialRegistration.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import com.fasterxml.jackson.annotation.JsonIgnore;
2828
import com.fasterxml.jackson.annotation.JsonProperty;
2929
import com.yubico.webauthn.RegisteredCredential;
30-
import com.yubico.webauthn.attestation.Attestation;
3130
import com.yubico.webauthn.data.AuthenticatorTransport;
3231
import com.yubico.webauthn.data.UserIdentity;
3332
import java.time.Instant;
@@ -49,7 +48,7 @@ public class CredentialRegistration {
4948
@JsonIgnore Instant registrationTime;
5049
RegisteredCredential credential;
5150

52-
Optional<Attestation> attestationMetadata;
51+
Optional<Object> attestationMetadata;
5352

5453
@JsonProperty("registrationTime")
5554
public String getRegistrationTimestamp() {

0 commit comments

Comments
 (0)