Skip to content

Commit

Permalink
Release 1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Apr 28, 2019
2 parents 9de35cf + 6871e19 commit 050de23
Show file tree
Hide file tree
Showing 28 changed files with 1,431 additions and 183 deletions.
16 changes: 16 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
== Version 1.2.0 ==

New features:

* RSA keys are now supported.
* New constructor functions `PublicKeyCredential.parseRegistrationResponseJson` and `.parseAssertionResponseJson`
* So users don't have to deal with the `TypeReference`s imposed by the generics, unless they want to.

Bug fixes:

* `android-key` attestation statements now don't throw an exception if
`allowUntrustedAttestation` is set to `true`.
* `tpm` attestation statements now don't throw an exception if
`allowUntrustedAttestation` is set to `true`.


== Version 1.1.0 ==

Changed behaviours:
Expand Down
13 changes: 6 additions & 7 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ Maven:
<dependency>
<groupId>com.yubico</groupId>
<artifactId>webauthn-server-core</artifactId>
<version>1.1.0</version>
<version>1.2.0</version>
<scope>compile</scope>
</dependency>
----------

Gradle:

----------
compile 'com.yubico:webauthn-server-core:1.1.0'
compile 'com.yubico:webauthn-server-core:1.2.0'
----------


Expand Down Expand Up @@ -146,7 +146,7 @@ Get the response from the client:
----------
String responseJson = /* ... */;
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
jsonMapper.readValue(responseJson, new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>>(){});
PublicKeyCredential.parseRegistrationResponseJson(responseJson);
----------

Validate the response:
Expand Down Expand Up @@ -190,8 +190,7 @@ Validate the response:
String responseJson = /* ... */;

PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc =
jsonMapper.readValue(responseJson, new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>>() {
});
PublicKeyCredential.parseAssertionResponseJson(responseJson);

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

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

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

. The library returns a POJO representation of the result of the ceremony. For
registration ceremonies, this will include the credential ID and public key of
the new credential. The application may also opt in to also getting
the new credential. The application may opt in to also getting
information about the authenticator model and whether the authenticator
attestation is trusted. For authentication ceremonies, this will include the
username and user handle, the credential ID of the credential used, and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.bouncycastle.jce.provider.BouncyCastleProvider;


