resources =
- AttestationResolver.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
+ FidoMetadataService.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
final URL resource = resources.nextElement();
diff --git a/test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts
similarity index 51%
rename from test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts
rename to test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts
index 37512414a..f558ba389 100644
--- a/test-dependent-projects/java-dep-webauthn-server-core-minimal/build.gradle.kts
+++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/build.gradle.kts
@@ -2,18 +2,21 @@ plugins {
`java-library`
}
-val coreTestsOutput = project(":webauthn-server-core-minimal").extensions.getByType(SourceSetContainer::class).test.get().output
+val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output
dependencies {
- implementation(project(":webauthn-server-core-minimal"))
+ implementation(project(":webauthn-server-core"))
+ implementation("org.bouncycastle:bcprov-jdk15on:[1.62,2)")
testImplementation(coreTestsOutput)
testImplementation("junit:junit:4.12")
testImplementation("org.mockito:mockito-core:[2.27.0,3)")
- // Runtime-only internal dependency of webauthn-server-core-minimal
+ // Runtime-only internal dependency of webauthn-server-core
testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)")
+ testRuntimeOnly("ch.qos.logback:logback-classic:[1.2.3,2)")
+
// Transitive dependencies from coreTestOutput
testImplementation("org.scala-lang:scala-library:[2.13.1,3)")
}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
new file mode 100644
index 000000000..e38997392
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
@@ -0,0 +1,108 @@
+package com.yubico.webauthn;
+
+import static org.junit.Assert.assertTrue;
+
+import COSE.CoseException;
+import com.yubico.webauthn.data.AttestationObject;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Test that the BouncyCastle provider is not loaded by default.
+ *
+ * Motivation: https://github.com/Yubico/java-webauthn-server/issues/97
+ */
+public class BouncyCastleProviderPresenceTest {
+
+ private List providersBefore;
+
+ @Before
+ public void setUp() {
+ providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList());
+ }
+
+ @After
+ public void tearDown() {
+ for (Provider prov : Security.getProviders()) {
+ Security.removeProvider(prov.getName());
+ }
+ providersBefore.forEach(Security::addProvider);
+ }
+
+ private static boolean isNamedBouncyCastle(Provider prov) {
+ return prov.getName().equals("BC") || prov.getClass().getCanonicalName().contains("bouncy");
+ }
+
+ @Test
+ public void bouncyCastleProviderIsInClasspath() {
+ new BouncyCastleProvider();
+ }
+
+ @Test
+ public void bouncyCastleProviderIsNotLoadedByDefault() {
+ assertTrue(
+ Arrays.stream(Security.getProviders())
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
+ }
+
+ @Test
+ public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() {
+ RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build())
+ .credentialRepository(Mockito.mock(CredentialRepository.class))
+ .build();
+
+ assertTrue(
+ Arrays.stream(Security.getProviders())
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
+ }
+
+ @Test
+ public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey()
+ throws IOException, CoseException, InvalidKeySpecException {
+ try {
+ WebAuthnCodecs.importCosePublicKey(
+ new AttestationObject(
+ RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject())
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey());
+ } catch (NoSuchAlgorithmException e) {
+ // OK
+ }
+
+ assertTrue(
+ Arrays.stream(Security.getProviders())
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
+ }
+
+ @Test(expected = NoSuchAlgorithmException.class)
+ public void doesNotFallBackToBouncyCastleAutomatically()
+ throws IOException, CoseException, InvalidKeySpecException, NoSuchAlgorithmException {
+ for (Provider prov : Security.getProviders()) {
+ Security.removeProvider(prov.getName());
+ }
+
+ WebAuthnCodecs.importCosePublicKey(
+ new AttestationObject(
+ RegistrationTestData.Packed$.MODULE$.BasicAttestationEdDsa().attestationObject())
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey());
+ }
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java
new file mode 100644
index 000000000..78201c6a2
--- /dev/null
+++ b/test-dependent-projects/java-dep-webauthn-server-core-and-bouncycastle/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java
@@ -0,0 +1,91 @@
+package com.yubico.webauthn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import COSE.CoseException;
+import com.yubico.webauthn.data.AttestationObject;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class CryptoAlgorithmsTest {
+
+ private List providersBefore;
+
+ @Before
+ public void setUp() {
+ providersBefore = Stream.of(Security.getProviders()).collect(Collectors.toList());
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ RelyingParty.builder()
+ .identity(RelyingPartyIdentity.builder().id("foo").name("foo").build())
+ .credentialRepository(Mockito.mock(CredentialRepository.class))
+ .build();
+ }
+
+ @After
+ public void tearDown() {
+ for (Provider prov : Security.getProviders()) {
+ Security.removeProvider(prov.getName());
+ }
+ providersBefore.forEach(Security::addProvider);
+ }
+
+ @Test
+ public void importRsa()
+ throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException {
+ PublicKey key =
+ WebAuthnCodecs.importCosePublicKey(
+ new AttestationObject(
+ RegistrationTestData.Packed$.MODULE$.BasicAttestationRsa().attestationObject())
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey());
+ assertEquals(key.getAlgorithm(), "RSA");
+ }
+
+ @Test
+ public void importEcdsa()
+ throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException {
+ PublicKey key =
+ WebAuthnCodecs.importCosePublicKey(
+ new AttestationObject(
+ RegistrationTestData.Packed$.MODULE$.BasicAttestation().attestationObject())
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey());
+ assertEquals(key.getAlgorithm(), "EC");
+ }
+
+ @Test
+ public void importEddsa()
+ throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException {
+ PublicKey key =
+ WebAuthnCodecs.importCosePublicKey(
+ new AttestationObject(
+ RegistrationTestData.Packed$.MODULE$
+ .BasicAttestationEdDsa()
+ .attestationObject())
+ .getAuthenticatorData()
+ .getAttestedCredentialData()
+ .get()
+ .getCredentialPublicKey());
+ assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm()));
+ }
+}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts
index 41166dddf..1e8977835 100644
--- a/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts
+++ b/test-dependent-projects/java-dep-webauthn-server-core/build.gradle.kts
@@ -2,22 +2,18 @@ plugins {
`java-library`
}
-val coreTestsOutput = project(":webauthn-server-core-minimal").extensions.getByType(SourceSetContainer::class).test.get().output
+val coreTestsOutput = project(":webauthn-server-core").extensions.getByType(SourceSetContainer::class).test.get().output
dependencies {
implementation(project(":webauthn-server-core"))
- testCompileOnly("org.bouncycastle:bcprov-jdk15on:[1.62,2)")
-
testImplementation(coreTestsOutput)
testImplementation("junit:junit:4.12")
testImplementation("org.mockito:mockito-core:[2.27.0,3)")
- // Runtime-only internal dependency of webauthn-server-core-minimal
+ // Runtime-only internal dependency of webauthn-server-core
testImplementation("com.augustcellars.cose:cose-java:[1.0.0,2)")
- testRuntimeOnly("ch.qos.logback:logback-classic:[1.2.3,2)")
-
// Transitive dependencies from coreTestOutput
testImplementation("org.scala-lang:scala-library:[2.13.1,3)")
}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
index 5201a6409..bc6da998b 100644
--- a/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/main/java/com/yubico/test/compilability/ThisShouldCompile.java
@@ -55,7 +55,7 @@ public ByteArray getByteArray() {
public PublicKeyCredentialType getPublicKeyCredentialType() {
PublicKeyCredentialType a = PublicKeyCredentialType.PUBLIC_KEY;
- String b = a.toJsonString();
+ String b = a.getId();
return a;
}
}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
similarity index 69%
rename from test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
rename to test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
index e3dc68d5a..6ce756bbc 100644
--- a/test-dependent-projects/java-dep-webauthn-server-core-minimal/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/BouncyCastleProviderPresenceTest.java
@@ -7,6 +7,7 @@
import com.yubico.webauthn.data.RelyingPartyIdentity;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
@@ -14,13 +15,16 @@
import org.mockito.Mockito;
/**
- * Test that the BouncyCastle provider is not loaded by default when depending on the
- * webauthn-server-core-minimal
package.
+ * Test that the BouncyCastle provider is not loaded by default.
*
* Motivation: https://github.com/Yubico/java-webauthn-server/issues/97
*/
public class BouncyCastleProviderPresenceTest {
+ private static boolean isNamedBouncyCastle(Provider prov) {
+ return prov.getName().equals("BC") || prov.getClass().getCanonicalName().contains("bouncy");
+ }
+
@Test(expected = ClassNotFoundException.class)
public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException {
Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
@@ -30,13 +34,11 @@ public void bouncyCastleProviderIsNotInClasspath() throws ClassNotFoundException
public void bouncyCastleProviderIsNotLoadedByDefault() {
assertTrue(
Arrays.stream(Security.getProviders())
- .noneMatch(prov -> prov.getName().toLowerCase().contains("bouncy")));
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
}
@Test
public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() {
- // The RelyingParty constructor has the possible side-effect of loading the BouncyCastle
- // provider
RelyingParty.builder()
.identity(RelyingPartyIdentity.builder().id("foo").name("foo").build())
.credentialRepository(Mockito.mock(CredentialRepository.class))
@@ -44,15 +46,12 @@ public void bouncyCastleProviderIsNotLoadedAfterInstantiatingRelyingParty() {
assertTrue(
Arrays.stream(Security.getProviders())
- .noneMatch(
- prov ->
- prov.getName().equals("BC")
- || prov.getClass().getCanonicalName().contains("bouncy")));
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
}
@Test
public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey()
- throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException {
+ throws IOException, CoseException, InvalidKeySpecException {
try {
WebAuthnCodecs.importCosePublicKey(
new AttestationObject(
@@ -67,9 +66,6 @@ public void bouncyCastleProviderIsNotLoadedAfterAttemptingToLoadEddsaKey()
assertTrue(
Arrays.stream(Security.getProviders())
- .noneMatch(
- prov ->
- prov.getName().equals("BC")
- || prov.getClass().getCanonicalName().contains("bouncy")));
+ .noneMatch(BouncyCastleProviderPresenceTest::isNamedBouncyCastle));
}
}
diff --git a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java
index d3b38f338..f35ce43ae 100644
--- a/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java
+++ b/test-dependent-projects/java-dep-webauthn-server-core/src/test/java/com/yubico/webauthn/CryptoAlgorithmsTest.java
@@ -1,7 +1,6 @@
package com.yubico.webauthn;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import COSE.CoseException;
import com.yubico.webauthn.data.AttestationObject;
@@ -71,20 +70,4 @@ public void importEcdsa()
.getCredentialPublicKey());
assertEquals(key.getAlgorithm(), "EC");
}
-
- @Test
- public void importEddsa()
- throws IOException, CoseException, NoSuchAlgorithmException, InvalidKeySpecException {
- PublicKey key =
- WebAuthnCodecs.importCosePublicKey(
- new AttestationObject(
- RegistrationTestData.Packed$.MODULE$
- .BasicAttestationEdDsa()
- .attestationObject())
- .getAuthenticatorData()
- .getAttestedCredentialData()
- .get()
- .getCredentialPublicKey());
- assertTrue("EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm()));
- }
}
diff --git a/webauthn-server-attestation/README.adoc b/webauthn-server-attestation/README.adoc
new file mode 100644
index 000000000..5eee6ddc0
--- /dev/null
+++ b/webauthn-server-attestation/README.adoc
@@ -0,0 +1,314 @@
+= webauthn-server-attestation
+:toc:
+:toc-placement: macro
+:toc-title:
+
+An optional module which extends link:../[`webauthn-server-core`]
+with a trust root source for verifying
+https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-attestation[attestation statements],
+by interfacing with the https://fidoalliance.org/metadata/[FIDO Metadata Service].
+
+
+toc::[]
+
+== Features
+
+This module does four things:
+
+- Download, verify and cache metadata BLOBs from the FIDO Metadata Service.
+- Re-download the metadata BLOB when out of date or invalid.
+- Provide utilities for selecting trusted metadata entries and authenticators.
+- Integrate with the
+ link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`]
+ class in the base library, to provide trust root certificates
+ for verifying attestation statements during credential registrations.
+
+Notable *non-features* include:
+
+- *Scheduled BLOB downloads.*
++
+The `FidoMetadataDownloader`
+class will attempt to download a new BLOB only when its `loadCachedBlob()` is executed,
+and then only if the cache is empty or if the cached BLOB is invalid or out of date.
+`FidoMetadataService`
+will never re-download a new BLOB once instantiated.
++
+You should use some external scheduling mechanism to re-run `loadCachedBlob()` periodically
+and rebuild new `FidoMetadataService` instances with the updated metadata contents.
+You can do this with minimal disruption since the `FidoMetadataService` and `RelyingParty`
+classes keep no internal mutable state.
+
+- *Revocation of already-registered credentials*
++
+The FIDO Metadata Service may from time to time report security issues with particular authenticator models.
+The `FidoMetadataService` class can be configured with a filter for which authenticators to trust,
+and untrusted authenticators can be rejected during registration by setting `.allowUntrustedAttestation(false)` on `RelyingParty`,
+but this will not affect any credentials already registered.
+
+
+== Before you start
+
+It is important to be aware that *requiring attestation is an invasive policy*,
+especially when used to restrict users' choice of authenticator.
+For some applications this is necessary; for most it is not.
+Similarly, *attestation does not automatically make your users more secure*.
+Attestation gives you information, but you have to know what to do with that information
+in order to get a security benefit from it; it is a powerful tool but does very little on its own.
+This library can help retrieve and verify additional information about an authenticator,
+and enforce some very basic policy based on it,
+but it is your responsibility to further leverage that information into improved security.
+
+When in doubt, err towards being more permissive, because _using WebAuthn is more secure than not using WebAuthn_.
+It may still be useful to request and store attestation information for future reference -
+for example, to warn users if security issues are discovered in their authenticators -
+but we recommend that you do not _require_ a trusted attestation unless you have specific reason to do so.
+
+
+== Migrating from version `1.x`
+
+See link:doc/Migrating_from_v1.adoc[the migration guide].
+
+
+== Getting started
+
+Using this module consists of 4 major steps:
+
+ 1. Create a
+ `FidoMetadataDownloader`
+ instance to download and cache metadata BLOBs,
+ and a
+ `FidoMetadataService`
+ instance to make use of the downloaded BLOB.
+ See the JavaDoc for these classes for details on how to construct them.
++
+[WARNING]
+=====
+Unlike other classes in this module and the core library,
+`FidoMetadataDownloader` is NOT THREAD SAFE since its `loadCachedBlob()` method reads and writes caches.
+`FidoMetadataService`, on the other hand, is thread safe,
+and `FidoMetadataDownloader` instances can be reused for subsequent `loadCachedBlob()` calls
+as long as only one `loadCachedBlob()` call executes at a time.
+=====
++
+[source,java]
+----------
+FidoMetadataDownloader downloader = FidoMetadataDownloader.builder()
+ .expectLegalHeader("Lorem ipsum dolor sit amet")
+ .useDefaultTrustRoot()
+ .useTrustRootCacheFile(new File("/var/cache/webauthn-server/fido-mds-trust-root.bin"))
+ .useDefaultBlob()
+ .useBlobCacheFile(new File("/var/cache/webauthn-server/fido-mds-blob.bin"))
+ .build();
+
+FidoMetadataService mds = FidoMetadataService.builder()
+ .useBlob(downloader.loadCachedBlob())
+ .build();
+----------
+
+ 2. Set the `FidoMetadataService` as the `attestationTrustSource` on your
+ link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`]
+ instance,
+ and set `attestationConveyancePreference(AttestationConveyancePreference.DIRECT)` on `RelyingParty`
+ to request an attestation statement for new registrations.
+ Optionally also set `.allowUntrustedAttestation(false)` on `RelyingParty` to require trusted attestation for new registrations.
++
+[source,java]
+----------
+RelyingParty rp = RelyingParty.builder()
+ .identity(/* ... */)
+ .credentialRepository(/* ... */)
+ .attestationTrustSource(mds)
+ .attestationConveyancePreference(AttestationConveyancePreference.DIRECT)
+ .allowUntrustedAttestation(true) // Optional step: set to true (default) or false
+ .build();
+----------
+
+ 3. After performing registrations, inspect the `isAttestationTrusted()` result in
+ link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RelyingParty.html[`RelyingParty`]
+ to determine whether the authenticator presented an attestation statement that could be verified
+ by any of the trusted attestation certificates in the FIDO Metadata Service.
++
+[source,java]
+----------
+RelyingParty rp = /* ... */;
+RegistrationResult result = rp.finishRegistration(/* ... */);
+
+if (result.isAttestationTrusted()) {
+ // Do something...
+} else {
+ // Do something else...
+}
+----------
+
+ 4. If needed, use the `findEntries` methods of `FidoMetadataService` to retrieve additional authenticator metadata for new registrations.
++
+[source,java]
+----------
+RelyingParty rp = /* ... */;
+RegistrationResult result = rp.finishRegistration(/* ... */);
+
+Set metadata = mds.findEntries(result);
+----------
+
+By default, `FidoMetadataDownloader` will probably use the SUN provider for the `PKIX` certificate path validation algorithm.
+This requires the `com.sun.security.enableCRLDP` system property set to `true` in order to verify the BLOB signature.
+For example, this can be done on the JVM command line using a `-Dcom.sun.security.enableCRLDP=true` option.
+See the https://docs.oracle.com/javase/9/security/java-pki-programmers-guide.htm#JSSEC-GUID-EB250086-0AC1-4D60-AE2A-FC7461374746[Java PKI Programmers Guide]
+for details.
+
+
+== Selecting trusted authenticators
+
+The
+`FidoMetadataService`
+class can be configured with filters for which authenticators to trust.
+When the `FidoMetadataService` is used as the `.attestationTrustSource()` in `RelyingParty`,
+this will be reflected in the `.isAttestationTrusted()` result in
+link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core-minimal/latest/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`].
+Any authenticators not trusted will also be rejected for new registrations if you set `.allowUntrustedAttestation(false)` on `RelyingParty`.
+
+The filter has two stages: a "prefilter" which selects metadata entries to include in the data source,
+and a registration-time filter which decides whether to associate a metadata entry
+with a particular authenticator.
+The prefilter executes only once (per metadata entry):
+when the `FidoMetadataService` instance is constructed.
+The registration-time filter takes effect during credential registration
+and in the `findEntries()` methods of `FidoMetadataService`.
+The following figure illustrates where each filter appears in the data flows:
+
+[source]
+----------
+ +----------+
+ | FIDO MDS |
+ +----------+
+ |
+ | Metadata BLOB
+ |
++--------------------------------------------------------------------------+
+| | FidoMetadataService |
+| v =================== |
+| +-----------+ |
+| | Prefilter | |
+| +-----------+ |
+| | |
+| | Selected metadata entries |
+| v Matching |
+| +-----------------------------+ metadata +-------------------+ |
+| | Search by AAGUID & | entries | Registration-time | |
+| | Attestation certificate key |------------------->| filter | |
+| +-----------------------------+ +-------------------+ |
+| ^ (1) ^ (2) | (1) (2) | |
+| | (internal) | findEntries() | | |
++--------------------------------------------------------------------------+
+ | | | |
+ | `-------------------------|--. |
+ | Get trust roots | | v
+ | Matched | | Matched
+ +-----------------------------------+ trust roots | | metadata entries
+ | RelyingParty.finishRegistration() |<----------------' |
+ +-----------------------------------+ |
+ ^ | |
+ | | Verify signature |
+ | PublicKeyCredential | Validate contents | Retrieve matching
+ | | Evaluate trust | metadata entries
+ | v |
+ +-------------+ +-----------------------------------+
+ | Registering | | RegistrationResult |
+ | user | | - getAaguid(): ByteArray |
+ +-------------+ | - getAttestationTrustPath(): List |
+ | - isAttestationTrusted(): boolean |
+ | - getPublicKeyCose(): ByteArray |
+ +-----------------------------------+
+----------
+
+The default prefilter excludes any authenticator with any `REVOKED`
+link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-metadatablobpayloadentry-statusreports[status report]
+entry,
+and the default registration-time filter excludes any authenticator
+with a matching `ATTESTATION_KEY_COMPROMISE` status report entry.
+To customize the filters, configure the `.prefilter(Predicate)` and `.filter(Predicate)` settings
+in the `FidoMetadataService` builder.
+The filters are predicate functions;
+each metadata entry will be trusted if and only if the prefilter predicate returns `true` for that entry.
+Similarly during registration or metadata lookup, the authenticator will be matched with each metadata entry
+only if the registration-time filter returns `true` for that pair of authenticator and metadata entry.
+You can also use the `FidoMetadataService.Filters.allOf()` combinator to merge several predicates into one.
+
+[NOTE]
+=====
+Setting a custom filter will replace the default filter.
+This is true for both the prefilter and the registration-time filter.
+If you want to maintain the default filter in addition to the new behaviour,
+you must include the default condition in the new filter.
+For example, you can use `FidoMetadataService.Filters.allOf()` to combine a predefined filter with a custom one.
+The default filters are available via static functions in `FidoMetadataService.Filters`.
+=====
+
+
+=== A note on "allow-lists" vs "deny-lists"
+
+The filtering functionality described above essentially expresses an "allow-list" policy.
+Any metadata entry that satisfies the filters is eligible as a trust root;
+any attestation statement that can be verified by one of those trust roots is trusted,
+and any that cannot is not trusted.
+There is no complementary "deny-list" option to reject some specific authenticators
+and implicitly trust everything else even with unknown trust roots.
+This is because you cannot use such a deny list to enforce an attestation policy.
+
+If unknown attestation trust roots were permitted,
+then a deny list could be easily circumvented by making up an attestation that is not on the deny list.
+Since it will have an unknown trust root, it would then be implicitly trusted.
+This is why any enforceable attestation policy must disallow unknown trust roots.
+
+Note that unknown and untrusted attestation is allowed by default,
+but can be disallowed by explicitly configuring `RelyingParty` with `.allowUntrustedAttestation(false)`.
+
+
+== Alignment with FIDO MDS spec
+
+The FIDO Metadata Service specification defines
+link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-object-processing-rules[processing rules for servers].
+The library implements these as closely as possible, but with some slight departures from the spec:
+
+* Processing rules steps 1-7 are implemented as specified, by the `FidoMetadataDownloader` class.
+ All "SHOULD" clauses are also respected, with some caveats:
+
+ ** Step 3 states "The `nextUpdate` field of the Metadata BLOB specifies a date when the download SHOULD occur at latest".
+ `FidoMetadataDownloader` does not automatically re-download the BLOB.
+ Instead, each time its `.loadCachedBlob()` method is executed it checks whether a new BLOB should be downloaded.
++
+If no BLOB exists in cache, or the cached BLOB is invalid, or if the current date is greater than or equal to `nextUpdate`,
+then a new BLOB is downloaded.
+If the new BLOB is valid, has a correct signature, and has a `no` field greater than the cached BLOB,
+then the new BLOB replaces the cached one;
+otherwise, the new BLOB is discarded and the cached one is kept until the next execution of `.loadCachedBlob()`.
+
+* Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole.
+ In processing rules step 8, neither `FidoMetadataDownloader` nor `FidoMetadataService`
+ performs any comparison between versions of a metadata entry.
+ Policy for ignoring metadata entries can be configured via the filter settings in `FidoMetadataService`.
+ See above for details.
+
+There are also some other requirements throughout the spec, which may not be obvious:
+
+* The
+ link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section]
+ states that "The Relying party MUST reject the Metadata Statement if the `authenticatorVersion` has not increased"
+ in an `UPDATE_AVAILABLE` status report.
+ Thus, `FidoMetadataService` silently ignores any `MetadataBLOBPayloadEntry`
+ whose `metadataStatement.authenticatorVersion` is present and not greater than or equal to
+ the `authenticatorVersion` in the respective status report.
+ Again, no comparison is made between metadata entries from different BLOB versions.
+
+* The
+ link:https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#info-statuses[AuthenticatorStatus section]
+ states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values".
+ Thus any unknown status valus will be parsed as `AuthenticatorStatus.UNKNOWN`,
+ and `MetadataBLOBPayloadEntry` will silently ignore any status report with that status.
+
+
+== Overriding certificate path validation
+
+The `FidoMetadataDownloader` class uses `CertPathValidator.getInstance("PKIX")` to retrieve a `CertPathValidator` instance.
+If you need to override any aspect of certificate path validation,
+such as CRL retrieval or OCSP, you may provide a custom `CertPathValidator` provider for the `"PKIX"` algorithm.
diff --git a/webauthn-server-attestation/build.gradle b/webauthn-server-attestation/build.gradle
index df8dfff19..7fe728b33 100644
--- a/webauthn-server-attestation/build.gradle
+++ b/webauthn-server-attestation/build.gradle
@@ -3,6 +3,7 @@ plugins {
id 'scala'
id 'maven-publish'
id 'signing'
+ id 'info.solidsoft.pitest'
id 'io.github.cosmicsilence.scalafix'
}
@@ -13,13 +14,25 @@ project.ext.publishMe = true
sourceCompatibility = 1.8
targetCompatibility = 1.8
-evaluationDependsOn(':webauthn-server-core-minimal')
+evaluationDependsOn(':webauthn-server-core')
+
+sourceSets {
+ integrationTest {
+ compileClasspath += sourceSets.main.output
+ runtimeClasspath += sourceSets.main.output
+ }
+}
+
+configurations {
+ integrationTestImplementation.extendsFrom testImplementation
+ integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
+}
dependencies {
api(platform(rootProject))
api(
- project(':webauthn-server-core-minimal'),
+ project(':webauthn-server-core'),
)
implementation(
@@ -31,21 +44,38 @@ dependencies {
)
testImplementation(
- project(':webauthn-server-core-minimal').sourceSets.test.output,
+ project(':webauthn-server-core').sourceSets.test.output,
project(':yubico-util-scala'),
+ 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8',
'junit:junit',
+ 'org.bouncycastle:bcpkix-jdk15on',
+ 'org.eclipse.jetty:jetty-server:[9.4.9.v20180320,10)',
'org.mockito:mockito-core',
'org.scala-lang:scala-library',
'org.scalacheck:scalacheck_2.13',
'org.scalatest:scalatest_2.13',
+ 'uk.org.lidalia:slf4j-test',
)
- testRuntimeOnly(
- // Transitive dependency from :webauthn-server-core:test
- 'org.bouncycastle:bcpkix-jdk15on',
- )
+ testImplementation('org.slf4j:slf4j-api') {
+ version {
+ strictly '[1.7.25,1.8-a)' // Pre-1.8 version required by slf4j-test
+ }
+ }
}
+tasks.register('integrationTest', Test) {
+ description = 'Runs integration tests.'
+ group = 'verification'
+
+ testClassesDirs = sourceSets.integrationTest.output.classesDirs
+ classpath = sourceSets.integrationTest.runtimeClasspath
+ shouldRunAfter test
+ check.dependsOn it
+
+ // Required for processing CRL distribution points extension
+ systemProperty 'com.sun.security.enableCRLDP', 'true'
+}
jar {
manifest {
@@ -59,3 +89,17 @@ jar {
}
}
+pitest {
+ pitestVersion = '1.4.11'
+
+ timestampedReports = false
+ outputFormats = ['XML', 'HTML']
+
+ avoidCallsTo = [
+ 'java.util.logging',
+ 'org.apache.log4j',
+ 'org.slf4j',
+ 'org.apache.commons.logging',
+ 'com.google.common.io.Closeables',
+ ]
+}
diff --git a/webauthn-server-attestation/doc/Migrating_from_v1.adoc b/webauthn-server-attestation/doc/Migrating_from_v1.adoc
new file mode 100644
index 000000000..cf0f035b7
--- /dev/null
+++ b/webauthn-server-attestation/doc/Migrating_from_v1.adoc
@@ -0,0 +1,148 @@
+= v1.x to v2.0 migration guide
+
+The `2.0` release of the `webauthn-server-attestation` module
+makes lots of breaking changes compared to the `1.x` versions.
+This guide aims to help migrating between versions.
+
+If you find this migration guide to be incomplete, incorrect,
+or otherwise difficult to follow, please
+link:https://github.com/Yubico/java-webauthn-server/issues/new[let us know!]
+
+Here is a high-level outline of what needs to be updated:
+
+- Replace uses of `StandardMetadataService` and its related classes
+ with `FidoMetadataService` and `FidoMetadataDownloader`.
+- Update the name of the `RelyingParty` integration point
+ from `metadataService` to `attestationTrustSource`.
+- `RegistrationResult` no longer includes attestation metadata,
+ instead you'll need to retrieve it separately after a successful registration.
+- Replace uses of the `Attestation` result type with `MetadataBLOBPayloadEntry`.
+
+
+== Replace `StandardMetadataService`
+
+`StandardMetadataService` and its constituent classes have been removed
+in favour of `FidoMetadataService` and `FidoMetadataDownloader`.
+See the link:../#getting-started[Getting started] documentation
+for details on how to configure and construct them.
+
+Example `1.x` code:
+
+[source,java]
+----------
+MetadataService metadataService =
+ new StandardMetadataService(
+ StandardMetadataService.createDefaultAttestationResolver(
+ StandardMetadataService.createDefaultTrustResolver()
+ ));
+----------
+
+Example `2.0` code:
+
+[source,java]
+----------
+FidoMetadataService metadataService = FidoMetadataService.builder()
+ .useBlob(FidoMetadataDownloader.builder()
+ .expectLegalHeader("Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/")
+ .useDefaultTrustRoot()
+ .useTrustRootCacheFile(new File("fido-mds-trust-root-cache.bin"))
+ .useDefaultBlob()
+ .useBlobCacheFile(new File("fido-mds-blob-cache.bin"))
+ .build()
+ .loadBlob()
+ )
+ .build();
+----------
+
+You may also need to add external logic to occasionally re-run `loadBlob()`
+and reconstruct the `FidoMetadataService`,
+as `FidoMetadataService` will not automatically update the BLOB on its own.
+
+
+== Update `RelyingParty` integration point
+
+`FidoMetadataService` integrates with `RelyingParty` in much the same way as `StandardMetadataService`,
+although the name of the setting has changed.
+
+Example `1.x` code:
+
+[source,diff]
+----------
+ RelyingParty rp = RelyingParty.builder()
+ .identity(rpIdentity)
+ .credentialRepository(credentialRepo)
+ .attestationConveyancePreference(AttestationConveyancePreference.DIRECT)
+- .metadataService(metadataService))
+ .allowUntrustedAttestation(true)
+ .build();
+----------
+
+Example `2.0` code:
+
+[source,diff]
+----------
+ RelyingParty rp = RelyingParty.builder()
+ .identity(rpIdentity)
+ .credentialRepository(credentialRepo)
+ .attestationConveyancePreference(AttestationConveyancePreference.DIRECT)
++ .attestationTrustSource(metadataService)
+ .allowUntrustedAttestation(true)
+ .build();
+----------
+
+
+== Retrieve attestation metadata separately
+
+In `1.x`, `RegistrationResult` could include an `Attestation` object with attestation metadata,
+if a metadata service was configured and the authenticator matched anything in the metadata service.
+In order to keep `RelyingParty` and the new `AttestationTrustSource` interface
+decoupled from any particular format of attestation metadata, this result field has been removed.
+Instead, use the `findEntries` methods of `FidoMetadataService`
+to retrieve attestation metadata after a successful registration, if needed.
+
+Example `1.x` code:
+
+[source,java]
+----------
+RegistrationResult result = rp.finishRegistration(/* ... */);
+Optional authenticatorName = result.getAttestationMetadata()
+ .flatMap(Attestation::getDeviceProperties)
+ .map(deviceProps -> deviceProps.get("description"));
+----------
+
+Example `2.0` code:
+
+[source,java]
+----------
+FidoMetadataService mds = /* ... */;
+RegistrationResult result = rp.finishRegistration(/* ... */);
+Optional authenticatorName = mds.findEntries(result)
+ .stream()
+ .findAny()
+ .flatMap(MetadataBLOBPayloadEntry::getMetadataStatement)
+ .flatMap(MetadataStatement::getDescription);
+----------
+
+
+== Replace `Attestation` with `MetadataBLOBPayloadEntry`
+
+This ties in with the previous step, and much of it will likely be done already.
+However if your front-end accesses and/or displays contents of an `Attestation` object,
+it will need to be updated to work with `MetadataBLOBPayloadEntry` or similar types instead.
+
+
+Example `1.x` code:
+
+[source,diff]
+----------
+ var registrationResult = fetch(/* ... */).then(response => response.json());
+-var authenticatorName = registrationResult.attestationMetadata?.deviceProperties?.description;
+----------
+
+Example `2.0` code:
+
+[source,diff]
+----------
+ var registrationResult = fetch(/* ... */).then(response => response.json());
++var authenticatorName = registrationResult.attestationMetadata?.metadataStatement?.description;
+----------
diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala
new file mode 100644
index 000000000..26100a559
--- /dev/null
+++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala
@@ -0,0 +1,44 @@
+package com.yubico.fido.metadata
+
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfter
+import org.scalatest.FunSpec
+import org.scalatest.Matchers
+import org.scalatest.tags.Network
+import org.scalatest.tags.Slow
+import org.scalatestplus.junit.JUnitRunner
+
+import java.util.Optional
+import scala.util.Success
+import scala.util.Try
+
+@Slow
+@Network
+@RunWith(classOf[JUnitRunner])
+class FidoMetadataDownloaderIntegrationTest
+ extends FunSpec
+ with Matchers
+ with BeforeAndAfter {
+
+ describe("FidoMetadataDownloader with default settings") {
+ val downloader =
+ FidoMetadataDownloader
+ .builder()
+ .expectLegalHeader(
+ "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/"
+ )
+ .useDefaultTrustRoot()
+ .useTrustRootCache(() => Optional.empty(), _ => {})
+ .useDefaultBlob()
+ .useBlobCache(() => Optional.empty(), _ => {})
+ .build()
+
+ it("downloads and verifies the root cert and BLOB successfully.") {
+ // This test requires the system property com.sun.security.enableCRLDP=true
+ val blob = Try(downloader.loadCachedBlob)
+ blob shouldBe a[Success[_]]
+ blob.get should not be null
+ }
+ }
+
+}
diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala
new file mode 100644
index 000000000..399c8ceb8
--- /dev/null
+++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataServiceIntegrationTest.scala
@@ -0,0 +1,242 @@
+package com.yubico.fido.metadata
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_EXTERNAL
+import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_NFC
+import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRED
+import com.yubico.fido.metadata.AttachmentHint.ATTACHMENT_HINT_WIRELESS
+import com.yubico.internal.util.CertificateParser
+import com.yubico.webauthn.data.AttestationObject
+import com.yubico.webauthn.test.RealExamples
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfter
+import org.scalatest.FunSpec
+import org.scalatest.Matchers
+import org.scalatest.tags.Network
+import org.scalatest.tags.Slow
+import org.scalatestplus.junit.JUnitRunner
+
+import java.io.IOException
+import java.security.cert.X509Certificate
+import java.util
+import java.util.Optional
+import scala.jdk.CollectionConverters.IteratorHasAsScala
+import scala.jdk.CollectionConverters.SetHasAsScala
+import scala.jdk.OptionConverters.RichOption
+import scala.jdk.OptionConverters.RichOptional
+import scala.util.Try
+
+@Slow
+@Network
+@RunWith(classOf[JUnitRunner])
+class FidoMetadataServiceIntegrationTest
+ extends FunSpec
+ with Matchers
+ with BeforeAndAfter {
+
+ describe("FidoMetadataService") {
+
+ describe("downloaded with default settings") {
+ val downloader = FidoMetadataDownloader
+ .builder()
+ .expectLegalHeader(
+ "Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/"
+ )
+ .useDefaultTrustRoot()
+ .useTrustRootCache(() => Optional.empty(), _ => {})
+ .useDefaultBlob()
+ .useBlobCache(() => Optional.empty(), _ => {})
+ .build()
+ val fidoMds =
+ Try(
+ FidoMetadataService
+ .builder()
+ .useBlob(downloader.loadCachedBlob())
+ .build()
+ )
+
+ val attachmentHintsUsb =
+ Set(ATTACHMENT_HINT_EXTERNAL, ATTACHMENT_HINT_WIRED)
+ val attachmentHintsNfc =
+ attachmentHintsUsb ++ Set(ATTACHMENT_HINT_WIRELESS, ATTACHMENT_HINT_NFC)
+
+ describe("by AAGUID") {
+ describe("correctly identifies") {}
+ }
+
+ describe("correctly identifies") {
+ def check(
+ expectedDescriptionRegex: String,
+ testData: RealExamples.Example,
+ attachmentHints: Set[AttachmentHint],
+ ): Unit = {
+
+ def getAttestationTrustPath(
+ attestationObject: AttestationObject
+ ): Option[util.List[X509Certificate]] = {
+ val x5cNode: JsonNode = getX5cArray(attestationObject)
+ if (x5cNode != null && x5cNode.isArray) {
+ val certs: util.List[X509Certificate] =
+ new util.ArrayList[X509Certificate](x5cNode.size)
+ for (binary <- x5cNode.elements().asScala) {
+ if (binary.isBinary)
+ try certs.add(
+ CertificateParser.parseDer(binary.binaryValue)
+ )
+ catch {
+ case e: IOException =>
+ throw new RuntimeException(
+ "binary.isBinary() was true but binary.binaryValue() failed",
+ e,
+ )
+ }
+ else
+ throw new IllegalArgumentException(
+ String.format(
+ "Each element of \"x5c\" property of attestation statement must be a binary value, was: %s",
+ binary.getNodeType,
+ )
+ )
+ }
+ Some(certs)
+ } else None
+ }
+
+ def getX5cArray(attestationObject: AttestationObject): JsonNode =
+ attestationObject.getAttestationStatement.get("x5c")
+
+ val entries = fidoMds.get
+ .findEntries(
+ getAttestationTrustPath(
+ testData.attestation.attestationObject
+ ).get,
+ Some(
+ new AAGUID(
+ testData.attestation.attestationObject.getAuthenticatorData.getAttestedCredentialData.get.getAaguid
+ )
+ ).toJava,
+ )
+ .asScala
+ entries should not be empty
+ val metadataStatements =
+ entries.flatMap(_.getMetadataStatement.toScala)
+
+ val descriptions =
+ metadataStatements.flatMap(_.getDescription.toScala).toSet
+ for { desc <- descriptions } {
+ desc should (fullyMatch regex expectedDescriptionRegex)
+ }
+
+ metadataStatements
+ .flatMap(_.getAttachmentHint.toScala.map(_.asScala))
+ .flatten
+ .toSet should equal(attachmentHints)
+ }
+
+ ignore("a YubiKey NEO.") { // TODO: Investigate why this fails
+ check("YubiKey NEO", RealExamples.YubiKeyNeo, attachmentHintsNfc)
+ }
+
+ it("a YubiKey 4.") {
+ check(
+ "YK4 Series Key by Yubico",
+ RealExamples.YubiKey4,
+ attachmentHintsUsb,
+ )
+ }
+
+ it("a YubiKey 5 NFC.") {
+ check(
+ "YubiKey 5 Series with NFC",
+ RealExamples.YubiKey5,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("an early YubiKey 5 NFC.") {
+ check(
+ "YubiKey 5 Series with NFC",
+ RealExamples.YubiKey5Nfc,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("a newer YubiKey 5 NFC.") {
+ check(
+ "YubiKey 5 Series with NFC",
+ RealExamples.YubiKey5NfcPost5cNfc,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("a YubiKey 5C NFC.") {
+ check(
+ "YubiKey 5 Series with NFC",
+ RealExamples.YubiKey5cNfc,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("a YubiKey 5 Nano.") {
+ check(
+ "YubiKey ?5 Series",
+ RealExamples.YubiKey5Nano,
+ attachmentHintsUsb,
+ )
+ }
+
+ it("a YubiKey 5Ci.") {
+ check("YubiKey 5Ci", RealExamples.YubiKey5Ci, attachmentHintsUsb)
+ }
+
+ ignore("a Security Key by Yubico.") { // TODO: Investigate why this fails
+ check(
+ "Security Key by Yubico",
+ RealExamples.SecurityKey,
+ attachmentHintsUsb,
+ )
+ }
+
+ it("a Security Key 2 by Yubico.") {
+ check(
+ "Security Key by Yubico",
+ RealExamples.SecurityKey2,
+ attachmentHintsUsb,
+ )
+ }
+
+ ignore("a Security Key NFC by Yubico.") { // TODO: Investigate why this fails
+ check(
+ "Security Key NFC by Yubico",
+ RealExamples.SecurityKeyNfc,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("a YubiKey 5.4 NFC FIPS.") {
+ check(
+ "YubiKey 5 FIPS Series with NFC",
+ RealExamples.YubikeyFips5Nfc,
+ attachmentHintsNfc,
+ )
+ }
+
+ it("a YubiKey 5.4 Ci FIPS.") {
+ check(
+ "YubiKey 5Ci FIPS",
+ RealExamples.Yubikey5ciFips,
+ attachmentHintsUsb,
+ )
+ }
+
+ it("a YubiKey Bio.") {
+ check(
+ "YubiKey Bio Series",
+ RealExamples.YubikeyBio_5_5_5,
+ attachmentHintsUsb,
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java
new file mode 100644
index 000000000..3ef4524cf
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAGUID.java
@@ -0,0 +1,126 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.yubico.internal.util.ExceptionUtil;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.exception.HexException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.Value;
+
+/**
+ * Some authenticators have an AAGUID, which is a 128-bit identifier that indicates the type (e.g.
+ * make and model) of the authenticator. The AAGUID MUST be chosen by the manufacturer to be
+ * identical across all substantially identical authenticators made by that manufacturer, and
+ * different (with probability 1-2-128 or greater) from the AAGUIDs of all other types of
+ * authenticators.
+ *
+ * The AAGUID is represented as a string (e.g. "7a98c250-6808-11cf-b73b-00aa00b677a7") consisting
+ * of 5 hex strings separated by a dash ("-"), see [RFC4122].
+ *
+ * @see FIDO
+ * Metadata Statement §3.1. Authenticator Attestation GUID (AAGUID) typedef
+ * @see RFC 4122: A Universally Unique IDentifier
+ * (UUID) URN Namespace
+ */
+@Value
+@Getter(AccessLevel.NONE)
+@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true)
+public class AAGUID {
+
+ private static final Pattern AAGUID_PATTERN =
+ Pattern.compile(
+ "^([0-9a-fA-F]{8})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{12})$");
+
+ private static final ByteArray ZERO =
+ new ByteArray(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
+
+ ByteArray value;
+
+ /**
+ * Construct an AAGUID from its raw binary representation.
+ *
+ *
This is the inverse of {@link #asBytes()}.
+ *
+ * @param value a {@link ByteArray} of length exactly 16.
+ */
+ public AAGUID(ByteArray value) {
+ ExceptionUtil.assure(
+ value.size() == 16,
+ "AAGUID as bytes must be exactly 16 bytes long, was %d: %s",
+ value.size(),
+ value);
+ this.value = value;
+ }
+
+ /**
+ * The 16-byte binary representation of this AAGUID, for example
+ * 7a98c250680811cfb73b00aa00b677a7
when hex-encoded.
+ *
+ *
This is the inverse of {@link #AAGUID(ByteArray)}.
+ */
+ public ByteArray asBytes() {
+ return value;
+ }
+
+ /**
+ * The 32-character hexadecimal representation of this AAGUID, for example
+ * "7a98c250680811cfb73b00aa00b677a7"
.
+ */
+ public String asHexString() {
+ return value.getHex();
+ }
+
+ /**
+ * The 36-character string representation of this AAGUID, for example
+ * "7a98c250-6808-11cf-b73b-00aa00b677a7"
.
+ */
+ @JsonValue
+ @ToString.Include
+ public String asGuidString() {
+ final String hex = value.getHex();
+ return String.format(
+ "%s-%s-%s-%s-%s",
+ hex.substring(0, 8),
+ hex.substring(8, 8 + 4),
+ hex.substring(8 + 4, 8 + 4 + 4),
+ hex.substring(8 + 4 + 4, 8 + 4 + 4 + 4),
+ hex.substring(8 + 4 + 4 + 4, 8 + 4 + 4 + 4 + 12));
+ }
+
+ /**
+ * true
if and only if this {@link AAGUID} consists of all zeroes. This typically
+ * indicates that an authenticator has no AAGUID, or that the AAGUID has been redacted.
+ */
+ public boolean isZero() {
+ return ZERO.equals(value);
+ }
+
+ private static ByteArray parse(String value) {
+ Matcher matcher = AAGUID_PATTERN.matcher(value);
+ if (matcher.find()) {
+ try {
+ return ByteArray.fromHex(matcher.group(1))
+ .concat(ByteArray.fromHex(matcher.group(2)))
+ .concat(ByteArray.fromHex(matcher.group(3)))
+ .concat(ByteArray.fromHex(matcher.group(4)))
+ .concat(ByteArray.fromHex(matcher.group(5)));
+ } catch (HexException e) {
+ throw new RuntimeException(
+ "This exception should be impossible, please file a bug report.", e);
+ }
+ } else {
+ throw new IllegalArgumentException("Value does not match AAGUID pattern: " + value);
+ }
+ }
+
+ @JsonCreator
+ private static AAGUID fromString(String aaguid) {
+ return new AAGUID(parse(aaguid));
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java
new file mode 100644
index 000000000..686fd6eb2
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AAID.java
@@ -0,0 +1,73 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.regex.Pattern;
+import lombok.Value;
+
+/**
+ * Each UAF authenticator MUST have an AAID to identify UAF enabled authenticator models globally.
+ * The AAID MUST uniquely identify a specific authenticator model within the range of all
+ * UAF-enabled authenticator models made by all authenticator vendors, where authenticators of a
+ * specific model must share identical security characteristics within the model (see Security
+ * Considerations).
+ *
+ *
The AAID is a string with format "V#M"
, where
+ *
+ *
+ * #
is a separator
+ * V
indicates the authenticator Vendor Code. This code consists of 4 hexadecimal
+ * digits.
+ * M
indicates the authenticator Model Code. This code consists of 4 hexadecimal
+ * digits.
+ *
+ *
+ * @see FIDO
+ * UAF Protocol Specification §3.1.4 Authenticator Attestation ID (AAID) typedef
+ */
+@Value
+public class AAID {
+
+ private static final Pattern AAID_PATTERN = Pattern.compile("^[0-9a-fA-F]{4}#[0-9a-fA-F]{4}$");
+
+ /**
+ * The underlying string value of this AAID.
+ *
+ * The AAID is a string with format "V#M"
, where
+ *
+ *
+ * #
is a separator
+ * V
indicates the authenticator Vendor Code. This code consists of 4
+ * hexadecimal digits.
+ * M
indicates the authenticator Model Code. This code consists of 4
+ * hexadecimal digits.
+ *
+ *
+ * @see Authenticator
+ * Attestation ID (AAID) typedef
+ */
+ @JsonValue String value;
+
+ /**
+ * Construct an {@link AAID} from its String representation.
+ *
+ * This is the inverse of {@link #getValue()}.
+ *
+ * @param value a {@link String} conforming to the rules specified in the {@link AAID} type.
+ */
+ @JsonCreator
+ public AAID(String value) {
+ this.value = validate(value);
+ }
+
+ private String validate(String value) {
+ if (AAID_PATTERN.matcher(value).matches()) {
+ return value;
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Value does not satisfy AAID format: %s", value));
+ }
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java
new file mode 100644
index 000000000..8dd82dc67
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AlternativeDescriptions.java
@@ -0,0 +1,49 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Map;
+import java.util.Optional;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Value;
+
+/**
+ * See:
+ * https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#alternativedescriptions-dictionary
+ *
+ * @see FIDO
+ * Metadata Statement §3.11. AlternativeDescriptions dictionary
+ */
+@Value
+@AllArgsConstructor(onConstructor_ = {@JsonCreator})
+public class AlternativeDescriptions {
+
+ @JsonValue
+ @Getter(AccessLevel.NONE)
+ Map values;
+
+ /**
+ * Get a map entry in accordance with the rules defined in AlternativeDescriptions
+ * dictionary.
+ *
+ * @see AlternativeDescriptions
+ * dictionary.
+ */
+ public Optional get(String languageCode) {
+ if (values.containsKey(languageCode)) {
+ return Optional.of(values.get(languageCode));
+ } else {
+ final String[] splits = languageCode.split("-");
+ if (splits.length > 1 && values.containsKey(splits[0])) {
+ return Optional.of(values.get(splits[0]));
+ } else {
+ return Optional.empty();
+ }
+ }
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java
new file mode 100644
index 000000000..6cbd675e7
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AttachmentHint.java
@@ -0,0 +1,142 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The ATTACHMENT_HINT constants are flags in a bit field represented as a 32 bit long. They
+ * describe the method FIDO authenticators use to communicate with the FIDO User Device. These
+ * constants are reported and queried through the UAF Discovery APIs [UAFAppAPIAndTransport], and
+ * used to form Authenticator policies in UAF protocol messages. Because the connection state and
+ * topology of an authenticator may be transient, these values are only hints that can be used by
+ * server-supplied policy to guide the user experience, e.g. to prefer a device that is connected
+ * and ready for authenticating or confirming a low-value transaction, rather than one that is more
+ * secure but requires more user effort. Each constant has a case-sensitive string representation
+ * (in quotes), which is used in the authoritative metadata for FIDO authenticators. Note
+ *
+ * These flags are not a mandatory part of authenticator metadata and, when present, only
+ * indicate possible states that may be reported during authenticator discovery.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+public enum AttachmentHint {
+
+ /**
+ * This flag MAY be set to indicate that the authenticator is permanently attached to the FIDO
+ * User Device.
+ *
+ *
A device such as a smartphone may have authenticator functionality that is able to be used
+ * both locally and remotely. In such a case, the FIDO client MUST filter and exclusively report
+ * only the relevant bit during Discovery and when performing policy matching.
+ *
+ *
This flag cannot be combined with any other {@link AttachmentHint} flags.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_INTERNAL(0x0001, "internal"),
+
+ /**
+ * This flag MAY be set to indicate, for a hardware-based authenticator, that it is removable or
+ * remote from the FIDO User Device.
+ *
+ *
A device such as a smartphone may have authenticator functionality that is able to be used
+ * both locally and remotely. In such a case, the FIDO UAF Client MUST filter and exclusively
+ * report only the relevant bit during discovery and when performing policy matching. This flag
+ * MUST be combined with one or more other {@link AttachmentHint} flag(s).
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_EXTERNAL(0x0002, "external"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator currently has an exclusive
+ * wired connection, e.g. through USB, Firewire or similar, to the FIDO User Device.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_WIRED(0x0004, "wired"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator communicates with the FIDO User
+ * Device through a personal area or otherwise non-routed wireless protocol, such as Bluetooth or
+ * NFC.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_WIRELESS(0x0008, "wireless"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator is able to communicate by NFC
+ * to the FIDO User Device. As part of authenticator metadata, or when reporting characteristics
+ * through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS} flag SHOULD also
+ * be set as well.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_NFC(0x0010, "nfc"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator is able to communicate using
+ * Bluetooth with the FIDO User Device. As part of authenticator metadata, or when reporting
+ * characteristics through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS}
+ * flag SHOULD also be set.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_BLUETOOTH(0x0020, "bluetooth"),
+
+ /**
+ * This flag MAY be set to indicate that the authenticator is connected to the FIDO User Device
+ * over a non-exclusive network (e.g. over a TCP/IP LAN or WAN, as opposed to a PAN or
+ * point-to-point connection).
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_NETWORK(0x0040, "network"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator is in a "ready" state. This
+ * flag is set by the ASM at its discretion.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_READY(0x0080, "ready"),
+
+ /**
+ * This flag MAY be set to indicate that an external authenticator is able to communicate using
+ * WiFi Direct with the FIDO User Device. As part of authenticator metadata and when reporting
+ * characteristics through discovery, if this flag is set, the {@link #ATTACHMENT_HINT_WIRELESS}
+ * flag SHOULD also be set.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.4 Authenticator Attachment Hints
+ */
+ ATTACHMENT_HINT_WIFI_DIRECT(0x0100, "wifi_direct");
+
+ private final int value;
+
+ @JsonValue private final String name;
+
+ AttachmentHint(int value, String name) {
+ this.value = value;
+ this.name = name;
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java
new file mode 100644
index 000000000..166645ea9
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticationAlgorithm.java
@@ -0,0 +1,152 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The ALG_SIGN
constants are 16 bit long integers indicating the specific signature
+ * algorithm and encoding.
+ *
+ *
Each constant has a case-sensitive string representation (in quotes), which is used in the
+ * authoritative metadata for FIDO authenticators.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+public enum AuthenticationAlgorithm {
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW(0x0001, "secp256r1_ecdsa_sha256_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP256R1_ECDSA_SHA256_DER(0x0002, "secp256r1_ecdsa_sha256_der"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PSS_SHA256_RAW(0x0003, "rsassa_pss_sha256_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PSS_SHA256_DER(0x0004, "rsassa_pss_sha256_der"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW(0x0005, "secp256k1_ecdsa_sha256_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP256K1_ECDSA_SHA256_DER(0x0006, "secp256k1_ecdsa_sha256_der"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW(0x0008, "rsa_emsa_pkcs1_sha256_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER(0x0009, "rsa_emsa_pkcs1_sha256_der"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PSS_SHA384_RAW(0x000A, "rsassa_pss_sha384_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PSS_SHA512_RAW(0x000B, "rsassa_pss_sha512_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW(0x000C, "rsassa_pkcsv15_sha256_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW(0x000D, "rsassa_pkcsv15_sha384_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW(0x000E, "rsassa_pkcsv15_sha512_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW(0x000F, "rsassa_pkcsv15_sha1_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW(0x0010, "secp384r1_ecdsa_sha384_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW(0x0011, "secp521r1_ecdsa_sha512_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_ED25519_EDDSA_SHA512_RAW(0x0012, "ed25519_eddsa_sha512_raw"),
+
+ /**
+ * @see FIDO
+ * Registry of Predefined Values §3.6.1 Authentication Algorithms
+ */
+ ALG_SIGN_ED448_EDDSA_SHA512_RAW(0x0013, "ed448_eddsa_sha512_raw");
+
+ private final int value;
+
+ @JsonValue private final String name;
+
+ AuthenticationAlgorithm(int value, String name) {
+ this.value = value;
+ this.name = name;
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java
new file mode 100644
index 000000000..6612fbfdd
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorAttestationType.java
@@ -0,0 +1,92 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * The ATTESTATION constants are 16 bit long integers indicating the specific attestation that
+ * authenticator supports.
+ *
+ *
Each constant has a case-sensitive string representation (in quotes), which is used in the
+ * authoritative metadata for FIDO authenticators. *
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.7 Authenticator Attestation Types
+ */
+public enum AuthenticatorAttestationType {
+
+ /**
+ * Indicates full basic attestation, based on an attestation private key shared among a class of
+ * authenticators (e.g. same model). Authenticators must provide its attestation signature during
+ * the registration process for the same reason. The attestation trust anchor is shared with FIDO
+ * Servers out of band (as part of the Metadata). This sharing process should be done according to
+ * [FIDOMetadataService].
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.7 Authenticator Attestation Types
+ */
+ ATTESTATION_BASIC_FULL(0x3E07, "basic_full"),
+
+ /**
+ * Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed
+ * using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the
+ * attestation object. As a consequence it does not provide a cryptographic proof of the security
+ * characteristics. But it is the best thing we can do if the authenticator is not able to have an
+ * attestation private key.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.7 Authenticator Attestation Types
+ */
+ ATTESTATION_BASIC_SURROGATE(0x3E08, "basic_surrogate"),
+
+ /**
+ * Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm].
+ * Support for this attestation type is optional at this time. It might be required by FIDO
+ * Certification.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.7 Authenticator Attestation Types
+ */
+ ATTESTATION_ECDAA(0x3E09, "ecdaa"),
+
+ /**
+ * Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll].
+ * Support for this attestation type is optional at this time. It might be required by FIDO
+ * Certification.
+ *
+ * @see FIDO
+ * Registry of Predefined Values §3.7 Authenticator Attestation Types
+ */
+ ATTESTATION_ATTCA(0x3E0A, "attca"),
+
+ /**
+ * In this case, the authenticator uses an Anonymization CA which dynamically generates
+ * per-credential attestation certificates such that the attestation statements presented to
+ * Relying Parties do not provide uniquely identifiable information, e.g., that might be used for
+ * tracking purposes. The applicable [WebAuthn]
+ * attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android
+ * Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application
+ * Attestation "apple-appattest".
+ */
+ ATTESTATION_ANONCA(0x3E0C, "anonca"),
+
+ /** Indicates absence of attestation. */
+ ATTESTATION_NONE(0x3E0B, "none");
+
+ private final int value;
+
+ @JsonValue private final String name;
+
+ AuthenticatorAttestationType(int value, String name) {
+ this.value = value;
+ this.name = name;
+ }
+}
diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java
new file mode 100644
index 000000000..78cefbd64
--- /dev/null
+++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/AuthenticatorGetInfo.java
@@ -0,0 +1,381 @@
+package com.yubico.fido.metadata;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.PublicKeyCredentialParameters;
+import com.yubico.webauthn.extension.uvm.UserVerificationMethod;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+import lombok.extern.jackson.Jacksonized;
+
+/**
+ * This dictionary describes supported versions, extensions, AAGUID of the device and its
+ * capabilities.
+ *
+ *
See: Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ *
+ * @see FIDO
+ * Metadata Statement §3.12. AuthenticatorGetInfo dictionary
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+@Value
+@Builder(toBuilder = true)
+@Jacksonized
+@JsonIgnoreProperties({
+ "maxAuthenticatorConfigLength",
+ "defaultCredProtect"
+}) // Present in example but not defined
+public class AuthenticatorGetInfo {
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ @NonNull Set versions;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Set extensions;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ AAGUID aaguid;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ SupportedCtapOptions options;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxMsgSize;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Set pinUvAuthProtocols;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxCredentialCountInList;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxCredentialIdLength;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Set transports;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ List algorithms;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxSerializedLargeBlobArray;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Boolean forcePINChange;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer minPINLength;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer firmwareVersion;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxCredBlobLength;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer maxRPIDsForSetMinPINLength;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ Integer preferredPlatformUvAttempts;
+
+ @JsonDeserialize(using = SetFromIntJsonDeserializer.class)
+ @JsonSerialize(contentUsing = IntFromSetJsonSerializer.class)
+ Set uvModality;
+
+ Map certifications;
+ Integer remainingDiscoverableCredentials;
+ Set vendorPrototypeConfigCommands;
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional> getExtensions() {
+ return Optional.ofNullable(extensions);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getAaguid() {
+ return Optional.ofNullable(aaguid);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getOptions() {
+ return Optional.ofNullable(options);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxMsgSize() {
+ return Optional.ofNullable(maxMsgSize);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional> getPinUvAuthProtocols() {
+ return Optional.ofNullable(pinUvAuthProtocols);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxCredentialCountInList() {
+ return Optional.ofNullable(maxCredentialCountInList);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxCredentialIdLength() {
+ return Optional.ofNullable(maxCredentialIdLength);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional> getTransports() {
+ return Optional.ofNullable(transports);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional> getAlgorithms() {
+ return Optional.ofNullable(algorithms);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxSerializedLargeBlobArray() {
+ return Optional.ofNullable(maxSerializedLargeBlobArray);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getForcePINChange() {
+ return Optional.ofNullable(forcePINChange);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMinPINLength() {
+ return Optional.ofNullable(minPINLength);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getFirmwareVersion() {
+ return Optional.ofNullable(firmwareVersion);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxCredBlobLength() {
+ return Optional.ofNullable(maxCredBlobLength);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getMaxRPIDsForSetMinPINLength() {
+ return Optional.ofNullable(maxRPIDsForSetMinPINLength);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional getPreferredPlatformUvAttempts() {
+ return Optional.ofNullable(preferredPlatformUvAttempts);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional> getUvModality() {
+ return Optional.ofNullable(uvModality);
+ }
+
+ /**
+ * @see Client
+ * to Authenticator Protocol (CTAP) §6.4. authenticatorGetInfo (0x04)
+ */
+ public Optional