Skip to content

Commit

Permalink
Merge pull request #44 from Yubico/rs1
Browse files Browse the repository at this point in the history
Add support for RS1 credentials
  • Loading branch information
emlun authored Oct 16, 2019
2 parents f9c0c6e + ffbcf22 commit 4b40a5e
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 18 deletions.
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ New features:
* The new `AuthenticatorTransport` can now contain any string value as the
transport identifier, as required in the editor's draft of the L2 spec. See:
https://github.com/w3c/webauthn/pull/1275
* Added support for RS1 credentials. Registration of RS1 credentials is not
enabled by default, but can be enabled by setting
`RelyingParty.preferredPubKeyCredParams` to a list containing
`PublicKeyCredentialParameters.RS1`.
* New constant `PublicKeyCredentialParameters.RS1`
* New constant `COSEAlgorithmIdentifier.RS1`


== Version 1.4.1 ==
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
case EdDSA: return "EDDSA";
case ES256: return "SHA256withECDSA";
case RS256: return "SHA256withRSA";
case RS1: return "SHA1withRSA";
default: throw new IllegalArgumentException("Unknown algorithm: " + alg);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
public enum COSEAlgorithmIdentifier implements JsonLongSerializable {
EdDSA(-8),
ES256(-7),
RS256(-257);
RS256(-257),
RS1(-65535);

@Getter
private final long id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ private PublicKeyCredentialParameters(
*/
public static final PublicKeyCredentialParameters ES256 = builder().alg(COSEAlgorithmIdentifier.ES256).build();

/**
* Algorithm {@link COSEAlgorithmIdentifier#RS1} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
*/
public static final PublicKeyCredentialParameters RS1 = builder().alg(COSEAlgorithmIdentifier.RS1).build();

/**
* Algorithm {@link COSEAlgorithmIdentifier#RS256} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
*/
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions
import com.yubico.webauthn.data.RelyingPartyIdentity
import com.yubico.webauthn.data.UserIdentity
import com.yubico.webauthn.data.UserVerificationRequirement
import com.yubico.webauthn.exception.InvalidSignatureCountException
import com.yubico.webauthn.extension.appid.AppId
Expand Down Expand Up @@ -121,6 +122,29 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv

private def getPublicKeyBytes(credentialKey: KeyPair): ByteArray = WebAuthnTestCodecs.ecPublicKeyToCose(credentialKey.getPublic.asInstanceOf[ECPublicKey])

private def credRepoWithUser(user: UserIdentity, credential: RegisteredCredential): CredentialRepository = new CredentialRepository {
override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] =
if (username == user.getName)
Set(PublicKeyCredentialDescriptor.builder().id(credential.getCredentialId).build()).asJava
else Set.empty.asJava
override def getUserHandleForUsername(username: String): Optional[ByteArray] =
if (username == user.getName)
Some(user.getId).asJava
else None.asJava
override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] =
if (userHandle == user.getId)
Some(user.getName).asJava
else None.asJava
override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] =
if (credentialId == credential.getCredentialId && userHandle == user.getId)
Some(credential).asJava
else None.asJava
override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] =
if (credentialId == credential.getCredentialId)
Set(credential).asJava
else Set.empty.asJava
}

def finishAssertion(
allowCredentials: Option[java.util.List[PublicKeyCredentialDescriptor]] = Some(List(PublicKeyCredentialDescriptor.builder().id(Defaults.credentialId).build()).asJava),
allowOriginPort: Boolean = false,
Expand Down Expand Up @@ -1469,6 +1493,66 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
result.getUserHandle should equal (registrationRequest.getUser.getId)
result.getCredentialId should equal (credId)
}

it("a generated Ed25519 key.") {
val registrationTestData = RegistrationTestData.Packed.BasicAttestationEdDsa
val testData = registrationTestData.assertion.get

val rp = RelyingParty.builder()
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
.credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
.credentialId(registrationTestData.response.getId)
.userHandle(registrationTestData.userId.getId)
.publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
.signatureCount(0)
.build()))
.build()

val result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(testData.request)
.response(testData.response)
.build()
)

result.isSuccess should be (true)
result.getUserHandle should equal (registrationTestData.userId.getId)
result.getCredentialId should equal (registrationTestData.response.getId)
result.getCredentialId should equal (testData.response.getId)
}

describe("an RS1 key") {
def test(registrationTestData: RegistrationTestData): Unit = {
val testData = registrationTestData.assertion.get

val rp = RelyingParty.builder()
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
.credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
.credentialId(registrationTestData.response.getId)
.userHandle(registrationTestData.userId.getId)
.publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
.signatureCount(0)
.build()))
.build()

val result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(testData.request)
.response(testData.response)
.build()
)

result.isSuccess should be (true)
result.getUserHandle should equal (registrationTestData.userId.getId)
result.getCredentialId should equal (registrationTestData.response.getId)
result.getCredentialId should equal (testData.response.getId)
}

it("with basic attestation.") {
test(RegistrationTestData.Packed.BasicAttestationRs1)
}
it("with self attestation.") {
test(RegistrationTestData.Packed.SelfAttestationRs1)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import java.security.MessageDigest
import java.security.PrivateKey
import java.security.SignatureException
import java.security.cert.X509Certificate
import java.security.interfaces.RSAPublicKey
import java.util.Optional

import com.fasterxml.jackson.databind.JsonNode
Expand Down Expand Up @@ -59,6 +60,7 @@ import com.yubico.webauthn.exception.RegistrationFailedException
import com.yubico.webauthn.test.Util.toStepWithUtilities
import com.yubico.webauthn.TestAuthenticator.AttestationCert
import com.yubico.webauthn.TestAuthenticator.AttestationMaker
import com.yubico.webauthn.data.PublicKeyCredentialParameters
import javax.security.auth.x500.X500Principal
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.x500.X500Name
Expand Down Expand Up @@ -1084,6 +1086,16 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
result should equal (Success(true))
}

