Skip to content

Commit 050de23

Browse files
committed
Release 1.2.0
2 parents 9de35cf + 6871e19 commit 050de23

28 files changed

+1431
-183
lines changed

NEWS

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
== Version 1.2.0 ==
2+
3+
New features:
4+
5+
* RSA keys are now supported.
6+
* New constructor functions `PublicKeyCredential.parseRegistrationResponseJson` and `.parseAssertionResponseJson`
7+
* So users don't have to deal with the `TypeReference`s imposed by the generics, unless they want to.
8+
9+
Bug fixes:
10+
11+
* `android-key` attestation statements now don't throw an exception if
12+
`allowUntrustedAttestation` is set to `true`.
13+
* `tpm` attestation statements now don't throw an exception if
14+
`allowUntrustedAttestation` is set to `true`.
15+
16+
117
== Version 1.1.0 ==
218

319
Changed behaviours:

README

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ Maven:
2727
<dependency>
2828
<groupId>com.yubico</groupId>
2929
<artifactId>webauthn-server-core</artifactId>
30-
<version>1.1.0</version>
30+
<version>1.2.0</version>
3131
<scope>compile</scope>
3232
</dependency>
3333
----------
3434

3535
Gradle:
3636

3737
----------
38-
compile 'com.yubico:webauthn-server-core:1.1.0'
38+
compile 'com.yubico:webauthn-server-core:1.2.0'
3939
----------
4040

4141

@@ -146,7 +146,7 @@ Get the response from the client:
146146
----------
147147
String responseJson = /* ... */;
148148
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
149-
jsonMapper.readValue(responseJson, new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>>(){});
149+
PublicKeyCredential.parseRegistrationResponseJson(responseJson);
150150
----------
151151

152152
Validate the response:
@@ -190,8 +190,7 @@ Validate the response:
190190
String responseJson = /* ... */;
191191

192192
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc =
193-
jsonMapper.readValue(responseJson, new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>>() {
194-
});
193+
PublicKeyCredential.parseAssertionResponseJson(responseJson);
195194

196195
try {
197196
AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
@@ -270,7 +269,7 @@ credentials.
270269
The application stores the `request` in temporary storage.
271270

272271
. The application's client-side script runs `navigator.credentials.create()` or
273-
`.get()` with `response` as the `publicKey` argument.
272+
`.get()` with `request` as the `publicKey` argument.
274273

275274
. The user confirms the operation and the client returns a
276275
https://www.w3.org/TR/webauthn/#public-key-credential[`PublicKeyCredential`]
@@ -292,7 +291,7 @@ credentials.
292291

293292
. The library returns a POJO representation of the result of the ceremony. For
294293
registration ceremonies, this will include the credential ID and public key of
295-
the new credential. The application may also opt in to also getting
294+
the new credential. The application may opt in to also getting
296295
information about the authenticator model and whether the authenticator
297296
attestation is trusted. For authentication ceremonies, this will include the
298297
username and user handle, the credential ID of the credential used, and the

webauthn-server-core/src/main/java/com/yubico/internal/util/WebAuthnCodecs.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@
3838
import com.yubico.webauthn.data.ByteArray;
3939
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
4040
import java.io.IOException;
41+
import java.math.BigInteger;
42+
import java.security.KeyFactory;
43+
import java.security.NoSuchAlgorithmException;
4144
import java.security.PublicKey;
4245
import java.security.interfaces.ECPublicKey;
46+
import java.security.spec.InvalidKeySpecException;
47+
import java.security.spec.RSAPublicKeySpec;
4348
import java.util.Arrays;
4449
import java.util.HashMap;
4550
import java.util.Map;
51+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
4652

4753

4854
public final class WebAuthnCodecs {
@@ -123,8 +129,27 @@ public static ByteArray ecPublicKeyToCose(ECPublicKey key) {
123129
return rawEcdaKeyToCose(ecPublicKeyToRaw(key));
124130
}
125131

126-
public static ECPublicKey importCoseP256PublicKey(ByteArray key) throws CoseException, IOException {
127-
return new COSE.ECPublicKey(new OneKey(CBORObject.DecodeFromBytes(key.getBytes())));
132+
public static PublicKey importCosePublicKey(ByteArray key) throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
133+
CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());
134+
final int kty = cose.get(CBORObject.FromObject(1)).AsInt32();
135+
switch (kty) {
136+
case 2: return importCoseP256PublicKey(cose);
137+
case 3: return importCoseRsaPublicKey(cose);
138+
default:
139+
throw new IllegalArgumentException("Unsupported key type: " + kty);
140+
}
141+
}
142+
143+
private static PublicKey importCoseRsaPublicKey(CBORObject cose) throws NoSuchAlgorithmException, InvalidKeySpecException {
144+
RSAPublicKeySpec spec = new RSAPublicKeySpec(
145+
new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()),
146+
new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())
147+
);
148+
return KeyFactory.getInstance("RSA", new BouncyCastleProvider()).generatePublic(spec);
149+
}
150+
151+
private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException, IOException {
152+
return new COSE.ECPublicKey(new OneKey(cose));
128153
}
129154

