diff --git a/NEWS b/NEWS index c3088166b..e6afcb434 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,9 @@ Fixes: properties are emitted by the `PublicKeyCredential.toJSON()` method added in WebAuthn Level 3. * Relaxed Guava dependency version constraint to include major version 32. +* `RelyingParty.finishAssertion` now behaves the same if + `StartAssertionOptions.allowCredentials` is explicitly set to a present, empty + list as when absent. `webauthn-server-attestation`: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java index 033b7a363..88b792435 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java @@ -111,6 +111,7 @@ public void validate() { request .getPublicKeyCredentialRequestOptions() .getAllowCredentials() + .filter(allowCredentials -> !allowCredentials.isEmpty()) .ifPresent( allowed -> { assertTrue( diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java index ee3797468..94ba42ea1 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/WebAuthnCodecs.java @@ -31,11 +31,13 @@ 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.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.HashMap; @@ -126,14 +128,29 @@ static PublicKey importCosePublicKey(ByteArray key) // COSE-JAVA is hardcoded to ed25519-java provider ("EdDSA") which would require an // additional dependency to parse EdDSA keys via the OneKey constructor return importCoseEdDsaPublicKey(cose); - case 2: // Fall through + case 2: + return importCoseP256PublicKey(cose); case 3: - return new OneKey(cose).AsPublicKey(); + // COSE-JAVA supports RSA in v1.1.0 but not in v1.0.0 + 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").generatePublic(spec); + } + + private static ECPublicKey importCoseP256PublicKey(CBORObject cose) throws CoseException { + return (ECPublicKey) new OneKey(cose).AsPublicKey(); + } + private static PublicKey importCoseEdDsaPublicKey(CBORObject cose) throws InvalidKeySpecException, NoSuchAlgorithmException { final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32(); diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index bd1b045db..07b0b7301 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -594,14 +594,21 @@ class RelyingPartyAssertionSpec } it("Succeeds if no credential IDs were requested.") { - val steps = finishAssertion( - allowCredentials = None, - credentialId = new ByteArray(Array(0, 1, 2, 3)), - ) - val step: FinishAssertionSteps#Step5 = steps.begin + for { + allowCredentials <- List( + None, + Some(List.empty[PublicKeyCredentialDescriptor].asJava), + ) + } { + val steps = finishAssertion( + allowCredentials = allowCredentials, + credentialId = new ByteArray(Array(0, 1, 2, 3)), + ) + val step: FinishAssertionSteps#Step5 = steps.begin - step.validations shouldBe a[Success[_]] - step.tryNext shouldBe a[Success[_]] + step.validations shouldBe a[Success[_]] + step.tryNext shouldBe a[Success[_]] + } } } diff --git a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java index bf7f661ce..d3fc6d4cd 100644 --- a/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java +++ b/webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java @@ -647,7 +647,17 @@ public void serialize( throw new RuntimeException(e); } }); - gen.writeObjectField("extensions", value.getExtensions()); + value + .getExtensions() + .ifPresent( + extensions -> { + try { + gen.writeObjectField( + "extensions", JacksonCodecs.cbor().readTree(extensions.EncodeToBytes())); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); gen.writeEndObject(); } } diff --git a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala index 1542d51f4..854ef65bb 100644 --- a/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala +++ b/webauthn-server-demo/src/test/scala/demo/webauthn/WebAuthnServerSpec.scala @@ -38,6 +38,7 @@ import com.yubico.webauthn.data.Generators.arbitraryAuthenticatorTransport import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions import com.yubico.webauthn.data.RelyingPartyIdentity import com.yubico.webauthn.data.ResidentKeyRequirement +import com.yubico.webauthn.test.RealExamples import demo.webauthn.data.AssertionRequestWrapper import demo.webauthn.data.CredentialRegistration import demo.webauthn.data.RegistrationRequest @@ -91,23 +92,27 @@ class WebAuthnServerSpec } it("has a finish method which accepts and outputs JSON.") { - val requestId = ByteArray.fromBase64Url("request1") - - val server = newServerWithRegistrationRequest( - RegistrationTestData.FidoU2f.BasicAttestation - ) + for { + testData <- List( + RegistrationTestData.FidoU2f.BasicAttestation, // This test case for no particular reason + RealExamples.LargeBlobWrite.asRegistrationTestData, // This test case because it has authenticator extensions + ) + } { + val requestId = ByteArray.fromBase64Url("request1") + val server = newServerWithRegistrationRequest(testData) - val authenticationAttestationResponseJson = - """{"attestationObject":"v2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAFOQABAgMEBQYHCAkKCwwNDg8AIIjjhj6nH3qL2QF3tkUogilFykuaXjJTw35O4m-0NSX0pSJYIA5Nt8eYkLco-NQfKPXaA6dD9UfX_SHaYo-L-YQb78HsAyYBAiFYIOuzRl1o1Hem2jVRYhjkbSeIydhqLln9iltAgsDYjXRTIAFjZm10aGZpZG8tdTJmZ2F0dFN0bXS_Y3g1Y59ZAekwggHlMIIBjKADAgECAgIFOTAKBggqhkjOPQQDAjBqMSYwJAYDVQQDDB1ZdWJpY28gV2ViQXV0aG4gdW5pdCB0ZXN0cyBDQTEPMA0GA1UECgwGWXViaWNvMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJTRTAeFw0xODA5MDYxNzQyMDBaFw0xODA5MDYxNzQyMDBaMGcxIzAhBgNVBAMMGll1YmljbyBXZWJBdXRobiB1bml0IHRlc3RzMQ8wDQYDVQQKDAZZdWJpY28xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xCzAJBgNVBAYTAlNFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ-8bFED9TnFhaArujgB0foNaV4gQIulP1mC5DO1wvSByw4eOyXujpPHkTw9y5e5J2J3N9coSReZJgBRpvFzYD6MlMCMwIQYLKwYBBAGC5RwBAQQEEgQQAAECAwQFBgcICQoLDA0ODzAKBggqhkjOPQQDAgNHADBEAiB4bL25EH06vPBOVnReObXrS910ARVOLJPPnKNoZbe64gIgX1Rg5oydH45zEMEVDjNPStwv6Z3nE_isMeY-szlQhv3_Y3NpZ1hHMEUCIQDBs1nbSuuKQ6yoHMQoRp8eCT_HZvR45F_aVP6qFX_wKgIgMCL58bv-crkLwTwiEL9ibCV4nDYM-DZuW5_BFCJbcxn__w","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJBQUVCQWdNRkNBMFZJamRaRUdsNVlscyIsIm9yaWdpbiI6ImxvY2FsaG9zdCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUiLCJ0b2tlbkJpbmRpbmciOnsic3RhdHVzIjoic3VwcG9ydGVkIn19"}""" - val publicKeyCredentialJson = - s"""{"id":"iOOGPqcfeovZAXe2RSiCKUXKS5peMlPDfk7ib7Q1JfQ","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" - val responseJson = - s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" + val authenticationAttestationResponseJson = + s"""{"attestationObject":"${testData.attestationObject.getBase64Url}","clientDataJSON":"${testData.clientDataJsonBytes.getBase64Url}"}""" + val publicKeyCredentialJson = + s"""{"id":"${testData.response.getId.getBase64Url}","response":${authenticationAttestationResponseJson},"clientExtensionResults":{},"type":"public-key"}""" + val responseJson = + s"""{"requestId":"${requestId.getBase64Url}","credential":${publicKeyCredentialJson}}""" - val response = server.finishRegistration(responseJson) - val json = jsonMapper.writeValueAsString(response.right.get) + val response = server.finishRegistration(responseJson) + val json = jsonMapper.writeValueAsString(response.right.get) - json should not be null + json should not be null + } } } @@ -393,8 +398,8 @@ class WebAuthnServerSpec new InMemoryRegistrationStorage, registrationRequests, newCache(), - rpId, - origins, + testData.rpId, + Set(testData.response.getResponse.getClientData.getOrigin).asJava, ) }