Skip to content

Commit b1fdf31

Browse files
committed
- Ensured Edwards Curve keys (X25519 and X448) worked with ECDH-ES algorithms
- Ensured JWT Header ephemeral PublicKey ('epk' field) could be any Public JWK, not just an EcPublicJwk - Updated README.md to ensure the installation instructions for uncommenting BouncyCastle were a little less confusing (having commented out stuff be at the end of the code block so it couldn't be confused with other lines)
1 parent 64634e3 commit b1fdf31

File tree

19 files changed

+577
-140
lines changed

19 files changed

+577
-140
lines changed

README.md

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ If you're building a (non-Android) JDK project, you will want to define the foll
554554
- JDK 10 or earlier, and you want to use RSASSA-PSS (PS256, PS384, PS512) signature algorithms.
555555
- JDK 10 or earlier, and you want to use EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman encryption.
556556
- JDK 14 or earlier, and you want to use EdDSA (Ed25519 or Ed448) Elliptic Curve signature algorithms.
557+
It is unnecessary for these algorithms on JDK 15 or later.
557558
<dependency>
558559
<groupId>org.bouncycastle</groupId>
559560
<artifactId>bcprov-jdk15on</artifactId>
@@ -570,9 +571,16 @@ If you're building a (non-Android) JDK project, you will want to define the foll
570571
```groovy
571572
dependencies {
572573
implementation 'io.jsonwebtoken:jjwt-api:JJWT_RELEASE_VERSION'
573-
runtimeOnly 'io.jsonwebtoken:jjwt-impl:JJWT_RELEASE_VERSION',
574-
//'org.bouncycastle:bcprov-jdk15on:1.70',
575-
'io.jsonwebtoken:jjwt-jackson:JJWT_RELEASE_VERSION' // or 'io.jsonwebtoken:jjwt-gson:JJWT_RELEASE_VERSION' for gson
574+
runtimeOnly 'io.jsonwebtoken:jjwt-impl:JJWT_RELEASE_VERSION'
575+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:JJWT_RELEASE_VERSION' // or 'io.jsonwebtoken:jjwt-gson:JJWT_RELEASE_VERSION' for gson
576+
/*
577+
Uncomment this next dependency if you are using:
578+
- JDK 10 or earlier, and you want to use RSASSA-PSS (PS256, PS384, PS512) signature algorithms.
579+
- JDK 10 or earlier, and you want to use EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman encryption.
580+
- JDK 14 or earlier, and you want to use EdDSA (Ed25519 or Ed448) Elliptic Curve signature algorithms.
581+
It is unnecessary for these algorithms on JDK 15 or later.
582+
*/
583+
// runtimeOnly 'org.bouncycastle:bcprov-jdk15on:1.70'
576584
}
577585
```
578586

@@ -594,8 +602,13 @@ dependencies {
594602
runtimeOnly('io.jsonwebtoken:jjwt-orgjson:JJWT_RELEASE_VERSION') {
595603
exclude(group: 'org.json', module: 'json') //provided by Android natively
596604
}
597-
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms
598-
// AND also enable the BouncyCastle provider as shown below
605+
/*
606+
Uncomment this next dependency if you want to use:
607+
- RSASSA-PSS (PS256, PS384, PS512) signature algorithms.
608+
- EdECDH (X25519 or X448) Elliptic Curve Diffie-Hellman encryption.
609+
- EdDSA (Ed25519 or Ed448) Elliptic Curve signature algorithms.
610+
** AND ALSO ensure you enable the BouncyCastle provider as shown below **
611+
*/
599612
//implementation('org.bouncycastle:bcprov-jdk15on:1.70')
600613
}
601614
```
@@ -1531,7 +1544,7 @@ If you want to generate sufficiently strong Elliptic Curve or RSA asymmetric key
15311544
algorithms, use an algorithm's respective `keyPairBuilder()` method:
15321545
15331546
```java
1534-
KeyPair keyPair = Jwts.SIG.RS256.keyPairBuilder().build(); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
1547+
KeyPair keyPair = Jwts.SIG.RS256.keyPairBuilder().build(); //or RS384, RS512, PS256, etc...
15351548
```
15361549
15371550
Once you've generated a `KeyPair`, you can use the private key (`keyPair.getPrivate()`) to create a JWS and the
@@ -1540,9 +1553,12 @@ public key (`keyPair.getPublic()`) to parse/verify a JWS.
15401553
> **Note**
15411554
>
15421555
> **The `PS256`, `PS384`, and `PS512` algorithms require JDK 11 or a compatible JCA Provider
1543-
> (like BouncyCastle) in the runtime classpath.** If you are using JDK 10 or earlier and you want to use them, see
1544-
> the [Installation](#Installation) section to see how to enable BouncyCastle. All other algorithms are natively
1545-
> supported by the JDK.
1556+
> (like BouncyCastle) in the runtime classpath.**
1557+
> **The `EdDSA`, `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider
1558+
> (like BouncyCastle) in the runtime classpath.**
1559+
> If you want to use either set of algorithms, and you are on an earlier JDK that does not support them,
1560+
> see the [Installation](#Installation) section to see how to enable BouncyCastle. All other algorithms are
1561+
> natively supported by the JDK.
15461562

15471563
<a name="jws-create"></a>
15481564
### Creating a JWS
@@ -2377,7 +2393,7 @@ All `Jwk` instances support [JWK Thumbprint](https://www.rfc-editor.org/rfc/rfc7
23772393
`thumbprint()` and `thumbprint(HashAlgorithm)` methods:
23782394

23792395
```java
2380-
HashAlgorithm hashAlg = getAHashAlgorithm();
2396+
HashAlgorithm hashAlg = Jwks.HASH.SHA256; // or SHA384, SHA512, etc.
23812397

23822398
Jwk<?> jwk = Jwks.builder(). /* ... */ .build();
23832399

@@ -2388,7 +2404,7 @@ JwkThumbprint anotherThumbprint = jwk.thumbprint(hashAlg); // thumbprint using s
23882404

23892405
The resulting `JwkThumbprint` instance provides some useful methods:
23902406

2391-
* `jwkThumbprint.toByteArray()`: the thumbprint's actual digest bytes - i.e. the output from the hash algorithm
2407+
* `jwkThumbprint.toByteArray()`: the thumbprint's actual digest bytes - i.e. the raw output from the hash algorithm
23922408
* `jwkThumbprint.toString()`: the digest bytes as a Base64URL-encoded string
23932409
* `jwkThumbprint.getHashAlgorithm()`: the specific `HashAlgorithm` used to compute the thumbprint
23942410
* `jwkThumbprint.toURI()`: the thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) specification
@@ -2405,6 +2421,7 @@ For example:
24052421
```java
24062422
String kid = jwk.thumbprint().toString(); // Thumbprint bytes as a Base64URL-encoded string
24072423
Key key = findKey(kid);
2424+
assert jwk.toKey().equals(key);
24082425
```
24092426
24102427
However, because `Jwk` instances are immutable, you can't set the key id after the JWK is created. For example, the
@@ -2496,7 +2513,7 @@ This code would print the following string literal to the System console:
24962513
{kty=oct, k=<redacted>, kid=HMAC key used in https://www.rfc-editor.org/rfc/rfc7515#appendix-A.1.1 example.}
24972514
```
24982515

2499-
This is true for all secret or private key values in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`,
2516+
This is true for all secret or private key members in `SecretJwk` and `PrivateJwk` (e.g. `RsaPrivateJwk`,
25002517
`EcPrivateJwk`, etc) instances.
25012518

25022519
<a name="compression"></a>
@@ -3110,6 +3127,50 @@ String subject = Jwts.parserBuilder()
31103127
assert "Alice".equals(subject);
31113128
```
31123129

3130+
<a name="example-jws-eddsa"></a>
3131+
### JWT Signed with EdDSA
3132+
3133+
This is an example showing how to digitally sign and verify a JWT using the
3134+
[Edwards Curve Digital Signature Algorithm](https://www.rfc-editor.org/rfc/rfc8032) using
3135+
`Ed25519` or `Ed448` keys.
3136+
3137+
> **Note**
3138+
>
3139+
> **The `Ed25519` and `Ed448` algorithms require JDK 15 or a compatible JCA Provider
3140+
> (like BouncyCastle) in the runtime classpath.**
3141+
>
3142+
> If you are using JDK 14 or earlier and you want to use them, see
3143+
> the [Installation](#Installation) section to see how to enable BouncyCastle.
3144+
3145+
The `EdDSA` signature algorithm is defined for JWS in [RFC 8037, Section 3.1](https://www.rfc-editor.org/rfc/rfc8037#section-3.1)
3146+
using keys for two Edwards curves:
3147+
3148+
* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 256 bits (32 bytes) long and produce
3149+
signatures 512 bits (64 bytes) long.
3150+
* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 456 bits (57 bytes) long and produce signatures
3151+
912 bits (114 bytes) long.
3152+
3153+
In this example, Bob will sign a JWT using his Edwards Curve private key, and Alice can verify it came from Bob
3154+
using Bob's Edwards Curve public key:
3155+
3156+
```java
3157+
// Create a test key suitable for the EdDSA signature algorithm using Ed25519 or Ed448 keys:
3158+
SignatureAlgorithm alg = Jwts.SIG.Ed25519; //or Ed448
3159+
KeyPair pair = alg.keyPairBuilder().build();
3160+
3161+
// Bob creates the compact JWS with his Edwards Curve private key:
3162+
String jws = Jwts.builder().setSubject("Alice")
3163+
.signWith(pair.getPrivate(), alg) // <-- Bob's Edwards Curve private key
3164+
.compact();
3165+
3166+
// Alice receives and verifies the compact JWS came from Bob:
3167+
String subject = Jwts.parserBuilder()
3168+
.verifyWith(pair.getPublic()) // <-- Bob's Edwards Curve public key
3169+
.build().parseClaimsJws(jws).getPayload().getSubject();
3170+
3171+
assert "Alice".equals(subject);
3172+
```
3173+
31133174
<a name="example-jwe-dir"></a>
31143175
### JWT Encrypted Directly with a SecretKey
31153176

api/src/main/java/io/jsonwebtoken/JweHeader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
package io.jsonwebtoken;
1717

1818
import io.jsonwebtoken.security.AeadAlgorithm;
19-
import io.jsonwebtoken.security.EcPublicJwk;
2019
import io.jsonwebtoken.security.KeyAlgorithm;
20+
import io.jsonwebtoken.security.PublicJwk;
2121
import io.jsonwebtoken.security.StandardKeyAlgorithms;
2222

2323
import javax.crypto.SecretKey;
@@ -72,7 +72,7 @@ public interface JweHeader extends ProtectedHeader<JweHeader>, JweHeaderMutator<
7272
* @see StandardKeyAlgorithms#ECDH_ES_A192KW
7373
* @see StandardKeyAlgorithms#ECDH_ES_A256KW
7474
*/
75-
EcPublicJwk getEphemeralPublicKey();
75+
PublicJwk<?> getEphemeralPublicKey();
7676

7777
/**
7878
* Returns any information about the JWE producer for use with key agreement algorithms, or {@code null} if not

api/src/main/java/io/jsonwebtoken/lang/Strings.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,5 +1270,30 @@ public static String arrayToCommaDelimitedString(Object[] arr) {
12701270
return arrayToDelimitedString(arr, ",");
12711271
}
12721272

1273+
/**
1274+
* Appends a space character (<code>' '</code>) if the argument is not empty, otherwise does nothing. This method
1275+
* can be thought of as &quot;non-empty space&quot;. Using this method allows reduction of this:
1276+
* <blockquote><pre>
1277+
* if (sb.length != 0) {
1278+
* sb.append(' ');
1279+
* }
1280+
* sb.append(nextWord);</pre></blockquote>
1281+
* <p>To this:</p>
1282+
* <blockquote><pre>
1283+
* nespace(sb).append(nextWord);</pre></blockquote>
1284+
* @param sb the string builder to append a space to if non-empty
1285+
* @return the string builder argument for method chaining.
1286+
* @since JJWT_RELEASE_VERSION
1287+
*/
1288+
public static StringBuilder nespace(StringBuilder sb) {
1289+
if (sb == null) {
1290+
return null;
1291+
}
1292+
if (sb.length() != 0) {
1293+
sb.append(' ');
1294+
}
1295+
return sb;
1296+
}
1297+
12731298
}
12741299

api/src/main/java/io/jsonwebtoken/security/UnsupportedKeyException.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* @since JJWT_RELEASE_VERSION
2222
*/
23-
public class UnsupportedKeyException extends KeyException {
23+
public class UnsupportedKeyException extends InvalidKeyException {
2424

2525
/**
2626
* Creates a new instance with the specified explanation message.
@@ -34,8 +34,8 @@ public UnsupportedKeyException(String message) {
3434
/**
3535
* Creates a new instance with the specified explanation message and underlying cause.
3636
*
37-
* @param msg the message explaining why the exception is thrown.
38-
* @param cause the underlying cause that resulted in this exception being thrown.
37+
* @param msg the message explaining why the exception is thrown.
38+
* @param cause the underlying cause that resulted in this exception being thrown.
3939
*/
4040
public UnsupportedKeyException(String msg, Throwable cause) {
4141
super(msg, cause);

api/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ class StringsTest {
2626
assertFalse Strings.hasText(null)
2727
assertFalse Strings.hasText("")
2828
assertFalse Strings.hasText(" ")
29-
assertTrue Strings.hasText(" foo ");
29+
assertTrue Strings.hasText(" foo ")
3030
assertTrue Strings.hasText("foo")
3131
}
32-
32+
3333
@Test
3434
void testClean() {
3535
assertEquals "this is a test", Strings.clean("this is a test")
@@ -41,34 +41,55 @@ class StringsTest {
4141
assertNull Strings.clean("\t")
4242
assertNull Strings.clean(" ")
4343
}
44-
44+
4545
@Test
4646
void testCleanCharSequence() {
47-
def result = Strings.clean(new StringBuilder("this is a test"))
48-
assertNotNull result
47+
def result = Strings.clean(new StringBuilder("this is a test"))
48+
assertNotNull result
4949
assertEquals "this is a test", result.toString()
50-
50+
5151
result = Strings.clean(new StringBuilder(" this is a test"))
52-
assertNotNull result
52+
assertNotNull result
5353
assertEquals "this is a test", result.toString()
54-
54+
5555
result = Strings.clean(new StringBuilder(" this is a test "))
56-
assertNotNull result
56+
assertNotNull result
5757
assertEquals "this is a test", result.toString()
58-
58+
5959
result = Strings.clean(new StringBuilder("\nthis is a test \t "))
60-
assertNotNull result
60+
assertNotNull result
6161
assertEquals "this is a test", result.toString()
62-
62+
6363
assertNull Strings.clean((StringBuilder) null)
6464
assertNull Strings.clean(new StringBuilder(""))
6565
assertNull Strings.clean(new StringBuilder("\t"))
6666
assertNull Strings.clean(new StringBuilder(" "))
6767
}
68-
69-
68+
69+
7070
@Test
7171
void testTrimWhitespace() {
72-
assertEquals "", Strings.trimWhitespace(" ")
72+
assertEquals "", Strings.trimWhitespace(" ")
73+
}
74+
75+
@Test
76+
void testNespaceNull() {
77+
assertNull Strings.nespace(null)
78+
}
79+
80+
@Test
81+
void testNespaceEmpty() {
82+
StringBuilder sb = new StringBuilder()
83+
Strings.nespace(sb)
84+
assertEquals 0, sb.length() // didn't add space because it's already empty
85+
assertEquals '', sb.toString()
86+
}
87+
88+
@Test
89+
void testNespaceNonEmpty() {
90+
StringBuilder sb = new StringBuilder()
91+
sb.append("Hello")
92+
Strings.nespace(sb).append("World")
93+
assertEquals 'Hello World', sb.toString()
7394
}
7495
}

impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeader.java

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import io.jsonwebtoken.impl.security.JwkConverter;
2525
import io.jsonwebtoken.lang.Collections;
2626
import io.jsonwebtoken.lang.Strings;
27-
import io.jsonwebtoken.security.EcPublicJwk;
27+
import io.jsonwebtoken.security.PublicJwk;
2828

2929
import java.nio.charset.StandardCharsets;
3030
import java.util.Map;
@@ -39,9 +39,10 @@ public class DefaultJweHeader extends AbstractProtectedHeader<JweHeader> impleme
3939

4040
static final Field<String> ENCRYPTION_ALGORITHM = Fields.string("enc", "Encryption Algorithm");
4141

42-
public static final Field<EcPublicJwk> EPK = Fields.builder(EcPublicJwk.class)
42+
@SuppressWarnings("unchecked")
43+
public static final Field<PublicJwk<?>> EPK = Fields.builder((Class<PublicJwk<?>>) (Class<?>) PublicJwk.class)
4344
.setId("epk").setName("Ephemeral Public Key")
44-
.setConverter(JwkConverter.EC_PUBLIC_JWK).build();
45+
.setConverter(JwkConverter.PUBLIC_JWK).build();
4546
static final Field<byte[]> APU = Fields.bytes("apu", "Agreement PartyUInfo").build();
4647
static final Field<byte[]> APV = Fields.bytes("apv", "Agreement PartyVInfo").build();
4748

@@ -79,14 +80,8 @@ public String getEncryptionAlgorithm() {
7980
return idiomaticGet(ENCRYPTION_ALGORITHM);
8081
}
8182

82-
// @Override
83-
// public JweHeader setEncryptionAlgorithm(String enc) {
84-
// put(ENCRYPTION_ALGORITHM, enc);
85-
// return this;
86-
// }
87-
8883
@Override
89-
public EcPublicJwk getEphemeralPublicKey() {
84+
public PublicJwk<?> getEphemeralPublicKey() {
9085
return idiomaticGet(EPK);
9186
}
9287

@@ -138,12 +133,6 @@ public byte[] getPbes2Salt() {
138133
return idiomaticGet(P2S);
139134
}
140135

141-
// @Override
142-
// public JweHeader setPbes2Salt(byte[] salt) {
143-
// put(P2S, salt);
144-
// return this;
145-
// }
146-
147136
@Override
148137
public Integer getPbes2Count() {
149138
return idiomaticGet(P2C);

impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,12 @@ protected void setContext(JwkContext<K> ctx) {
5757

5858
@Override
5959
public T setProvider(Provider provider) {
60-
Assert.notNull(provider, "Provider cannot be null.");
6160
jwkContext.setProvider(provider);
6261
return self();
6362
}
6463

6564
@Override
6665
public T setRandom(SecureRandom random) {
67-
Assert.notNull(random, "SecureRandom cannot be null.");
6866
jwkContext.setRandom(random);
6967
return self();
7068
}

impl/src/main/java/io/jsonwebtoken/impl/security/Curves.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ public final class Curves {
3030
public static final Curve P_256 = new ECCurve("P-256", "secp256r1"); // JDK standard
3131
public static final Curve P_384 = new ECCurve("P-384", "secp384r1"); // JDK standard
3232
public static final Curve P_521 = new ECCurve("P-521", "secp521r1"); // JDK standard
33-
public static final EdwardsCurve X25519 = EdwardsCurve.X25519;
34-
public static final EdwardsCurve X448 = EdwardsCurve.X448;
35-
public static final EdwardsCurve Ed25519 = EdwardsCurve.Ed25519;
36-
public static final EdwardsCurve Ed448 = EdwardsCurve.Ed448;
3733

3834
private static final Collection<ECCurve> EC_CURVES = Collections.setOf((ECCurve) P_256, (ECCurve) P_384, (ECCurve) P_521);
3935

0 commit comments

Comments
 (0)