New KeyOperation, KeyOperationPolicy and builder concepts (#814)

* Added new KeyOperation and KeyOperationBuilder concepts
Changed Jwk#getOperations and JwkBuilder#operations from methods that accepted and returned Strings to use new KeyOperation instances
Added Jwks.OP#builder() method to create a KeyOperationBuilder

* Changed Jwks.OP#WRAP to WRAP_KEY and UNWRAP to UNWRAP_KEY to match RFC names

* Added new KeyOperationPolicy and KeyOperationPolicyBuilder concepts
Added Jwks.OP#policy() builder method to create a KeyOperationPolicyBuilder
Added JwkBuilder#operationPolicy and JwkParserBuilder#operationPolicy methods for configuring custom KeyOperationPolicy instances during JWK building and parsing, respectively.
This commit is contained in:
lhazlewood 2023-09-08 16:03:47 -07:00 committed by GitHub
parent a6792d938f
commit 847ad1332c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1612 additions and 180 deletions

View File

@ -16,7 +16,7 @@
package io.jsonwebtoken.security; package io.jsonwebtoken.security;
import java.security.Key; import java.security.Key;
import java.util.Set; import java.util.Collection;
/** /**
* A {@link JwkBuilder} that builds asymmetric (public or private) JWKs. * A {@link JwkBuilder} that builds asymmetric (public or private) JWKs.
@ -69,7 +69,7 @@ public interface AsymmetricJwkBuilder<K extends Key, J extends AsymmetricJwk<K>,
* *
* <p>Per * <p>Per
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">JWK RFC 7517, Section 4.3, last paragraph</a>, * <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">JWK RFC 7517, Section 4.3, last paragraph</a>,
* the {@code use} (Public Key Use) and {@link #operations(Set) key_ops (Key Operations)} members * the <code>use (Public Key Use)</code> and {@link #operations(Collection) key_ops (Key Operations)} members
* <em>SHOULD NOT</em> be used together; however, if both are used, the information they convey <em>MUST</em> be * <em>SHOULD NOT</em> be used together; however, if both are used, the information they convey <em>MUST</em> be
* consistent. Applications should specify which of these members they use, if either is to be used by the * consistent. Applications should specify which of these members they use, if either is to be used by the
* application.</p> * application.</p>

View File

@ -95,69 +95,15 @@ public interface Jwk<K extends Key> extends Identifiable, Map<String, Object> {
String getAlgorithm(); String getAlgorithm();
/** /**
* Returns the JWK * Returns the JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops}
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops} (Key Operations) * (Key Operations) parameter</a> values or {@code null} if not present. All JWK standard Key Operations are
* parameter</a> values or {@code null} if not present. Any values within the returned {@code Set} are * available via the {@link Jwks.OP} registry, but other (custom) values <em>MAY</em> be present in the returned
* CaSe-SeNsItIvE. * set.
*
* <p>The JWK specification <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">defines</a> the
* following values:</p>
*
* <table>
* <caption>JWK Key Operations</caption>
* <thead>
* <tr>
* <th>Value</th>
* <th>Operation</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><b>{@code sign}</b></td>
* <td>compute digital signatures or MAC</td>
* </tr>
* <tr>
* <td><b>{@code verify}</b></td>
* <td>verify digital signatures or MAC</td>
* </tr>
* <tr>
* <td><b>{@code encrypt}</b></td>
* <td>encrypt content</td>
* </tr>
* <tr>
* <td><b>{@code decrypt}</b></td>
* <td>decrypt content and validate decryption, if applicable</td>
* </tr>
* <tr>
* <td><b>{@code wrapKey}</b></td>
* <td>encrypt key</td>
* </tr>
* <tr>
* <td><b>{@code unwrapKey}</b></td>
* <td>decrypt key and validate decryption, if applicable</td>
* </tr>
* <tr>
* <td><b>{@code deriveKey}</b></td>
* <td>derive key</td>
* </tr>
* <tr>
* <td><b>{@code deriveBits}</b></td>
* <td>derive bits not to be used as a key</td>
* </tr>
* </tbody>
* </table>
*
* <p>Other values <em>MAY</em> be used. For best interoperability with other applications however, it is
* recommended to use only the values above.</p>
*
* <p>Multiple unrelated key operations <em>SHOULD NOT</em> be specified for a key because of the potential
* vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations
* {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with
* {@code unwrapKey} are permitted, but other combinations <em>SHOULD NOT</em> be used.</p>
* *
* @return the JWK {@code key_ops} value or {@code null} if not present. * @return the JWK {@code key_ops} value or {@code null} if not present.
* @see <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3"><code>key_ops</code>(Key Operations) Parameter</a>
*/ */
Set<String> getOperations(); Set<KeyOperation> getOperations();
/** /**
* Returns the required JWK * Returns the required JWK
@ -188,6 +134,11 @@ public interface Jwk<K extends Key> extends Identifiable, Map<String, Object> {
* <td><b>{@code oct}</b></td> * <td><b>{@code oct}</b></td>
* <td>Octet sequence (used to represent symmetric keys)</td> * <td>Octet sequence (used to represent symmetric keys)</td>
* </tr> * </tr>
* <tr>
* <td><b>{@code OKP}</b></td>
* <td><a href="https://www.rfc-editor.org/rfc/rfc8037#section-2">Octet Key Pair</a> (used to represent Edwards
* Elliptic Curve keys)</td>
* </tr>
* </tbody> * </tbody>
* </table> * </table>
* *

View File

@ -18,7 +18,7 @@ package io.jsonwebtoken.security;
import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.lang.MapMutator;
import java.security.Key; import java.security.Key;
import java.util.Set; import java.util.Collection;
/** /**
* A {@link SecurityBuilder} that produces a JWK. A JWK is an immutable set of name/value pairs that represent a * A {@link SecurityBuilder} that produces a JWK. A JWK is an immutable set of name/value pairs that represent a
@ -103,6 +103,47 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
*/ */
T idFromThumbprint(HashAlgorithm alg); T idFromThumbprint(HashAlgorithm alg);
/**
* Specifies an operation for which the key may be used by adding it to the
* JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops} (Key Operations)
* Parameter</a> values. This method may be called multiple times.
*
* <p>The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is
* intended to be used. The {@code key_ops} parameter is intended for use cases in which public,
* private, or symmetric keys may be present.</p>
*
* <p><b>Security Vulnerability Notice</b></p>
*
* <p>Multiple unrelated key operations <em>SHOULD NOT</em> be specified for a key because of the potential
* vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations
* {@link Jwks.OP#SIGN sign} with {@link Jwks.OP#VERIFY verify},
* {@link Jwks.OP#ENCRYPT encrypt} with {@link Jwks.OP#DECRYPT decrypt}, and
* {@link Jwks.OP#WRAP_KEY wrapKey} with {@link Jwks.OP#UNWRAP_KEY unwrapKey} are permitted, but other combinations
* <em>SHOULD NOT</em> be used. This is enforced by the builder's key operation
* {@link #operationPolicy(KeyOperationPolicy) policy}.</p>
*
* <p><b>Standard {@code KeyOperation}s and Overrides</b></p>
*
* <p>All RFC-standard JWK Key Operations in the {@link Jwks.OP} registry are supported via the builder's default
* operations {@link #operationPolicy(KeyOperationPolicy) policy}, but other (custom) values
* <em>MAY</em> be specified (for example, using a {@link Jwks.OP#builder()}).</p>
*
* <p>If the {@code JwkBuilder} is being used to rebuild or parse an existing JWK however, any custom operations
* should be enabled for the {@code JwkBuilder} by {@link #operationPolicy(KeyOperationPolicy) specifying}
* an operations policy that includes the custom values (e.g. via
* {@link Jwks.OP#policy()}.{@link KeyOperationPolicyBuilder#add(KeyOperation) add(customKeyOperation)}).</p>
*
* <p>For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP}
* constants.</p>
*
* @param operation the value to add to the JWK {@code key_ops} value set
* @return the builder for method chaining.
* @throws IllegalArgumentException if {@code op} is {@code null} or if the operation is not permitted
* by the operations {@link #operationPolicy(KeyOperationPolicy) policy}.
* @see Jwks.OP
*/
T operation(KeyOperation operation) throws IllegalArgumentException;
/** /**
* Sets the JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops} * Sets the JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops}
* (Key Operations) Parameter</a> values. * (Key Operations) Parameter</a> values.
@ -111,68 +152,63 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
* intended to be used. The {@code key_ops} parameter is intended for use cases in which public, * intended to be used. The {@code key_ops} parameter is intended for use cases in which public,
* private, or symmetric keys may be present.</p> * private, or symmetric keys may be present.</p>
* *
* <p>The JWK specification <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">defines</a> the * <p><b>Security Vulnerability Notice</b></p>
* following values:</p>
*
* <table>
* <caption>JWK Key Operations</caption>
* <thead>
* <tr>
* <th>Value</th>
* <th>Operation</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><b>{@code sign}</b></td>
* <td>compute digital signatures or MAC</td>
* </tr>
* <tr>
* <td><b>{@code verify}</b></td>
* <td>verify digital signatures or MAC</td>
* </tr>
* <tr>
* <td><b>{@code encrypt}</b></td>
* <td>encrypt content</td>
* </tr>
* <tr>
* <td><b>{@code decrypt}</b></td>
* <td>decrypt content and validate decryption, if applicable</td>
* </tr>
* <tr>
* <td><b>{@code wrapKey}</b></td>
* <td>encrypt key</td>
* </tr>
* <tr>
* <td><b>{@code unwrapKey}</b></td>
* <td>decrypt key and validate decryption, if applicable</td>
* </tr>
* <tr>
* <td><b>{@code deriveKey}</b></td>
* <td>derive key</td>
* </tr>
* <tr>
* <td><b>{@code deriveBits}</b></td>
* <td>derive bits not to be used as a key</td>
* </tr>
* </tbody>
* </table>
*
* <p>(Note that {@code key_ops} values intentionally match the {@code KeyUsage} values defined in the
* <a href="https://www.w3.org/TR/WebCryptoAPI/">Web Cryptography API</a> specification.)</p>
*
* <p>Other values <em>MAY</em> be used. For best interoperability with other applications however, it is
* recommended to use only the values above. Each value is a CaSe-SeNsItIvE string. Use of the
* {@code key_ops} member is <em>OPTIONAL</em>, unless the application requires its presence.</p>
* *
* <p>Multiple unrelated key operations <em>SHOULD NOT</em> be specified for a key because of the potential * <p>Multiple unrelated key operations <em>SHOULD NOT</em> be specified for a key because of the potential
* vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations * vulnerabilities associated with using the same key with multiple algorithms. Thus, the combinations
* {@code sign} with {@code verify}, {@code encrypt} with {@code decrypt}, and {@code wrapKey} with * {@link Jwks.OP#SIGN sign} with {@link Jwks.OP#VERIFY verify},
* {@code unwrapKey} are permitted, but other combinations <em>SHOULD NOT</em> be used.</p> * {@link Jwks.OP#ENCRYPT encrypt} with {@link Jwks.OP#DECRYPT decrypt}, and
* {@link Jwks.OP#WRAP_KEY wrapKey} with {@link Jwks.OP#UNWRAP_KEY unwrapKey} are permitted, but other combinations
* <em>SHOULD NOT</em> be used. This is enforced by the builder's default
* operation {@link #operationPolicy(KeyOperationPolicy) policy}.</p>
* *
* @param ops the JWK {@code key_ops} value set. * <p><b>Standard {@code KeyOperation}s and Overrides</b></p>
*
* <p>All RFC-standard JWK Key Operations in the {@link Jwks.OP} registry are supported via the builder's default
* operations {@link #operationPolicy(KeyOperationPolicy) policy}, but other (custom) values
* <em>MAY</em> be specified (for example, using a {@link Jwks.OP#builder()}).</p>
*
* <p>If the {@code JwkBuilder} is being used to rebuild or parse an existing JWK however, any custom operations
* should be enabled for the {@code JwkBuilder} by {@link #operationPolicy(KeyOperationPolicy) specifying}
* an operations policy that includes the custom values (e.g. via
* {@link Jwks.OP#policy()}.{@link KeyOperationPolicyBuilder#add(KeyOperation) add(customKeyOperation)}).</p>
*
* <p>For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP}
* constants.</p>
*
* @param ops the JWK {@code key_ops} value set, or {@code null} if not present.
* @return the builder for method chaining. * @return the builder for method chaining.
* @throws IllegalArgumentException if {@code ops} is {@code null} or empty. * @throws IllegalArgumentException {@code ops} is {@code null} or empty, or if any of the operations are not
* permitted by the operations {@link #operationPolicy(KeyOperationPolicy) policy}.
* @see Jwks.OP
*/ */
T operations(Set<String> ops) throws IllegalArgumentException; T operations(Collection<KeyOperation> ops) throws IllegalArgumentException;
/**
* Sets the builder's {@link KeyOperationPolicy} that determines which key
* {@link #operations(Collection) operations} may be assigned to the JWK. Unless overridden by this method, the
* builder uses the default RFC-recommended policy where:
* <ul>
* <li>All {@link Jwks.OP RFC-standard key operations} are supported.</li>
* <li>Multiple unrelated operations may not be assigned to the JWK per the
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">RFC 7517, Section 4.3</a> recommendation:
* <blockquote><pre>
* Multiple unrelated key operations SHOULD NOT be specified for a key
* because of the potential vulnerabilities associated with using the
* same key with multiple algorithms.
* </pre></blockquote></li>
* </ul>
*
* <p>If you wish to enable a different policy, perhaps to support additional custom {@code KeyOperation} values,
* one can be created by using the {@link Jwks.OP#policy()} builder, or by implementing the
* {@link KeyOperationPolicy} interface directly.</p>
*
* @param policy the policy to apply during JWK construction
* @return the builder for method chaining.
* @throws IllegalArgumentException if the specified policy is null, or the policy's
* {@link KeyOperationPolicy#getOperations() operations} collection is null or
* empty.
* @see Jwks.OP#policy()
*/
T operationPolicy(KeyOperationPolicy policy) throws IllegalArgumentException;
} }