130155
public static String getSignatureAlgorithmName(PublicKey key) {

webauthn-server-core/src/main/java/com/yubico/webauthn/BouncyCastleCrypto.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,32 @@ public boolean verifySignature(X509Certificate attestationCertificate, ByteArray
5757
return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature);
5858
}
5959

60-
public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signature) {
60+
public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signatureBytes) {
6161
try {
62-
Signature ecdsaSignature = Signature.getInstance("SHA256withECDSA", provider);
63-
ecdsaSignature.initVerify(publicKey);
64-
ecdsaSignature.update(signedBytes.getBytes());
65-
return ecdsaSignature.verify(signature.getBytes());
66-
} catch (GeneralSecurityException e) {
62+
final String algName;
63+
switch (publicKey.getAlgorithm()) {
64+
case "EC":
65+
algName = "SHA256withECDSA";
66+
break;
67+
68+
case "RSA":
69+
algName = "SHA256withRSA";
70+
break;
71+
72+
default:
73+
throw new IllegalArgumentException("Unsupported public key algorithm: " + publicKey);
74+
}
75+
Signature signature = Signature.getInstance(algName, provider);
76+
signature.initVerify(publicKey);
77+
signature.update(signedBytes.getBytes());
78+
return signature.verify(signatureBytes.getBytes());
79+
} catch (GeneralSecurityException | IllegalArgumentException e) {
6780
throw new RuntimeException(
6881
String.format(
6982
"Failed to verify signature. This could be a problem with your JVM environment, or a bug in webauthn-server-core. Public key: %s, signed data: %s , signature: %s",
7083
publicKey,
7184
signedBytes.getBase64Url(),
72-
signature.getBase64Url()
85+
signatureBytes.getBase64Url()
7386
),
7487
e
7588
);

webauthn-server-core/src/main/java/com/yubico/webauthn/FidoU2fAttestationStatementVerifier.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,21 @@
2626

2727
import COSE.CoseException;
2828
import com.fasterxml.jackson.databind.JsonNode;
29+
import com.yubico.internal.util.ExceptionUtil;
2930
import com.yubico.internal.util.WebAuthnCodecs;
30-
import com.yubico.webauthn.data.AttestedCredentialData;
3131
import com.yubico.webauthn.data.AttestationObject;
3232
import com.yubico.webauthn.data.AttestationType;
33+
import com.yubico.webauthn.data.AttestedCredentialData;
3334
import com.yubico.webauthn.data.ByteArray;
3435
import java.io.IOException;
3536
import java.math.BigInteger;
37+
import java.security.NoSuchAlgorithmException;
38+
import java.security.PublicKey;
3639
import java.security.cert.CertificateException;
3740
import java.security.cert.X509Certificate;
3841
import java.security.interfaces.ECPublicKey;
3942
import java.security.spec.ECParameterSpec;
43+
import java.security.spec.InvalidKeySpecException;
4044
import java.util.Objects;
4145
import java.util.Optional;
4246
import lombok.extern.slf4j.Slf4j;
@@ -80,17 +84,32 @@ private static boolean validSelfSignature(X509Certificate cert) {
8084
}
8185
}
8286

87+
private static ByteArray getRawUserPublicKey(AttestationObject attestationObject) throws IOException, CoseException {
88+
final ByteArray pubkeyCose = attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey();
89+
final PublicKey pubkey;
90+
try {
91+
pubkey = WebAuthnCodecs.importCosePublicKey(pubkeyCose);
92+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
93+
throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose.getHex(), e);
94+
}
95+
96+
final ECPublicKey ecPubkey;
97+
try {
98+
ecPubkey = (ECPublicKey) pubkey;
99+
} catch (ClassCastException e) {
100+
throw new RuntimeException( "U2F supports only EC keys, was: " + pubkey);
101+
}
102+
103+
return WebAuthnCodecs.ecPublicKeyToRaw(ecPubkey);
104+
}
105+
83106
@Override
84107
public AttestationType getAttestationType(AttestationObject attestationObject) throws CoseException, IOException, CertificateException {
85108
X509Certificate attestationCertificate = getAttestationCertificate(attestationObject);
86109

87110
if (attestationCertificate.getPublicKey() instanceof ECPublicKey
88111
&& validSelfSignature(attestationCertificate)
89-
&& WebAuthnCodecs.ecPublicKeyToRaw(
90-
WebAuthnCodecs.importCoseP256PublicKey(
91-
attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey()
92-
)
93-
)
112+
&& getRawUserPublicKey(attestationObject)
94113
.equals(
95114
WebAuthnCodecs.ecPublicKeyToRaw((ECPublicKey) attestationCertificate.getPublicKey())
96115
)
@@ -128,14 +147,10 @@ && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams())
128147
}
129148

