Skip to content

Commit 44d3a06

Browse files
committed
Merge branch 'release-2.5.0' into feature/expose-public-key
2 parents db3f868 + 1c8a8ad commit 44d3a06

File tree

24 files changed

+640
-117
lines changed

24 files changed

+640
-117
lines changed

NEWS

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,45 @@
1-
== Version 2.4.2 (unreleased) ==
1+
== Version 2.5.0 (unreleased) ==
22

3+
`webauthn-server-core`:
4+
5+
Breaking changes to experimental features:
6+
7+
* Added Jackson annotation `@JsonProperty` to method
8+
`RegisteredCredential.isBackedUp()`, changing the property name from
9+
`backedUp` to `backupState`. `backedUp` is still accepted during
10+
deserialization but will no longer be emitted during serialization.
11+
12+
New features:
13+
14+
* Added method `.isUserVerified()` to `RegistrationResult` and `AssertionResult`
15+
as a shortcut for accessing the UV flag in authenticator data.
316
* Updated README and JavaDoc to use the "passkey" term and provide more guidance
417
around passkey use cases.
18+
* Added `Automatic-Module-Name` to jar manifest.
19+
20+
Fixes:
21+
22+
* `AuthenticatorAttestationResponse` now tolerates and ignores properties
23+
`"publicKey"` and `"publicKeyAlgorithm"` during JSON deserialization. These
24+
properties are emitted by the `PublicKeyCredential.toJSON()` method added in
25+
WebAuthn Level 3.
26+
* Relaxed Guava dependency version constraint to include major version 32.
27+
28+
29+
`webauthn-server-attestation`:
30+
31+
New features:
32+
33+
* Added option `verifyDownloadsOnly(boolean)` to `FidoMetadataDownloader`. When
34+
set to `true`, the BLOB signature will not be verified when loading a BLOB
35+
from cache or when explicitly given. Default setting is `false`, which
36+
preserves the previous behaviour.
37+
* Added `Automatic-Module-Name` to jar manifest.
38+
39+
Fixes:
40+
41+
* Made Jackson setting `PROPAGATE_TRANSIENT_MARKER` unnecessary for JSON
42+
serialization with Jackson version 2.15.0-rc1 and later.
543

644

745
== Version 2.4.1 ==

README

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,50 @@ PublicKeyCredentialCreationOptions request = rp.startRegistration(
489489
.build());
490490
----------
491491

492+
You can also request that user verification be used if possible, but is not required:
493+
494+
[source,java]
495+
----------
496+
PublicKeyCredentialCreationOptions request = rp.startRegistration(
497+
StartRegistrationOptions.builder()
498+
.user(/* ... */)
499+
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
500+
.userVerification(UserVerificationRequirement.PREFERRED)
501+
.build())
502+
.build());
503+
504+
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
505+
.username("alice")
506+
.userVerification(UserVerificationRequirement.PREFERRED)
507+
.build());
508+
----------
509+
510+
In this case
511+
link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishRegistration(com.yubico.webauthn.FinishRegistrationOptions)[`RelyingParty.finishRegistration(...)`]
512+
and
513+
link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.4.1/com/yubico/webauthn/RelyingParty.html#finishAssertion(com.yubico.webauthn.FinishAssertionOptions)[`RelyingParty.finishAssertion(...)`]
514+
will NOT enforce user verification,
515+
but instead the `isUserVerified()` method of
516+
link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/RegistrationResult.html[`RegistrationResult`]
517+
and
518+
link:https://developers.yubico.com/java-webauthn-server/JavaDoc/webauthn-server-core/2.5.0/com/yubico/webauthn/AssertionResult.html[`AssertionResult`]
519+
will tell whether user verification was used.
520+
521+
For example, you could prompt for a password as the second factor if `isUserVerified()` returns `false`:
522+
523+
[source,java]
524+
----------
525+
AssertionResult result = rp.finishAssertion(/* ... */);
526+
527+
if (result.isSuccess()) {
528+
if (result.isUserVerified()) {
529+
return successfulLogin(result.getUsername());
530+
} else {
531+
return passwordRequired(result.getUsername());
532+
}
533+
}
534+
----------
535+
492536
User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials.
493537

494538

build.gradle

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ buildscript {
44
}
55
dependencies {
66
classpath 'com.cinnober.gradle:semver-git:2.5.0'
7+
8+
if (project.findProperty('yubicoPublish') == 'true') {
9+
classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
10+
}
711
}
812
}
913
plugins {
1014
id 'java-platform'
11-
id 'io.github.gradle-nexus.publish-plugin' version '1.3.0'
1215

1316
// The root project has no sources, but the dependency platform also needs to be published as an artifact
1417
// See https://docs.gradle.org/current/userguide/java_platform_plugin.html
@@ -21,10 +24,7 @@ import com.yubico.gradle.GitUtils
2124
rootProject.description = "Metadata root for the com.yubico:webauthn-server-* module family"
2225

2326
project.ext.isCiBuild = System.env.CI == 'true'
24-
25-
project.ext.publishEnabled = !isCiBuild &&
26-
project.hasProperty('yubicoPublish') && project.yubicoPublish &&
27-
project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')
27+
project.ext.publishEnabled = !isCiBuild && project.findProperty('yubicoPublish') == 'true'
2828

2929
wrapper {
3030
gradleVersion = '8.1.1'
@@ -65,6 +65,8 @@ allprojects {
6565
}
6666

6767
if (publishEnabled) {
68+
apply plugin: 'io.github.gradle-nexus.publish-plugin'
69+
6870
nexusPublishing {
6971
repositories {
7072
sonatype {

buildSrc/src/main/groovy/project-convention-publish.gradle

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ project.afterEvaluate {
4949
}
5050
}
5151

52-
signing {
53-
useGpgCmd()
54-
sign(publishing.publications.jars)
52+
if (project.findProperty("yubicoPublish") == "true") {
53+
signing {
54+
useGpgCmd()
55+
sign(publishing.publications.jars)
56+
}
5557
}
5658
}

doc/development.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ Developer docs
22
===
33

44

5+
Setup for publishing
6+
---
7+
8+
To enable publishing to Maven Central via Sonatype Nexus, set
9+
`yubicoPublish=true` in `$HOME/.gradle/gradle.properties` and add your Sonatype
10+
username and password. Example:
11+
12+
```properties
13+
yubicoPublish=true
14+
ossrhUsername=8pnmjKQP
15+
ossrhPassword=bmjuyWSIik8P3Nq/ZM2G0Xs0sHEKBg+4q4zTZ8JDDRCr
16+
```
17+
18+
519
Code formatting
620
---
721

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ dependencyResolutionManagement {
1616
create("constraintLibs") {
1717
library("cbor", "com.upokecenter:cbor:[4.5.1,5)")
1818
library("cose", "com.augustcellars.cose:cose-java:[1.0.0,2)")
19-
library("guava", "com.google.guava:guava:[24.1.1,32)")
19+
library("guava", "com.google.guava:guava:[24.1.1,33)")
2020
library("httpclient5", "org.apache.httpcomponents.client5:httpclient5:[5.0.0,6)")
2121
library("slf4j", "org.slf4j:slf4j-api:[1.7.25,3)")
2222

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/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ tasks["check"].dependsOn(integrationTest)
7272
tasks.jar {
7373
manifest {
7474
attributes(mapOf(
75+
"Automatic-Module-Name" to "com.yubico.webauthn.attestation",
7576
"Implementation-Id" to "java-webauthn-server-attestation",
7677
"Implementation-Title" to project.description,
7778
))

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)