View File

@ -58,4 +58,28 @@ public interface JwkParserBuilder extends Builder<JwkParser> {
*/ */
JwkParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer); JwkParserBuilder deserializeJsonWith(Deserializer<Map<String, ?>> deserializer);
/**
* Sets the parser's key operation policy that determines which {@link KeyOperation}s may be assigned to parsed
* JWKs. Unless overridden by this method, the parser uses the default RFC-recommended policy where:
* <ul>
* <li>All {@link Jwks.OP RFC-standard key operations} are supported.</li>
* <li>Multiple unrelated operations may <b>not</b> be assigned to the JWK per the
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">RFC 7517, Section 4.3</a> recommendation:
* <blockquote><pre>
* Multiple unrelated key operations SHOULD NOT be specified for a key
* because of the potential vulnerabilities associated with using the
* same key with multiple algorithms.
* </pre></blockquote></li>
* </ul>
*
* <p>If you wish to enable a different policy, perhaps to support additional custom {@code KeyOperation} values,
* one can be created by using the {@link Jwks.OP#policy()} builder, or by implementing the
* {@link KeyOperationPolicy} interface directly.</p>
*
* @param policy the policy to use to determine which {@link KeyOperation}s may be assigned to parsed JWKs.
* @return the builder for method chaining.
* @throws IllegalArgumentException if {@code policy} is null
*/
JwkParserBuilder operationPolicy(KeyOperationPolicy policy) throws IllegalArgumentException;
} }

View File

@ -45,12 +45,13 @@ public final class Jwks {
private static final String PARSERBUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder"; private static final String PARSERBUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultJwkParserBuilder";
/** /**
* Constants for all standard Elliptic Curves in the {@code JSON Web Key Elliptic Curve Registry} * Constants for all standard JWK
* defined by <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-7.6">RFC 7518, Section 7.6</a> * <a href="https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1">crv (Curve)</a> parameter values
* (for Weierstrass Elliptic Curves) and * defined in the <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-7.6">JSON Web Key Elliptic
* <a href="https://www.rfc-editor.org/rfc/rfc8037#section-5">RFC 8037, Section 5</a> (for Edwards Elliptic Curves). * Curve Registry</a> (including its
* Each standard algorithm is available as a * <a href="https://www.rfc-editor.org/rfc/rfc8037#section-5">Edwards Elliptic Curve additions</a>).
* ({@code public static final}) constant for direct type-safe reference in application code. For example: * Each standard algorithm is available as a ({@code public static final}) constant for direct type-safe
* reference in application code. For example:
* <blockquote><pre> * <blockquote><pre>
* Jwks.CRV.P256.keyPair().build();</pre></blockquote> * Jwks.CRV.P256.keyPair().build();</pre></blockquote>
* <p>They are also available together as a {@link Registry} instance via the {@link #get()} method.</p> * <p>They are also available together as a {@link Registry} instance via the {@link #get()} method.</p>
@ -262,6 +263,137 @@ public final class Jwks {
} }
} }
/**
* Constants for all standard JWK
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">key_ops (Key Operations)</a> parameter values
* defined in the <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3">JSON Web Key Operations
* Registry</a>. Each standard key operation is available as a ({@code public static final}) constant for
* direct type-safe reference in application code. For example:
* <blockquote><pre>
* Jwks.builder()
* .operations(Jwks.OP.SIGN)
* // ... etc ...
* .build();</pre></blockquote>
* <p>They are also available together as a {@link Registry} instance via the {@link #get()} method.</p>
*
* @see #get()
* @since JJWT_RELEASE_VERSION
*/
public static final class OP {
private static final String IMPL_CLASSNAME = "io.jsonwebtoken.impl.security.StandardKeyOperations";
private static final Registry<String, KeyOperation> REGISTRY = Classes.newInstance(IMPL_CLASSNAME);
private static final String BUILDER_CLASSNAME = "io.jsonwebtoken.impl.security.DefaultKeyOperationBuilder";
private static final String POLICY_BUILDER_CLASSNAME =
"io.jsonwebtoken.impl.security.DefaultKeyOperationPolicyBuilder";
/**
* Creates a new {@link KeyOperationBuilder} for creating custom {@link KeyOperation} instances.
*
* @return a new {@link KeyOperationBuilder} for creating custom {@link KeyOperation} instances.
*/
public static KeyOperationBuilder builder() {
return Classes.newInstance(BUILDER_CLASSNAME);
}
/**
* Creates a new {@link KeyOperationPolicyBuilder} for creating custom {@link KeyOperationPolicy} instances.
*
* @return a new {@link KeyOperationPolicyBuilder} for creating custom {@link KeyOperationPolicy} instances.
*/
public static KeyOperationPolicyBuilder policy() {
return Classes.newInstance(POLICY_BUILDER_CLASSNAME);
}
/**
* Returns a registry of all standard Key Operations in the {@code JSON Web Key Operations Registry}
* defined by <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3">RFC 7517, Section 8.3</a>.
*
* @return a registry of all standard Key Operations in the {@code JSON Web Key Operations Registry}.
*/
public static Registry<String, KeyOperation> get() {
return REGISTRY;
}
/**
* {@code sign} operation indicating a key is intended to be used to compute digital signatures or
* MACs. It's related operation is {@link #VERIFY}.
*
* @see #VERIFY
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation SIGN = get().forKey("sign");
/**
* {@code verify} operation indicating a key is intended to be used to verify digital signatures or
* MACs. It's related operation is {@link #SIGN}.
*
* @see #SIGN
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation VERIFY = get().forKey("verify");
/**
* {@code encrypt} operation indicating a key is intended to be used to encrypt content. It's
* related operation is {@link #DECRYPT}.
*
* @see #DECRYPT
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation ENCRYPT = get().forKey("encrypt");
/**
* {@code decrypt} operation indicating a key is intended to be used to decrypt content. It's
* related operation is {@link #ENCRYPT}.
*
* @see #ENCRYPT
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation DECRYPT = get().forKey("decrypt");
/**
* {@code wrapKey} operation indicating a key is intended to be used to encrypt another key. It's
* related operation is {@link #UNWRAP_KEY}.
*
* @see #UNWRAP_KEY
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation WRAP_KEY = get().forKey("wrapKey");
/**
* {@code unwrapKey} operation indicating a key is intended to be used to decrypt another key and validate
* decryption, if applicable. It's related operation is
* {@link #WRAP_KEY}.
*
* @see #WRAP_KEY
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation UNWRAP_KEY = get().forKey("unwrapKey");
/**
* {@code deriveKey} operation indicating a key is intended to be used to derive another key. It does not have
* a related operation.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation DERIVE_KEY = get().forKey("deriveKey");
/**
* {@code deriveBits} operation indicating a key is intended to be used to derive bits that are not to be
* used as key. It does not have a related operation.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3.2">Key Operation Registry Contents</a>
*/
public static final KeyOperation DERIVE_BITS = get().forKey("deriveBits");
//prevent instantiation
private OP() {
}
}
/** /**
* Return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair. * Return a new JWK builder instance, allowing for type-safe JWK builder coercion based on a provided key or key pair.
* *

View File

@ -0,0 +1,55 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.security;
import io.jsonwebtoken.Identifiable;
/**
* A {@code KeyOperation} identifies a behavior for which a key may be used. Key validation
* algorithms may inspect a key's operations and reject the key if it is being used in a manner inconsistent
* with its indicated operations.
*
* <p><b>KeyOperation Identifier</b></p>
*
* <p>This interface extends {@link Identifiable}; the value returned from {@link #getId()} is a
* CaSe-SeNsItIvE value that uniquely identifies the operation among other KeyOperation instances.</p>
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">JWK key_ops (Key Operations) Parameter</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#section-8.3">JSON Web Key Operations Registry</a>
* @since JJWT_RELEASE_VERSION
*/
public interface KeyOperation extends Identifiable {
/**
* Returns a brief description of the key operation behavior.
*
* @return a brief description of the key operation behavior.
*/
String getDescription();
/**
* Returns {@code true} if the specified {@code operation} is an acceptable use case for the key already assigned
* this operation, {@code false} otherwise. As described in the
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">JWK key_ops (Key Operations) Parameter</a>
* specification, Key validation algorithms will likely reject keys with inconsistent or unrelated operations
* because of the security vulnerabilities that could occur otherwise.
*
* @param operation the key operation to check if it is related to (consistent or compatible with) this operation.
* @return {@code true} if the specified {@code operation} is an acceptable use case for the key already assigned
* this operation, {@code false} otherwise.
*/
boolean isRelated(KeyOperation operation);
}

View File

@ -0,0 +1,75 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.security;
import io.jsonwebtoken.lang.Builder;
import java.util.Collection;
/**
* A {@code KeyOperationBuilder} produces {@link KeyOperation} instances that may be added to a JWK's
* {@link JwkBuilder#operations(Collection) key operations} parameter. This is primarily only useful for creating
* custom (non-standard) {@code KeyOperation}s for use with a custom {@link KeyOperationPolicy}, as all standard ones
* are available already via the {@link Jwks.OP} registry singleton.
*
* @see Jwks.OP#builder()
* @see Jwks.OP#policy()
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
* @since JJWT_RELEASE_VERSION
*/
public interface KeyOperationBuilder extends Builder<KeyOperation> {
/**
* Sets the CaSe-SeNsItIvE {@link KeyOperation#getId() id} expected to be unique compared to all other
* {@code KeyOperation}s.
*
* @param id the key operation id
* @return the builder for method chaining
*/
KeyOperationBuilder id(String id);
/**
* Sets the key operation {@link KeyOperation#getDescription() description}.
*
* @param description the key operation description
* @return the builder for method chaining
*/
KeyOperationBuilder description(String description);
/**
* Indicates that the {@code KeyOperation} with the given {@link KeyOperation#getId() id} is cryptographically
* related (and complementary) to this one, and may be specified together in a JWK's
* {@link Jwk#getOperations() operations} set.
*
* <p>More concretely, calling this method will ensure the following:</p>
* <blockquote><pre>
* KeyOperation built = Jwks.operation()&#47;*...*&#47;.related(otherId).build();
* KeyOperation other = getKeyOperation(otherId);
* assert built.isRelated(other);</pre></blockquote>
*
* <p>A {@link JwkBuilder}'s key operation {@link JwkBuilder#operationPolicy(KeyOperationPolicy) policy} is likely
* to {@link KeyOperationPolicyBuilder#allowUnrelated(boolean) reject} any <em>un</em>related operations specified
* together due to the potential security vulnerabilities that could occur.</p>
*
* <p>This method may be called multiple times to add/append a related {@code id} to the constructed
* {@code KeyOperation}'s total set of related ids.</p>
*
* @param id the id of a KeyOperation that will be considered cryptographically related to this one.
* @return the builder for method chaining.
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
*/
KeyOperationBuilder related(String id);
}

View File

@ -0,0 +1,43 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.security;
import java.util.Collection;
/**
* A key operation policy determines which {@link KeyOperation}s may be assigned to a JWK.
*
* @since JJWT_RELEASE_VERSION
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
*/
public interface KeyOperationPolicy {
/**
* Returns all supported {@code KeyOperation}s that may be assigned to a JWK.
*
* @return all supported {@code KeyOperation}s that may be assigned to a JWK.
*/
Collection<KeyOperation> getOperations();
/**
* Returns quietly if all of the specified key operations are allowed to be assigned to a JWK,
* or throws an {@link IllegalArgumentException} otherwise.
*
* @param ops the operations to validate
*/
@SuppressWarnings("GrazieInspection")
void validate(Collection<KeyOperation> ops) throws IllegalArgumentException;
}

View File

@ -0,0 +1,112 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.security;
import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.lang.Builder;
import java.util.Collection;
/**
* A {@code KeyOperationPolicyBuilder} produces a {@link KeyOperationPolicy} that determines
* which {@link KeyOperation}s may be assigned to a JWK. Custom {@code KeyOperation}s (such as those created by a
* {@link Jwks.OP#builder()}) may be added to a policy via the {@link #add(KeyOperation)} or {@link #add(Collection)}
* methods.
*
* @see Jwks.OP#policy()
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
* @see Jwks.OP#builder()
* @since JJWT_RELEASE_VERSION
*/
public interface KeyOperationPolicyBuilder extends Builder<KeyOperationPolicy> {
/**
* Sets if a JWK is allowed to have unrelated {@link KeyOperation}s in its {@code key_ops} parameter values.
* The default value is {@code false} per the JWK
* <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">RFC 7517, Section 4.3</a> recommendation:
*
* <blockquote><pre>
* Multiple unrelated key operations SHOULD NOT be specified for a key
* because of the potential vulnerabilities associated with using the
* same key with multiple algorithms.
* </pre></blockquote>
*
* <p>Only set this value to {@code true} if you fully understand the security implications of using the same key
* with multiple algorithms in your application. Otherwise it is best not to use this builder method, or
* explicitly set it to {@code false}.</p>
*
* @param allow if a JWK is allowed to have unrelated key {@link KeyOperation}s in its {@code key_ops}
* parameter values.
* @return the builder for method chaining
*/
KeyOperationPolicyBuilder allowUnrelated(boolean allow);
/**
* Adds the specified key operation to the policy's total set of supported key operations
* used to validate a key's intended usage, replacing any existing one with an identical (CaSe-SeNsItIvE)
* {@link Identifiable#getId() id}.
*
* <p><b>Standard {@code KeyOperation}s and Overrides</b></p>
*
* <p>The RFC standard {@link Jwks.OP} key operations are supported by default and do not need
* to be added via this method, but beware: <b>If the {@code op} argument has a JWK standard
* {@link Identifiable#getId() id}, it will replace the JJWT standard operation implementation</b>.
* This is to allow application developers to favor their own implementations over JJWT's default implementations
* if necessary (for example, to support legacy or custom behavior).</p>
*
* <p>If a custom {@code KeyOperation} is desired, one may be easily created with a {@link Jwks.OP#builder()}.</p>
*
* @param op a key operation to add to the policy's total set of supported operations, replacing any
* existing one with the same exact (CaSe-SeNsItIvE) {@link KeyOperation#getId() id}.
* @return the builder for method chaining.
* @see Jwks.OP
* @see Jwks.OP#builder()
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
* @see JwkBuilder#operations(Collection)
*/
KeyOperationPolicyBuilder add(KeyOperation op);
/**
* Adds the specified key operations to the policy's total set of supported key operations
* used to validate a key's intended usage, replacing any existing ones with identical
* {@link Identifiable#getId() id}s.
*
* <p>There may be only one registered {@code KeyOperation} per CaSe-SeNsItIvE {@code id}, and the
* {@code ops} collection is added in iteration order; if a duplicate id is found when iterating the {@code ops}
* collection, the later operation will evict any existing operation with the same {@code id}.</p>
*
* <p><b>Standard {@code KeyOperation}s and Overrides</b></p>
*
* <p>The RFC standard {@link Jwks.OP} key operations are supported by default and do not need
* to be added via this method, but beware: <b>any operation in the {@code ops} argument with a
* JWK standard {@link Identifiable#getId() id} will replace the JJWT standard operation implementation</b>.
* This is to allow application developers to favor their own implementations over JJWT's default implementations
* if necessary (for example, to support legacy or custom behavior).</p>
*
* <p>If custom {@code KeyOperation}s are desired, they may be easily created with a {@link Jwks.OP#builder()}.</p>
*
* @param ops collection of key operations to add to the policy's total set of supported operations, replacing any
* existing ones with the same exact (CaSe-SeNsItIvE) {@link KeyOperation#getId() id}s.
* @return the builder for method chaining.
* @see Jwks.OP
* @see Jwks.OP#builder()
* @see JwkBuilder#operationPolicy(KeyOperationPolicy)
* @see JwkBuilder#operations(Collection)
*/
KeyOperationPolicyBuilder add(Collection<KeyOperation> ops);
}

View File

@ -15,9 +15,21 @@
*/ */
package io.jsonwebtoken.impl.lang; package io.jsonwebtoken.impl.lang;
public interface Converter<A,B> { public interface Converter<A, B> {
/**
* Converts the specified (Java idiomatic type) value to the canonical RFC-required data type.
*
* @param a the preferred idiomatic value
* @return the canonical RFC-required data type value.
*/
B applyTo(A a); B applyTo(A a);
/**
* Converts the specified canonical (RFC-compliant data type) value to the preferred Java idiomatic type.
*
* @param b the canonical value to convert
* @return the preferred Java idiomatic type value.
*/
A applyFrom(B b); A applyFrom(B b);
} }

View File

@ -29,6 +29,7 @@ import io.jsonwebtoken.security.HashAlgorithm;
import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwk;
import io.jsonwebtoken.security.JwkThumbprint; import io.jsonwebtoken.security.JwkThumbprint;
import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Key; import java.security.Key;
@ -42,7 +43,9 @@ public abstract class AbstractJwk<K extends Key> implements Jwk<K>, FieldReadabl
static final Field<String> ALG = Fields.string("alg", "Algorithm"); static final Field<String> ALG = Fields.string("alg", "Algorithm");
public static final Field<String> KID = Fields.string("kid", "Key ID"); public static final Field<String> KID = Fields.string("kid", "Key ID");
static final Field<Set<String>> KEY_OPS = Fields.stringSet("key_ops", "Key Operations"); static final Field<Set<KeyOperation>> KEY_OPS =
Fields.builder(KeyOperation.class).setConverter(KeyOperationConverter.DEFAULT)
.set().setId("key_ops").setName("Key Operations").build();
static final Field<String> KTY = Fields.string("kty", "Key Type"); static final Field<String> KTY = Fields.string("kty", "Key Type");
static final Set<Field<?>> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY); static final Set<Field<?>> FIELDS = Collections.setOf(ALG, KID, KEY_OPS, KTY);
@ -124,7 +127,7 @@ public abstract class AbstractJwk<K extends Key> implements Jwk<K>, FieldReadabl
} }
@Override @Override
public Set<String> getOperations() { public Set<KeyOperation> getOperations() {
return Collections.immutable(this.context.getOperations()); return Collections.immutable(this.context.getOperations());
} }

View File

@ -16,11 +16,18 @@
package io.jsonwebtoken.impl.security; package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.DelegatingMapMutator;
import io.jsonwebtoken.impl.lang.Field;
import io.jsonwebtoken.impl.lang.Fields;
import io.jsonwebtoken.impl.lang.IdRegistry;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Registry;
import io.jsonwebtoken.security.HashAlgorithm; import io.jsonwebtoken.security.HashAlgorithm;
import io.jsonwebtoken.security.Jwk; import io.jsonwebtoken.security.Jwk;
import io.jsonwebtoken.security.JwkBuilder; import io.jsonwebtoken.security.JwkBuilder;
import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
import io.jsonwebtoken.security.KeyOperationPolicy;
import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.MalformedKeyException;
import io.jsonwebtoken.security.SecretJwk; import io.jsonwebtoken.security.SecretJwk;
import io.jsonwebtoken.security.SecretJwkBuilder; import io.jsonwebtoken.security.SecretJwkBuilder;
@ -29,6 +36,8 @@ import javax.crypto.SecretKey;
import java.security.Key; import java.security.Key;
import java.security.Provider; import java.security.Provider;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilder<K, J, T>> abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilder<K, J, T>>
@ -37,6 +46,10 @@ abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends Jwk
protected final JwkFactory<K, J> jwkFactory; protected final JwkFactory<K, J> jwkFactory;
static final KeyOperationPolicy DEFAULT_OPERATION_POLICY = Jwks.OP.policy().build();
protected KeyOperationPolicy opsPolicy = DEFAULT_OPERATION_POLICY; // default
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected AbstractJwkBuilder(JwkContext<K> jwkContext) { protected AbstractJwkBuilder(JwkContext<K> jwkContext) {
this(jwkContext, (JwkFactory<K, J>) DispatchingJwkFactory.DEFAULT_INSTANCE); this(jwkContext, (JwkFactory<K, J>) DispatchingJwkFactory.DEFAULT_INSTANCE);
@ -95,9 +108,39 @@ abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends Jwk
} }
@Override @Override
public T operations(Set<String> ops) { public T operation(KeyOperation operation) throws IllegalArgumentException {
Assert.notEmpty(ops, "Operations cannot be null or empty."); Assert.notNull(operation, "KeyOperation cannot be null.");
this.DELEGATE.setOperations(ops); return operations(Collections.setOf(operation));
}
@Override
public T operations(Collection<KeyOperation> ops) {
Assert.notEmpty(ops, "KeyOperations collection argument cannot be null or empty.");
Set<KeyOperation> set = new LinkedHashSet<>(ops); // new ones override existing ones
Set<KeyOperation> existing = this.DELEGATE.getOperations();
if (!Collections.isEmpty(existing)) {
set.addAll(existing);
}
this.opsPolicy.validate(set);
this.DELEGATE.setOperations(set);
return self();
}
@Override
public T operationPolicy(KeyOperationPolicy policy) throws IllegalArgumentException {
Assert.notNull(policy, "Policy cannot be null.");
Collection<KeyOperation> ops = policy.getOperations();
Assert.notEmpty(ops, "Policy operations cannot be null or empty.");
this.opsPolicy = policy;
// update the JWK internal field to enable the policy's values:
Registry<String, KeyOperation> registry = new IdRegistry<>("JSON Web Key Operation", ops);
Field<Set<KeyOperation>> field = Fields.builder(KeyOperation.class)
.setConverter(new KeyOperationConverter(registry)).set()
.setId(AbstractJwk.KEY_OPS.getId())
.setName(AbstractJwk.KEY_OPS.getName())
.build();
setDelegate(this.DELEGATE.field(field));
return self(); return self();
} }
@ -112,7 +155,9 @@ abstract class AbstractJwkBuilder<K extends Key, J extends Jwk<K>, T extends Jwk
String msg = "A " + Key.class.getName() + " or one or more name/value pairs must be provided to create a JWK."; String msg = "A " + Key.class.getName() + " or one or more name/value pairs must be provided to create a JWK.";
throw new IllegalStateException(msg); throw new IllegalStateException(msg);
} }
try { try {
this.opsPolicy.validate(this.DELEGATE.get(AbstractJwk.KEY_OPS));
return jwkFactory.createJwk(this.DELEGATE); return jwkFactory.createJwk(this.DELEGATE);
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
//if we get an IAE, it means the builder state wasn't configured enough in order to create //if we get an IAE, it means the builder state wasn't configured enough in order to create

View File

@ -47,7 +47,11 @@ public class DefaultDynamicJwkBuilder<K extends Key, J extends Jwk<K>>
extends AbstractJwkBuilder<K, J, DynamicJwkBuilder<K, J>> implements DynamicJwkBuilder<K, J> { extends AbstractJwkBuilder<K, J, DynamicJwkBuilder<K, J>> implements DynamicJwkBuilder<K, J> {
public DefaultDynamicJwkBuilder() { public DefaultDynamicJwkBuilder() {
super(new DefaultJwkContext<K>()); this(new DefaultJwkContext<K>());
}
public DefaultDynamicJwkBuilder(JwkContext<K> ctx) {
super(ctx);
} }
@Override @Override

View File

@ -21,12 +21,16 @@ import io.jsonwebtoken.impl.lang.Fields;
import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.HashAlgorithm; import io.jsonwebtoken.security.HashAlgorithm;
import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
import java.security.Key; import java.security.Key;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.Provider; import java.security.Provider;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -118,6 +122,18 @@ public class DefaultJwkContext<K extends Key> extends AbstractX509Context<JwkCon
} }
} }
@Override
public JwkContext<K> field(Field<?> field) {
Assert.notNull(field, "Field cannot be null.");
Map<String, Field<?>> newFields = new LinkedHashMap<>(this.FIELDS);
newFields.remove(field.getId()); // remove old/default
newFields.put(field.getId(), field); // add new one
Set<Field<?>> fieldSet = new LinkedHashSet<>(newFields.values());
return this.key != null ?
new DefaultJwkContext<>(fieldSet, this, key) :
new DefaultJwkContext<K>(fieldSet, this, false);
}
@Override @Override
public String getName() { public String getName() {
String value = get(AbstractJwk.KTY); String value = get(AbstractJwk.KTY);
@ -177,12 +193,12 @@ public class DefaultJwkContext<K extends Key> extends AbstractX509Context<JwkCon
} }
@Override @Override
public Set<String> getOperations() { public Set<KeyOperation> getOperations() {
return get(AbstractJwk.KEY_OPS); return get(AbstractJwk.KEY_OPS);
} }
@Override @Override
public JwkContext<K> setOperations(Set<String> ops) { public JwkContext<K> setOperations(Collection<KeyOperation> ops) {
put(AbstractJwk.KEY_OPS, ops); put(AbstractJwk.KEY_OPS, ops);
return this; return this;
} }
@ -216,11 +232,11 @@ public class DefaultJwkContext<K extends Key> extends AbstractX509Context<JwkCon
if ("sig".equals(getPublicKeyUse())) { if ("sig".equals(getPublicKeyUse())) {
return true; return true;
} }
Set<String> ops = getOperations(); Set<KeyOperation> ops = getOperations();
if (Collections.isEmpty(ops)) { if (Collections.isEmpty(ops)) {
return false; return false;
} }
return ops.contains("sign") || ops.contains("verify"); return ops.contains(Jwks.OP.SIGN) || ops.contains(Jwks.OP.VERIFY);
} }
@Override @Override

View File

@ -22,6 +22,7 @@ import io.jsonwebtoken.security.JwkBuilder;
import io.jsonwebtoken.security.JwkParser; import io.jsonwebtoken.security.JwkParser;
import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyException; import io.jsonwebtoken.security.KeyException;
import io.jsonwebtoken.security.KeyOperationPolicy;
import io.jsonwebtoken.security.MalformedKeyException; import io.jsonwebtoken.security.MalformedKeyException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -34,9 +35,14 @@ public class DefaultJwkParser implements JwkParser {
private final Deserializer<Map<String, ?>> deserializer; private final Deserializer<Map<String, ?>> deserializer;
public DefaultJwkParser(Provider provider, Deserializer<Map<String, ?>> deserializer) { private final KeyOperationPolicy opsPolicy;
public DefaultJwkParser(Provider provider, Deserializer<Map<String, ?>> deserializer, KeyOperationPolicy policy) {
this.provider = provider; this.provider = provider;
this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null."); this.deserializer = Assert.notNull(deserializer, "Deserializer cannot be null.");
Assert.notNull(policy, "KeyOperationPolicy cannot be null.");
Assert.notEmpty(policy.getOperations(), "KeyOperationPolicy's operations cannot be null or empty.");
this.opsPolicy = policy;
} }
// visible for testing // visible for testing
@ -56,7 +62,7 @@ public class DefaultJwkParser implements JwkParser {
throw new MalformedKeyException(msg); throw new MalformedKeyException(msg);
} }
JwkBuilder<?, ?, ?> builder = Jwks.builder(); JwkBuilder<?, ?, ?> builder = Jwks.builder().operationPolicy(this.opsPolicy);
if (this.provider != null) { if (this.provider != null) {
builder.provider(this.provider); builder.provider(this.provider);

View File

@ -17,18 +17,21 @@ package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Services; import io.jsonwebtoken.impl.lang.Services;
import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.JwkParser; import io.jsonwebtoken.security.JwkParser;
import io.jsonwebtoken.security.JwkParserBuilder; import io.jsonwebtoken.security.JwkParserBuilder;
import io.jsonwebtoken.security.KeyOperationPolicy;
import java.security.Provider; import java.security.Provider;
import java.util.Map; import java.util.Map;
@SuppressWarnings("unused") //used via reflection by Jwks.parser()
public class DefaultJwkParserBuilder implements JwkParserBuilder { public class DefaultJwkParserBuilder implements JwkParserBuilder {
private Provider provider; private Provider provider;
private Deserializer<Map<String,?>> deserializer; private Deserializer<Map<String, ?>> deserializer;
private KeyOperationPolicy opsPolicy = AbstractJwkBuilder.DEFAULT_OPERATION_POLICY;
@Override @Override
public JwkParserBuilder provider(Provider provider) { public JwkParserBuilder provider(Provider provider) {
@ -42,6 +45,14 @@ public class DefaultJwkParserBuilder implements JwkParserBuilder {
return this; return this;
} }
@Override
public JwkParserBuilder operationPolicy(KeyOperationPolicy policy) throws IllegalArgumentException {
this.opsPolicy = Assert.notNull(policy, "KeyOperationPolicy may not be null.");
Assert.notEmpty(policy.getOperations(), "KeyOperationPolicy's operations may not be null or empty.");
this.opsPolicy = policy;
return this;
}
@Override @Override
public JwkParser build() { public JwkParser build() {
if (this.deserializer == null) { if (this.deserializer == null) {
@ -50,6 +61,6 @@ public class DefaultJwkParserBuilder implements JwkParserBuilder {
this.deserializer = Services.loadFirst(Deserializer.class); this.deserializer = Services.loadFirst(Deserializer.class);
} }
return new DefaultJwkParser(this.provider, this.deserializer); return new DefaultJwkParser(this.provider, this.deserializer, this.opsPolicy);
} }
} }

View File

@ -0,0 +1,89 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.KeyOperation;
import java.util.Set;
final class DefaultKeyOperation implements KeyOperation {
private static final String CUSTOM_DESCRIPTION = "Custom key operation";
static final KeyOperation SIGN = of("sign", "Compute digital signature or MAC", "verify");
static final KeyOperation VERIFY = of("verify", "Verify digital signature or MAC", "sign");
static final KeyOperation ENCRYPT = of("encrypt", "Encrypt content", "decrypt");
static final KeyOperation DECRYPT =
of("decrypt", "Decrypt content and validate decryption, if applicable", "encrypt");
static final KeyOperation WRAP = of("wrapKey", "Encrypt key", "unwrapKey");
static final KeyOperation UNWRAP =
of("unwrapKey", "Decrypt key and validate decryption, if applicable", "wrapKey");
static final KeyOperation DERIVE_KEY = of("deriveKey", "Derive key", null);
static final KeyOperation DERIVE_BITS =
of("deriveBits", "Derive bits not to be used as a key", null);
final String id;
final String description;
final Set<String> related;
static KeyOperation of(String id, String description, String related) {
return new DefaultKeyOperation(id, description, Collections.setOf(related));
}
DefaultKeyOperation(String id) {
this(id, null, null);
}
DefaultKeyOperation(String id, String description, Set<String> related) {
this.id = Assert.hasText(id, "id cannot be null or empty.");
this.description = Strings.hasText(description) ? description : CUSTOM_DESCRIPTION;
this.related = related != null ? Collections.immutable(related) : Collections.<String>emptySet();
}
@Override
public String getId() {
return this.id;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public boolean isRelated(KeyOperation operation) {
return equals(operation) || (operation != null && this.related.contains(operation.getId()));
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj == this ||
(obj instanceof KeyOperation && this.id.equals(((KeyOperation) obj).getId()));
}
@Override
public String toString() {
return "'" + this.id + "' (" + this.description + ")";
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.KeyOperation;
import io.jsonwebtoken.security.KeyOperationBuilder;
import java.util.LinkedHashSet;
import java.util.Set;
public class DefaultKeyOperationBuilder implements KeyOperationBuilder {
private String id;
private String description;
private final Set<String> related = new LinkedHashSet<>();
@Override
public KeyOperationBuilder id(String id) {
this.id = id;
return this;
}
@Override
public KeyOperationBuilder description(String description) {
this.description = description;
return this;
}
@Override
public KeyOperationBuilder related(String related) {
if (Strings.hasText(related)) {
this.related.add(related);
}
return this;
}
@Override
public KeyOperation build() {
return new DefaultKeyOperation(this.id, this.description, this.related);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.security.KeyOperation;
import io.jsonwebtoken.security.KeyOperationPolicy;
import java.util.Collection;
final class DefaultKeyOperationPolicy implements KeyOperationPolicy {
private final Collection<KeyOperation> ops;
private final boolean allowUnrelated;
DefaultKeyOperationPolicy(Collection<KeyOperation> ops, boolean allowUnrelated) {
Assert.notEmpty(ops, "KeyOperation collection cannot be null or empty.");
this.ops = Collections.immutable(ops);
this.allowUnrelated = allowUnrelated;
}
@Override
public Collection<KeyOperation> getOperations() {
return this.ops;
}
@Override
public void validate(Collection<KeyOperation> ops) {
if (allowUnrelated || Collections.isEmpty(ops)) return;
for (KeyOperation operation : ops) {
for (KeyOperation inner : ops) {
if (!operation.isRelated(inner)) {
String msg = "Unrelated key operations are not allowed. KeyOperation [" + inner +
"] is unrelated to [" + operation + "].";
throw new IllegalArgumentException(msg);
}
}
}
}
@Override
public int hashCode() {
int hash = Boolean.valueOf(this.allowUnrelated).hashCode();
KeyOperation[] ops = this.ops.toArray(new KeyOperation[0]);
hash = 31 * hash + Objects.nullSafeHashCode((Object[]) ops);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof DefaultKeyOperationPolicy)) {
return false;
}
DefaultKeyOperationPolicy other = (DefaultKeyOperationPolicy) obj;
return this.allowUnrelated == other.allowUnrelated &&
Collections.size(this.ops) == Collections.size(other.ops) &&
this.ops.containsAll(other.ops);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
import io.jsonwebtoken.security.KeyOperationPolicy;
import io.jsonwebtoken.security.KeyOperationPolicyBuilder;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
public class DefaultKeyOperationPolicyBuilder implements KeyOperationPolicyBuilder {
private final Map<String, KeyOperation> ops;
private boolean allowUnrelated = false;
public DefaultKeyOperationPolicyBuilder() {
this.ops = new LinkedHashMap<>(Jwks.OP.get());
}
@Override
public KeyOperationPolicyBuilder allowUnrelated(boolean allow) {
this.allowUnrelated = allow;
return this;
}
@Override
public KeyOperationPolicyBuilder add(KeyOperation op) {
if (op != null) {
String id = Assert.hasText(op.getId(), "KeyOperation id cannot be null or empty.");
this.ops.remove(id);
this.ops.put(id, op);
}
return this;
}
@Override
public KeyOperationPolicyBuilder add(Collection<KeyOperation> ops) {
if (!Collections.isEmpty(ops)) {
for (KeyOperation op : ops) {
add(op);
}
}
return this;
}
@Override
public KeyOperationPolicy build() {
return new DefaultKeyOperationPolicy(Collections.immutable(this.ops.values()), this.allowUnrelated);
}
}

View File

@ -17,20 +17,25 @@ package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.Identifiable;
import io.jsonwebtoken.impl.X509Context; import io.jsonwebtoken.impl.X509Context;
import io.jsonwebtoken.impl.lang.Field;
import io.jsonwebtoken.impl.lang.FieldReadable; import io.jsonwebtoken.impl.lang.FieldReadable;
import io.jsonwebtoken.impl.lang.Nameable; import io.jsonwebtoken.impl.lang.Nameable;
import io.jsonwebtoken.security.HashAlgorithm; import io.jsonwebtoken.security.HashAlgorithm;
import io.jsonwebtoken.security.KeyOperation;
import java.security.Key; import java.security.Key;
import java.security.Provider; import java.security.Provider;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
public interface JwkContext<K extends Key> extends Identifiable, Map<String, Object>, FieldReadable, Nameable, public interface JwkContext<K extends Key> extends Identifiable, Map<String, Object>, FieldReadable, Nameable,
X509Context<JwkContext<K>> { X509Context<JwkContext<K>> {
JwkContext<K> field(Field<?> field);
JwkContext<K> setId(String id); JwkContext<K> setId(String id);
JwkContext<K> setIdThumbprintAlgorithm(HashAlgorithm alg); JwkContext<K> setIdThumbprintAlgorithm(HashAlgorithm alg);
@ -41,9 +46,9 @@ public interface JwkContext<K extends Key> extends Identifiable, Map<String, Obj
JwkContext<K> setType(String type); JwkContext<K> setType(String type);
Set<String> getOperations(); Set<KeyOperation> getOperations();
JwkContext<K> setOperations(Set<String> operations); JwkContext<K> setOperations(Collection<KeyOperation> operations);
String getAlgorithm(); String getAlgorithm();

View File

@ -0,0 +1,50 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Registry;
import io.jsonwebtoken.security.Jwks;
import io.jsonwebtoken.security.KeyOperation;
final class KeyOperationConverter implements Converter<KeyOperation, Object> {
static final Converter<KeyOperation, Object> DEFAULT = new KeyOperationConverter(Jwks.OP.get());
private final Registry<String, KeyOperation> registry;
KeyOperationConverter(Registry<String, KeyOperation> registry) {
this.registry = Assert.notEmpty(registry, "KeyOperation registry cannot be null or empty.");
}
@Override
public String applyTo(KeyOperation operation) {
Assert.notNull(operation, "KeyOperation cannot be null.");
return operation.getId();
}
@Override
public KeyOperation applyFrom(Object o) {
if (o instanceof KeyOperation) {
return (KeyOperation) o;
}
String id = Assert.isInstanceOf(String.class, o, "Argument must be a KeyOperation or String.");
Assert.hasText(id, "KeyOperation string value cannot be null or empty.");
KeyOperation keyOp = this.registry.get(id);
return keyOp != null ? keyOp : Jwks.OP.builder().id(id).build(); // custom operations are allowed
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security;
import io.jsonwebtoken.impl.lang.DelegatingRegistry;
import io.jsonwebtoken.impl.lang.IdRegistry;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.security.KeyOperation;
public class StandardKeyOperations extends DelegatingRegistry<String, KeyOperation> {
public StandardKeyOperations() {
super(new IdRegistry<>("JSON Web Key Operation", Collections.of(
DefaultKeyOperation.SIGN,
DefaultKeyOperation.VERIFY,
DefaultKeyOperation.ENCRYPT,
DefaultKeyOperation.DECRYPT,
DefaultKeyOperation.WRAP,
DefaultKeyOperation.UNWRAP,
DefaultKeyOperation.DERIVE_KEY,
DefaultKeyOperation.DERIVE_BITS
)));
}
}

View File

@ -15,7 +15,7 @@
*/ */
package io.jsonwebtoken.impl.security package io.jsonwebtoken.impl.security
import io.jsonwebtoken.lang.Collections
import io.jsonwebtoken.security.Jwk import io.jsonwebtoken.security.Jwk
import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.Jwks
import io.jsonwebtoken.security.MalformedKeyException import io.jsonwebtoken.security.MalformedKeyException
@ -112,34 +112,137 @@ class AbstractJwkBuilderTest {
assertEquals kid, jwk.kid //test raw get via JWA member id assertEquals kid, jwk.kid //test raw get via JWA member id
} }
@Test
//ensures that even if a raw single String value is present, it is represented as a Set per the JWA spec (string array)
void testOperationsByPutSingleStringValue() {
def s = 'wrapKey'
def op = Jwks.OP.get().get(s)
def canonical = Collections.setOf(s)
def idiomatic = Collections.setOf(op)
def jwk = builder().add('key_ops', s).build() // <-- put uses single raw String value, not a set
assertEquals idiomatic, jwk.getOperations() // <-- still get an idiomatic set
assertEquals canonical, jwk.key_ops // <-- still get a canonical set
}
@Test
//ensures that even if a raw single KeyOperation value is present, it is represented as a Set per the JWA spec (string array)
void testOperationsByPutSingleIdiomaticValue() {
def s = 'wrapKey'
def op = Jwks.OP.get().get(s)
def canonical = Collections.setOf(s)
def idiomatic = Collections.setOf(op)
def jwk = builder().add('key_ops', op).build() // <-- put uses single raw KeyOperation value, not a set
assertEquals idiomatic, jwk.getOperations() // <-- still get an idiomatic set
assertEquals canonical, jwk.key_ops // <-- still get a canonical set
}
@Test
void testOperation() {
def s = 'wrapKey'
def op = Jwks.OP.get().get(s)
def canonical = Collections.setOf(s)
def idiomatic = Collections.setOf(op)
def jwk = builder().operation(op).build()
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
}
@Test
void testOperationCustom() {
def s = UUID.randomUUID().toString()
def op = Jwks.OP.builder().id(s).build()
def canonical = Collections.setOf(s)
def idiomatic = Collections.setOf(op)
def jwk = builder().operation(op).build()
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
}
@Test
void testOperationCustomOverridesDefault() {
def s = 'sign'
def op = Jwks.OP.builder().id(s).related('verify').build()
def canonical = Collections.setOf(s)
def idiomatic = Collections.setOf(op)
def jwk = builder().operation(op).build()
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
assertSame op, jwk.getOperations().iterator().next()
//now assert that the standard VERIFY operation treats this as related since it has the same ID:
canonical = Collections.setOf(s, 'verify')
idiomatic = Collections.setOf(op, Jwks.OP.VERIFY)
jwk = builder().operation(op).operation(Jwks.OP.VERIFY).build() as Jwk
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
}
@Test @Test
void testOperations() { void testOperations() {
def a = UUID.randomUUID().toString() def a = 'sign'
def b = UUID.randomUUID().toString() def b = 'verify'
def set = [a, b] as Set<String> def canonical = Collections.setOf(a, b)
def jwk = builder().operations(set).build() def idiomatic = Collections.setOf(Jwks.OP.SIGN, Jwks.OP.VERIFY)
assertEquals set, jwk.getOperations() def jwk = builder().operations(idiomatic).build()
assertEquals set, jwk.key_ops assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
} }
@Test @Test
void testOperationsByPut() { void testOperationsUnrelated() {
def a = UUID.randomUUID().toString() try {
def b = UUID.randomUUID().toString() // exception thrown on setter, before calling build:
def set = [a, b] as Set<String> builder().operations(Collections.setOf(Jwks.OP.SIGN, Jwks.OP.ENCRYPT))
def jwk = builder().add('key_ops', set).build() fail()
assertEquals set, jwk.getOperations() } catch (IllegalArgumentException e) {
assertEquals set, jwk.key_ops String msg = 'Unrelated key operations are not allowed. KeyOperation [\'encrypt\' (Encrypt content)] is ' +
'unrelated to [\'sign\' (Compute digital signature or MAC)].'
assertEquals msg, e.getMessage()
}
} }
@Test @Test
//ensures that even if a raw single value is present it is represented as a Set per the JWA spec (string array) void testOperationsPutUnrelatedStrings() {
void testOperationsByPutSingleValue() { try {
def a = UUID.randomUUID().toString() builder().add('key_ops', ['sign', 'encrypt']).build()
def set = [a] as Set<String> fail()
def jwk = builder().add('key_ops', a).build() // <-- put uses single raw value, not a set } catch (MalformedKeyException e) {
assertEquals set, jwk.getOperations() // <-- still get a set String msg = 'Unable to create JWK: Unrelated key operations are not allowed. KeyOperation ' +
assertEquals set, jwk.key_ops // <-- still get a set '[\'encrypt\' (Encrypt content)] is unrelated to [\'sign\' (Compute digital signature or MAC)].'
assertEquals msg, e.getMessage()
}
}
@Test
void testOperationsByCanonicalPut() {
def a = 'encrypt'
def b = 'decrypt'
def canonical = Collections.setOf(a, b)
def idiomatic = Collections.setOf(Jwks.OP.ENCRYPT, Jwks.OP.DECRYPT)
def jwk = builder().add('key_ops', canonical).build() // Set of String values, not KeyOperation objects
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
}
@Test
void testOperationsByIdiomaticPut() {
def a = 'encrypt'
def b = 'decrypt'
def canonical = Collections.setOf(a, b)
def idiomatic = Collections.setOf(Jwks.OP.ENCRYPT, Jwks.OP.DECRYPT)
def jwk = builder().add('key_ops', idiomatic).build() // Set of KeyOperation values, not strings
assertEquals idiomatic, jwk.getOperations()
assertEquals canonical, jwk.key_ops
}
@Test
void testCustomOperationOverridesDefault() {
def op = Jwks.OP.builder().id('sign').description('Different Description')
.related(Jwks.OP.VERIFY.id).build()
def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build())
def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build()
println jwk
} }
@Test @Test
@ -159,6 +262,7 @@ class AbstractJwkBuilderTest {
JwkContext newContext(JwkContext src, Key key) { JwkContext newContext(JwkContext src, Key key) {
return null return null
} }
@Override @Override
Jwk createJwk(JwkContext jwkContext) { Jwk createJwk(JwkContext jwkContext) {
throw new IllegalArgumentException("foo") throw new IllegalArgumentException("foo")

View File

@ -16,11 +16,12 @@
package io.jsonwebtoken.impl.security package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.Field
import io.jsonwebtoken.impl.lang.Fields
import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Encoders
import org.junit.Test import org.junit.Test
import static org.junit.Assert.assertArrayEquals import static org.junit.Assert.*
import static org.junit.Assert.assertEquals
class DefaultJwkContextTest { class DefaultJwkContextTest {
@ -120,4 +121,24 @@ class DefaultJwkContextTest {
String s = '{kty=oct, k=<redacted>}' String s = '{kty=oct, k=<redacted>}'
assertEquals "$s", "${ctx.toString()}" assertEquals "$s", "${ctx.toString()}"
} }
@Test
void testFieldWithoutKey() {
def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS)
Field field = Fields.string('kid', 'My Key ID')
def newCtx = ctx.field(field)
assertSame field, newCtx.@FIELDS.get('kid')
assertNull newCtx.getKey()
}
@Test
void testFieldWithKey() {
def key = TestKeys.HS256
def ctx = new DefaultJwkContext(DefaultSecretJwk.FIELDS)
ctx.setKey(key)
Field field = Fields.string('kid', 'My Key ID')
def newCtx = ctx.field(field)
assertSame field, newCtx.@FIELDS.get('kid') // registry created with custom field instead of default
assertSame key, newCtx.getKey() // copied over correctly
}
} }

View File

@ -16,7 +16,9 @@
package io.jsonwebtoken.impl.security package io.jsonwebtoken.impl.security
import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Jwks import io.jsonwebtoken.security.Jwks
import io.jsonwebtoken.security.MalformedKeyException
import org.junit.Test import org.junit.Test
import java.security.Provider import java.security.Provider
@ -26,6 +28,22 @@ import static org.junit.Assert.*
class DefaultJwkParserBuilderTest { class DefaultJwkParserBuilderTest {
// This JSON was borrowed from RFC7520Section3Test.FIGURE_2 and modified to
// replace the 'use' member with 'key_ops` for this test:
static String UNRELATED_OPS_JSON = Strings.trimAllWhitespace('''
{
"kty": "EC",
"kid": "bilbo.baggins@hobbiton.example",
"key_ops": ["sign", "encrypt"],
"crv": "P-521",
"x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9
A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
"y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVy
SsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1",
"d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zb
KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
}''')
@Test @Test
void testDefault() { void testDefault() {
def builder = Jwks.parser() as DefaultJwkParserBuilder def builder = Jwks.parser() as DefaultJwkParserBuilder
@ -50,4 +68,26 @@ class DefaultJwkParserBuilderTest {
def parser = Jwks.parser().deserializeJsonWith(deserializer).build() as DefaultJwkParser def parser = Jwks.parser().deserializeJsonWith(deserializer).build() as DefaultJwkParser
assertSame deserializer, parser.deserializer assertSame deserializer, parser.deserializer
} }
@Test
void testOperationPolicy() {
def parser = Jwks.parser().build() as DefaultJwkParser
try {
// parse a JWK that has unrelated operations (prevented by default):
parser.parse(UNRELATED_OPS_JSON)
fail()
} catch (MalformedKeyException expected) {
String msg = "Unable to create JWK: Unrelated key operations are not allowed. KeyOperation " +
"['encrypt' (Encrypt content)] is unrelated to ['sign' (Compute digital signature or MAC)]."
assertEquals msg, expected.message
}
}
@Test
void testOperationPolicyOverride() {
def policy = Jwks.OP.policy().allowUnrelated(true).build()
def parser = Jwks.parser().operationPolicy(policy).build() as DefaultJwkParser
assertNotNull parser.parse(UNRELATED_OPS_JSON) // no exception because policy allows it
}
} }

View File

@ -78,7 +78,7 @@ class DefaultJwkParserTest {
@Test @Test
void testDeserializationFailure() { void testDeserializationFailure() {
def parser = new DefaultJwkParser(null, Services.loadFirst(Deserializer)) { def parser = new DefaultJwkParser(null, Services.loadFirst(Deserializer), AbstractJwkBuilder.DEFAULT_OPERATION_POLICY) {
@Override @Override
protected Map<String, ?> deserialize(String json) { protected Map<String, ?> deserialize(String json) {
throw new DeserializationException("test") throw new DeserializationException("test")

View File

@ -0,0 +1,76 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.security.Jwks
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*
class DefaultKeyOperationBuilderTest {
private DefaultKeyOperationBuilder builder
@Before
void setUp() {
this.builder = new DefaultKeyOperationBuilder()
}
@Test
void testId() {
def id = 'foo'
def op = builder.id(id).build() as DefaultKeyOperation
assertEquals id, op.id
assertEquals DefaultKeyOperation.CUSTOM_DESCRIPTION, op.description
assertFalse op.isRelated(Jwks.OP.SIGN)
}
@Test
void testDescription() {
def id = 'foo'
def description = 'test'
def op = builder.id(id).description(description).build()
assertEquals id, op.id
assertEquals 'test', op.description
}
@Test
void testRelated() {
def id = 'foo'
def related = 'related'
def opA = builder.id(id).related(related).build()
def opB = builder.id(related).related(id).build()
assertEquals id, opA.id
assertEquals related, opB.id
assertTrue opA.isRelated(opB)
assertTrue opB.isRelated(opA)
assertFalse opA.isRelated(Jwks.OP.SIGN)
assertFalse opA.isRelated(Jwks.OP.SIGN)
}
@Test
void testRelatedNull() {
def op = builder.id('foo').related(null).build()
assertTrue op.related.isEmpty()
}
@Test
void testRelatedEmpty() {
def op = builder.id('foo').related(' ').build()
assertTrue op.related.isEmpty()
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.security.Jwks
import io.jsonwebtoken.security.KeyOperation
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.*
class DefaultKeyOperationPolicyBuilderTest {
DefaultKeyOperationPolicyBuilder builder
@Before
void setUp() {
builder = new DefaultKeyOperationPolicyBuilder()
}
@Test
void testDefault() {
def policy = builder.build()
assertTrue policy.operations.containsAll(Jwks.OP.get().values())
// unrelated operations not allowed:
def op = Jwks.OP.builder().id('foo').build()
try {
policy.validate([op, Jwks.OP.SIGN])
fail("Unrelated operations are not allowed by default.")
} catch (IllegalArgumentException expected) {
String msg = 'Unrelated key operations are not allowed. KeyOperation ' +
'[\'sign\' (Compute digital signature or MAC)] is unrelated to [\'foo\' (Custom key operation)].'
assertEquals msg, expected.getMessage()
}
}
@Test
void testAdd() {
def op = Jwks.OP.builder().id('foo').build()
def policy = builder.add(op).build()
assertTrue policy.operations.contains(op)
}
@Test
void testAddNull() {
def orig = builder.build()
def policy = builder.add((KeyOperation) null).build()
assertEquals orig, policy
}
@Test
void testAddCollection() {
def foo = Jwks.OP.builder().id('foo').build()
def bar = Jwks.OP.builder().id('bar').build()
def policy = builder.add([foo, bar]).build()
assertTrue policy.operations.contains(foo)
assertTrue policy.operations.contains(bar)
}
@Test
void testAddNullCollection() {
def orig = builder.build()
def policy = builder.add((Collection<KeyOperation>) null).build()
assertEquals orig, policy
}
@Test
void testAllowUnrelatedTrue() { // testDefault has it false as expected
def foo = Jwks.OP.builder().id('foo').build()
def policy = builder.allowUnrelated(true).build()
policy.validate([foo, Jwks.OP.SIGN]) // no exception thrown since unrelated == true
}
@Test
void testHashCode() {
def a = builder.add(Jwks.OP.builder().id('foo').build()).build()
def b = builder.build()
assertFalse a.is(b) // identity equals is different
def ahc = a.hashCode()
def bhc = b.hashCode()
assertEquals ahc, bhc // still same hashcode
}
@Test
void testEquals() {
def a = builder.add(Jwks.OP.builder().id('foo').build()).build()
def b = builder.build()
assertFalse a.is(b) // identity equals is different
assertEquals a, b // but still equals
}
@Test
void testEqualsIdentity() {
def policy = builder.build()
assertEquals policy, policy
}
@SuppressWarnings('ChangeToOperator')
@Test
void testEqualsUnexpectedType() {
assertFalse builder.build().equals(new Object())
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.security.Jwks
import org.junit.Test
import static org.junit.Assert.*
class DefaultKeyOperationTest {
@Test
void testCustom() {
def op = new DefaultKeyOperation('foo')
assertEquals DefaultKeyOperation.CUSTOM_DESCRIPTION, op.getDescription()
assertNotNull op.related
assertTrue op.related.isEmpty()
}
@Test
void testUnrelated() {
assertFalse new DefaultKeyOperation('foo').isRelated(Jwks.OP.SIGN)
}
@Test
void testRelatedNull() {
assertFalse Jwks.OP.SIGN.isRelated(null)
}
@Test
void testRelatedEquals() {
def op = Jwks.OP.SIGN as DefaultKeyOperation
assertTrue op.isRelated(op)
}
@Test
void testRelatedTrue() {
def op = Jwks.OP.SIGN as DefaultKeyOperation
assertTrue op.isRelated(Jwks.OP.VERIFY)
}
@Test
void testRelatedFalse() {
def op = Jwks.OP.SIGN as DefaultKeyOperation
assertFalse op.isRelated(Jwks.OP.ENCRYPT)
}
}

View File

@ -19,6 +19,7 @@ import io.jsonwebtoken.Jwts
import io.jsonwebtoken.impl.lang.Converters import io.jsonwebtoken.impl.lang.Converters
import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.Collections
import io.jsonwebtoken.security.* import io.jsonwebtoken.security.*
import org.junit.Test import org.junit.Test
@ -143,7 +144,9 @@ class JwksTest {
@Test @Test
void testOperations() { void testOperations() {
testProperty('operations', 'key_ops', ['foo', 'bar'] as Set<String>) def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set<KeyOperation>
def canonical = Collections.setOf('sign', 'verify')
testProperty('operations', 'key_ops', val, canonical)
} }
@Test @Test

View File

@ -0,0 +1,42 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.security.Jwks
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertSame
class KeyOperationConverterTest {
@Test
void testApplyFromStandardId() {
Jwks.OP.get().values().each {
def id = it.id
def op = KeyOperationConverter.DEFAULT.applyFrom(id)
assertSame it, op
}
}
@Test
void testApplyFromCustomId() {
def id = 'custom'
def op = KeyOperationConverter.DEFAULT.applyFrom(id)
assertEquals id, op.id
assertEquals DefaultKeyOperation.CUSTOM_DESCRIPTION, op.description
}
}

View File

@ -34,5 +34,6 @@ class PrivateConstructorsTest {
new Jwts.ZIP() new Jwts.ZIP()
new Jwks.CRV() new Jwks.CRV()
new Jwks.HASH() new Jwks.HASH()
new Jwks.OP()
} }
} }

View File

@ -30,7 +30,8 @@ import static org.junit.Assert.*
*/ */
class SecretJwkFactoryTest { class SecretJwkFactoryTest {
@Test // if a jwk does not have an 'alg' or 'use' field, we default to an AES key @Test
// if a jwk does not have an 'alg' or 'use' field, we default to an AES key
void testNoAlgNoSigJcaName() { void testNoAlgNoSigJcaName() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build()
SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk
@ -47,7 +48,7 @@ class SecretJwkFactoryTest {
@Test @Test
void testSignOpSetsKeyHmacSHA256() { void testSignOpSetsKeyHmacSHA256() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build()
SecretJwk result = Jwks.builder().add(jwk).operations(["sign"] as Set<String>).build() as SecretJwk SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk
assertNull result.getAlgorithm() assertNull result.getAlgorithm()
assertNull result.get('use') assertNull result.get('use')
assertEquals 'HmacSHA256', result.toKey().getAlgorithm() assertEquals 'HmacSHA256', result.toKey().getAlgorithm()
@ -63,7 +64,7 @@ class SecretJwkFactoryTest {
@Test @Test
void testSignOpSetsKeyHmacSHA384() { void testSignOpSetsKeyHmacSHA384() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build()
SecretJwk result = Jwks.builder().add(jwk).operations(["sign"] as Set<String>).build() as SecretJwk SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk
assertNull result.getAlgorithm() assertNull result.getAlgorithm()
assertNull result.get('use') assertNull result.get('use')
assertEquals 'HmacSHA384', result.toKey().getAlgorithm() assertEquals 'HmacSHA384', result.toKey().getAlgorithm()
@ -79,13 +80,14 @@ class SecretJwkFactoryTest {
@Test @Test
void testSignOpSetsKeyHmacSHA512() { void testSignOpSetsKeyHmacSHA512() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build()
SecretJwk result = Jwks.builder().add(jwk).operations(["sign"] as Set<String>).build() as SecretJwk SecretJwk result = Jwks.builder().add(jwk).operations([Jwks.OP.SIGN]).build() as SecretJwk
assertNull result.getAlgorithm() assertNull result.getAlgorithm()
assertNull result.get('use') assertNull result.get('use')
assertEquals 'HmacSHA512', result.toKey().getAlgorithm() assertEquals 'HmacSHA512', result.toKey().getAlgorithm()
} }
@Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256 @Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256
void testNoAlgAndSigUseForHS256() { void testNoAlgAndSigUseForHS256() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build()
assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('alg')
@ -94,7 +96,8 @@ class SecretJwkFactoryTest {
assertEquals 'HmacSHA256', result.toKey().getAlgorithm() // jcaName has been changed to a sig algorithm assertEquals 'HmacSHA256', result.toKey().getAlgorithm() // jcaName has been changed to a sig algorithm
} }
@Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384 @Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384
void testNoAlgAndSigUseForHS384() { void testNoAlgAndSigUseForHS384() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).build()
assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('alg')
@ -103,7 +106,8 @@ class SecretJwkFactoryTest {
assertEquals 'HmacSHA384', result.toKey().getAlgorithm() assertEquals 'HmacSHA384', result.toKey().getAlgorithm()
} }
@Test // no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512 @Test
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512
void testNoAlgAndSigUseForHS512() { void testNoAlgAndSigUseForHS512() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).build()
assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('alg')
@ -112,7 +116,8 @@ class SecretJwkFactoryTest {
assertEquals 'HmacSHA512', result.toKey().getAlgorithm() assertEquals 'HmacSHA512', result.toKey().getAlgorithm()
} }
@Test // no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES @Test
// no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES
void testNoAlgAndNonSigUse() { void testNoAlgAndNonSigUse() {
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build() SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).build()
assertFalse jwk.containsKey('alg') assertFalse jwk.containsKey('alg')

View File

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package io.jsonwebtoken package io.jsonwebtoken.security
import io.jsonwebtoken.impl.security.ECCurve import io.jsonwebtoken.impl.security.ECCurve
import io.jsonwebtoken.impl.security.EdwardsCurve import io.jsonwebtoken.impl.security.EdwardsCurve
import io.jsonwebtoken.impl.security.StandardCurves import io.jsonwebtoken.impl.security.StandardCurves
import io.jsonwebtoken.security.Jwks
import org.junit.Test import org.junit.Test
import static org.junit.Assert.assertSame import static org.junit.Assert.assertSame

View File

@ -0,0 +1,60 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.security
import io.jsonwebtoken.impl.security.StandardKeyOperations
import org.junit.Test
import static org.junit.Assert.*
class JwksOPTest {
@Test
void testRegistry() {
assertTrue Jwks.OP.get() instanceof StandardKeyOperations
}
static void testInstance(KeyOperation op, String id, String description, KeyOperation related) {
assertEquals id, op.getId()
assertEquals description, op.getDescription()
if (related) {
assertTrue op.isRelated(related)
}
assertEquals id.hashCode(), op.hashCode()
assertEquals "'$id' ($description)" as String, op.toString()
assertTrue op.equals(op)
assertTrue op.is(op)
assertTrue op == op
assertEquals op, Jwks.OP.get().get(id)
assertSame op, Jwks.OP.get().get(id)
}
@Test
void testInstances() {
testInstance(Jwks.OP.SIGN, 'sign', 'Compute digital signature or MAC', Jwks.OP.VERIFY)
testInstance(Jwks.OP.VERIFY, 'verify', 'Verify digital signature or MAC', Jwks.OP.SIGN)
testInstance(Jwks.OP.ENCRYPT, 'encrypt', 'Encrypt content', Jwks.OP.DECRYPT)
testInstance(Jwks.OP.DECRYPT, 'decrypt', 'Decrypt content and validate decryption, if applicable', Jwks.OP.ENCRYPT)
testInstance(Jwks.OP.WRAP_KEY, 'wrapKey', 'Encrypt key', Jwks.OP.UNWRAP_KEY)
testInstance(Jwks.OP.UNWRAP_KEY, 'unwrapKey', 'Decrypt key and validate decryption, if applicable', Jwks.OP.WRAP_KEY)
testInstance(Jwks.OP.DERIVE_KEY, 'deriveKey', 'Derive key', null)
assertFalse Jwks.OP.DERIVE_KEY.isRelated(Jwks.OP.DERIVE_BITS)
testInstance(Jwks.OP.DERIVE_BITS, 'deriveBits', 'Derive bits not to be used as a key', null)
assertFalse Jwks.OP.DERIVE_BITS.isRelated(Jwks.OP.DERIVE_KEY)
}
}