130149
if (signature.isBinary()) {
131-
ByteArray userPublicKey;
150+
final ByteArray userPublicKey;
132151

133152
try {
134-
userPublicKey = WebAuthnCodecs.ecPublicKeyToRaw(
135-
WebAuthnCodecs.importCoseP256PublicKey(
136-
attestedCredentialData.getCredentialPublicKey()
137-
)
138-
);
153+
userPublicKey = getRawUserPublicKey(attestationObject);
139154
} catch (IOException | CoseException e) {
140155
RuntimeException err = new RuntimeException(String.format("Failed to parse public key from attestation data %s", attestedCredentialData));
141156
log.error(err.getMessage(), err);

webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import com.yubico.webauthn.exception.InvalidSignatureCountException;
3838
import com.yubico.webauthn.extension.appid.AppId;
3939
import java.io.IOException;
40+
import java.security.NoSuchAlgorithmException;
4041
import java.security.PublicKey;
42+
import java.security.spec.InvalidKeySpecException;
4143
import java.util.ArrayList;
4244
import java.util.Collections;
4345
import java.util.LinkedList;
@@ -546,13 +548,18 @@ public void validate() {
546548
final PublicKey key;
547549

548550
try {
549-
key = WebAuthnCodecs.importCoseP256PublicKey(cose);
550-
} catch (CoseException | IOException e) {
551-
throw new IllegalArgumentException(String.format(
552-
"Failed to decode public key: Credential ID: %s COSE: %s",
553-
credential.getCredentialId().getBase64Url(),
554-
cose.getBase64Url()
555-
));
551+
key = WebAuthnCodecs.importCosePublicKey(cose);
552+
} catch (CoseException | IOException | InvalidKeySpecException e) {
553+
throw new IllegalArgumentException(
554+
String.format(
555+
"Failed to decode public key: Credential ID: %s COSE: %s",
556+
credential.getCredentialId().getBase64Url(),
557+
cose.getBase64Url()
558+
),
559+
e
560+
);
561+
} catch (NoSuchAlgorithmException e) {
562+
throw new RuntimeException(e);
556563
}
557564

558565
if (!

webauthn-server-core/src/main/java/com/yubico/webauthn/FinishRegistrationSteps.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,11 +487,14 @@ public Optional<AttestationTrustResolver> trustResolver() {
487487
case SELF_ATTESTATION:
488488
return Optional.empty();
489489

490+
case ATTESTATION_CA:
490491
case BASIC:
491492
switch (attestation.getFormat()) {
493+
case "android-key":
492494
case "android-safetynet":
493495
case "fido-u2f":
494496
case "packed":
497+
case "tpm":
495498
return metadataService.map(KnownX509TrustAnchorsTrustResolver::new);
496499
default:
497500
throw new UnsupportedOperationException(String.format(
@@ -529,6 +532,7 @@ public void validate() {
529532
assure(allowUntrustedAttestation, "Self attestation is not allowed.");
530533
break;
531534

535+
case ATTESTATION_CA:
532536
case BASIC:
533537
assure(allowUntrustedAttestation || attestationTrusted(), "Failed to derive trust for attestation key.");
534538
break;
@@ -553,6 +557,7 @@ public boolean attestationTrusted() {
553557
case NONE:
554558
return false;
555559

560+
case ATTESTATION_CA:
556561
case BASIC:
557562
return attestationMetadata().filter(Attestation::isTrusted).isPresent();
558563
default:

webauthn-server-core/src/main/java/com/yubico/webauthn/PackedAttestationStatementVerifier.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.security.SignatureException;
4747
import java.security.cert.CertificateException;
4848
import java.security.cert.X509Certificate;
49+
import java.security.spec.InvalidKeySpecException;
4950
import java.util.Arrays;
5051
import java.util.HashSet;
5152
import java.util.Locale;
@@ -98,15 +99,17 @@ private boolean verifyEcdaaSignature(AttestationObject attestationObject, ByteAr
9899
private boolean verifySelfAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) {
99100
final PublicKey pubkey;
100101
try {
101-
pubkey = WebAuthnCodecs.importCoseP256PublicKey(
102+
pubkey = WebAuthnCodecs.importCosePublicKey(
102103
attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey()
103104
);
104-
} catch (IOException | CoseException e) {
105+
} catch (IOException | CoseException | InvalidKeySpecException e) {
105106
throw ExceptionUtil.wrapAndLog(
106107
log,
107108
String.format("Failed to parse public key from attestation data %s", attestationObject.getAuthenticatorData().getAttestedCredentialData()),
108109
e
109110
);
111+
} catch (NoSuchAlgorithmException e) {
112+
throw new RuntimeException(e);
110113
}
111114

112115
final Long keyAlgId = CBORObject.DecodeFromBytes(attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey().getBytes())

0 commit comments

Comments
 (0)