it("Succeeds for an RS1 test case.") {
val testData = RegistrationTestData.Packed.BasicAttestationRs1

val result = verifier.verifyAttestationSignature(
new AttestationObject(testData.attestationObject),
testData.clientDataJsonHash
)
result should equal (true)
}

it("Fail if the default test case is mutated.") {
val testData = RegistrationTestData.Packed.BasicAttestation

Expand Down Expand Up @@ -1218,14 +1230,33 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}

it("Fails if the alg is a different value.") {
val testData = RegistrationTestData.Packed.SelfAttestationWithWrongAlgValue
def modifyAuthdataPubkeyAlg(authDataBytes: Array[Byte]): Array[Byte] = {
val authData = new AuthenticatorData(new ByteArray(authDataBytes))
val key = WebAuthnCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[RSAPublicKey]
val reencodedKey = WebAuthnTestCodecs.rsaPublicKeyToCose(key, COSEAlgorithmIdentifier.RS256)
new ByteArray(java.util.Arrays.copyOfRange(authDataBytes, 0, 32 + 1 + 4 + 16 + 2))
.concat(authData.getAttestedCredentialData.get.getCredentialId)
.concat(reencodedKey)
.getBytes
}
def modifyAttobjPubkeyAlg(attObjBytes: ByteArray): ByteArray = {
val attObj = JacksonCodecs.cbor.readTree(attObjBytes.getBytes)
new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(
attObj.asInstanceOf[ObjectNode]
.set("authData", jsonFactory.binaryNode(modifyAuthdataPubkeyAlg(attObj.get("authData").binaryValue())))
))
}

val testData = RegistrationTestData.Packed.SelfAttestationRs1
val attObj = new AttestationObject(modifyAttobjPubkeyAlg(testData.response.getResponse.getAttestationObject))

val result = Try(verifier.verifyAttestationSignature(
new AttestationObject(testData.attestationObject),
attObj,
testData.clientDataJsonHash
))

CBORObject.DecodeFromBytes(new AttestationObject(testData.attestationObject).getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes).get(CBORObject.FromObject(3)).AsInt64 should equal (-7)
new AttestationObject(testData.attestationObject).getAttestationStatement.get("alg").longValue should equal (-257)
CBORObject.DecodeFromBytes(attObj.getAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey.getBytes).get(CBORObject.FromObject(3)).AsInt64 should equal (-257)
attObj.getAttestationStatement.get("alg").longValue should equal (-65535)
result shouldBe a [Failure[_]]
result.failed.get shouldBe an [IllegalArgumentException]
}
Expand All @@ -1240,6 +1271,18 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
result should equal (true)
}

it("Succeeds for an RS1 test case.") {
val testData = RegistrationTestData.Packed.SelfAttestationRs1
val alg = WebAuthnCodecs.getCoseKeyAlg(testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey).get
alg should be (COSEAlgorithmIdentifier.RS1)

val result = verifier.verifyAttestationSignature(
new AttestationObject(testData.attestationObject),
testData.clientDataJsonHash
)
result should equal (true)
}

it("Fails if the attestation object is mutated.") {
val testData = testDataBase.editAuthenticatorData { authData: ByteArray => new ByteArray(authData.getBytes.updated(16, if (authData.getBytes()(16) == 0) 1: Byte else 0: Byte)) }
val result = verifier.verifyAttestationSignature(
Expand Down Expand Up @@ -1934,7 +1977,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}

describe("accept all test examples in the validExamples list.") {
RegistrationTestData.validExamples.zipWithIndex.foreach { case (testData, i) =>
RegistrationTestData.defaultSettingsValidExamples.zipWithIndex.foreach { case (testData, i) =>
it(s"Succeeds for example index ${i}.") {
val rp = {
val builder = RelyingParty.builder()
Expand All @@ -1954,6 +1997,46 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
}
}
}

describe("generate pubKeyCredParams which") {
val rp = RelyingParty.builder()
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
.credentialRepository(emptyCredentialRepository)
.build()
val pkcco = rp.startRegistration(StartRegistrationOptions.builder()
.user(UserIdentity.builder()
.name("foo")
.displayName("Foo")
.id(ByteArray.fromHex("aabbccdd"))
.build())
.build())

val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala

describe("include") {
it("ES256.") {
pubKeyCredParams should contain (PublicKeyCredentialParameters.ES256)
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.ES256)
}

it("EdDSA.") {
pubKeyCredParams should contain (PublicKeyCredentialParameters.EdDSA)
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.EdDSA)
}

it("RS256.") {
pubKeyCredParams should contain (PublicKeyCredentialParameters.RS256)
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.RS256)
}
}

describe("do not include") {
it("RS1.") {
pubKeyCredParams should not contain PublicKeyCredentialParameters.RS1
pubKeyCredParams map (_.getAlg) should not contain COSEAlgorithmIdentifier.RS1
}
}
}
}

describe("RelyingParty supports registering") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ object TestAuthenticator {
case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair()
case COSEAlgorithmIdentifier.ES256 => generateEcKeypair()
case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair()
case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair()
}

def generateEcKeypair(curve: String = "P-256"): KeyPair = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ object WebAuthnTestCodecs {
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
keyFactory.generatePrivate(spec)

case COSEAlgorithmIdentifier.RS256 =>
case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 =>
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA", new BouncyCastleCrypto().getProvider)
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
keyFactory.generatePrivate(spec)
Expand Down

0 comments on commit 4b40a5e

Please sign in to comment.