Skip to content

Commit 61f9300

Browse files
committed
Add option to ignore MDS BLOB signature when loading from cache or explicit config
1 parent b46bf8c commit 61f9300

File tree

4 files changed

+270
-30
lines changed

4 files changed

+270
-30
lines changed

NEWS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
== Version 2.5.0 (unreleased) ==
22

3+
`webauthn-server-core`:
4+
35
New features:
46

57
* Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult`
68
as a shortcut for accessing the UV flag in authenticator data.
79
* Updated README and JavaDoc to use the "passkey" term and provide more guidance
810
around passkey use cases.
911

12+
`webauthn-server-attestation`:
13+
14+
New features:
15+
16+
* Added option `verifyDownloadsOnly(boolean)` to `FidoMetadataDownloader`. When
17+
set to `true`, the BLOB signature will not be verified when loading a BLOB
18+
from cache or when explicitly given. Default setting is `false`, which
19+
preserves the previous behaviour.
20+
1021

1122
== Version 2.4.1 ==
1223

webauthn-server-attestation/README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ FidoMetadataDownloader downloader = FidoMetadataDownloader.builder()
154154
.useTrustRootCacheFile(new File("/var/cache/webauthn-server/fido-mds-trust-root.bin"))
155155
.useDefaultBlob()
156156
.useBlobCacheFile(new File("/var/cache/webauthn-server/fido-mds-blob.bin"))
157+
.verifyDownloadsOnly(true) // Recommended, otherwise cache may expire if BLOB certificate expires
158+
// See: https://github.com/Yubico/java-webauthn-server/issues/294
157159
.build();
158160
159161
FidoMetadataService mds = FidoMetadataService.builder()

webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import lombok.AllArgsConstructor;
8888
import lombok.NonNull;
8989
import lombok.RequiredArgsConstructor;
90+
import lombok.Value;
9091
import lombok.extern.slf4j.Slf4j;
9192

9293
/**
@@ -119,6 +120,7 @@ public final class FidoMetadataDownloader {
119120
private final CertStore certStore;
120121
@NonNull private final Clock clock;
121122
private final KeyStore httpsTrustStore;
123+
private final boolean verifyDownloadsOnly;
122124

123125
/**
124126
* Begin configuring a {@link FidoMetadataDownloader} instance. See the {@link
@@ -148,6 +150,7 @@ public static class FidoMetadataDownloaderBuilder {
148150
private CertStore certStore = null;
149151
@NonNull private Clock clock = Clock.systemUTC();
150152
private KeyStore httpsTrustStore = null;
153+
private boolean verifyDownloadsOnly = false;
151154

152155
public FidoMetadataDownloader build() {
153156
return new FidoMetadataDownloader(
@@ -165,7 +168,8 @@ public FidoMetadataDownloader build() {
165168
blobCacheConsumer,
166169
certStore,
167170
clock,
168-
httpsTrustStore);
171+
httpsTrustStore,
172+
verifyDownloadsOnly);
169173
}
170174

171175
/**
@@ -611,6 +615,26 @@ public FidoMetadataDownloaderBuilder trustHttpsCerts(@NonNull X509Certificate...
611615

612616
return this;
613617
}
618+
619+
/**
620+
* If set to <code>true</code>, the BLOB signature will not be verified when loading the BLOB
621+
* from cache or when explicitly set via {@link Step4#useBlob(String)}. This means that if a
622+
* BLOB was successfully verified once and written to cache, that cached value will be
623+
* implicitly trusted when loaded in the future.
624+
*
625+
* <p>If set to <code>false</code>, the BLOB signature will always be verified no matter where
626+
* the BLOB came from. This means that a cached BLOB may become invalid if the BLOB certificate
627+
* expires, even if the BLOB was successfully verified at the time it was downloaded.
628+
*
629+
* <p>The default setting is <code>false</code>.
630+
*
631+
* @param verifyDownloadsOnly <code>true</code> if the BLOB signature should be ignored when
632+
* loading the BLOB from cache or when explicitly set via {@link Step4#useBlob(String)}.
633+
*/
634+
public FidoMetadataDownloaderBuilder verifyDownloadsOnly(final boolean verifyDownloadsOnly) {
635+
this.verifyDownloadsOnly = verifyDownloadsOnly;
636+
return this;
637+
}
614638
}
615639

616640
/**
@@ -960,7 +984,7 @@ private Optional<MetadataBLOB> loadExplicitBlobOnly(X509Certificate trustRootCer
960984
FidoMetadataDownloaderException {
961985
if (blobJwt != null) {
962986
return Optional.of(
963-
parseAndVerifyBlob(
987+
parseAndMaybeVerifyBlob(
964988
new ByteArray(blobJwt.getBytes(StandardCharsets.UTF_8)), trustRootCertificate));
965989

966990
} else {
@@ -987,7 +1011,7 @@ private Optional<MetadataBLOB> loadCachedBlobOnly(X509Certificate trustRootCerti
9871011
return cachedContents.map(
9881012
cached -> {
9891013
try {
990-
return parseAndVerifyBlob(cached, trustRootCertificate);
1014+
return parseAndMaybeVerifyBlob(cached, trustRootCertificate);
9911015
} catch (Exception e) {
9921016
log.warn("Failed to read or parse cached BLOB.", e);
9931017
return null;
@@ -1044,18 +1068,27 @@ private MetadataBLOB parseAndVerifyBlob(ByteArray jwt, X509Certificate trustRoot
10441068
InvalidKeyException,
10451069
Base64UrlException,
10461070
FidoMetadataDownloaderException {
1047-
Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\.");
1048-
final ByteArray header = ByteArray.fromBase64Url(s.next());
1049-
final ByteArray payload = ByteArray.fromBase64Url(s.next());
1050-
final ByteArray signature = ByteArray.fromBase64Url(s.next());
1051-
return verifyBlob(header, payload, signature, trustRootCertificate);
1071+
return verifyBlob(parseBlob(jwt), trustRootCertificate);
10521072
}
10531073

1054-
private MetadataBLOB verifyBlob(
1055-
ByteArray jwtHeader,
1056-
ByteArray jwtPayload,
1057-
ByteArray jwtSignature,
1058-
X509Certificate trustRootCertificate)
1074+
private MetadataBLOB parseAndMaybeVerifyBlob(ByteArray jwt, X509Certificate trustRootCertificate)
1075+
throws CertPathValidatorException,
1076+
InvalidAlgorithmParameterException,
1077+
CertificateException,
1078+
IOException,
1079+
NoSuchAlgorithmException,
1080+
SignatureException,
1081+
InvalidKeyException,
1082+
Base64UrlException,
1083+
FidoMetadataDownloaderException {
1084+
if (verifyDownloadsOnly) {
1085+
return parseBlob(jwt).blob;
1086+
} else {
1087+
return verifyBlob(parseBlob(jwt), trustRootCertificate);
1088+
}
1089+
}
1090+
1091+
private MetadataBLOB verifyBlob(ParseResult parseResult, X509Certificate trustRootCertificate)
10591092
throws IOException,
10601093
CertificateException,
10611094
NoSuchAlgorithmException,
@@ -1064,12 +1097,7 @@ private MetadataBLOB verifyBlob(
10641097
CertPathValidatorException,
10651098
InvalidAlgorithmParameterException,
10661099
FidoMetadataDownloaderException {
1067-
final ObjectMapper headerJsonMapper =
1068-
com.yubico.internal.util.JacksonCodecs.json()
1069-
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
1070-
.setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS);
1071-
final MetadataBLOBHeader header =
1072-
headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class);
1100+
final MetadataBLOBHeader header = parseResult.blob.getHeader();
10731101

10741102
final List<X509Certificate> certChain;
10751103
if (header.getX5u().isPresent()) {
@@ -1117,9 +1145,9 @@ private MetadataBLOB verifyBlob(
11171145

11181146
signature.initVerify(leafCert.getPublicKey());
11191147
signature.update(
1120-
(jwtHeader.getBase64Url() + "." + jwtPayload.getBase64Url())
1148+
(parseResult.jwtHeader.getBase64Url() + "." + parseResult.jwtPayload.getBase64Url())
11211149
.getBytes(StandardCharsets.UTF_8));
1122-
if (!signature.verify(jwtSignature.getBytes())) {
1150+
if (!signature.verify(parseResult.jwtSignature.getBytes())) {
11231151
throw new FidoMetadataDownloaderException(Reason.BAD_SIGNATURE);
11241152
}
11251153

@@ -1134,10 +1162,28 @@ private MetadataBLOB verifyBlob(
11341162
pathParams.setDate(Date.from(clock.instant()));
11351163
cpv.validate(blobCertPath, pathParams);
11361164

1137-
return new MetadataBLOB(
1138-
header,
1139-
JacksonCodecs.jsonWithDefaultEnums()
1140-
.readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class));
1165+
return parseResult.blob;
1166+
}
1167+
1168+
private static ParseResult parseBlob(ByteArray jwt) throws IOException, Base64UrlException {
1169+
Scanner s = new Scanner(new ByteArrayInputStream(jwt.getBytes())).useDelimiter("\\.");
1170+
final ByteArray jwtHeader = ByteArray.fromBase64Url(s.next());
1171+
final ByteArray jwtPayload = ByteArray.fromBase64Url(s.next());
1172+
final ByteArray jwtSignature = ByteArray.fromBase64Url(s.next());
1173+
1174+
final ObjectMapper headerJsonMapper =
1175+
com.yubico.internal.util.JacksonCodecs.json()
1176+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
1177+
.setBase64Variant(Base64Variants.MIME_NO_LINEFEEDS);
1178+
1179+
return new ParseResult(
1180+
new MetadataBLOB(
1181+
headerJsonMapper.readValue(jwtHeader.getBytes(), MetadataBLOBHeader.class),
1182+
JacksonCodecs.jsonWithDefaultEnums()
1183+
.readValue(jwtPayload.getBytes(), MetadataBLOBPayload.class)),
1184+
jwtHeader,
1185+
jwtPayload,
1186+
jwtSignature);
11411187
}
11421188

11431189
private static ByteArray readAll(InputStream is) throws IOException {
@@ -1158,4 +1204,12 @@ private static ByteArray verifyHash(ByteArray contents, Set<ByteArray> acceptedC
11581204
return null;
11591205
}
11601206
}
1207+
1208+
@Value
1209+
private static class ParseResult {
1210+
private MetadataBLOB blob;
1211+
private ByteArray jwtHeader;
1212+
private ByteArray jwtPayload;
1213+
private ByteArray jwtSignature;
1214+
}
11611215
}

0 commit comments

Comments
 (0)