Skip to content

Commit 0763191

Browse files
authored
NIST Elliptic Curve JWKs: field element byte array padding (#903)
* Ensured NIST Elliptic Curve JWKs pre-pad their X, Y and D byte arrays with zero bytes before Base64URL-encoding if necessary per length requirements defined in: - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2 - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3 - https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1 Fixes #901.
1 parent 3e8f8a8 commit 0763191

12 files changed

+209
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ This patch release:
88
`java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882).
99
* Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a
1010
source Claims instance to a destination Claims builder. [Issue 890](https://github.com/jwtk/jjwt/issues/890).
11+
* Ensures P-256, P-384 and P-521 Elliptic Curve JWKs zero-pad their field element (`x`, `y`, and `d`) byte array values
12+
if necessary before Base64Url-encoding per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518), Sections
13+
[6.2.1.2](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2),
14+
[6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and
15+
[6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively.
16+
[Issue 901](https://github.com/jwtk/jjwt/issues/901).
1117

1218
### 0.12.3
1319

impl/src/main/java/io/jsonwebtoken/impl/lang/Bytes.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,25 @@ public static void increment(byte[] a) {
240240
}
241241
}
242242
}
243+
244+
/**
245+
* Pads the front of the specified byte array with zeros if necessary, returning a new padded result, or the
246+
* original array unmodified if padding isn't necessary. Padding is only performed if {@code length} is greater
247+
* than {@code bytes.length}.
248+
*
249+
* @param bytes the byte array to pre-pad with zeros if necessary
250+
* @param length the length of the required output array
251+
* @return the potentially pre-padded byte array, or the existing {@code bytes} array if padding wasn't necessary.
252+
* @since 0.12.4
253+
*/
254+
public static byte[] prepad(byte[] bytes, int length) {
255+
Assert.notNull(bytes, "byte array cannot be null.");
256+
Assert.gt(length, 0, "length must be positive (> 0).");
257+
if (bytes.length < length) { // need to pad with leading zero(es):
258+
byte[] padded = new byte[length];
259+
System.arraycopy(bytes, 0, padded, length - bytes.length, bytes.length);
260+
bytes = padded;
261+
}
262+
return bytes;
263+
}
243264
}

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.jsonwebtoken.impl.security;
1717

18+
import io.jsonwebtoken.impl.lang.Bytes;
1819
import io.jsonwebtoken.impl.lang.Converters;
1920
import io.jsonwebtoken.impl.lang.Parameter;
2021
import io.jsonwebtoken.io.Encoders;
@@ -24,6 +25,7 @@
2425
import java.math.BigInteger;
2526
import java.security.Key;
2627
import java.security.interfaces.ECKey;
28+
import java.security.spec.EllipticCurve;
2729
import java.util.Set;
2830

2931
abstract class AbstractEcJwkFactory<K extends Key & ECKey, J extends Jwk<K>> extends AbstractFamilyJwkFactory<K, J> {
@@ -41,19 +43,16 @@ protected static ECCurve getCurveByJwaId(String jwaCurveId) {
4143
* https://tools.ietf.org/html/rfc7518#section-6.2.1.2 indicates that this algorithm logic is defined in
4244
* http://www.secg.org/sec1-v2.pdf Section 2.3.5.
4345
*
44-
* @param fieldSize EC field size
45-
* @param coordinate EC point coordinate (e.g. x or y)
46+
* @param curve EllipticCurve
47+
* @param coordinate EC point coordinate (e.g. x or y) on the {@code curve}
4648
* @return A base64Url-encoded String representing the EC field element per the RFC format
4749
*/
4850
// Algorithm defined in http://www.secg.org/sec1-v2.pdf Section 2.3.5
49-
static String toOctetString(int fieldSize, BigInteger coordinate) {
51+
static String toOctetString(EllipticCurve curve, BigInteger coordinate) {
5052
byte[] bytes = Converters.BIGINT_UBYTES.applyTo(coordinate);
51-
int mlen = (int) Math.ceil(fieldSize / 8d);
52-
if (mlen > bytes.length) {
53-
byte[] m = new byte[mlen];
54-
System.arraycopy(bytes, 0, m, mlen - bytes.length, bytes.length);
55-
bytes = m;
56-
}
53+
int fieldSizeInBits = curve.getField().getFieldSize();
54+
int mlen = Bytes.length(fieldSizeInBits);
55+
bytes = Bytes.prepad(bytes, mlen);
5756
return Encoders.BASE64URL.encode(bytes);
5857
}
5958

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131

3232
class DefaultEcPrivateJwk extends AbstractPrivateJwk<ECPrivateKey, ECPublicKey, EcPublicJwk> implements EcPrivateJwk {
3333

34-
static final Parameter<BigInteger> D = Parameters.secretBigInt("d", "ECC Private Key");
34+
static final Parameter<BigInteger> D = Parameters.bigInt("d", "ECC Private Key")
35+
.setConverter(FieldElementConverter.B64URL_CONVERTER)
36+
.setSecret(true) // important!
37+
.build();
3538
static final Set<Parameter<?>> PARAMS = Collections.concat(DefaultEcPublicJwk.PARAMS, D);
3639

3740
DefaultEcPrivateJwk(JwkContext<ECPrivateKey> ctx, EcPublicJwk pubJwk) {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
class DefaultEcPublicJwk extends AbstractPublicJwk<ECPublicKey> implements EcPublicJwk {
3131

3232
static final String TYPE_VALUE = "EC";
33+
3334
static final Parameter<String> CRV = Parameters.string("crv", "Curve");
34-
static final Parameter<BigInteger> X = Parameters.bigInt("x", "X Coordinate").build();
35-
static final Parameter<BigInteger> Y = Parameters.bigInt("y", "Y Coordinate").build();
35+
static final Parameter<BigInteger> X = Parameters.bigInt("x", "X Coordinate")
36+
.setConverter(FieldElementConverter.B64URL_CONVERTER).build();
37+
static final Parameter<BigInteger> Y = Parameters.bigInt("y", "Y Coordinate")
38+
.setConverter(FieldElementConverter.B64URL_CONVERTER).build();
3639
static final Set<Parameter<?>> PARAMS = Collections.concat(AbstractAsymmetricJwk.PARAMS, CRV, X, Y);
3740

3841
// https://www.rfc-editor.org/rfc/rfc7638#section-3.2
@@ -52,4 +55,5 @@ static boolean equalsPublic(ParameterReadable self, Object candidate) {
5255
protected boolean equals(PublicJwk<?> jwk) {
5356
return jwk instanceof EcPublicJwk && equalsPublic(this, jwk);
5457
}
58+
5559
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535

3636
class EcPrivateJwkFactory extends AbstractEcJwkFactory<ECPrivateKey, EcPrivateJwk> {
3737

38-
private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() + " instance.";
38+
private static final String ECPUBKEY_ERR_MSG = "JwkContext publicKey must be an " + ECPublicKey.class.getName() +
39+
" instance.";
3940

4041
private static final EcPublicJwkFactory PUB_FACTORY = EcPublicJwkFactory.INSTANCE;
4142

@@ -96,8 +97,7 @@ protected EcPrivateJwk createJwkFromKey(JwkContext<ECPrivateKey> ctx) {
9697
ctx.setId(pubJwk.getId());
9798
}
9899

99-
int fieldSize = key.getParams().getCurve().getField().getFieldSize();
100-
String d = toOctetString(fieldSize, key.getS());
100+
String d = toOctetString(key.getParams().getCurve(), key.getS());
101101
ctx.put(DefaultEcPrivateJwk.D.getId(), d);
102102

103103
return new DefaultEcPrivateJwk(ctx, pubJwk);

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ protected EcPublicJwk createJwkFromKey(JwkContext<ECPublicKey> ctx) {
8181

8282
ctx.put(DefaultEcPublicJwk.CRV.getId(), curveId);
8383

84-
int fieldSize = curve.getField().getFieldSize();
85-
String x = toOctetString(fieldSize, point.getAffineX());
84+
String x = toOctetString(curve, point.getAffineX());
8685
ctx.put(DefaultEcPublicJwk.X.getId(), x);
8786

88-
String y = toOctetString(fieldSize, point.getAffineY());
87+
String y = toOctetString(curve, point.getAffineY());
8988
ctx.put(DefaultEcPublicJwk.Y.getId(), y);
9089

9190
return new DefaultEcPublicJwk(ctx);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright © 2024 jsonwebtoken.io
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.jsonwebtoken.impl.security;
17+
18+
import io.jsonwebtoken.impl.io.Codec;
19+
import io.jsonwebtoken.impl.lang.Bytes;
20+
import io.jsonwebtoken.impl.lang.Converter;
21+
import io.jsonwebtoken.impl.lang.Converters;
22+
23+
import java.math.BigInteger;
24+
25+
/**
26+
* Hotfix for <a href="https://github.com/jwtk/jjwt/issues/901">JJWT Issue 901</a>. This is currently hard-coded
27+
* expecting field elements for NIST P-256, P-384, or P-521 curves. Ideally this should be refactored to work for
28+
* <em>any</em> curve based on its field size, not just for these NIST curves. However, the
29+
* {@link EcPublicJwkFactory} and {@link EcPrivateJwkFactory} implementations only work with JWA NIST curves,
30+
* so this implementation is acceptable until (and if) different Weierstrass elliptic curves (ever) need to be
31+
* supported.
32+
*
33+
* @since 0.12.4
34+
*/
35+
final class FieldElementConverter implements Converter<BigInteger, byte[]> {
36+
37+
static final FieldElementConverter INSTANCE = new FieldElementConverter();
38+
39+
static final Converter<BigInteger, Object> B64URL_CONVERTER = Converters.forEncoded(BigInteger.class,
40+
Converters.compound(INSTANCE, Codec.BASE64URL));
41+
42+
private static int bytelen(ECCurve curve) {
43+
return Bytes.length(curve.toParameterSpec().getCurve().getField().getFieldSize());
44+
}
45+
46+
private static final int P256_BYTE_LEN = bytelen(ECCurve.P256);
47+
private static final int P384_BYTE_LEN = bytelen(ECCurve.P384);
48+
private static final int P521_BYTE_LEN = bytelen(ECCurve.P521);
49+
50+
@Override
51+
public byte[] applyTo(BigInteger bigInteger) {
52+
byte[] bytes = Converters.BIGINT_UBYTES.applyTo(bigInteger);
53+
int len = bytes.length;
54+
if (len == P256_BYTE_LEN || len == P384_BYTE_LEN || len == P521_BYTE_LEN) return bytes;
55+
if (len < P256_BYTE_LEN) {
56+
bytes = Bytes.prepad(bytes, P256_BYTE_LEN);
57+
} else if (len < P384_BYTE_LEN) {
58+
bytes = Bytes.prepad(bytes, P384_BYTE_LEN);
59+
} else { // > P-384, so must be P-521:
60+
bytes = Bytes.prepad(bytes, P521_BYTE_LEN);
61+
}
62+
return bytes;
63+
}
64+
65+
@Override
66+
public BigInteger applyFrom(byte[] bytes) {
67+
return Converters.BIGINT_UBYTES.applyFrom(bytes);
68+
}
69+
}

impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractEcJwkFactoryTest.groovy

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
*/
1616
package io.jsonwebtoken.impl.security
1717

18-
18+
import io.jsonwebtoken.impl.lang.Bytes
19+
import io.jsonwebtoken.impl.lang.Services
20+
import io.jsonwebtoken.io.Decoders
21+
import io.jsonwebtoken.io.Deserializer
22+
import io.jsonwebtoken.security.Jwks
1923
import io.jsonwebtoken.security.UnsupportedKeyException
2024
import org.junit.Test
2125

26+
import java.security.interfaces.ECPrivateKey
27+
2228
import static org.junit.Assert.assertEquals
2329
import static org.junit.Assert.fail
2430

@@ -35,4 +41,36 @@ class AbstractEcJwkFactoryTest {
3541
assertEquals msg, e.getMessage()
3642
}
3743
}
44+
45+
/**
46+
* Asserts correct behavior per https://github.com/jwtk/jjwt/issues/901
47+
*/
48+
@Test
49+
void fieldElementByteArrayLength() {
50+
51+
EcSignatureAlgorithmTest.algs().each { alg ->
52+
53+
def key = alg.keyPair().build().getPrivate() as ECPrivateKey
54+
def jwk = Jwks.builder().key(key).build()
55+
56+
def json = Jwks.UNSAFE_JSON(jwk)
57+
def map = Services.get(Deserializer).deserialize(new StringReader(json)) as Map<String, ?>
58+
def xs = map.get("x") as String
59+
def ys = map.get("y") as String
60+
def ds = map.get("d") as String
61+
62+
def x = Decoders.BASE64URL.decode(xs)
63+
def y = Decoders.BASE64URL.decode(ys)
64+
def d = Decoders.BASE64URL.decode(ds)
65+
66+
// most important part of the test: the decoded byte arrays must have a length equal to the curve
67+
// field size (in bytes):
68+
int fieldSizeInBits = key.getParams().getCurve().getField().getFieldSize()
69+
int fieldSizeInBytes = Bytes.length(fieldSizeInBits)
70+
71+
assertEquals fieldSizeInBytes, x.length
72+
assertEquals fieldSizeInBytes, y.length
73+
assertEquals fieldSizeInBytes, d.length
74+
}
75+
}
3876
}

impl/src/test/groovy/io/jsonwebtoken/impl/security/DispatchingJwkFactoryTest.groovy

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ class DispatchingJwkFactoryTest {
109109
assertTrue jwk instanceof EcPrivateJwk
110110
def key = jwk.toKey()
111111
assertTrue key instanceof ECPrivateKey
112-
String x = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineX)
113-
String y = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, jwk.toPublicJwk().toKey().w.affineY)
114-
String d = AbstractEcJwkFactory.toOctetString(key.params.curve.field.fieldSize, key.s)
112+
String x = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineX)
113+
String y = AbstractEcJwkFactory.toOctetString(key.params.curve, jwk.toPublicJwk().toKey().w.affineY)
114+
String d = AbstractEcJwkFactory.toOctetString(key.params.curve, key.s)
115115
assertEquals jwk.d.get(), d
116116

117117
//remove the 'd' mapping to represent only a public key:

0 commit comments

Comments
 (0)