Skip to content

Commit 4b40a5e

Browse files
authored
Merge pull request #44 from Yubico/rs1
Add support for RS1 credentials
2 parents f9c0c6e + ffbcf22 commit 4b40a5e

File tree

9 files changed

+218
-18
lines changed

9 files changed

+218
-18
lines changed

NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ New features:
2222
* The new `AuthenticatorTransport` can now contain any string value as the
2323
transport identifier, as required in the editor's draft of the L2 spec. See:
2424
https://github.com/w3c/webauthn/pull/1275
25+
* Added support for RS1 credentials. Registration of RS1 credentials is not
26+
enabled by default, but can be enabled by setting
27+
`RelyingParty.preferredPubKeyCredParams` to a list containing
28+
`PublicKeyCredentialParameters.RS1`.
29+
* New constant `PublicKeyCredentialParameters.RS1`
30+
* New constant `COSEAlgorithmIdentifier.RS1`
2531

2632

2733
== Version 1.4.1 ==

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
124124
case EdDSA: return "EDDSA";
125125
case ES256: return "SHA256withECDSA";
126126
case RS256: return "SHA256withRSA";
127+
case RS1: return "SHA1withRSA";
127128
default: throw new IllegalArgumentException("Unknown algorithm: " + alg);
128129
}
129130
}

webauthn-server-core/src/main/java/com/yubico/webauthn/data/COSEAlgorithmIdentifier.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
public enum COSEAlgorithmIdentifier implements JsonLongSerializable {
4444
EdDSA(-8),
4545
ES256(-7),
46-
RS256(-257);
46+
RS256(-257),
47+
RS1(-65535);
4748

4849
@Getter
4950
private final long id;

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialParameters.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ private PublicKeyCredentialParameters(
7373
*/
7474
public static final PublicKeyCredentialParameters ES256 = builder().alg(COSEAlgorithmIdentifier.ES256).build();
7575

76+
/**
77+
* Algorithm {@link COSEAlgorithmIdentifier#RS1} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
78+
*/
79+
public static final PublicKeyCredentialParameters RS1 = builder().alg(COSEAlgorithmIdentifier.RS1).build();
80+
7681
/**
7782
* Algorithm {@link COSEAlgorithmIdentifier#RS256} and type {@link PublicKeyCredentialType#PUBLIC_KEY}.
7883
*/

webauthn-server-core/src/test/scala/com/yubico/webauthn/RegistrationTestData.scala

Lines changed: 30 additions & 11 deletions
Large diffs are not rendered by default.

webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions
4747
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
4848
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions
4949
import com.yubico.webauthn.data.RelyingPartyIdentity
50+
import com.yubico.webauthn.data.UserIdentity
5051
import com.yubico.webauthn.data.UserVerificationRequirement
5152
import com.yubico.webauthn.exception.InvalidSignatureCountException
5253
import com.yubico.webauthn.extension.appid.AppId
@@ -121,6 +122,29 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
121122

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

125+
private def credRepoWithUser(user: UserIdentity, credential: RegisteredCredential): CredentialRepository = new CredentialRepository {
126+
override def getCredentialIdsForUsername(username: String): java.util.Set[PublicKeyCredentialDescriptor] =
127+
if (username == user.getName)
128+
Set(PublicKeyCredentialDescriptor.builder().id(credential.getCredentialId).build()).asJava
129+
else Set.empty.asJava
130+
override def getUserHandleForUsername(username: String): Optional[ByteArray] =
131+
if (username == user.getName)
132+
Some(user.getId).asJava
133+
else None.asJava
134+
override def getUsernameForUserHandle(userHandle: ByteArray): Optional[String] =
135+
if (userHandle == user.getId)
136+
Some(user.getName).asJava
137+
else None.asJava
138+
override def lookup(credentialId: ByteArray, userHandle: ByteArray): Optional[RegisteredCredential] =
139+
if (credentialId == credential.getCredentialId && userHandle == user.getId)
140+
Some(credential).asJava
141+
else None.asJava
142+
override def lookupAll(credentialId: ByteArray): java.util.Set[RegisteredCredential] =
143+
if (credentialId == credential.getCredentialId)
144+
Set(credential).asJava
145+
else Set.empty.asJava
146+
}
147+
124148
def finishAssertion(
125149
allowCredentials: Option[java.util.List[PublicKeyCredentialDescriptor]] = Some(List(PublicKeyCredentialDescriptor.builder().id(Defaults.credentialId).build()).asJava),
126150
allowOriginPort: Boolean = false,
@@ -1469,6 +1493,66 @@ class RelyingPartyAssertionSpec extends FunSpec with Matchers with GeneratorDriv
14691493
result.getUserHandle should equal (registrationRequest.getUser.getId)
14701494
result.getCredentialId should equal (credId)
14711495
}
1496+
1497+
it("a generated Ed25519 key.") {
1498+
val registrationTestData = RegistrationTestData.Packed.BasicAttestationEdDsa
1499+
val testData = registrationTestData.assertion.get
1500+
1501+
val rp = RelyingParty.builder()
1502+
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
1503+
.credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
1504+
.credentialId(registrationTestData.response.getId)
1505+
.userHandle(registrationTestData.userId.getId)
1506+
.publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
1507+
.signatureCount(0)
1508+
.build()))
1509+
.build()
1510+
1511+
val result = rp.finishAssertion(FinishAssertionOptions.builder()
1512+
.request(testData.request)
1513+
.response(testData.response)
1514+
.build()
1515+
)
1516+
1517+
result.isSuccess should be (true)
1518+
result.getUserHandle should equal (registrationTestData.userId.getId)
1519+
result.getCredentialId should equal (registrationTestData.response.getId)
1520+
result.getCredentialId should equal (testData.response.getId)
1521+
}
1522+
1523+
describe("an RS1 key") {
1524+
def test(registrationTestData: RegistrationTestData): Unit = {
1525+
val testData = registrationTestData.assertion.get
1526+
1527+
val rp = RelyingParty.builder()
1528+
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
1529+
.credentialRepository(credRepoWithUser(registrationTestData.userId, RegisteredCredential.builder()
1530+
.credentialId(registrationTestData.response.getId)
1531+
.userHandle(registrationTestData.userId.getId)
1532+
.publicKeyCose(registrationTestData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey)
1533+
.signatureCount(0)
1534+
.build()))
1535+
.build()
1536+
1537+
val result = rp.finishAssertion(FinishAssertionOptions.builder()
1538+
.request(testData.request)
1539+
.response(testData.response)
1540+
.build()
1541+
)
1542+
1543+
result.isSuccess should be (true)
1544+
result.getUserHandle should equal (registrationTestData.userId.getId)
1545+
result.getCredentialId should equal (registrationTestData.response.getId)
1546+
result.getCredentialId should equal (testData.response.getId)
1547+
}
1548+
1549+
it("with basic attestation.") {
1550+
test(RegistrationTestData.Packed.BasicAttestationRs1)
1551+
}
1552+
it("with self attestation.") {
1553+
test(RegistrationTestData.Packed.SelfAttestationRs1)
1554+
}
1555+
}
14721556
}
14731557