public final class WebAuthnCodecs {
Expand Down Expand Up @@ -123,8 +129,27 @@ public static ByteArray ecPublicKeyToCose(ECPublicKey key) {
return rawEcdaKeyToCose(ecPublicKeyToRaw(key));
}

public static ECPublicKey importCoseP256PublicKey(ByteArray key) throws CoseException, IOException {
return new COSE.ECPublicKey(new OneKey(CBORObject.DecodeFromBytes(key.getBytes())));
public static PublicKey importCosePublicKey(ByteArray key) throws CoseException, IOException, InvalidKeySpecException, NoSuchAlgorithmException {
CBORObject cose = CBORObject.DecodeFromBytes(key.getBytes());
final int kty = cose.get(CBORObject.FromObject(1)).AsInt32();
switch (kty) {
case 2: return importCoseP256PublicKey(cose);
case 3: return importCoseRsaPublicKey(cose);
default:
throw new IllegalArgumentException("Unsupported key type: " + kty);
}
}

private static PublicKey importCoseRsaPublicKey(CBORObject cose) throws NoSuchAlgorithmException, InvalidKeySpecException {
RSAPublicKeySpec spec = new RSAPublicKeySpec(
new BigInteger(1, cose.get(CBORObject.FromObject(-1)).GetByteString()),
new BigInteger(1, cose.get(CBORObject.FromObject(-2)).GetByteString())
);
return KeyFactory.getInstance("RSA", new BouncyCastleProvider()).generatePublic(spec);
}

private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException, IOException {
return new COSE.ECPublicKey(new OneKey(cose));
}

public static String getSignatureAlgorithmName(PublicKey key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,32 @@ public boolean verifySignature(X509Certificate attestationCertificate, ByteArray
return verifySignature(attestationCertificate.getPublicKey(), signedBytes, signature);
}

public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signature) {
public boolean verifySignature(PublicKey publicKey, ByteArray signedBytes, ByteArray signatureBytes) {
try {
Signature ecdsaSignature = Signature.getInstance("SHA256withECDSA", provider);
ecdsaSignature.initVerify(publicKey);
ecdsaSignature.update(signedBytes.getBytes());
return ecdsaSignature.verify(signature.getBytes());
} catch (GeneralSecurityException e) {
final String algName;
switch (publicKey.getAlgorithm()) {
case "EC":
algName = "SHA256withECDSA";
break;

case "RSA":
algName = "SHA256withRSA";
break;

default:
throw new IllegalArgumentException("Unsupported public key algorithm: " + publicKey);
}
Signature signature = Signature.getInstance(algName, provider);
signature.initVerify(publicKey);
signature.update(signedBytes.getBytes());
return signature.verify(signatureBytes.getBytes());
} catch (GeneralSecurityException | IllegalArgumentException e) {
throw new RuntimeException(
String.format(
"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",
publicKey,
signedBytes.getBase64Url(),
signature.getBase64Url()
signatureBytes.getBase64Url()
),
e
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@

import COSE.CoseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.yubico.internal.util.ExceptionUtil;
import com.yubico.internal.util.WebAuthnCodecs;
import com.yubico.webauthn.data.AttestedCredentialData;
import com.yubico.webauthn.data.AttestationObject;
import com.yubico.webauthn.data.AttestationType;
import com.yubico.webauthn.data.AttestedCredentialData;
import com.yubico.webauthn.data.ByteArray;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -80,17 +84,32 @@ private static boolean validSelfSignature(X509Certificate cert) {
}
}

private static ByteArray getRawUserPublicKey(AttestationObject attestationObject) throws IOException, CoseException {
final ByteArray pubkeyCose = attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey();
final PublicKey pubkey;
try {
pubkey = WebAuthnCodecs.importCosePublicKey(pubkeyCose);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw ExceptionUtil.wrapAndLog(log, "Failed to decode public key: " + pubkeyCose.getHex(), e);
}

final ECPublicKey ecPubkey;
try {
ecPubkey = (ECPublicKey) pubkey;
} catch (ClassCastException e) {
throw new RuntimeException( "U2F supports only EC keys, was: " + pubkey);
}

return WebAuthnCodecs.ecPublicKeyToRaw(ecPubkey);
}

@Override
public AttestationType getAttestationType(AttestationObject attestationObject) throws CoseException, IOException, CertificateException {
X509Certificate attestationCertificate = getAttestationCertificate(attestationObject);

if (attestationCertificate.getPublicKey() instanceof ECPublicKey
&& validSelfSignature(attestationCertificate)
&& WebAuthnCodecs.ecPublicKeyToRaw(
WebAuthnCodecs.importCoseP256PublicKey(
attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey()
)
)
&& getRawUserPublicKey(attestationObject)
.equals(
WebAuthnCodecs.ecPublicKeyToRaw((ECPublicKey) attestationCertificate.getPublicKey())
)
Expand Down Expand Up @@ -128,14 +147,10 @@ && isP256(((ECPublicKey) attestationCertificate.getPublicKey()).getParams())
}

if (signature.isBinary()) {
ByteArray userPublicKey;
final ByteArray userPublicKey;

try {
userPublicKey = WebAuthnCodecs.ecPublicKeyToRaw(
WebAuthnCodecs.importCoseP256PublicKey(
attestedCredentialData.getCredentialPublicKey()
)
);
userPublicKey = getRawUserPublicKey(attestationObject);
} catch (IOException | CoseException e) {
RuntimeException err = new RuntimeException(String.format("Failed to parse public key from attestation data %s", attestedCredentialData));
log.error(err.getMessage(), err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
import com.yubico.webauthn.exception.InvalidSignatureCountException;
import com.yubico.webauthn.extension.appid.AppId;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
Expand Down Expand Up @@ -546,13 +548,18 @@ public void validate() {
final PublicKey key;

try {
key = WebAuthnCodecs.importCoseP256PublicKey(cose);
} catch (CoseException | IOException e) {
throw new IllegalArgumentException(String.format(
"Failed to decode public key: Credential ID: %s COSE: %s",
credential.getCredentialId().getBase64Url(),
cose.getBase64Url()
));
key = WebAuthnCodecs.importCosePublicKey(cose);
} catch (CoseException | IOException | InvalidKeySpecException e) {
throw new IllegalArgumentException(
String.format(
"Failed to decode public key: Credential ID: %s COSE: %s",
credential.getCredentialId().getBase64Url(),
cose.getBase64Url()
),
e
);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}

if (!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,14 @@ public Optional<AttestationTrustResolver> trustResolver() {
case SELF_ATTESTATION:
return Optional.empty();

case ATTESTATION_CA:
case BASIC:
switch (attestation.getFormat()) {
case "android-key":
case "android-safetynet":
case "fido-u2f":
case "packed":
case "tpm":
return metadataService.map(KnownX509TrustAnchorsTrustResolver::new);
default:
throw new UnsupportedOperationException(String.format(
Expand Down Expand Up @@ -529,6 +532,7 @@ public void validate() {
assure(allowUntrustedAttestation, "Self attestation is not allowed.");
break;

case ATTESTATION_CA:
case BASIC:
assure(allowUntrustedAttestation || attestationTrusted(), "Failed to derive trust for attestation key.");
break;
Expand All @@ -553,6 +557,7 @@ public boolean attestationTrusted() {
case NONE:
return false;

case ATTESTATION_CA:
case BASIC:
return attestationMetadata().filter(Attestation::isTrusted).isPresent();
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
Expand Down Expand Up @@ -98,15 +99,17 @@ private boolean verifyEcdaaSignature(AttestationObject attestationObject, ByteAr
private boolean verifySelfAttestationSignature(AttestationObject attestationObject, ByteArray clientDataJsonHash) {
final PublicKey pubkey;
try {
pubkey = WebAuthnCodecs.importCoseP256PublicKey(
pubkey = WebAuthnCodecs.importCosePublicKey(
attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey()
);
} catch (IOException | CoseException e) {
} catch (IOException | CoseException | InvalidKeySpecException e) {
throw ExceptionUtil.wrapAndLog(
log,
String.format("Failed to parse public key from attestation data %s", attestationObject.getAuthenticatorData().getAttestedCredentialData()),
e
);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}

final Long keyAlgId = CBORObject.DecodeFromBytes(attestationObject.getAuthenticatorData().getAttestedCredentialData().get().getCredentialPublicKey().getBytes())
Expand Down
Loading

0 comments on commit 050de23

Please sign in to comment.