Skip to content

Commit 93f91d4

Browse files
committed
Add ML-DSA to COSE encoding
1 parent 2b69839 commit 93f91d4

2 files changed

Lines changed: 76 additions & 5 deletions

File tree

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import java.security.interfaces.RSAPublicKey
8585
import java.security.spec.ECGenParameterSpec
8686
import java.security.spec.ECPoint
8787
import java.security.spec.ECPublicKeySpec
88+
import java.security.spec.NamedParameterSpec
8889
import java.security.spec.PKCS8EncodedKeySpec
8990
import java.security.spec.X509EncodedKeySpec
9091
import java.time.Instant
@@ -416,6 +417,8 @@ object TestAuthenticator {
416417
case pub: BCEdDSAPublicKey => WebAuthnTestCodecs.eddsaPublicKeyToCose(pub)
417418
case pub: RSAPublicKey =>
418419
WebAuthnTestCodecs.rsaPublicKeyToCose(pub, keyAlgorithm)
420+
case pub if pub.getAlgorithm == "ML-DSA" =>
421+
WebAuthnTestCodecs.mlDsaPublicKeyToCose(pub, keyAlgorithm)
419422
}
420423

421424
val authDataBytes: ByteArray = makeAuthDataBytes(
@@ -1098,8 +1101,15 @@ object TestAuthenticator {
10981101
): ByteArray = {
10991102
val jAlg = WebAuthnCodecs.getJavaAlgorithmName(alg)
11001103

1101-
// Need to use BouncyCastle provider here because JDK15 standard providers do not support secp256k1
1102-
val sig = Signature.getInstance(jAlg, new BouncyCastleProvider())
1104+
val sig = alg match {
1105+
case COSEAlgorithmIdentifier.ML_DSA_44 |
1106+
COSEAlgorithmIdentifier.ML_DSA_65 |
1107+
COSEAlgorithmIdentifier.ML_DSA_87 =>
1108+
Signature.getInstance(jAlg)
1109+
case _ =>
1110+
// Need to use BouncyCastle provider here because JDK15 standard providers do not support secp256k1
1111+
Signature.getInstance(jAlg, new BouncyCastleProvider())
1112+
}
11031113

11041114
sig.initSign(key)
11051115
sig.update(data.getBytes)
@@ -1117,6 +1127,10 @@ object TestAuthenticator {
11171127
case COSEAlgorithmIdentifier.RS256 | COSEAlgorithmIdentifier.RS384 |
11181128
COSEAlgorithmIdentifier.RS512 | COSEAlgorithmIdentifier.RS1 =>
11191129
generateRsaKeypair()
1130+
case COSEAlgorithmIdentifier.ML_DSA_44 |
1131+
COSEAlgorithmIdentifier.ML_DSA_65 |
1132+
COSEAlgorithmIdentifier.ML_DSA_87 =>
1133+
generateMlDsaKeypair(algorithm)
11201134
}
11211135

11221136
def generateEcKeypair(curve: String = "secp256r1"): KeyPair = {
@@ -1138,6 +1152,15 @@ object TestAuthenticator {
11381152
keyPairGenerator.generateKeyPair()
11391153
}
11401154

1155+
def generateMlDsaKeypair(
1156+
algorithm: COSEAlgorithmIdentifier
1157+
): KeyPair = {
1158+
val alg = WebAuthnCodecs.getJavaAlgorithmName(algorithm)
1159+
val keyPairGenerator = KeyPairGenerator.getInstance("ML-DSA")
1160+
keyPairGenerator.initialize(new NamedParameterSpec(alg))
1161+
keyPairGenerator.generateKeyPair()
1162+
}
1163+
11411164
def importEcKeypair(
11421165
privateBytes: ByteArray,
11431166
publicBytes: ByteArray,
@@ -1312,9 +1335,15 @@ object TestAuthenticator {
13121335
val signerBuilder = new JcaContentSignerBuilder(
13131336
WebAuthnCodecs.getJavaAlgorithmName(signingAlg)
13141337
)
1315-
.setProvider(
1316-
new BouncyCastleProvider()
1317-
) // Needed because JDK15 standard providers do not support secp256k1
1338+
signingAlg match {
1339+
case COSEAlgorithmIdentifier.ML_DSA_44 |
1340+
COSEAlgorithmIdentifier.ML_DSA_65 |
1341+
COSEAlgorithmIdentifier.ML_DSA_87 =>
1342+
case _ =>
1343+
signerBuilder.setProvider(
1344+
new BouncyCastleProvider()
1345+
) // Needed because JDK15 standard providers do not support secp256k1
1346+
}
13181347

13191348
builder.build(signerBuilder.build(signingKey)).getEncoded
13201349
})

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ object WebAuthnTestCodecs {
6464
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
6565
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
6666
keyFactory.generatePrivate(spec)
67+
68+
case COSEAlgorithmIdentifier.ML_DSA_44 =>
69+
val keyFactory: KeyFactory = KeyFactory.getInstance("ML-DSA-44")
70+
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
71+
keyFactory.generatePrivate(spec)
72+
73+
case COSEAlgorithmIdentifier.ML_DSA_65 =>
74+
val keyFactory: KeyFactory = KeyFactory.getInstance("ML-DSA-65")
75+
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
76+
keyFactory.generatePrivate(spec)
77+
78+
case COSEAlgorithmIdentifier.ML_DSA_87 =>
79+
val keyFactory: KeyFactory = KeyFactory.getInstance("ML-DSA-87")
80+
val spec = new PKCS8EncodedKeySpec(encodedKey.getBytes)
81+
keyFactory.generatePrivate(spec)
6782
}
6883

6984
def importEcdsaPrivateKey(encodedKey: ByteArray): PrivateKey = {
@@ -94,6 +109,33 @@ object WebAuthnTestCodecs {
94109
new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
95110
}
96111

112+
def mlDsaPublicKeyToCose(
113+
key: PublicKey,
114+
alg: COSEAlgorithmIdentifier,
115+
): ByteArray = {
116+
val encoded = key.getEncoded
117+
val algId: Array[Byte] =
118+
Try(encoded.slice(4, 17)).getOrElse(
119+
throw new IllegalArgumentException("Unknown ML-DSA ASN.1 OID prefix")
120+
)
121+
122+
val keyBytesLength =
123+
if (algId.sameElements(WebAuthnCodecs.ML_DSA_44_ALG_ID.getBytes))
124+
1312
125+
else if (algId.sameElements(WebAuthnCodecs.ML_DSA_65_ALG_ID.getBytes))
126+
1952
127+
else if (algId.sameElements(WebAuthnCodecs.ML_DSA_87_ALG_ID.getBytes))
128+
2592
129+
else
130+
throw new IllegalArgumentException("Unknown ML-DSA ASN.1 OID prefix")
131+
132+
val coseKey: java.util.Map[Long, Any] = new java.util.HashMap[Long, Any]
133+
coseKey.put(1L, 7L) // Key type: AKP
134+
coseKey.put(3L, alg.getId)
135+
coseKey.put(-1L, encoded.takeRight(keyBytesLength))
136+
new ByteArray(CBORObject.FromObject(coseKey).EncodeToBytes)
137+
}
138+
97139
def rsaPublicKeyToCose(
98140
key: RSAPublicKey,
99141
alg: COSEAlgorithmIdentifier,

0 commit comments

Comments
 (0)