14741558
}

webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import java.security.MessageDigest
3131
import java.security.PrivateKey
3232
import java.security.SignatureException
3333
import java.security.cert.X509Certificate
34+
import java.security.interfaces.RSAPublicKey
3435
import java.util.Optional
3536

3637
import com.fasterxml.jackson.databind.JsonNode
@@ -59,6 +60,7 @@ import com.yubico.webauthn.exception.RegistrationFailedException
5960
import com.yubico.webauthn.test.Util.toStepWithUtilities
6061
import com.yubico.webauthn.TestAuthenticator.AttestationCert
6162
import com.yubico.webauthn.TestAuthenticator.AttestationMaker
63+
import com.yubico.webauthn.data.PublicKeyCredentialParameters
6264
import javax.security.auth.x500.X500Principal
6365
import org.bouncycastle.asn1.DEROctetString
6466
import org.bouncycastle.asn1.x500.X500Name
@@ -1084,6 +1086,16 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
10841086
result should equal (Success(true))
10851087
}
10861088

1089+
it("Succeeds for an RS1 test case.") {
1090+
val testData = RegistrationTestData.Packed.BasicAttestationRs1
1091+
1092+
val result = verifier.verifyAttestationSignature(
1093+
new AttestationObject(testData.attestationObject),
1094+
testData.clientDataJsonHash
1095+
)
1096+
result should equal (true)
1097+
}
1098+
10871099
it("Fail if the default test case is mutated.") {
10881100
val testData = RegistrationTestData.Packed.BasicAttestation
10891101

@@ -1218,14 +1230,33 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
12181230
}
12191231

12201232
it("Fails if the alg is a different value.") {
1221-
val testData = RegistrationTestData.Packed.SelfAttestationWithWrongAlgValue
1233+
def modifyAuthdataPubkeyAlg(authDataBytes: Array[Byte]): Array[Byte] = {
1234+
val authData = new AuthenticatorData(new ByteArray(authDataBytes))
1235+
val key = WebAuthnCodecs.importCosePublicKey(authData.getAttestedCredentialData.get.getCredentialPublicKey).asInstanceOf[RSAPublicKey]
1236+
val reencodedKey = WebAuthnTestCodecs.rsaPublicKeyToCose(key, COSEAlgorithmIdentifier.RS256)
1237+
new ByteArray(java.util.Arrays.copyOfRange(authDataBytes, 0, 32 + 1 + 4 + 16 + 2))
1238+
.concat(authData.getAttestedCredentialData.get.getCredentialId)
1239+
.concat(reencodedKey)
1240+
.getBytes
1241+
}
1242+
def modifyAttobjPubkeyAlg(attObjBytes: ByteArray): ByteArray = {
1243+
val attObj = JacksonCodecs.cbor.readTree(attObjBytes.getBytes)
1244+
new ByteArray(JacksonCodecs.cbor.writeValueAsBytes(
1245+
attObj.asInstanceOf[ObjectNode]
1246+
.set("authData", jsonFactory.binaryNode(modifyAuthdataPubkeyAlg(attObj.get("authData").binaryValue())))
1247+
))
1248+
}
1249+
1250+
val testData = RegistrationTestData.Packed.SelfAttestationRs1
1251+
val attObj = new AttestationObject(modifyAttobjPubkeyAlg(testData.response.getResponse.getAttestationObject))
1252+
12221253
val result = Try(verifier.verifyAttestationSignature(
1223-
new AttestationObject(testData.attestationObject),
1254+
attObj,
12241255
testData.clientDataJsonHash
12251256
))
12261257

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

1274+
it("Succeeds for an RS1 test case.") {
1275+
val testData = RegistrationTestData.Packed.SelfAttestationRs1
1276+
val alg = WebAuthnCodecs.getCoseKeyAlg(testData.response.getResponse.getParsedAuthenticatorData.getAttestedCredentialData.get.getCredentialPublicKey).get
1277+
alg should be (COSEAlgorithmIdentifier.RS1)
1278+
1279+
val result = verifier.verifyAttestationSignature(
1280+
new AttestationObject(testData.attestationObject),
1281+
testData.clientDataJsonHash
1282+
)
1283+
result should equal (true)
1284+
}
1285+
12431286
it("Fails if the attestation object is mutated.") {
12441287
val testData = testDataBase.editAuthenticatorData { authData: ByteArray => new ByteArray(authData.getBytes.updated(16, if (authData.getBytes()(16) == 0) 1: Byte else 0: Byte)) }
12451288
val result = verifier.verifyAttestationSignature(
@@ -1934,7 +1977,7 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
19341977
}
19351978

19361979
describe("accept all test examples in the validExamples list.") {
1937-
RegistrationTestData.validExamples.zipWithIndex.foreach { case (testData, i) =>
1980+
RegistrationTestData.defaultSettingsValidExamples.zipWithIndex.foreach { case (testData, i) =>
19381981
it(s"Succeeds for example index ${i}.") {
19391982
val rp = {
19401983
val builder = RelyingParty.builder()
@@ -1954,6 +1997,46 @@ class RelyingPartyRegistrationSpec extends FunSpec with Matchers with GeneratorD
19541997
}
19551998
}
19561999
}
2000+
2001+
describe("generate pubKeyCredParams which") {
2002+
val rp = RelyingParty.builder()
2003+
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test RP").build())
2004+
.credentialRepository(emptyCredentialRepository)
2005+
.build()
2006+
val pkcco = rp.startRegistration(StartRegistrationOptions.builder()
2007+
.user(UserIdentity.builder()
2008+
.name("foo")
2009+
.displayName("Foo")
2010+
.id(ByteArray.fromHex("aabbccdd"))
2011+
.build())
2012+
.build())
2013+
2014+
val pubKeyCredParams = pkcco.getPubKeyCredParams.asScala
2015+
2016+
describe("include") {
2017+
it("ES256.") {
2018+
pubKeyCredParams should contain (PublicKeyCredentialParameters.ES256)
2019+
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.ES256)
2020+
}
2021+
2022+
it("EdDSA.") {
2023+
pubKeyCredParams should contain (PublicKeyCredentialParameters.EdDSA)
2024+
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.EdDSA)
2025+
}
2026+
2027+
it("RS256.") {
2028+
pubKeyCredParams should contain (PublicKeyCredentialParameters.RS256)
2029+
pubKeyCredParams map (_.getAlg) should contain (COSEAlgorithmIdentifier.RS256)
2030+
}
2031+
}
2032+
2033+
describe("do not include") {
2034+
it("RS1.") {
2035+
pubKeyCredParams should not contain PublicKeyCredentialParameters.RS1
2036+
pubKeyCredParams map (_.getAlg) should not contain COSEAlgorithmIdentifier.RS1
2037+
}
2038+
}
2039+
}
19572040
}
19582041

19592042
describe("RelyingParty supports registering") {

webauthn-server-core/src/test/scala/com/yubico/webauthn/TestAuthenticator.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ object TestAuthenticator {
586586
case COSEAlgorithmIdentifier.EdDSA => generateEddsaKeypair()
587587
case COSEAlgorithmIdentifier.ES256 => generateEcKeypair()
588588
case COSEAlgorithmIdentifier.RS256 => generateRsaKeypair()
589+
case COSEAlgorithmIdentifier.RS1 => generateRsaKeypair()
589590
}
590591

591592
def generateEcKeypair(curve: String = "P-256"): KeyPair = {

webauthn-server-core/src/test/scala/com/yubico/webauthn/WebAuthnTestCodecs.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ object WebAuthnTestCodecs {
5757
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
5858
keyFactory.generatePrivate(spec)
5959

60-
case COSEAlgorithmIdentifier.RS256 =>
60+
case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS1 =>
6161
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA", new BouncyCastleCrypto().getProvider)
6262
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
6363
keyFactory.generatePrivate(spec)

0 commit comments

Comments
 (0)