webauthn-server-attestation

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

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 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() or refreshBlob() 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, while refreshBlob() 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/or refreshBlob() 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

Dependency configuration

Maven:

<dependency>
  <groupId>com.yubico</groupId>
  <artifactId>webauthn-server-attestation</artifactId>
  <version>2.5.1</version>
  <scope>compile</scope>
</dependency>

Gradle:

implementation("com.yubico:webauthn-server-attestation:2.5.1")

Semantic versioning

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.

Getting started

Using this module consists of 5 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() and refreshBlob() methods read and write caches. FidoMetadataService, on the other hand, is thread safe, and FidoMetadataDownloader instances can be reused for subsequent loadCachedBlob() and refreshBlob() 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();
    
  2. Set the FidoMetadataService as the attestationTrustSource on your 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.

    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 RegistrationResult 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...
    }
    
  4. If needed, use the findEntries methods of FidoMetadataService to retrieve additional authenticator metadata for new registrations.

    RelyingParty rp = /* ... */;
    RegistrationResult result = rp.finishRegistration(/* ... */);
    
    Set<MetadataBLOBPayloadEntry> metadata = mds.findEntries(result);
    

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 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:

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 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 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 the loadCachedBlob() method is executed it checks whether a new BLOB should be downloaded. The refreshBlob() 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 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 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 AuthenticatorStatus section states that "FIDO Servers MUST silently ignore all unknown AuthenticatorStatus values". Thus any unknown status values 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.