All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xbib.net.security.signatures.Signature Maven / Gradle / Ivy

The newest version!
package org.xbib.net.security.signatures;

import java.security.spec.AlgorithmParameterSpec;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Signature {

    /**
     * Regular expression pattern for fields present in the Authorization field.
     * Fields value may be double-quoted strings, e.g. algorithm="hs2019"
     * Some fields may be numerical values without double-quotes, e.g. created=123456
     */
    private static final Pattern RFC_2617_PARAM = Pattern
            .compile("(?\\w+)=((\"(?[^\"]*)\")|(?\\d+\\.?\\d*))");
    /**
     * The maximum time skew between the client and the server.
     * This is used to validate the (created) and (expires) fields in the HTTP signature.
     */
    public static long maxTimeSkewInMilliseconds = 30 * 1000L;
    /**
     * REQUIRED.  The `keyId` field is an opaque string that the server can
     * use to look up the component they need to validate the signature.  It
     * could be an SSH key fingerprint, a URL to machine-readable key data,
     * an LDAP DN, etc.  Management of keys and assignment of `keyId` is out
     * of scope for this document.
     */
    private final String keyId;
    /**
     * RECOMMENDED.  The `signingAlgorithm` parameter is used to specify the digital
     * signature algorithm to use when generating the signature.  Valid
     * values for this parameter can be found in the Signature Algorithms
     * registry located at http://www.iana.org/assignments/signature-
     * algorithms and MUST NOT be marked "deprecated".
     * 

* Verifiers MUST determine the signature's Algorithm from the keyId parameter * rather than from algorithm. If algorithm is provided and differs from or * is incompatible with the algorithm or key material identified by keyId * (for example, algorithm has a value of rsa-sha256 but keyId identifies * an EdDSA key), then implementations MUST produce an error. *

* https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/ */ private final SigningAlgorithm signingAlgorithm; /** * REQUIRED. The `algorithm` parameter is used to specify the digital * signature algorithm to use when generating the signature. Valid * values for this parameter can be found in the Signature Algorithms * registry located at http://www.iana.org/assignments/signature- * algorithms and MUST NOT be marked "deprecated". */ private final Algorithm algorithm; /** * REQUIRED. The `signature` parameter is a base 64 encoded digital * signature, as described in RFC 4648 [RFC4648], Section 4 [4]. The * client uses the `algorithm` and `headers` signature parameters to * form a canonicalized `signing string`. This `signing string` is then * signed with the key associated with `keyId` and the algorithm * corresponding to `algorithm`. The `signature` parameter is then set * to the base 64 encoding of the signature. *

* Signing: this field is calculated from the input data. * Verification: this field is parsed from the signature field in the * Authorization header. */ private final String signature; /** * OPTIONAL. The `headers` parameter is used to specify the list of * HTTP headers included when generating the signature for the message. * If specified, it should be a lowercased, quoted list of HTTP header * fields, separated by a single space character. If not specified, * implementations MUST operate as if the field were specified with a * single value, the `Date` header, in the list of HTTP headers. Note * that the list order is important, and MUST be specified in the order * the HTTP header field-value pairs are concatenated together during * signing. */ private final List headers; /** * OPTIONAL. The `parameterSpec` is used to specify the cryptographic * parameters. Some cryptographic algorithm such as RSASSA-PSS * require parameters. */ private final AlgorithmParameterSpec parameterSpec; /** * OPTIONAL. The configurable signature's maximum validation duration, in milliseconds. * This field is applicable when the signed headers include '(expires)'. * In that case, the value of the '(expires)' field is calculated by adding * maxSignatureValidityDuration to the timestamp of the signature creation time. *

* This field is used to derive the signature expiration time when a cryptographic signature * is generated. */ private final Long maxSignatureValidityDuration; /** * The signature's Creation Time, in milliseconds since the epoch. *

* This field is set at the time the cryptographic signature is generated, or when * the 'Authorization' header is parsed. */ private final Long signatureCreatedTime; /** * The signature's Expiration Time, in milliseconds since the epoch. *

* This field is set at the time the cryptographic signature is generated, or when * the 'Authorization' header is parsed. */ private final Long signatureExpiresTime; /** * Construct a signature configuration instance with the specified keyId, algorithm and HTTP headers. * * @param keyId An opaque string that the server can use to look up the component they need to validate the signature. * @param signingAlgorithm An identifier for the HTTP Signature algorithm. * This should be "hs2019" except for legacy applications that use an older version of the draft HTTP signature specification. * @param algorithm The detailed algorithm used to sign the message. * @param parameterSpec optional cryptographic parameters for the signature. * @param headers The list of HTTP headers that will be used in the signature. */ public Signature(final String keyId, final String signingAlgorithm, final String algorithm, final AlgorithmParameterSpec parameterSpec, final List headers) { this(keyId, getSigningAlgorithm(signingAlgorithm), getAlgorithm(algorithm), parameterSpec, null, headers); } public Signature(final String keyId, final String signingAlgorithm, final String algorithm, final AlgorithmParameterSpec parameterSpec, final String signature, final List headers) { this(keyId, getSigningAlgorithm(signingAlgorithm), getAlgorithm(algorithm), parameterSpec, signature, headers); } public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, final AlgorithmParameterSpec parameterSpec, final String signature, final List headers) { this(keyId, signingAlgorithm, algorithm, parameterSpec, signature, headers, null); } public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, final AlgorithmParameterSpec parameterSpec, final String signature, final List headers, final Long maxSignatureValidityDuration) { this(keyId, signingAlgorithm, algorithm, parameterSpec, signature, headers, maxSignatureValidityDuration, null, null); } public Signature(final String keyId, final SigningAlgorithm signingAlgorithm, final Algorithm algorithm, final AlgorithmParameterSpec parameterSpec, final String signature, final List headers, final Long maxSignatureValidityDuration, final Long signatureCreatedTime, final Long signatureExpiresTime) { if (keyId == null || keyId.trim().isEmpty()) { throw new IllegalArgumentException("keyId is required."); } if (algorithm == null) { throw new IllegalArgumentException("algorithm is required."); } if (signingAlgorithm != null && signingAlgorithm.getSupportedAlgorithms() != null && !signingAlgorithm.getSupportedAlgorithms().contains(algorithm)) { throw new IllegalArgumentException("Signing algorithm " + signingAlgorithm.getAlgorithmName() + " is not compatible with " + algorithm.getPortableName()); } this.keyId = keyId; this.signingAlgorithm = signingAlgorithm; this.algorithm = algorithm; if (maxSignatureValidityDuration != null && maxSignatureValidityDuration <= 0) { throw new IllegalArgumentException("Signature max validity must be a positive number"); } this.maxSignatureValidityDuration = maxSignatureValidityDuration; this.signatureCreatedTime = signatureCreatedTime; this.signatureExpiresTime = signatureExpiresTime; this.signature = signature; this.parameterSpec = parameterSpec; if (headers == null || headers.size() == 0) { this.headers = List.of("date"); } else { this.headers = Collections.unmodifiableList(lowercase(headers)); } } private static Algorithm getAlgorithm(final String algorithm) { if (algorithm == null) throw new IllegalArgumentException("Algorithm cannot be null"); return Algorithm.get(algorithm); } private static SigningAlgorithm getSigningAlgorithm(final String scheme) { if (scheme == null) { throw new IllegalArgumentException("Signing scheme cannot be null"); } return SigningAlgorithm.get(scheme); } /** * Constructs a Signature object by parsing the 'Authorization' header. *

* As stated in the HTTP signature specification, the value of the algorithm parameter in * the 'Authorization' header should be set to generic identifier. The detailed algorithm * should be derived from the keyId. Hence it is not possible to determine the detailed * algorithm by inspecting the signature data. * * @param authorization The value of the HTTP 'Authorization' header containing the signature data. * @param algorithm The detailed cryptographic algorithm for the HTTP signature. * @return The Signature object. */ public static Signature fromString(String authorization, final Algorithm algorithm) { /* * A HTTP signature field value in the authorization header. */ class FieldValue { /** * The field value. It may be a string or number. */ private final Object value; /** * A flag indicating whether the field is a string or number. */ private final boolean isNumber; FieldValue(final String value, final boolean isNumber) throws ParseException { this.isNumber = isNumber; if (isNumber) { this.value = NumberFormat.getInstance().parse(value); } else { this.value = value; } } /** Returns true if the field is a string */ boolean isString() { return !isNumber; } /** Returns true if the field is a number */ boolean isNumber() { return isNumber; } /** Returns true if the field is an integer value */ boolean isInteger() { return value instanceof Long; } /** Returns the field as a string, or null if the field is not a string. */ String getValueAsString() { if (!isString()) return null; return (String) value; } /** Returns the field as a long value, or null if the field is not a integer number. */ Long getValueAsLong() { if (!isInteger()) return null; return ((Number) value).longValue(); } /** Returns the field as a double value, or null if the field is not a number. */ Double getValueAsDouble() { if (!isNumber()) return null; return ((Number) value).doubleValue(); } } try { authorization = normalize(authorization); final Map map = new HashMap<>(); final Matcher matcher = RFC_2617_PARAM.matcher(authorization); while (matcher.find()) { final String key = matcher.group("key").toLowerCase(); boolean isNumber = false; String value = matcher.group("stringValue"); if (value == null) { value = matcher.group("numberValue"); isNumber = true; } map.put(key, new FieldValue(value, isNumber)); } final List headers = new ArrayList(); FieldValue fieldValue = map.get("headers"); if (fieldValue != null) { if (!fieldValue.isString()) { throw new IllegalArgumentException("headers field must be a double-quoted string"); } Collections.addAll(headers, fieldValue.getValueAsString().toLowerCase().split(" +")); } String keyid = null; fieldValue = map.get("keyid"); if (fieldValue != null && fieldValue.isString()) { keyid = fieldValue.getValueAsString(); } if (keyid == null) { throw new MissingKeyIdException(); } String algorithmField = null; fieldValue = map.get("algorithm"); if (fieldValue != null && fieldValue.isString()) { algorithmField = fieldValue.getValueAsString(); } if (algorithmField == null) { throw new MissingAlgorithmException(); } String signature = null; fieldValue = map.get("signature"); if (fieldValue != null && fieldValue.isString()) { signature = fieldValue.getValueAsString(); } if (signature == null) { throw new MissingSignatureException(); } Long created = null; fieldValue = map.get("created"); if (fieldValue != null) { if (!fieldValue.isInteger()) { throw new InvalidCreatedFieldException("Field must be an integer value"); } created = fieldValue.getValueAsLong() * 1000L; } Long expires = null; // The signature expiration time, in milliseconds since the epoch. fieldValue = map.get("expires"); if (fieldValue != null) { if (!fieldValue.isNumber()) { throw new InvalidExpiresFieldException("Field must be a number"); } expires = (long) (fieldValue.getValueAsDouble() * 1000L); } SigningAlgorithm parsedSigningAlgorithm = null; try { parsedSigningAlgorithm = SigningAlgorithm.get(algorithmField); } catch (final UnsupportedAlgorithmException ex) { // This may happen for older implementations that pass the serialize the detailed // algorithm instead of using 'hs2019'. In that case, the value of 'algorithm' // should be one of the supported values in the Algorithm enum. If not, an // exception is raised. } Algorithm parsedAlgorithm = null; try { parsedAlgorithm = Algorithm.get(algorithmField); if (algorithm != null && parsedAlgorithm.getPortableName() != algorithm.getPortableName()) { throw new IllegalArgumentException("The algorithm does not match the value of the 'Authorization' header."); } } catch (final UnsupportedAlgorithmException ex) { // This is expected for new conformant implementations that set the algorithm // field in the 'Authorization' header to 'hs2019'. The algorithm must be // derived from the keyId. The client is responsible for maintaining the // mapping between the keyId and the detailed cryptographic algorithm. if (algorithm == null) { throw new IllegalArgumentException("The algorithm is required."); } parsedAlgorithm = algorithm; } final Signature s = new Signature(keyid, parsedSigningAlgorithm, parsedAlgorithm, null, signature, headers, null, created, expires); s.verifySignatureValidityDates(); return s; } catch (final AuthenticationException e) { throw e; } catch (final Throwable e) { throw new UnparsableSignatureException(authorization, e); } } public static Signature fromString(String authorization) { return fromString(authorization, null); } private static String normalize(String authorization) { final String start = "signature "; final String prefix = authorization.substring(0, start.length()).toLowerCase(); if (prefix.equals(start)) { authorization = authorization.substring(start.length()); } return authorization.trim(); } /** * Returns the signature creation time. * * @return the signature creation time. */ public Date getSignatureCreation() { if (signatureCreatedTime == null) return null; return new Date(signatureCreatedTime); } /** * Returns the signature creation time in milliseconds since the epoch. * * @return the signature creation time in milliseconds since the epoch. */ public Long getSignatureCreationTimeMilliseconds() { return signatureCreatedTime; } /** * Returns the signature max validity duration, in milliseconds. * * @return the signature max validity duration, in milliseconds. */ public Long getSignatureMaxValidityMilliseconds() { return maxSignatureValidityDuration; } /** * Returns the signature expiration time. * * @return the signature expiration time. */ public Date getSignatureExpiration() { if (signatureExpiresTime == null) return null; return new Date(signatureExpiresTime); } /** * Returns the signature expiration time in milliseconds since the epoch. * * @return the signature expiration time in milliseconds since the epoch. */ public Long getSignatureExpirationTimeMilliseconds() { return signatureExpiresTime; } private List lowercase(final List headers) { final List list = new ArrayList(headers.size()); for (final String header : headers) { list.add(header.toLowerCase()); } return list; } public String getKeyId() { return keyId; } /** * Returns the detailed implementation algorithm for HTTP signatures. * * @return the cryptographic algorithm. */ public Algorithm getAlgorithm() { return algorithm; } /** * Returns the identifier for the HTTP Signature Algorithm, as registered * in the HTTP Signature Algorithms Registry. * * @return the identifier for the HTTP Signature Algorithm. */ public SigningAlgorithm getSigningAlgorithm() { return signingAlgorithm; } /** * Returns the base-64 encoded value of the signature. * * @return the base-64 encoded value of the signature. */ public String getSignature() { return signature; } /** * Returns the specification of cryptographic parameters. * * @return specification of cryptographic parameters. */ public AlgorithmParameterSpec getParameterSpec() { return parameterSpec; } public List getHeaders() { return headers; } /** * Verify the signature is valid with regards to the (created) and (expires) fields. *

* When the '(created)' field is present in the HTTP signature, the '(created)' field * represents the date when the signature has been created. * When the '(expires)' field is present in the HTTP signature, the '(expires)' field * represents the date when the signature expires. */ public void verifySignatureValidityDates() { if (signatureCreatedTime != null && signatureCreatedTime > System.currentTimeMillis() + maxTimeSkewInMilliseconds) { throw new InvalidCreatedFieldException("Signature is not valid yet"); } if (signatureExpiresTime != null && signatureExpiresTime < System.currentTimeMillis()) { throw new InvalidExpiresFieldException("Signature has expired"); } } @Override public String toString() { return toString("Signature"); } /** * Returns the formatted signature parameters without any "Signature " prefix */ public String toParamString() { return toString(null); } public String toString(final String prefix) { if (SigningAlgorithm.HS2019.equals(signingAlgorithm)) { // When the signing algorithm is set to 'hs2019', the value of the algorithm // field must be set to 'hs2019'. The specific crypto algorithm is not // serialized in the 'Authorization' header, the server must derive the value // from the keyId. return (prefix != null ? prefix + " " : "") + "keyId=\"" + keyId + '\"' + (signatureCreatedTime != null ? String.format(",created=%d", signatureCreatedTime / 1000L) : "") + (signatureExpiresTime != null ? String.format(",expires=%.3f", signatureExpiresTime / 1000.0) : "") + ",algorithm=\"" + signingAlgorithm + '\"' + ",headers=\"" + Joiner.join(" ", headers) + '\"' + ",signature=\"" + signature + '\"'; } else { return (prefix != null ? prefix + " " : "") + "keyId=\"" + keyId + '\"' + ",algorithm=\"" + algorithm + '\"' + ",headers=\"" + Joiner.join(" ", headers) + '\"' + ",signature=\"" + signature + '\"'; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy