mirror of https://github.com/jwtk/jjwt.git
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:
parent
a6792d938f
commit
847ad1332c
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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()/*...*/.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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue