Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ public class RelyingParty {
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256}
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS384 RS384}
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS512 RS512}
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_44 ML_DSA_44}
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_65 ML_DSA_65}
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ML_DSA_87 ML_DSA_87}
* </ol>
*
* @since 0.2.0
Expand All @@ -234,7 +237,10 @@ public class RelyingParty {
PublicKeyCredentialParameters.Ed448,
PublicKeyCredentialParameters.RS256,
PublicKeyCredentialParameters.RS384,
PublicKeyCredentialParameters.RS512));
PublicKeyCredentialParameters.RS512,
PublicKeyCredentialParameters.ML_DSA_44,
PublicKeyCredentialParameters.ML_DSA_65,
PublicKeyCredentialParameters.ML_DSA_87));

/**
* If <code>true</code>, the origin matching rule is relaxed to allow any port number.
Expand Down Expand Up @@ -448,6 +454,18 @@ static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
KeyFactory.getInstance("RSA");
break;

case ML_DSA_44:
KeyFactory.getInstance("ML-DSA-44");
break;

case ML_DSA_65:
KeyFactory.getInstance("ML-DSA-65");
break;

case ML_DSA_87:
KeyFactory.getInstance("ML-DSA-87");
break;

default:
log.warn(
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ public class RelyingPartyV2<C extends CredentialRecord> {
* <li>{@link PublicKeyCredentialParameters#RS256 RS256}
* <li>{@link PublicKeyCredentialParameters#RS384 RS384}
* <li>{@link PublicKeyCredentialParameters#RS512 RS512}
* <li>{@link PublicKeyCredentialParameters#ML_DSA_44 ML_DSA_44}
* <li>{@link PublicKeyCredentialParameters#ML_DSA_65 ML_DSA_65}
* <li>{@link PublicKeyCredentialParameters#ML_DSA_87 ML_DSA_87}
* </ol>
*
* @see PublicKeyCredentialCreationOptions#getAttestation()
Expand All @@ -247,7 +250,10 @@ public class RelyingPartyV2<C extends CredentialRecord> {
PublicKeyCredentialParameters.Ed448,
PublicKeyCredentialParameters.RS256,
PublicKeyCredentialParameters.RS384,
PublicKeyCredentialParameters.RS512));
PublicKeyCredentialParameters.RS512,
PublicKeyCredentialParameters.ML_DSA_44,
PublicKeyCredentialParameters.ML_DSA_65,
PublicKeyCredentialParameters.ML_DSA_87));

/**
* If <code>true</code>, the origin matching rule is relaxed to allow any port number.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,69 @@ final class WebAuthnCodecs {
113
});

static final ByteArray ML_DSA_44_ALG_ID =
new ByteArray(
new byte[] {
// SEQUENCE (11 bytes)
0x30,
0x0B,
// OID (9 bytes)
0x06,
0x09,
// OID 2.16.840.1.101.3.4.3.17
0x60,
(byte) 0x86,
0x48,
0x01,
0x65,
0x03,
0x04,
0x03,
0x11
});

static final ByteArray ML_DSA_65_ALG_ID =
new ByteArray(
new byte[] {
// SEQUENCE (11 bytes)
0x30,
0x0B,
// OID (9 bytes)
0x06,
0x09,
// OID 2.16.840.1.101.3.4.3.18
0x60,
(byte) 0x86,
0x48,
0x01,
0x65,
0x03,
0x04,
0x03,
0x12
});

static final ByteArray ML_DSA_87_ALG_ID =
new ByteArray(
new byte[] {
// SEQUENCE (11 bytes)
0x30,
0x0B,
// OID (9 bytes)
0x06,
0x09,
// OID 2.16.840.1.101.3.4.3.19
0x60,
(byte) 0x86,
0x48,
0x01,
0x65,
0x03,
0x04,
0x03,
0x13
});

// See: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
static final int COSE_CRV_P256 = 1;
static final int COSE_CRV_P384 = 2;
Expand Down Expand Up @@ -178,6 +241,8 @@ static PublicKey importCosePublicKey(ByteArray key)
return importCoseEcdsaPublicKey(cose);
case 3:
return importCoseRsaPublicKey(cose);
case 7:
return importCoseMlDsaPublicKey(cose);
default:
throw new IllegalArgumentException("Unsupported key type: " + kty);
}
Expand Down Expand Up @@ -263,6 +328,36 @@ private static ByteArray coseCurveToEddsaAlgorithmOid(int curveId) {
}
}

private static PublicKey importCoseMlDsaPublicKey(CBORObject cose)
throws InvalidKeySpecException, NoSuchAlgorithmException {
final int alg = cose.get(CBORObject.FromObject(3)).AsInt32();
final ByteArray algorithmId = mlDsaAlgorithmId(alg);
final byte[] rawKey = cose.get(CBORObject.FromObject(-1)).GetByteString();
final byte[] x509Key =
BinaryUtil.encodeDerSequence(
algorithmId.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey));

KeyFactory kFact =
KeyFactory.getInstance(
getJavaAlgorithmName(
COSEAlgorithmIdentifier.fromId(alg)
.orElseThrow(() -> new IllegalArgumentException("Unknown algorithm: " + alg))));
return kFact.generatePublic(new X509EncodedKeySpec(x509Key));
}

private static ByteArray mlDsaAlgorithmId(int alg) {
switch (alg) {
case -48:
return ML_DSA_44_ALG_ID;
case -49:
return ML_DSA_65_ALG_ID;
case -50:
return ML_DSA_87_ALG_ID;
default:
throw new IllegalArgumentException("Unsupported ML-DSA algorithm: " + alg);
}
}

static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
switch (alg) {
case EdDSA:
Expand All @@ -284,6 +379,12 @@ static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
return "SHA512withRSA";
case RS1:
return "SHA1withRSA";
case ML_DSA_44:
return "ML-DSA-44";
case ML_DSA_65:
return "ML-DSA-65";
case ML_DSA_87:
return "ML-DSA-87";
default:
throw new IllegalArgumentException("Unknown algorithm: " + alg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,40 @@ public enum COSEAlgorithmIdentifier {
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
* registry</a>
*/
RS1(-65535);
RS1(-65535),

/**
* ML-DSA-44 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
* registry</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
*/
ML_DSA_44(-48),

/**
* ML-DSA-65 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
* registry</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
*/
ML_DSA_65(-49),

/**
* ML-DSA-87 as defined in <a href="https://csrc.nist.gov/pubs/fips/204/final">NIST FIPS 204</a>.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
* registry</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9964">RFC 9964</a>
*/
ML_DSA_87(-50);

@JsonValue @Getter private final long id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,18 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
KeyFactory.getInstance("RSA");
break;

case ML_DSA_44:
KeyFactory.getInstance("ML-DSA-44");
break;

case ML_DSA_65:
KeyFactory.getInstance("ML-DSA-65");
break;

case ML_DSA_87:
KeyFactory.getInstance("ML-DSA-87");
break;

default:
log.warn(
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());
Expand Down Expand Up @@ -593,6 +605,18 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
Signature.getInstance("SHA1withRSA");
break;

case ML_DSA_44:
Signature.getInstance("ML-DSA-44");
break;

case ML_DSA_65:
Signature.getInstance("ML-DSA-65");
break;

case ML_DSA_87:
Signature.getInstance("ML-DSA-87");
break;

default:
log.warn(
"Unknown algorithm: {}. Please file a bug report.", param.getAlg());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,33 @@ private PublicKeyCredentialParameters(
public static final PublicKeyCredentialParameters RS512 =
builder().alg(COSEAlgorithmIdentifier.RS512).build();

/**
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_44} and type {@link
* PublicKeyCredentialType#PUBLIC_KEY}.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*/
public static final PublicKeyCredentialParameters ML_DSA_44 =
builder().alg(COSEAlgorithmIdentifier.ML_DSA_44).build();

/**
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_65} and type {@link
* PublicKeyCredentialType#PUBLIC_KEY}.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*/
public static final PublicKeyCredentialParameters ML_DSA_65 =
builder().alg(COSEAlgorithmIdentifier.ML_DSA_65).build();

/**
* Algorithm {@link COSEAlgorithmIdentifier#ML_DSA_87} and type {@link
* PublicKeyCredentialType#PUBLIC_KEY}.
*
* <p>Note: This algorithm requires a Java runtime that supports ML-DSA (Java 24 or later).
*/
public static final PublicKeyCredentialParameters ML_DSA_87 =
builder().alg(COSEAlgorithmIdentifier.ML_DSA_87).build();

public static PublicKeyCredentialParametersBuilder.MandatoryStages builder() {
return new PublicKeyCredentialParametersBuilder.MandatoryStages();
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import java.io.IOException
import java.nio.charset.Charset
import java.security.KeyFactory
import java.security.KeyPair
import java.security.MessageDigest
import java.security.interfaces.ECPublicKey
Expand Down Expand Up @@ -2925,6 +2926,62 @@ class RelyingPartyAssertionSpec
)
}

for { algName <- List("ML-DSA-44", "ML-DSA-65", "ML-DSA-87") } it(
s"a generated ${algName} key, when available."
) {
assume(Try(KeyFactory.getInstance(algName)).isSuccess)

val registrationTestData = algName match {
case "ML-DSA-44" =>
RegistrationTestData.Packed.BasicAttestationMlDsa44
case "ML-DSA-65" =>
RegistrationTestData.Packed.BasicAttestationMlDsa65
case "ML-DSA-87" =>
RegistrationTestData.Packed.BasicAttestationMlDsa87
}
val testData = registrationTestData.assertion.get

val rp = RelyingParty
.builder()
.identity(
RelyingPartyIdentity.builder().id("localhost").name("Test RP").build()
)
.credentialRepository(
Helpers.CredentialRepository.withUser(
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.getCredential.getUserHandle should equal(
registrationTestData.userId.getId
)
result.getCredential.getCredentialId should equal(
registrationTestData.response.getId
)
result.getCredential.getCredentialId should equal(
testData.response.getId
)
}

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