An optional module which extends webauthn-server-core
with a trust root source for verifying
attestation statements,
by interfacing with the FIDO Metadata Service.
Table of contents
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
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 itsloadCachedBlob()
orrefreshBlob()
method is executed. As the names suggest,loadCachedBlob()
downloads a new BLOB only if the cache is empty or the cached BLOB is invalid or out of date, whilerefreshBlob()
always downloads a new BLOB and falls back to the cached BLOB only when the new BLOB is invalid in some way.FidoMetadataService
will never re-download a new BLOB once instantiated.You should use some external scheduling mechanism to re-run
loadCachedBlob()
and/orrefreshBlob()
periodically and rebuild newFidoMetadataService
instances with the updated metadata contents. You can do this with minimal disruption since theFidoMetadataService
andRelyingParty
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)
onRelyingParty
, but this will not affect any credentials already registered.
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.
See the migration guide.
Maven:
<dependency> <groupId>com.yubico</groupId> <artifactId>webauthn-server-attestation</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency>
Gradle:
implementation("com.yubico:webauthn-server-attestation:2.5.0")
This library uses semantic versioning.
The public API consists of all public classes, methods and fields in the com.yubico.fido.metadata
package and its subpackages,
i.e., everything covered by the
Javadoc.
Package-private classes and methods are NOT part of the public API.
The com.yubico:yubico-util
module is NOT part of the public API.
Breaking changes to these will NOT be reflected in version numbers.
Using this module consists of 5 major steps:
-
Create a
FidoMetadataDownloader
instance to download and cache metadata BLOBs, and aFidoMetadataService
instance to make use of the downloaded BLOB. See the JavaDoc for these classes for details on how to construct them.WarningUnlike other classes in this module and the core library,
FidoMetadataDownloader
is NOT THREAD SAFE since itsloadCachedBlob()
andrefreshBlob()
methods read and write caches.FidoMetadataService
, on the other hand, is thread safe, andFidoMetadataDownloader
instances can be reused for subsequentloadCachedBlob()
andrefreshBlob()
calls as long as only one call executes at a time.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")) .verifyDownloadsOnly(true) // Recommended, otherwise cache may expire if BLOB certificate expires // See: https://github.com/Yubico/java-webauthn-server/issues/294 .build(); FidoMetadataService mds = FidoMetadataService.builder() .useBlob(downloader.loadCachedBlob()) .build();
-
Set the
FidoMetadataService
as theattestationTrustSource
on yourRelyingParty
instance, and setattestationConveyancePreference(AttestationConveyancePreference.DIRECT)
onRelyingParty
to request an attestation statement for new registrations. Optionally also set.allowUntrustedAttestation(false)
onRelyingParty
to require trusted attestation for new registrations.RelyingParty rp = RelyingParty.builder() .identity(/* ... */) .credentialRepository(/* ... */) .attestationTrustSource(mds) .attestationConveyancePreference(AttestationConveyancePreference.DIRECT) .allowUntrustedAttestation(true) // Optional step: set to true (default) or false .build();
-
After performing registrations, inspect the
isAttestationTrusted()
result inRegistrationResult
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.RelyingParty rp = /* ... */; RegistrationResult result = rp.finishRegistration(/* ... */); if (result.isAttestationTrusted()) { // Do something... } else { // Do something else... }
-
If needed, use the
findEntries
methods ofFidoMetadataService
to retrieve additional authenticator metadata for new registrations.RelyingParty rp = /* ... */; RegistrationResult result = rp.finishRegistration(/* ... */); Set<MetadataBLOBPayloadEntry> metadata = mds.findEntries(result);
-
If you use the SUN provider for the
PKIX
certificate path validation algorithm, which many deployments do by default: set thecom.sun.security.enableCRLDP
system property totrue
. This is required for the SUNPKIX
provider to support the CRL Distribution Points extension, which is needed 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 Java PKI Programmers Guide for details.This step may not be necessary if you use a different provider for the
PKIX
certificate path validation algorithm.
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
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:
+----------+
| 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
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
FidoMetadataService
.
The filters are predicate functions;
each metadata entry will be included in the data source 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
|
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)
.
The FIDO Metadata Service specification defines 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 theloadCachedBlob()
method is executed it checks whether a new BLOB should be downloaded. TherefreshBlob()
method always attempts to download a new BLOB when executed, but also does not trigger re-downloads automatically.Whenever a newly downloaded BLOB is valid, has a correct signature, and has a
no
field greater than the cached BLOB (if any), 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()
or.refreshBlob()
.
-
-
Metadata entries are not stored or cached individually, instead the BLOB is cached as a whole. In processing rules step 8, neither
FidoMetadataDownloader
norFidoMetadataService
performs any comparison between versions of a metadata entry. Policy for ignoring metadata entries can be configured via the filter settings inFidoMetadataService
. See above for details.
There are also some other requirements throughout the spec, which may not be obvious:
-
The AuthenticatorStatus section states that "The Relying party MUST reject the Metadata Statement if the
authenticatorVersion
has not increased" in anUPDATE_AVAILABLE
status report. Thus,FidoMetadataService
silently ignores anyMetadataBLOBPayloadEntry
whosemetadataStatement.authenticatorVersion
is present and not greater than or equal to theauthenticatorVersion
in the respective status report. Again, no comparison is made between metadata entries from different BLOB versions. -
The AuthenticatorStatus section states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values". Thus any unknown status values will be parsed as
AuthenticatorStatus.UNKNOWN
, andMetadataBLOBPayloadEntry
will silently ignore any status report with that status.
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.