Skip to content

Commit 39899be

Browse files
authored
Merge pull request #429 from Yubico/cose-alg-id
Add support for Ed25519 and Ed448
2 parents 7ecdb1f + 5e6177c commit 39899be

17 files changed

+447
-36
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
New features:
66

77
* Added JavaDoc to `COSEAlgorithmIdentifier` constants.
8+
* Added support for Ed448 signatures.
9+
* New constants `COSEAlgorithmIdentifier.Ed25519`,
10+
`COSEAlgorithmIdentifier.Ed448` and `PublicKeyCredentialParameters.Ed448`
811
* (Experimental) Added a new suite of interfaces, starting with
912
`CredentialRepositoryV2`. `RelyingParty` can now be configured with a
1013
`CredentialRepositoryV2` instance instead of a `CredentialRepository`

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ public class RelyingParty {
212212
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#EdDSA EdDSA}
213213
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES384 ES384}
214214
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#ES512 ES512}
215+
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#Ed448 Ed448}
215216
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS256 RS256}
216217
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS384 RS384}
217218
* <li>{@link com.yubico.webauthn.data.PublicKeyCredentialParameters#RS512 RS512}
@@ -230,6 +231,7 @@ public class RelyingParty {
230231
PublicKeyCredentialParameters.EdDSA,
231232
PublicKeyCredentialParameters.ES384,
232233
PublicKeyCredentialParameters.ES512,
234+
PublicKeyCredentialParameters.Ed448,
233235
PublicKeyCredentialParameters.RS256,
234236
PublicKeyCredentialParameters.RS384,
235237
PublicKeyCredentialParameters.RS512));
@@ -425,7 +427,12 @@ static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
425427
try {
426428
switch (param.getAlg()) {
427429
case EdDSA:
428-
KeyFactory.getInstance("EdDSA");
430+
case Ed25519:
431+
KeyFactory.getInstance("Ed25519");
432+
break;
433+
434+
case Ed448:
435+
KeyFactory.getInstance("Ed448");
429436
break;
430437

431438
case ES256:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ public class RelyingPartyV2<C extends CredentialRecord> {
226226
* <li>{@link PublicKeyCredentialParameters#EdDSA EdDSA}
227227
* <li>{@link PublicKeyCredentialParameters#ES384 ES384}
228228
* <li>{@link PublicKeyCredentialParameters#ES512 ES512}
229+
* <li>{@link PublicKeyCredentialParameters#Ed448 Ed448}
229230
* <li>{@link PublicKeyCredentialParameters#RS256 RS256}
230231
* <li>{@link PublicKeyCredentialParameters#RS384 RS384}
231232
* <li>{@link PublicKeyCredentialParameters#RS512 RS512}
@@ -243,6 +244,7 @@ public class RelyingPartyV2<C extends CredentialRecord> {
243244
PublicKeyCredentialParameters.EdDSA,
244245
PublicKeyCredentialParameters.ES384,
245246
PublicKeyCredentialParameters.ES512,
247+
PublicKeyCredentialParameters.Ed448,
246248
PublicKeyCredentialParameters.RS256,
247249
PublicKeyCredentialParameters.RS384,
248250
PublicKeyCredentialParameters.RS512));

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

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ final class WebAuthnCodecs {
5959
private static final ByteArray P512_CURVE_OID =
6060
new ByteArray(new byte[] {0x2B, (byte) 0x81, 0x04, 0, 35}); // OID 1.3.132.0.35
6161

62-
private static final ByteArray ED25519_ALG_ID =
62+
static final ByteArray ED25519_ALG_ID =
6363
new ByteArray(
6464
new byte[] {
6565
// SEQUENCE (5 bytes)
@@ -74,6 +74,28 @@ final class WebAuthnCodecs {
7474
112
7575
});
7676

77+
static final ByteArray ED448_ALG_ID =
78+
new ByteArray(
79+
new byte[] {
80+
// SEQUENCE (5 bytes)
81+
0x30,
82+
5,
83+
// OID (3 bytes)
84+
0x06,
85+
3,
86+
// OID 1.3.101.113
87+
0x2B,
88+
101,
89+
113
90+
});
91+
92+
// See: https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
93+
static final int COSE_CRV_P256 = 1;
94+
static final int COSE_CRV_P384 = 2;
95+
static final int COSE_CRV_P521 = 3;
96+
static final int COSE_CRV_ED25519 = 6;
97+
static final int COSE_CRV_ED448 = 7;
98+
7799
static ByteArray ecPublicKeyToRaw(ECPublicKey key) {
78100

79101
final int fieldSizeBytes =
@@ -120,15 +142,15 @@ static ByteArray rawEcKeyToCose(ByteArray key) {
120142
switch (len - start) {
121143
case 64:
122144
coseAlg = COSEAlgorithmIdentifier.ES256;
123-
coseCrv = 1;
145+
coseCrv = COSE_CRV_P256;
124146
break;
125147
case 96:
126148
coseAlg = COSEAlgorithmIdentifier.ES384;
127-
coseCrv = 2;
149+
coseCrv = COSE_CRV_P384;
128150
break;
129151
case 132:
130152
coseAlg = COSEAlgorithmIdentifier.ES512;
131-
coseCrv = 3;
153+
coseCrv = COSE_CRV_P521;
132154
break;
133155
default:
134156
throw new RuntimeException(
@@ -178,15 +200,15 @@ private static PublicKey importCoseEcdsaPublicKey(CBORObject cose)
178200

179201
final byte[] curveOid;
180202
switch (crv) {
181-
case 1:
203+
case COSE_CRV_P256:
182204
curveOid = P256_CURVE_OID.getBytes();
183205
break;
184206

185-
case 2:
207+
case COSE_CRV_P384:
186208
curveOid = P384_CURVE_OID.getBytes();
187209
break;
188210

189-
case 3:
211+
case COSE_CRV_P521:
190212
curveOid = P512_CURVE_OID.getBytes();
191213
break;
192214

@@ -214,30 +236,40 @@ private static PublicKey importCoseEcdsaPublicKey(CBORObject cose)
214236

215237
private static PublicKey importCoseEdDsaPublicKey(CBORObject cose)
216238
throws InvalidKeySpecException, NoSuchAlgorithmException {
239+
final int alg = cose.get(CBORObject.FromObject(3)).AsInt32();
217240
final int curveId = cose.get(CBORObject.FromObject(-1)).AsInt32();
218-
switch (curveId) {
219-
case 6:
220-
return importCoseEd25519PublicKey(cose);
221-
default:
222-
throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId);
223-
}
224-
}
225-
226-
private static PublicKey importCoseEd25519PublicKey(CBORObject cose)
227-
throws InvalidKeySpecException, NoSuchAlgorithmException {
241+
final ByteArray algorithmOid = coseCurveToEddsaAlgorithmOid(curveId);
228242
final byte[] rawKey = cose.get(CBORObject.FromObject(-2)).GetByteString();
229243
final byte[] x509Key =
230244
BinaryUtil.encodeDerSequence(
231-
ED25519_ALG_ID.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey));
245+
algorithmOid.getBytes(), BinaryUtil.encodeDerBitStringWithZeroUnused(rawKey));
232246

233-
KeyFactory kFact = KeyFactory.getInstance("EdDSA");
247+
KeyFactory kFact =
248+
KeyFactory.getInstance(
249+
getJavaAlgorithmName(
250+
COSEAlgorithmIdentifier.fromId(alg)
251+
.orElseThrow(() -> new IllegalArgumentException("Unknown algorithm: " + alg))));
234252
return kFact.generatePublic(new X509EncodedKeySpec(x509Key));
235253
}
236254

255+
private static ByteArray coseCurveToEddsaAlgorithmOid(int curveId) {
256+
switch (curveId) {
257+
case COSE_CRV_ED25519:
258+
return ED25519_ALG_ID;
259+
case COSE_CRV_ED448:
260+
return ED448_ALG_ID;
261+
default:
262+
throw new IllegalArgumentException("Unsupported EdDSA curve: " + curveId);
263+
}
264+
}
265+
237266
static String getJavaAlgorithmName(COSEAlgorithmIdentifier alg) {
238267
switch (alg) {
239268
case EdDSA:
240-
return "EDDSA";
269+
case Ed25519:
270+
return "Ed25519";
271+
case Ed448:
272+
return "Ed448";
241273
case ES256:
242274
return "SHA256withECDSA";
243275
case ES384:

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,40 @@ public enum COSEAlgorithmIdentifier {
6262
*/
6363
EdDSA(-8),
6464

65+
/**
66+
* The signature scheme Ed25519 as defined in <a href="https://www.rfc-editor.org/rfc/rfc8032">RFC
67+
* 8032</a>.
68+
*
69+
* <p>This value is NOT RECOMMENDED, see the <a
70+
* href="https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-pubkeycredparams">documentation
71+
* of <code>pubKeyCredParams</code></a>. Use {@link #EdDSA} instead or in addition.
72+
*
73+
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
74+
* registry</a>
75+
* @see <a
76+
* href="https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-13.html#name-edwards-curve-digital-signa">Fully-Specified
77+
* Algorithms for JOSE and COSE</a>
78+
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-alg-identifier">WebAuthn
79+
* §5.8.5. Cryptographic Algorithm Identifier (typedef <code>COSEAlgorithmIdentifier</code>
80+
* )</a>
81+
*/
82+
Ed25519(-19),
83+
84+
/**
85+
* The signature scheme Ed448 as defined in <a href="https://www.rfc-editor.org/rfc/rfc8032">RFC
86+
* 8032</a>.
87+
*
88+
* @see <a href="https://www.iana.org/assignments/cose/cose.xhtml#algorithms">COSE Algorithms
89+
* registry</a>
90+
* @see <a
91+
* href="https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-13.html#name-edwards-curve-digital-signa">Fully-Specified
92+
* Algorithms for JOSE and COSE</a>
93+
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-alg-identifier">WebAuthn
94+
* §5.8.5. Cryptographic Algorithm Identifier (typedef <code>COSEAlgorithmIdentifier</code>
95+
* )</a>
96+
*/
97+
Ed448(-53),
98+
6599
/**
66100
* ECDSA with SHA-256 on the NIST P-256 curve.
67101
*

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,12 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
522522
try {
523523
switch (param.getAlg()) {
524524
case EdDSA:
525-
KeyFactory.getInstance("EdDSA");
525+
case Ed25519:
526+
KeyFactory.getInstance("Ed25519");
527+
break;
528+
529+
case Ed448:
530+
KeyFactory.getInstance("Ed448");
526531
break;
527532

528533
case ES256:
@@ -552,7 +557,12 @@ private static List<PublicKeyCredentialParameters> filterAvailableAlgorithms(
552557
try {
553558
switch (param.getAlg()) {
554559
case EdDSA:
555-
Signature.getInstance("EDDSA");
560+
case Ed25519:
561+
Signature.getInstance("Ed25519");
562+
break;
563+
564+
case Ed448:
565+
Signature.getInstance("Ed448");
556566
break;
557567

558568
case ES256:

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ private PublicKeyCredentialParameters(
7474
public static final PublicKeyCredentialParameters EdDSA =
7575
builder().alg(COSEAlgorithmIdentifier.EdDSA).build();
7676

77+
/**
78+
* Algorithm {@link COSEAlgorithmIdentifier#Ed448} and type {@link
79+
* PublicKeyCredentialType#PUBLIC_KEY}.
80+
*/
81+
public static final PublicKeyCredentialParameters Ed448 =
82+
builder().alg(COSEAlgorithmIdentifier.Ed448).build();
83+
7784
/**
7885
* Algorithm {@link COSEAlgorithmIdentifier#ES256} and type {@link
7986
* PublicKeyCredentialType#PUBLIC_KEY}.

webauthn-server-core/src/test/java/com/yubico/webauthn/RelyingPartyTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.yubico.webauthn.attestation.AttestationTrustSource;
88
import com.yubico.webauthn.data.AttestationConveyancePreference;
99
import com.yubico.webauthn.data.ByteArray;
10+
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
1011
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
1112
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
1213
import com.yubico.webauthn.data.PublicKeyCredentialParameters;
@@ -18,6 +19,7 @@
1819
import java.security.Provider;
1920
import java.security.Security;
2021
import java.security.cert.X509Certificate;
22+
import java.util.Arrays;
2123
import java.util.Collections;
2224
import java.util.HashSet;
2325
import java.util.List;
@@ -29,6 +31,7 @@
2931
import org.junit.Before;
3032
import org.junit.Test;
3133
import uk.org.lidalia.slf4jext.Level;
34+
import uk.org.lidalia.slf4jtest.LoggingEvent;
3235
import uk.org.lidalia.slf4jtest.TestLogger;
3336
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
3437

@@ -306,6 +309,29 @@ public void doesNotLogWarningIfAllAlgorithmsAvailable() {
306309
assertEquals(0, testLog.getAllLoggingEvents().size());
307310
}
308311

312+
@Test
313+
public void doesNotLogUnknownAlgorithmWarningForDefinedAlgorithms() {
314+
RelyingParty.builder()
315+
.identity(RelyingPartyIdentity.builder().id("localhost").name("Test").build())
316+
.credentialRepository(unimplementedCredentialRepository())
317+
.preferredPubkeyParams(
318+
Arrays.stream(COSEAlgorithmIdentifier.values())
319+
.map(alg -> PublicKeyCredentialParameters.builder().alg(alg).build())
320+
.collect(Collectors.toList()))
321+
.build();
322+
323+
for (LoggingEvent event : testLog.getLoggingEvents()) {
324+
final String msg = event.getMessage().toLowerCase();
325+
if (msg.contains("bug") || msg.contains("unknown")) {
326+
fail(
327+
String.format(
328+
"Expected no log messages mentioning \"bug\" or \"unknown\", found: %s [arguments: %s]",
329+
event.getMessage(), event.getArguments()));
330+
}
331+
}
332+
;
333+
}
334+
309335
private static CredentialRepository unimplementedCredentialRepository() {
310336
return new CredentialRepository() {
311337
@Override

webauthn-server-core/src/test/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptionsTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import static org.junit.Assert.assertEquals;
44
import static org.junit.Assert.assertTrue;
5+
import static org.junit.Assert.fail;
56

67
import com.yubico.webauthn.data.exception.HexException;
78
import java.security.Provider;
89
import java.security.Security;
10+
import java.util.Arrays;
911
import java.util.Collections;
1012
import java.util.List;
1113
import java.util.Optional;
@@ -15,6 +17,7 @@
1517
import org.junit.Before;
1618
import org.junit.Test;
1719
import uk.org.lidalia.slf4jext.Level;
20+
import uk.org.lidalia.slf4jtest.LoggingEvent;
1821
import uk.org.lidalia.slf4jtest.TestLogger;
1922
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
2023

@@ -156,4 +159,33 @@ public void doesNotLogWarningIfAllAlgorithmsAvailable() throws HexException {
156159

157160
assertEquals(0, testLog.getAllLoggingEvents().size());
158161
}
162+
163+
@Test
164+
public void doesNotLogUnknownAlgorithmWarningForDefinedAlgorithms() throws HexException {
165+
PublicKeyCredentialCreationOptions.builder()
166+
.rp(RelyingPartyIdentity.builder().id("localhost").name("Test").build())
167+
.user(
168+
UserIdentity.builder()
169+
.name("foo")
170+
.displayName("Foo User")
171+
.id(ByteArray.fromHex("00010203"))
172+
.build())
173+
.challenge(ByteArray.fromHex("04050607"))
174+
.pubKeyCredParams(
175+
Arrays.stream(COSEAlgorithmIdentifier.values())
176+
.map(alg -> PublicKeyCredentialParameters.builder().alg(alg).build())
177+
.collect(Collectors.toList()))
178+
.build();
179+
180+
for (LoggingEvent event : testLog.getLoggingEvents()) {
181+
final String msg = event.getMessage().toLowerCase();
182+
if (msg.contains("bug") || msg.contains("unknown")) {
183+
fail(
184+
String.format(
185+
"Expected no log messages mentioning \"bug\" or \"unknown\", found: %s [arguments: %s]",
186+
event.getMessage(), event.getArguments()));
187+
}
188+
}
189+
;
190+
}
159191
}

0 commit comments

Comments
 (0)