Java tutorial
/* * Copyright (c) 2013-2017, Joyent, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.joyent.http.signature; import com.joyent.http.signature.crypto.NativeRSAProvider; import org.bouncycastle.util.encoders.Base64; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Signature; import java.security.SignatureException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Date; import java.util.Locale; import java.util.Objects; /** * HTTP authorization signer. This adheres to the specs of the node-http-signature spec. * * @see <a href="http://tools.ietf.org/html/draft-cavage-http-signatures-05">Signing HTTP Messages</a> * @see <a href="https://github.com/joyent/java-manta/blob/b2a180ff8a3ec3795ccc258904888f8305619756/src/main/java/com/joyent/manta/client/crypto/HttpSigner.java">Original Version</a> * @author Yunong Xiao * @author <a href="https://github.com/dekobon">Elijah Zupancic</a> * @since 1.0.0 */ public class Signer { /** * The format for the http date header. * * @deprecated In java8 and later a RFC appropriate format is * defined in the standard library using modern classes. */ @Deprecated @SuppressWarnings("DateFormatConstant") public static final DateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy zzz", Locale.ENGLISH); /** * The template for the Authorization header. */ private static final String AUTHZ_HEADER = "Signature keyId=\"/%s/keys/%s\",algorithm=\"%s\",signature=\"%s\""; /** * The template for the authorization signing signing string. */ private static final String AUTHZ_SIGNING_STRING = "date: %s"; /** * The prefix for the signature component of the authorization header. */ private static final String AUTHZ_PATTERN = "signature=\""; /** * Cryptographic signature used for signing requests. */ private final Signature signature; /** * Private field with the computed http header algorithm. */ private final String httpHeaderAlgorithm; /** * Creates a new instance of the class and enables native code acceleration of * cryptographic signing by default. * * @deprecated Prefer use of {@link Signer.Builder} */ @Deprecated public Signer() { this(true); } /** * Creates a new instance of the class. * * @param useNativeCodeToSign true to enable native code acceleration of cryptographic singing * * @deprecated Prefer use of {@link Signer.Builder} */ @Deprecated @SuppressWarnings("checkstyle:avoidinlineconditionals") public Signer(final boolean useNativeCodeToSign) { this(new Builder("RSA").providerCode(useNativeCodeToSign ? "native.jnagmp" : "stdlib")); } /** * {@link Signer.Builder} This is public (a difference from the * normal Builder pattern) for use by {@link ThreadLocalSigner}. * * @param builder {@link Signer.Builder} */ public Signer(final Builder builder) { Provider provider = builder.algHelper.makeProvider(builder.providerCode); httpHeaderAlgorithm = builder.httpHeaderAlgorithm(); if (provider == null) { try { signature = Signature.getInstance(builder.javaStandardName(provider)); } catch (NoSuchAlgorithmException nsae) { throw new CryptoException(nsae); } } else { try { signature = Signature.getInstance(builder.javaStandardName(provider), provider); } catch (NoSuchAlgorithmException nsae) { throw new CryptoException(nsae); } } } /** * @see KeyPairLoader#getKeyPair * * @param keyPath The path to the key * @return public-private keypair object * @throws IOException If unable to read the private key from the file * * @deprecated Since a {@code KeyPair} is needed to instantiate, * is is now backwards for this to be an instance method. */ @Deprecated public KeyPair getKeyPair(final Path keyPath) throws IOException { return KeyPairLoader.getKeyPair(keyPath); } /** * @see KeyPairLoader#getKeyPair * * @param privateKeyContent private key content as a string * @param password password associated with key * @return public-private keypair object * @throws IOException If unable to read the private key from the string * * @deprecated Since a {@code KeyPair} is needed to instantiate, * is is now backwards for this to be an instance method. */ @Deprecated public KeyPair getKeyPair(final String privateKeyContent, final char[] password) throws IOException { return KeyPairLoader.getKeyPair(privateKeyContent, password); } /** * @see KeyPairLoader#getKeyPair * * @param pKeyBytes private key content as a byte array * @param password password associated with key * @return public-private keypair object * @throws IOException If unable to read the private key from the string * * @deprecated Since a {@code KeyPair} is needed to instantiate, * is is now backwards for this to be an instance method. */ @Deprecated public KeyPair getKeyPair(final byte[] pKeyBytes, final char[] password) throws IOException { return KeyPairLoader.getKeyPair(pKeyBytes, password); } /** * @see KeyPairLoader#getKeyPair * * @param is private key content as a stream * @param password password associated with key * @return public/private keypair object * @throws IOException If unable to read the private key from the string * * @deprecated Since a {@code KeyPair} is needed to instantiate, * is is now backwards for this to be an instance method. */ @Deprecated public KeyPair getKeyPair(final InputStream is, final char[] password) throws IOException { return KeyPairLoader.getKeyPair(is, password); } /** * Generate a signature for an authorization HTTP header using the * current time as a timestamp. * * @param login Account/login name * @param fingerprint key fingerprint (ignored) * @param keyPair public/private keypair * @return value to Authorization header * * @deprecated The fingerprint is now calculated from the given key. */ @Deprecated public String createAuthorizationHeader(final String login, final String fingerprint, final KeyPair keyPair) { return createAuthorizationHeader(login, keyPair, defaultSignDateAsString()); } /** * Generate a signature for an authorization HTTP header using the * current time as a timestamp. * * @param login Account/login name * @param keyPair public/private keypair * @return value to Authorization header */ public String createAuthorizationHeader(final String login, final KeyPair keyPair) { return createAuthorizationHeader(login, keyPair, defaultSignDateAsString()); } /** * Generate a signature for an authorization HTTP header. * * @param login Account/login name * @param fingerprint key fingerprint (ignored) * @param keyPair public/private keypair * @param date Date to be converted to a RFC 822 compliant string * @return value to Authorization header * * @deprecated The fingerprint is now calculated from the given key. */ @Deprecated public String createAuthorizationHeader(final String login, final String fingerprint, final KeyPair keyPair, final Date date) { return createAuthorizationHeader(login, keyPair, date); } /** * Generate a signature for an authorization HTTP header. * * @param login Account/login name * @param keyPair public/private keypair * @param date DateTime to be converted to a RFC 822 compliant string * @return value to Authorization header * * @deprecated Prefer ZonedDateTime to java.util.Date */ @Deprecated public String createAuthorizationHeader(final String login, final KeyPair keyPair, final Date date) { final ZonedDateTime zdt; if (date == null) { zdt = null; } else { zdt = ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); } return createAuthorizationHeader(login, keyPair, zdt); } /** * Generate a signature for an authorization HTTP header. * * @param login Account/login name * @param keyPair public/private keypair * @param dateTime DateTime to be converted to a RFC 822 compliant string * @return value to Authorization header */ public String createAuthorizationHeader(final String login, final KeyPair keyPair, final ZonedDateTime dateTime) { final String stringDate; if (dateTime == null) { stringDate = defaultSignDateAsString(); } else { stringDate = DateTimeFormatter.RFC_1123_DATE_TIME.format(dateTime); } return createAuthorizationHeader(login, keyPair, stringDate); } /** * Generate a signature for an authorization HTTP header. * * @param login Account/login name * @param fingerprint key fingerprint (ignored) * @param keyPair public/private keypair * @param date Date as RFC 822 compliant string * @return value to Authorization header * * @deprecated The fingerprint is now calculated from the given key. */ @Deprecated public String createAuthorizationHeader(final String login, final String fingerprint, final KeyPair keyPair, final String date) { Objects.requireNonNull(login, "Login must be present"); Objects.requireNonNull(keyPair, "Keypair must be present"); return createAuthorizationHeader(login, keyPair, date); } /** * Generate a signature for an authorization HTTP header. * * @param login Account/login name * @param keyPair public/private keypair * @param date Date as RFC 822 compliant string * @return value to Authorization header */ public String createAuthorizationHeader(final String login, final KeyPair keyPair, final String date) { Objects.requireNonNull(login, "Login must be present"); Objects.requireNonNull(keyPair, "Keypair must be present"); try { signature.initSign(keyPair.getPrivate()); final String signingString = String.format(AUTHZ_SIGNING_STRING, date); signature.update(signingString.getBytes(StandardCharsets.UTF_8)); final byte[] signedDate = signature.sign(); final byte[] encodedSignedDate = Base64.encode(signedDate); final String fingerprint = KeyFingerprinter.md5Fingerprint(keyPair); return String.format(AUTHZ_HEADER, login, fingerprint, httpHeaderAlgorithm, new String(encodedSignedDate, StandardCharsets.US_ASCII)); } catch (final InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (final SignatureException e) { throw new CryptoException("invalid signature", e); } } /** * Cryptographically signs an any data input. * * @param login Account/login name * @param fingerprint key fingerprint (ignored) * @param keyPair public/private keypair * @param data data to be signed * @return signed value of data * * @deprecated The fingerprint is now calculated from the given key. */ @Deprecated public byte[] sign(final String login, final String fingerprint, final KeyPair keyPair, final byte[] data) { return sign(login, keyPair, data); } /** * Cryptographically signs an any data input. * * @param login Account/login name * @param keyPair public/private keypair * @param data data to be signed * @return signed value of data */ public byte[] sign(final String login, final KeyPair keyPair, final byte[] data) { Objects.requireNonNull(login, "Login must be present"); Objects.requireNonNull(keyPair, "Keypair must be present"); Objects.requireNonNull(data, "Data must be present"); try { signature.initSign(keyPair.getPrivate()); signature.update(data); return signature.sign(); } catch (final InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (final SignatureException e) { throw new CryptoException("invalid signature", e); } } /** * Cryptographically signs an any data input. * * @param login Account/login name * @param fingerprint key fingerprint (ignored) * @param keyPair public/private keypair * @param data data that was signed * @param signedData data to verify against signature * @return signed value of data * * @deprecated The fingerprint is now calculated from the given key. */ @Deprecated public boolean verify(final String login, final String fingerprint, final KeyPair keyPair, final byte[] data, final byte[] signedData) { return verify(login, keyPair, data, signedData); } /** * Cryptographically signs an any data input. * * @param login Account/login name * @param keyPair public/private keypair * @param data data that was signed * @param signedData data to verify against signature * @return signed value of data */ public boolean verify(final String login, final KeyPair keyPair, final byte[] data, final byte[] signedData) { Objects.requireNonNull(login, "Login must be present"); Objects.requireNonNull(keyPair, "Keypair must be present"); Objects.requireNonNull(signedData, "Data must be present"); try { signature.initVerify(keyPair.getPublic()); signature.update(data); return signature.verify(signedData); } catch (final InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (final SignatureException e) { throw new CryptoException("invalid signature", e); } } /** * The current timestamp in UTC as a RFC 822 compliant string. * @return Date as RFC 822 compliant string */ public String defaultSignDateAsString() { return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)); } /** * Verify a signed HTTP Authorization header. * * @param keyPair public/private keypair * @param authzHeader authorization header value * @param date Date as RFC 822 compliant string * @return True if the request is valid, false if not. * @throws CryptoException If unable to verify the request. */ public boolean verifyAuthorizationHeader(final KeyPair keyPair, final String authzHeader, final String date) { Objects.requireNonNull(keyPair, "Keypair must be present"); Objects.requireNonNull(authzHeader, "AuthzHeader must be present"); Objects.requireNonNull(date, "Date must be present"); String myDate = String.format(AUTHZ_SIGNING_STRING, date); try { signature.initVerify(keyPair.getPublic()); final int startIndex = authzHeader.indexOf(AUTHZ_PATTERN); if (startIndex == -1) { throw new CryptoException(String.format("invalid authorization header %s", authzHeader)); } final String encodedSignedDate = authzHeader.substring(startIndex + AUTHZ_PATTERN.length(), authzHeader.length() - 1); final byte[] signedDate = Base64.decode(encodedSignedDate.getBytes(StandardCharsets.UTF_8)); signature.update(myDate.getBytes(StandardCharsets.UTF_8)); return signature.verify(signedDate); } catch (final InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (final SignatureException e) { throw new CryptoException("invalid signature", e); } } /** * Return a string representation of the full algorithm. For * example: "rsa-sha256" * * @return Algorithm name. */ public String getHttpHeaderAlgorithm() { return httpHeaderAlgorithm; } /** * This method is visible for tests or benchmarks. * * @return instance of the signature cipher implementation */ Signature getSignature() { return signature; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Signer{"); sb.append("signature=").append(signature); sb.append(",provider=").append(signature.getProvider().getName()); sb.append(",httpHeaderAlgorithm=").append(httpHeaderAlgorithm); sb.append('}'); return sb.toString(); } /** * Builder class for {@link Signer}. * * The signing algorithm can be identified by a string (using the * same names as {@link java.security.PrivateKey#getAlgorithm}), * or by just passing in a {@link java.security.KeyPair}. The * supported singing algorithms are RSA, DSA, and ECDSA. * * Signers can be further configured by specifying a string * representation of a hashing algorithms. For example, {@code * SHA512} instead of {@code SHA256}. The default is {@code * SHA256} in for all cases. The supported hash names are: * * <ul> * <li>RSA: {@code SHA1}, {@code SHA256}, {@code SHA512}</li> * <li>DSA: {@code SHA1}, {@code SHA256}</li> * <li>ECDSA: {@code SHA256}, {@code SHA384}, {@code SHA512}</li> * </ul> * * {@code providerCode} is designate and alternative provider to * the standard library. Currently the only algorithm that * supports a custom provider is {@code RSA} with {@code * native.jnagmp}. This is the default. See {@link * com.joyent.http.signature.crypto.NativeRSAWithSHA} for more * information. All singing algorithms support {@code stdlib} to * use the standard library. */ @SuppressWarnings("checkstyle:javadocvariable") public static class Builder { private final SigningAlgorithmHelper algHelper; private String hash; private String providerCode; /** * Instantiate a new Builder based on the algorithm of the * given keypair. * * @param keyPair The given KeyPair. */ public Builder(final KeyPair keyPair) { this.algHelper = SigningAlgorithmHelper.create(keyPair); hash = algHelper.defaultHash(); providerCode = algHelper.defaultProviderCode(); } /** * Instantiate a new Builder based on the explicitly given * algorithm. * * @param algorithm {@link java.security.PrivateKey#getAlgorithm} */ public Builder(final String algorithm) { this.algHelper = SigningAlgorithmHelper.create(algorithm); hash = algHelper.defaultHash(); providerCode = algHelper.defaultProviderCode(); } /** * Overrides the default hash type. * * @param hash New hash type * @return This {@code Builder} object */ @SuppressWarnings("checkstyle:hiddenfield") public Builder hash(final String hash) { algHelper.checkSupportedHash(hash); this.hash = hash; return this; } /** * Overrides the default provider code. * * @param providerCode New provider code * @return This {@code Builder} object */ @SuppressWarnings("checkstyle:hiddenfield") public Builder providerCode(final String providerCode) { algHelper.checkSupportedProviderCode(providerCode); this.providerCode = providerCode; return this; } /** * From the configured singing algorithm and hash, return a * string representation as used by the @see <a * href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature">Java * Cryptography Architecture Standard Algorithm Name * Documentation</a>. * * @param provider Provider used for signing. * @return The standard representation */ private String javaStandardName(final Provider provider) { return hash + "with" + algHelper.providerPrefix(provider) + algHelper.getAlgorithm(); } /** * From the configured signing algorithm and hash, return the * representation formatted for the HTTP Signature field. * * @return The header string */ private String httpHeaderAlgorithm() { return algHelper.getAlgorithm().toLowerCase() + "-" + hash.toLowerCase(); } /** * Returns a newly-created {@code Signer} based on the contents of the * {@code Builder}. * * @return The new {@code Builder} */ public Signer build() { return new Signer(this); } /** * Helper class with per algorithm configuration. */ private abstract static class SigningAlgorithmHelper { /** * Create a new {@code SigningAlgorithmHelper} based on * the given {@code KeyPair}. * * @param keyPair {@code} KeyPair to sign for * @return New {@code SigningAlgorithmHelper} instance. */ public static SigningAlgorithmHelper create(final KeyPair keyPair) { return create(keyPair.getPrivate().getAlgorithm()); } /** * Create a new {@code SigningAlgorithmHelper} based on * the given algorithm code. * @param algorithm {@see java.security.KeyPair#getAlgorithm} * @return New {@code SigningAlgorithmHelper} instance. */ public static SigningAlgorithmHelper create(final String algorithm) { if (algorithm.equals("RSA")) { return new RsaHelper(); } else if (algorithm.equals("DSA")) { return new DsaHelper(); // See NssBridgeKeyConverter on the two names } else if (algorithm.equals("ECDSA") || algorithm.equals("EC")) { return new EcdsaHelper(); } else { throw new IllegalArgumentException("invalid signing algorithm: " + algorithm); } } /** * Return the string code for the instantiated algorithm helper. * * @return {@see java.security.KeyPair#getAlgorithm} */ public abstract String getAlgorithm(); /** * Get all of the hash algorithms supported by the * algorithm, in sorted order. * * @return The sorted hash algorihtm names. */ public abstract String[] getSupportedHashes(); /** * Get the default hash name for this signing algorithm. * * @return The default hash name. */ public abstract String defaultHash(); /** * Get all of the provider codes supported by the * algorithm, in sorted order. * * @return The sorted provider codes. */ public abstract String[] getSupportedProviderCodes(); /** * Get the default provider code for this signing algorithm. * * @return The default provider code */ public abstract String defaultProviderCode(); /** * Throws {@code IllegalArgumentException} if the given * {@code String} does not match a supported hash algorithm. * * @param hash Name to check. */ public void checkSupportedHash(final String hash) { if (Arrays.binarySearch(getSupportedHashes(), hash) == -1) { throw new IllegalArgumentException("invalid hash algorithm: " + hash); } } /** * Throws {@code IllegalArgumentException} if the given * {@code String} does not match a supported provider code. * * @param providerCode Name to check. */ public void checkSupportedProviderCode(final String providerCode) { if (Arrays.binarySearch(getSupportedProviderCodes(), providerCode) == -1) { throw new IllegalArgumentException("invalid providerCode algorithm: " + providerCode); } } /** * A {@code Provider} outside of the Java standard * library, might have a special "Algorithm Name". @see * Signer.Builder#javaStandardName and @see #makeProvider * * @param provider The {@code Provider} from @see #makeProvider. * @return The "Algorithm Name" modification, or the empty string. */ public String providerPrefix(final Provider provider) { return ""; } /** * If a special {@link java.security.Provider} is * requested, construct and return it, otherwise return * {@code null} to use the Java standard library. * * @param providerCode The configured {@code Provider} * code. * @return The new {@link java.security.Provider}, or * {@code null} if using the standard library. */ public Provider makeProvider(final String providerCode) { return null; } } /** * RSA implementation of {@code SigningAlgorithmHelper}. */ @SuppressWarnings({ "checkstyle:javadocmethod", "checkstyle:javadoctype" }) private static class RsaHelper extends SigningAlgorithmHelper { private static final String[] SUPPORTED_HASHES = { "SHA1", "SHA256", "SHA512" }; private static final String[] SUPPORTED_PROVIDER_CODES = { "native.jnagmp", "stdlib" }; /** * OS names with native support in jnagmp. * Always keep values sorted because we binary search them. */ private static final String[] SUPPORTED_NATIVE_OS = new String[] { "linux", "mac os x", "sunos" }; /** * Architectures with native support in jnagmp. * Always keep values sorted because we binary search them. */ private static final String[] SUPPORTED_NATIVE_ARCH = new String[] { "amd64", "x86_64" }; /** * When true we are on a platform that supports native libgmp for modpow. */ private static final boolean JNAGMP_SUPPORTED; static { final String os = System.getProperty("os.name").toLowerCase(); final String arch = System.getProperty("os.arch").toLowerCase(); JNAGMP_SUPPORTED = Arrays.binarySearch(SUPPORTED_NATIVE_OS, os) >= 0 && Arrays.binarySearch(SUPPORTED_NATIVE_ARCH, arch) >= 0; System.setProperty("native.jnagmp", Objects.toString(JNAGMP_SUPPORTED)); } @Override public String getAlgorithm() { return "RSA"; } @Override public String[] getSupportedHashes() { return SUPPORTED_HASHES; } @Override public String defaultHash() { return "SHA256"; } @Override public String[] getSupportedProviderCodes() { return SUPPORTED_PROVIDER_CODES; } @Override public String defaultProviderCode() { return "native.jnagmp"; } @Override public String providerPrefix(final Provider provider) { if (provider != null) { return "Native"; } else { return ""; } } @Override public Provider makeProvider(final String providerCode) { if (providerCode.equals("native.jnagmp") && JNAGMP_SUPPORTED) { try { return new NativeRSAProvider(); // if ANYTHING goes wrong, we default to the JVM implementation of the signing algo } catch (Exception e) { e.printStackTrace(); return null; } } else { return null; } } } /** * DSA implementation {@code SigningAlgorithmHelper}. */ @SuppressWarnings({ "checkstyle:javadocmethod", "checkstyle:javadoctype" }) private static class DsaHelper extends SigningAlgorithmHelper { private static final String[] SUPPORTED_HASHES = { "SHA1", "SHA256" }; private static final String[] SUPPORTED_PROVIDER_CODES = { "stdlib" }; @Override public String getAlgorithm() { return "DSA"; } @Override public String[] getSupportedHashes() { return SUPPORTED_HASHES; } @Override public String defaultHash() { return "SHA256"; } @Override public String[] getSupportedProviderCodes() { return SUPPORTED_PROVIDER_CODES; } @Override public String defaultProviderCode() { return "stdlib"; } } /** * ECDSA implementation {@code SigningAlgorithmHelper}. */ @SuppressWarnings({ "checkstyle:javadocmethod", "checkstyle:javadoctype" }) private static class EcdsaHelper extends SigningAlgorithmHelper { private static final String[] SUPPORTED_HASHES = { "SHA256", "SHA384", "SHA512" }; private static final String[] SUPPORTED_PROVIDER_CODES = { "stdlib" }; @Override public String getAlgorithm() { return "ECDSA"; } @Override public String[] getSupportedHashes() { return SUPPORTED_HASHES; } @Override public String defaultHash() { return "SHA256"; } @Override public String[] getSupportedProviderCodes() { return SUPPORTED_PROVIDER_CODES; } @Override public String defaultProviderCode() { return "stdlib"; } } } }