Skip to content

Commit

Permalink
- removed DynamicJwkBuilder chain methods with array arguments
Browse files Browse the repository at this point in the history
- added generic DynamicJwkBuilder#keyPair(KeyPair) method
- added generic DynamicJwkBuilder#chain method
  • Loading branch information
lhazlewood committed Aug 8, 2023
1 parent 4138b91 commit 083da95
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 76 deletions.
143 changes: 93 additions & 50 deletions api/src/main/java/io/jsonwebtoken/security/DynamicJwkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,48 @@
*/
public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkBuilder<K, J, DynamicJwkBuilder<K, J>> {

/**
* Ensures the builder will create a {@link PublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at array index 0) <em>MUST</em> contain a {@link PublicKey}
* instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method.
*
* <p>This method is provided for congruence with the other {@code chain} methods and is expected to be used when
* the calling code has a variable {@code PublicKey} reference. Based on the argument type, it will
* delegate to one of the following methods if possible:
* <ul>
* <li>{@link #rsaChain(List)}</li>
* <li>{@link #ecChain(List)}</li>
* <li>{@link #octetChain(List)}</li>
* </ul>
*
* <p>If the specified {@code chain} argument is not capable of being supported by one of those methods, an
* {@link UnsupportedKeyException} will be thrown.</p>
*
* <p><b>Type Parameters</b></p>
*
* <p>In addition to the public key type <code>A</code>, the public key's associated private key type
* <code>B</code> is parameterized as well. This ensures that any subsequent call to the builder's
* {@link PublicJwkBuilder#privateKey(PrivateKey) privateKey} method will be type-safe. For example:</p>
*
* <blockquote><pre>Jwks.builder().&lt;EdECPublicKey, <b>EdECPrivateKey</b>&gt;chain(edECPublicKeyX509CertificateChain)
* .privateKey(<b>aPrivateKey</b>) // &lt;-- must be an EdECPrivateKey instance
* ... etc ...
* .build();</pre></blockquote>
*
* @param <A> the type of {@link PublicKey} provided by the created public JWK.
* @param <B> the type of {@link PrivateKey} that may be paired with the {@link PublicKey} to produce a
* {@link PrivateJwk} if desired.
* @param chain the {@link X509Certificate} chain to inspect to find the {@link PublicKey} to represent as a
* {@link PublicJwk}.
* @return the builder coerced as a {@link PublicJwkBuilder} for continued method chaining.
* @throws UnsupportedKeyException if the specified key is not a supported type and cannot be used to delegate to
* other {@code key} methods.
* @see PublicJwk
* @see PrivateJwk
*/
<A extends PublicKey, B extends PrivateKey> PublicJwkBuilder<A, B, ?, ?, ?, ?> chain(List<X509Certificate> chain)
throws UnsupportedKeyException;

/**
* Ensures the builder will create a {@link SecretJwk} for the specified Java {@link SecretKey}.
*
Expand Down Expand Up @@ -87,7 +129,6 @@ public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkB
*/
EcPrivateJwkBuilder key(ECPrivateKey key);


/**
* Ensures the builder will create a {@link PublicJwk} for the specified Java {@link PublicKey} argument. This
* method is provided for congruence with the other {@code key} methods and is expected to be used when
Expand Down Expand Up @@ -161,6 +202,45 @@ public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkB
*/
<A extends PublicKey, B extends PrivateKey> PrivateJwkBuilder<B, A, ?, ?, ?> key(B key) throws UnsupportedKeyException;

/**
* Ensures the builder will create a {@link PrivateJwk} for the specified Java {@link KeyPair} argument. This
* method is provided for congruence with the other {@code keyPair} methods and is expected to be used when
* the calling code has a variable {@code PrivateKey} reference. Based on the argument's {@code PrivateKey} type,
* it will delegate to one of the following methods if possible:
* <ul>
* <li>{@link #key(RSAPrivateKey)}</li>
* <li>{@link #key(ECPrivateKey)}</li>
* <li>{@link #octetKey(PrivateKey)}</li>
* </ul>
* <p>and automatically set the resulting builder's {@link PrivateJwkBuilder#publicKey(PublicKey) publicKey} with
* the pair's {@code PublicKey}.</p>
*
* <p>If the specified {@code key} argument is not capable of being supported by one of those methods, an
* {@link UnsupportedKeyException} will be thrown.</p>
*
* <p><b>Type Parameters</b></p>
*
* <p>In addition to the private key type <code>B</code>, the private key's associated public key type
* <code>A</code> is parameterized as well. This ensures that any subsequent call to the builder's
* {@link PrivateJwkBuilder#publicKey(PublicKey) publicKey} method will be type-safe. For example:</p>
*
* <blockquote><pre>Jwks.builder().&lt;<b>EdECPublicKey</b>, EdECPrivateKey&gt;keyPair(anEdECKeyPair)
* .publicKey(<b>aPublicKey</b>) // &lt;-- must be an EdECPublicKey instance
* ... etc ...
* .build();</pre></blockquote>
*
* @param <A> the {@code keyPair} argument's {@link PublicKey} type
* @param <B> the {@code keyPair} argument's {@link PrivateKey} type
* @param keyPair the {@code KeyPair} containing the public and private key
* @return the builder coerced as a {@link PrivateJwkBuilder} for continued method chaining.
* @throws UnsupportedKeyException if the specified {@code KeyPair}'s keys are not supported and cannot be used to
* delegate to other {@code key} methods.
* @see PublicJwk
* @see PrivateJwk
*/
<A extends PublicKey, B extends PrivateKey> PrivateJwkBuilder<B, A, ?, ?, ?> keyPair(KeyPair keyPair)
throws UnsupportedKeyException;

/**
* Ensures the builder will create an {@link OctetPublicJwk} for the specified Edwards-curve {@code PublicKey}
* argument. The {@code PublicKey} must be an instance of one of the following:
Expand Down Expand Up @@ -227,22 +307,6 @@ public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkB
*/
<A extends PrivateKey, B extends PublicKey> OctetPrivateJwkBuilder<A, B> octetKey(A key);

/**
* Ensures the builder will create an {@link OctetPrivateJwk} for the specified Java Edwards-curve
* {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} <em>MUST</em> be an
* Edwards-curve public key as defined by {@link #octetKey(PublicKey)}. The pair's
* {@link KeyPair#getPrivate() private key} <em>MUST</em> be an Edwards-curve private key as defined by
* {@link #octetKey(PrivateKey)}.
*
* @param <A> the type of Edwards-curve {@link PublicKey} contained in the key pair.
* @param <B> the type of the Edwards-curve {@link PrivateKey} contained in the key pair.
* @param keyPair the Edwards-curve {@link KeyPair} to represent as an {@link OctetPrivateJwk}.
* @return the builder coerced as an {@link OctetPrivateJwkBuilder} for continued method chaining.
* @throws IllegalArgumentException if the {@code keyPair} does not contain Edwards-curve public and private key
* instances.
*/
<A extends PrivateKey, B extends PublicKey> OctetPrivateJwkBuilder<A, B> octetKeyPair(KeyPair keyPair);

/**
* Ensures the builder will create an {@link OctetPublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at list index 0) <em>MUST</em>
Expand All @@ -259,30 +323,20 @@ public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkB
<A extends PublicKey, B extends PrivateKey> OctetPublicJwkBuilder<A, B> octetChain(List<X509Certificate> chain);

/**
* Ensures the builder will create an {@link OctetPublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at array index 0) <em>MUST</em>
* {@link X509Certificate#getPublicKey() contain} an Edwards-curve public key as defined by
* {@link #octetKey(PublicKey)}.
*
* @param <A> the type of Edwards-curve {@link PublicKey} contained in the first {@code X509Certificate}.
* @param <B> the type of Edwards-curve {@link PrivateKey} that may be paired with the {@link PublicKey} to produce
* an {@link OctetPrivateJwk} if desired.
* @param chain the {@link X509Certificate} chain to inspect to find the Edwards-curve {@code PublicKey} to
* represent as an {@link OctetPublicJwk}.
* @return the builder coerced as an {@link OctetPublicJwkBuilder} for continued method chaining.
*/
<A extends PublicKey, B extends PrivateKey> OctetPublicJwkBuilder<A, B> octetChain(X509Certificate... chain);

/**
* Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at array index 0) <em>MUST</em> contain an {@link ECPublicKey}
* instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method.
* Ensures the builder will create an {@link OctetPrivateJwk} for the specified Java Edwards-curve
* {@link KeyPair}. The pair's {@link KeyPair#getPublic() public key} <em>MUST</em> be an
* Edwards-curve public key as defined by {@link #octetKey(PublicKey)}. The pair's
* {@link KeyPair#getPrivate() private key} <em>MUST</em> be an Edwards-curve private key as defined by
* {@link #octetKey(PrivateKey)}.
*
* @param chain the {@link X509Certificate} chain to inspect to find the {@link ECPublicKey} to represent as a
* {@link EcPublicJwk}.
* @return the builder coerced as an {@link EcPublicJwkBuilder}.
* @param <A> the type of Edwards-curve {@link PublicKey} contained in the key pair.
* @param <B> the type of the Edwards-curve {@link PrivateKey} contained in the key pair.
* @param keyPair the Edwards-curve {@link KeyPair} to represent as an {@link OctetPrivateJwk}.
* @return the builder coerced as an {@link OctetPrivateJwkBuilder} for continued method chaining.
* @throws IllegalArgumentException if the {@code keyPair} does not contain Edwards-curve public and private key
* instances.
*/
EcPublicJwkBuilder ecChain(X509Certificate... chain);
<A extends PrivateKey, B extends PublicKey> OctetPrivateJwkBuilder<A, B> octetKeyPair(KeyPair keyPair);

/**
* Ensures the builder will create an {@link EcPublicJwk} for the specified Java {@link X509Certificate} chain.
Expand All @@ -308,17 +362,6 @@ public interface DynamicJwkBuilder<K extends Key, J extends Jwk<K>> extends JwkB
*/
EcPrivateJwkBuilder ecKeyPair(KeyPair keyPair) throws IllegalArgumentException;

/**
* Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at array index 0) <em>MUST</em> contain an {@link RSAPublicKey}
* instance when calling the certificate's {@link X509Certificate#getPublicKey() getPublicKey()} method.
*
* @param chain the {@link X509Certificate} chain to inspect to find the {@link RSAPublicKey} to represent as a
* {@link RsaPublicJwk}.
* @return the builder coerced as an {@link RsaPublicJwkBuilder}.
*/
RsaPublicJwkBuilder rsaChain(X509Certificate... chain);

/**
* Ensures the builder will create an {@link RsaPublicJwk} for the specified Java {@link X509Certificate} chain.
* The first {@code X509Certificate} in the chain (at list index 0) <em>MUST</em> contain an {@link RSAPublicKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.jsonwebtoken.impl.security;

import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.DynamicJwkBuilder;
Expand Down Expand Up @@ -125,10 +124,14 @@ public <A extends PrivateKey, B extends PublicKey> OctetPrivateJwkBuilder<A, B>
return new AbstractAsymmetricJwkBuilder.DefaultOctetPrivateJwkBuilder<>(newContext(key));
}

@SuppressWarnings("unchecked")
@Override
public RsaPublicJwkBuilder rsaChain(X509Certificate... chain) {
public <A extends PublicKey, B extends PrivateKey> PublicJwkBuilder<A, B, ?, ?, ?, ?> chain(List<X509Certificate> chain)
throws UnsupportedKeyException {
Assert.notEmpty(chain, "chain cannot be null or empty.");
return rsaChain(Arrays.asList(chain));
X509Certificate cert = Assert.notNull(chain.get(0), "The first X509Certificate cannot be null.");
PublicKey key = Assert.notNull(cert.getPublicKey(), "The first X509Certificate's PublicKey cannot be null.");
return this.<A,B>key((A)key).x509CertificateChain(chain);
}

@Override
Expand All @@ -140,12 +143,6 @@ public RsaPublicJwkBuilder rsaChain(List<X509Certificate> chain) {
return key(pubKey).x509CertificateChain(chain);
}

@Override
public EcPublicJwkBuilder ecChain(X509Certificate... chain) {
Assert.notEmpty(chain, "chain cannot be null or empty.");
return ecChain(Arrays.asList(chain));
}

@Override
public EcPublicJwkBuilder ecChain(List<X509Certificate> chain) {
Assert.notEmpty(chain, "X509Certificate chain cannot be empty.");
Expand All @@ -165,12 +162,6 @@ public <A extends PrivateKey, B extends PublicKey> OctetPrivateJwkBuilder<A, B>
return (OctetPrivateJwkBuilder<A, B>) octetKey(priv).publicKey(pub);
}

@Override
public <A extends PublicKey, B extends PrivateKey> OctetPublicJwkBuilder<A, B> octetChain(X509Certificate... chain) {
Assert.notEmpty(chain, "X509Certificate chain cannot be null or empty.");
return octetChain(Arrays.asList(chain));
}

@SuppressWarnings("unchecked") // ok because of the EdwardsCurve.assertEdwards calls
@Override
public <A extends PublicKey, B extends PrivateKey> OctetPublicJwkBuilder<A, B> octetChain(List<X509Certificate> chain) {
Expand All @@ -196,6 +187,15 @@ public EcPrivateJwkBuilder ecKeyPair(KeyPair pair) {
return key(priv).publicKey(pub);
}

@SuppressWarnings("unchecked")
@Override
public <A extends PublicKey, B extends PrivateKey> PrivateJwkBuilder<B, A, ?, ?, ?> keyPair(KeyPair keyPair)
throws UnsupportedKeyException {
A pub = (A)KeyPairs.getKey(keyPair, PublicKey.class);
B priv = (B)KeyPairs.getKey(keyPair, PrivateKey.class);
return this.<A,B>key(priv).publicKey(pub);
}

@Override
public J build() {
if (Strings.hasText(this.DELEGATE.get(AbstractJwk.KTY))) {
Expand Down
82 changes: 72 additions & 10 deletions impl/src/test/groovy/io/jsonwebtoken/impl/security/JwksTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import java.security.cert.X509Certificate
import java.security.interfaces.ECKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.ECParameterSpec
import java.security.spec.ECPoint

Expand Down Expand Up @@ -186,16 +187,7 @@ class JwksTest {
for (def alg : algs) {
//get test cert:
X509Certificate cert = TestCertificates.readTestCertificate(alg)
def pubKey = cert.getPublicKey()

def builder = Jwks.builder()
if (pubKey instanceof ECKey) {
builder = builder.ecChain(cert)
} else if (pubKey instanceof RSAKey) {
builder = builder.rsaChain(cert)
} else {
builder = builder.octetChain(cert)
}
def builder = Jwks.builder().chain(Arrays.asList(cert))

if (number == 1) {
builder.withX509Sha1Thumbprint(true)
Expand Down Expand Up @@ -415,6 +407,76 @@ class JwksTest {
}
}

@Test
void testEcChain() {
TestKeys.EC.each {
ECPublicKey key = it.pair.public as ECPublicKey
def jwk = Jwks.builder().ecChain(it.chain).build()
assertEquals key, jwk.toKey()
assertEquals it.chain, jwk.getX509CertificateChain()
}
}

@Test
void testRsaChain() {
TestKeys.RSA.each {
RSAPublicKey key = it.pair.public as RSAPublicKey
def jwk = Jwks.builder().rsaChain(it.chain).build()
assertEquals key, jwk.toKey()
assertEquals it.chain, jwk.getX509CertificateChain()
}
}

@Test
void testOctetChain() {
TestKeys.EdEC.findAll({ it -> it.cert != null }).each { // no chains for XEC keys
PublicKey key = it.pair.public
def jwk = Jwks.builder().octetChain(it.chain).build()
assertEquals key, jwk.toKey()
assertEquals it.chain, jwk.getX509CertificateChain()
}
}

@Test
void testRsaKeyPair() {
TestKeys.RSA.each {
java.security.KeyPair pair = it.pair
PrivateJwk jwk = Jwks.builder().rsaKeyPair(pair).build()
assertEquals it.pair.public, jwk.toPublicJwk().toKey()
assertEquals it.pair.private, jwk.toKey()
}
}

@Test
void testEcKeyPair() {
TestKeys.EC.each {
java.security.KeyPair pair = it.pair
PrivateJwk jwk = Jwks.builder().ecKeyPair(pair).build()
assertEquals it.pair.public, jwk.toPublicJwk().toKey()
assertEquals it.pair.private, jwk.toKey()
}
}

@Test
void testOctetKeyPair() {
TestKeys.EdEC.findAll(it -> it.cert != null).each {
java.security.KeyPair pair = it.pair
PrivateJwk jwk = Jwks.builder().octetKeyPair(pair).build()
assertEquals it.pair.public, jwk.toPublicJwk().toKey()
assertEquals it.pair.private, jwk.toKey()
}
}

@Test
void testKeyPair() {
TestKeys.ASYM.each {
java.security.KeyPair pair = it.pair
PrivateJwk jwk = Jwks.builder().keyPair(pair).build()
assertEquals it.pair.public, jwk.toPublicJwk().toKey()
assertEquals it.pair.private, jwk.toKey()
}
}

private static class InvalidECPublicKey implements ECPublicKey {

private final ECPublicKey good
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The Elliptic Curve `*.key.pem`, `*.crt.pem` and `*.pub.pem` files in this direct
openssl x509 -pubkey -noout -in ES384.crt.pem > ES384.pub.pem
openssl x509 -pubkey -noout -in ES512.crt.pem > ES512.pub.pem

The Edwards Curve `*.key.pem`, `*.crt.pem` and `*.pub.pem` files in this directory were created for testing as follows
The Edwards Curve `*.key.pem`, `*.crt.pem` and `*.pub.pem` files in this directory were created for testing as follows.
Note that we don't/can't create self-signed certificates (`*.crt.pem` files) for X25519 and X448 because these
algorithms cannot be used for signing (perhaps we could have signed them with another key, but it wasn't necessary
for our testing):
Expand Down

0 comments on commit 083da95

Please sign in to comment.