diff --git a/common/src/main/java/org/conscrypt/OpenSSLKey.java b/common/src/main/java/org/conscrypt/OpenSSLKey.java index e0f384dc5..1472ebfe0 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLKey.java +++ b/common/src/main/java/org/conscrypt/OpenSSLKey.java @@ -336,10 +336,12 @@ static PrivateKey getPrivateKey(PKCS8EncodedKeySpec keySpec, int type) throw new InvalidKeySpecException(e); } - if (NativeCrypto.EVP_PKEY_type(key.getNativeRef()) != type) { + int decodedKeyType = NativeCrypto.EVP_PKEY_type(key.getNativeRef()); + if (decodedKeyType != type && + !(decodedKeyType == NativeConstants.EVP_PKEY_SM2 && + type == NativeConstants.EVP_PKEY_EC)) { throw new InvalidKeySpecException("Unexpected key type"); } - try { return key.getPrivateKey(); } catch (NoSuchAlgorithmException e) { diff --git a/openjdk/src/main/java/net/tongsuo/TongsuoProvider.java b/openjdk/src/main/java/net/tongsuo/TongsuoProvider.java index 9a60cd3fa..0f3cad8d0 100644 --- a/openjdk/src/main/java/net/tongsuo/TongsuoProvider.java +++ b/openjdk/src/main/java/net/tongsuo/TongsuoProvider.java @@ -24,5 +24,6 @@ public TongsuoProvider() { // Register TlcpKeyManagerFactoryImpl and TlcpKeyManagerImpl put("KeyManagerFactory.TlcpKeyManagerFactory", TlcpKeyManagerFactoryImpl.class.getName()); // put("X509ExtendedKeyManager.TlcpKeyManager", TlcpKeyManagerImpl.class.getName()); + put("KeyStore.PKCS12", "net.tongsuo.sun.security.pkcs12.PKCS12KeyStore"); } } diff --git a/openjdk/src/main/java/net/tongsuo/crypto/CryptoInsts.java b/openjdk/src/main/java/net/tongsuo/crypto/CryptoInsts.java new file mode 100644 index 000000000..22638ce0f --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/crypto/CryptoInsts.java @@ -0,0 +1,186 @@ +package net.tongsuo.crypto; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import java.security.AlgorithmParameterGenerator; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Signature; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class CryptoInsts { + + static final String PROV_NAME = "Tongsuo_Security_Provider"; + + private static final Set ALGO_PARAMS_ALGOS + = new HashSet<>(Arrays.asList("SM4")); + + public static AlgorithmParameters getAlgorithmParameters(String algorithm) + throws NoSuchAlgorithmException { + AlgorithmParameters algoParams = null; + if (ALGO_PARAMS_ALGOS.contains(algorithm)) { + try { + algoParams = AlgorithmParameters.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + algoParams = AlgorithmParameters.getInstance(algorithm); + } + return algoParams; + } + + private static final Set KEY_FACTORY_ALGOS + = new HashSet<>(Arrays.asList("EC", "SM2")); + + public static KeyFactory getKeyFactory(String algorithm) + throws NoSuchAlgorithmException { + KeyFactory keyFactory = null; + if (KEY_FACTORY_ALGOS.contains(algorithm)) { + try { + keyFactory = KeyFactory.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + keyFactory = KeyFactory.getInstance(algorithm); + } + return keyFactory; + } + + private static final Set KEY_GEN_ALGOS + = new HashSet<>(Arrays.asList("HMacSM3", "SM4")); + + public static KeyGenerator getKeyGenerator(String algorithm) + throws NoSuchAlgorithmException { + KeyGenerator keyGenerator = null; + if (KEY_GEN_ALGOS.contains(algorithm)) { + try { + keyGenerator = KeyGenerator.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + keyGenerator = KeyGenerator.getInstance(algorithm); + } + return keyGenerator; + } + + private static final Set KEY_PAIR_GEN_ALGOS + = new HashSet<>(Arrays.asList("SM2")); + + public static KeyPairGenerator getKeyPairGenerator(String algorithm) + throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = null; + if (KEY_PAIR_GEN_ALGOS.contains(algorithm)) { + try { + keyPairGenerator = KeyPairGenerator.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + keyPairGenerator = KeyPairGenerator.getInstance(algorithm); + } + return keyPairGenerator; + } + + private static final Set CIPHER_ALGOS + = new HashSet<>(Arrays.asList("SM2", "SM4")); + + public static Cipher getCipher(String algorithm) + throws NoSuchPaddingException, NoSuchAlgorithmException { + Cipher cipher = null; + if (CIPHER_ALGOS.contains(algorithm)) { + try { + cipher = Cipher.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + cipher = Cipher.getInstance(algorithm); + } + return cipher; + } + + private static final Set MESSAGE_DIGEST_ALGOS + = new HashSet<>(Collections.singletonList("SM3")); + + public static MessageDigest getMessageDigest(String algorithm) + throws NoSuchAlgorithmException { + MessageDigest messageDigest = null; + if (MESSAGE_DIGEST_ALGOS.contains(algorithm)) { + try { + messageDigest = MessageDigest.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + messageDigest = MessageDigest.getInstance(algorithm); + } + + return messageDigest; + } + + private static final Set MAC_ALGOS + = new HashSet<>(Collections.singletonList("HMacSM3")); + + public static Mac getMac(String algorithm) throws NoSuchAlgorithmException { + Mac mac = null; + if (MAC_ALGOS.contains(algorithm)) { + try { + mac = Mac.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + mac = Mac.getInstance(algorithm); + } + return mac; + } + + private static final Set SIGNATURE_ALGOS + = new HashSet<>(Arrays.asList("SM3withSM2")); + + public static Signature getSignature(String algorithm) + throws NoSuchAlgorithmException { + Signature signature = null; + if (SIGNATURE_ALGOS.contains(algorithm)) { + try { + signature = Signature.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + signature = Signature.getInstance(algorithm); + } + return signature; + } + + private static final Set KEY_AGREEMENT_ALGOS + = new HashSet<>(Arrays.asList("SM2", "ECDH")); + + public static KeyAgreement getKeyAgreement(String algorithm) + throws NoSuchAlgorithmException { + KeyAgreement keyAgreement = null; + if (KEY_AGREEMENT_ALGOS.contains(algorithm)) { + try { + keyAgreement = KeyAgreement.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + keyAgreement = KeyAgreement.getInstance(algorithm); + } + return keyAgreement; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/crypto/CryptoUtils.java b/openjdk/src/main/java/net/tongsuo/crypto/CryptoUtils.java new file mode 100644 index 000000000..dfcfc554d --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/crypto/CryptoUtils.java @@ -0,0 +1,42 @@ +package net.tongsuo.crypto; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +public final class CryptoUtils { + + public static String privilegedGetProperty(String key, String def) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> System.getProperty(key, def)); + } + + public static String privilegedGetProperty(String key) { + return privilegedGetProperty(key, null); + } + + public static Boolean privilegedGetBoolProperty(String key, String def) { + return AccessController.doPrivileged( + (PrivilegedAction) () -> Boolean.parseBoolean( + System.getProperty(key, def))); + } + + public static Boolean privilegedGetBoolProperty(String key) { + return privilegedGetBoolProperty(key, "false"); + } + + public static boolean isJdk8() { + return privilegedGetProperty("java.specification.version").equals("1.8"); + } + + public static boolean isJdk11() { + return privilegedGetProperty("java.specification.version").equals("11"); + } + + public static boolean isJdk17() { + return privilegedGetProperty("java.specification.version").equals("17"); + } + + public static boolean isAndroid() { + return privilegedGetProperty("java.specification.vendor").equals("Android"); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/java/util/HexFormat.java b/openjdk/src/main/java/net/tongsuo/java/util/HexFormat.java new file mode 100644 index 000000000..074ed61d9 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/java/util/HexFormat.java @@ -0,0 +1,1093 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.java.util; + +import net.tongsuo.jdk.internal.misc.SharedSecretsUtil; +import net.tongsuo.jdk.internal.util.Preconditions; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + * {@code HexFormat} converts between bytes and chars and hex-encoded strings which may include + * additional formatting markup such as prefixes, suffixes, and delimiters. + *

+ * There are two factories of {@code HexFormat} with preset parameters {@link #of()} and + * {@link #ofDelimiter(String) ofDelimiter(delimiter)}. For other parameter combinations + * the {@code withXXX} methods return copies of {@code HexFormat} modified + * {@link #withPrefix(String)}, {@link #withSuffix(String)}, {@link #withDelimiter(String)} + * or choice of {@link #withUpperCase()} or {@link #withLowerCase()} parameters. + *

+ * For primitive to hexadecimal string conversions the {@code toHexDigits} + * methods include {@link #toHexDigits(byte)}, {@link #toHexDigits(int)}, and + * {@link #toHexDigits(long)}, etc. The default is to use lowercase characters {@code "0-9","a-f"}. + * For conversions producing uppercase hexadecimal the characters are {@code "0-9","A-F"}. + * Only the {@link HexFormat#isUpperCase() HexFormat.isUpperCase()} parameter is + * considered; the delimiter, prefix and suffix are not used. + * + *

+ * For hexadecimal string to primitive conversions the {@code fromHexDigits} + * methods include {@link #fromHexDigits(CharSequence) fromHexDigits(string)}, + * {@link #fromHexDigitsToLong(CharSequence) fromHexDigitsToLong(string)}, and + * {@link #fromHexDigit(int) fromHexDigit(int)} converts a single character or codepoint. + * For conversions from hexadecimal characters the digits and uppercase and lowercase + * characters in {@code "0-9", "a-f", and "A-F"} are converted to corresponding values + * {@code 0-15}. The delimiter, prefix, suffix, and uppercase parameters are not used. + * + *

+ * For byte array to formatted hexadecimal string conversions + * the {@code formatHex} methods include {@link #formatHex(byte[]) formatHex(byte[])} + * and {@link #formatHex(Appendable, byte[]) formatHex(Appendable, byte[])}. + * The formatted output is a string or is appended to an {@link Appendable} such as + * {@link StringBuilder} or {@link java.io.PrintStream}. + * Each byte value is formatted as the prefix, two hexadecimal characters from the + * uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * For conversions producing uppercase hexadecimal strings use {@link #withUpperCase()}. + * + *

+ * For formatted hexadecimal string to byte array conversions the + * {@code parseHex} methods include {@link #parseHex(CharSequence) parseHex(CharSequence)} and + * {@link #parseHex(char[], int, int) parseHex(char[], offset, length)}. + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * + * @apiNote + * For example, an individual byte is converted to a string of hexadecimal digits using + * {@link HexFormat#toHexDigits(int) toHexDigits(int)} and converted from a string to a + * primitive value using {@link HexFormat#fromHexDigits(CharSequence) fromHexDigits(string)}. + *

{@code
+ *     HexFormat hex = HexFormat.of();
+ *     byte b = 127;
+ *     String byteStr = hex.toHexDigits(b);
+ *
+ *     byte byteVal = (byte)hex.fromHexDigits(byteStr);
+ *     assert(byteStr.equals("7f"));
+ *     assert(b == byteVal);
+ *
+ *     // The hexadecimal digits are: "7f"
+ * }
+ *

+ * For a comma ({@code ", "}) separated format with a prefix ({@code "#"}) + * using lowercase hex digits the {@code HexFormat} is: + *

{@code
+ *     HexFormat commaFormat = HexFormat.ofDelimiter(", ").withPrefix("#");
+ *     byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
+ *     String str = commaFormat.formatHex(bytes);
+ *
+ *     byte[] parsed = commaFormat.parseHex(str);
+ *     assert(Arrays.equals(bytes, parsed));
+ *
+ *     // The formatted string is: "#00, #01, #02, #03, #7c, #7d, #7e, #7f"
+ * }
+ *

+ * For a fingerprint of byte values that uses the delimiter colon ({@code ":"}) + * and uppercase characters the {@code HexFormat} is: + *

{@code
+ *     HexFormat formatFingerprint = HexFormat.ofDelimiter(":").withUpperCase();
+ *     byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
+ *     String str = formatFingerprint.formatHex(bytes);
+ *     byte[] parsed = formatFingerprint.parseHex(str);
+ *     assert(Arrays.equals(bytes, parsed));
+ *
+ *     // The formatted string is: "00:01:02:03:7C:7D:7E:7F"
+ * }
+ * + *

+ * This is a value-based + * class; use of identity-sensitive operations (including reference equality + * ({@code ==}), identity hash code, or synchronization) on instances of + * {@code HexFormat} may have unpredictable results and should be avoided. + * The {@code equals} method should be used for comparisons. + *

+ * This class is immutable and thread-safe. + *

+ * Unless otherwise noted, passing a null argument to any method will cause a + * {@link NullPointerException NullPointerException} to be thrown. + * + * @since 17 + */ + +public final class HexFormat { + + // Access to create strings from a byte array. +// private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess(); + + private static final byte[] UPPERCASE_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + }; + private static final byte[] LOWERCASE_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', + }; + // Analysis has shown that generating the whole array allows the JIT to generate + // better code compared to a slimmed down array, such as one cutting off after 'f' + private static final byte[] DIGITS = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + /** + * Format each byte of an array as a pair of hexadecimal digits. + * The hexadecimal characters are from lowercase alpha digits. + */ + private static final HexFormat HEX_FORMAT = + new HexFormat("", "", "", LOWERCASE_DIGITS); + + private static final byte[] EMPTY_BYTES = {}; + + private final String delimiter; + private final String prefix; + private final String suffix; + private final byte[] digits; + + /** + * Returns a HexFormat with a delimiter, prefix, suffix, and array of digits. + * + * @param delimiter a delimiter, non-null + * @param prefix a prefix, non-null + * @param suffix a suffix, non-null + * @param digits byte array of digits indexed by low nibble, non-null + * @throws NullPointerException if any argument is null + */ + private HexFormat(String delimiter, String prefix, String suffix, byte[] digits) { + this.delimiter = Objects.requireNonNull(delimiter, "delimiter"); + this.prefix = Objects.requireNonNull(prefix, "prefix"); + this.suffix = Objects.requireNonNull(suffix, "suffix"); + this.digits = digits; + } + + /** + * Returns a hexadecimal formatter with no delimiter and lowercase characters. + * The delimiter, prefix, and suffix are empty. + * The methods {@link #withDelimiter(String) withDelimiter}, + * {@link #withUpperCase() withUpperCase}, {@link #withLowerCase() withLowerCase}, + * {@link #withPrefix(String) withPrefix}, and {@link #withSuffix(String) withSuffix} + * return copies of formatters with new parameters. + * + * @return a hexadecimal formatter with no delimiter and lowercase characters + */ + public static HexFormat of() { + return HEX_FORMAT; + } + + /** + * Returns a hexadecimal formatter with the delimiter and lowercase characters. + * The prefix and suffix are empty. + * The methods {@link #withDelimiter(String) withDelimiter}, + * {@link #withUpperCase() withUpperCase}, {@link #withLowerCase() withLowerCase}, + * {@link #withPrefix(String) withPrefix}, and {@link #withSuffix(String) withSuffix} + * return copies of formatters with new parameters. + * + * @param delimiter a delimiter, non-null, may be empty + * @return a {@link HexFormat} with the delimiter and lowercase characters + */ + public static HexFormat ofDelimiter(String delimiter) { + return new HexFormat(delimiter, "", "", LOWERCASE_DIGITS); + } + + /** + * Returns a copy of this {@code HexFormat} with the delimiter. + * @param delimiter the delimiter, non-null, may be empty + * @return a copy of this {@code HexFormat} with the delimiter + */ + public HexFormat withDelimiter(String delimiter) { + return new HexFormat(delimiter, this.prefix, this.suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} with the prefix. + * + * @param prefix a prefix, non-null, may be empty + * @return a copy of this {@code HexFormat} with the prefix + */ + public HexFormat withPrefix(String prefix) { + return new HexFormat(this.delimiter, prefix, this.suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} with the suffix. + * + * @param suffix a suffix, non-null, may be empty + * @return a copy of this {@code HexFormat} with the suffix + */ + public HexFormat withSuffix(String suffix) { + return new HexFormat(this.delimiter, this.prefix, suffix, this.digits); + } + + /** + * Returns a copy of this {@code HexFormat} to use uppercase hexadecimal characters. + * The uppercase hexadecimal characters are {@code "0-9", "A-F"}. + * + * @return a copy of this {@code HexFormat} with uppercase hexadecimal characters + */ + public HexFormat withUpperCase() { + return new HexFormat(this.delimiter, this.prefix, this.suffix, UPPERCASE_DIGITS); + } + + /** + * Returns a copy of this {@code HexFormat} to use lowercase hexadecimal characters. + * The lowercase hexadecimal characters are {@code "0-9", "a-f"}. + * + * @return a copy of this {@code HexFormat} with lowercase hexadecimal characters + */ + public HexFormat withLowerCase() { + return new HexFormat(this.delimiter, this.prefix, this.suffix, LOWERCASE_DIGITS); + } + + /** + * Returns the delimiter between hexadecimal values in formatted hexadecimal strings. + * + * @return the delimiter, non-null, may be empty {@code ""} + */ + public String delimiter() { + return delimiter; + } + + /** + * Returns the prefix used for each hexadecimal value in formatted hexadecimal strings. + * + * @return the prefix, non-null, may be empty {@code ""} + */ + public String prefix() { + return prefix; + } + + /** + * Returns the suffix used for each hexadecimal value in formatted hexadecimal strings. + * + * @return the suffix, non-null, may be empty {@code ""} + */ + public String suffix() { + return suffix; + } + + /** + * Returns {@code true} if the hexadecimal digits are uppercase, + * otherwise {@code false}. + * + * @return {@code true} if the hexadecimal digits are uppercase, + * otherwise {@code false} + */ + public boolean isUpperCase() { + return Arrays.equals(digits, UPPERCASE_DIGITS); + } + + /** + * Returns a hexadecimal string formatted from a byte array. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * + * The behavior is equivalent to + * {@link #formatHex(byte[], int, int) formatHex(bytes, 0, bytes.length))}. + * + * @param bytes a non-null array of bytes + * @return a string hexadecimal formatting of the byte array + */ + public String formatHex(byte[] bytes) { + return formatHex(bytes, 0, bytes.length); + } + + /** + * Returns a hexadecimal string formatted from a byte array range. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * + * @param bytes a non-null array of bytes + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive + * @return a string hexadecimal formatting each byte of the array range + * @throws IndexOutOfBoundsException if the array range is out of bounds + */ + public String formatHex(byte[] bytes, int fromIndex, int toIndex) { + Objects.requireNonNull(bytes,"bytes"); + Preconditions.checkFromToIndex(fromIndex, toIndex, bytes.length, null); + if (toIndex - fromIndex == 0) { + return ""; + } + // Format efficiently if possible + String s = formatOptDelimiter(bytes, fromIndex, toIndex); + if (s == null) { + long stride = prefix.length() + 2L + suffix.length() + delimiter.length(); + int capacity = checkMaxArraySize((toIndex - fromIndex) * stride - delimiter.length()); + StringBuilder sb = new StringBuilder(capacity); + formatHex(sb, bytes, fromIndex, toIndex); + s = sb.toString(); + } + return s; + } + + /** + * Appends formatted hexadecimal strings from a byte array to the {@link Appendable}. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * The formatted hexadecimal strings are appended in zero or more calls to the {@link Appendable} methods. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param bytes a byte array + * @return the {@code Appendable} + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A formatHex(A out, byte[] bytes) { + return formatHex(out, bytes, 0, bytes.length); + } + + /** + * Appends formatted hexadecimal strings from a byte array range to the {@link Appendable}. + * Each byte value is formatted as the prefix, two hexadecimal characters + * {@linkplain #isUpperCase selected from} uppercase or lowercase digits, and the suffix. + * A delimiter follows each formatted value, except the last. + * The formatted hexadecimal strings are appended in zero or more calls to the {@link Appendable} methods. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param bytes a byte array, non-null + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the {@code Appendable} + * @throws IndexOutOfBoundsException if the array range is out of bounds + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A formatHex(A out, byte[] bytes, int fromIndex, int toIndex) { + Objects.requireNonNull(out, "out"); + Objects.requireNonNull(bytes, "bytes"); + Preconditions.checkFromToIndex(fromIndex, toIndex, bytes.length, null); + + int length = toIndex - fromIndex; + if (length > 0) { + try { + String between = suffix + delimiter + prefix; + out.append(prefix); + toHexDigits(out, bytes[fromIndex]); + if (between.isEmpty()) { + for (int i = 1; i < length; i++) { + toHexDigits(out, bytes[fromIndex + i]); + } + } else { + for (int i = 1; i < length; i++) { + out.append(between); + toHexDigits(out, bytes[fromIndex + i]); + } + } + out.append(suffix); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe.getMessage(), ioe); + } + } + return out; + } + + /** + * Returns a string formatting of the range of bytes optimized + * for a single allocation. + * Prefix and suffix must be empty and the delimiter + * must be empty or a single byte character, otherwise null is returned. + * + * @param bytes the bytes, non-null + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a String formatted or null for non-single byte delimiter + * or non-empty prefix or suffix + */ + private String formatOptDelimiter(byte[] bytes, int fromIndex, int toIndex) { + byte[] rep; + if (!prefix.isEmpty() || !suffix.isEmpty()) { + return null; + } + int length = toIndex - fromIndex; + if (delimiter.isEmpty()) { + // Allocate the byte array and fill in the hex pairs for each byte + rep = new byte[checkMaxArraySize(length * 2L)]; + for (int i = 0; i < length; i++) { + rep[i * 2] = (byte)toHighHexDigit(bytes[fromIndex + i]); + rep[i * 2 + 1] = (byte)toLowHexDigit(bytes[fromIndex + i]); + } + } else if (delimiter.length() == 1 && delimiter.charAt(0) < 256) { + // Allocate the byte array and fill in the characters for the first byte + // Then insert the delimiter and hexadecimal characters for each of the remaining bytes + char sep = delimiter.charAt(0); + rep = new byte[checkMaxArraySize(length * 3L - 1L)]; + rep[0] = (byte) toHighHexDigit(bytes[fromIndex]); + rep[1] = (byte) toLowHexDigit(bytes[fromIndex]); + for (int i = 1; i < length; i++) { + rep[i * 3 - 1] = (byte) sep; + rep[i * 3 ] = (byte) toHighHexDigit(bytes[fromIndex + i]); + rep[i * 3 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]); + } + } else { + // Delimiter formatting not to a single byte + return null; + } + try { + // Return a new string using the bytes without making a copy +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Checked that the requested size for the result string is + * less than or equal to the max array size. + * + * @param length the requested size of a byte array. + * @return the length + * @throws OutOfMemoryError if the size is larger than Integer.MAX_VALUE + */ + private static int checkMaxArraySize(long length) { + if (length > Integer.MAX_VALUE) + throw new OutOfMemoryError("String size " + length + + " exceeds maximum " + Integer.MAX_VALUE); + return (int)length; + } + + /** + * Returns a byte array containing hexadecimal values parsed from the string. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid string consists only of the above format. + * + * @param string a string containing the byte values with prefix, hexadecimal digits, suffix, + * and delimiters + * @return a byte array with the values parsed from the string + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + */ + public byte[] parseHex(CharSequence string) { + return parseHex(string, 0, string.length()); + } + + /** + * Returns a byte array containing hexadecimal values parsed from a range of the string. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid string consists only of the above format. + * + * @param string a string range containing hexadecimal digits, + * delimiters, prefix, and suffix. + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a byte array with the values parsed from the string range + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + * @throws IndexOutOfBoundsException if the string range is out of bounds + */ + public byte[] parseHex(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Preconditions.checkFromToIndex(fromIndex, toIndex, string.length(), null); + + if (fromIndex != 0 || toIndex != string.length()) { + string = string.subSequence(fromIndex, toIndex); + } + + if (string.length() == 0) + return EMPTY_BYTES; + if (delimiter.isEmpty() && prefix.isEmpty() && suffix.isEmpty()) + return parseNoDelimiter(string); + + // avoid overflow for max length prefix or suffix + long valueChars = prefix.length() + 2L + suffix.length(); + long stride = valueChars + delimiter.length(); + if ((string.length() - valueChars) % stride != 0) + throw new IllegalArgumentException("extra or missing delimiters " + + "or values consisting of prefix, two hexadecimal digits, and suffix"); + + checkLiteral(string, 0, prefix); + checkLiteral(string, string.length() - suffix.length(), suffix); + String between = suffix + delimiter + prefix; + final int len = (int)((string.length() - valueChars) / stride + 1L); + byte[] bytes = new byte[len]; + int i, offset; + for (i = 0, offset = prefix.length(); i < len - 1; i++, offset += 2 + between.length()) { + bytes[i] = (byte) fromHexDigits(string, offset); + checkLiteral(string, offset + 2, between); + } + bytes[i] = (byte) fromHexDigits(string, offset); + + return bytes; + } + + /** + * Returns a byte array containing hexadecimal values parsed from + * a range of the character array. + * + * Each byte value is parsed from the prefix, two case insensitive hexadecimal characters, + * and the suffix. A delimiter follows each formatted value, except the last. + * The delimiters, prefixes, and suffixes strings must be present; they may be empty strings. + * A valid character array range consists only of the above format. + * + * @param chars a character array range containing an even number of hexadecimal digits, + * delimiters, prefix, and suffix. + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return a byte array with the values parsed from the character array range + * @throws IllegalArgumentException if the prefix or suffix is not present for each byte value, + * the byte values are not hexadecimal characters, or if the delimiter is not present + * after all but the last byte value + * @throws IndexOutOfBoundsException if the character array range is out of bounds + */ + public byte[] parseHex(char[] chars, int fromIndex, int toIndex) { + Objects.requireNonNull(chars, "chars"); + Preconditions.checkFromToIndex(fromIndex, toIndex, chars.length, null); + CharBuffer cb = CharBuffer.wrap(chars, fromIndex, toIndex - fromIndex); + return parseHex(cb); + } + + /** + * Compare the literal and throw an exception if it does not match. + * Pre-condition: {@code index + literal.length() <= string.length()}. + * + * @param string a CharSequence + * @param index the index of the literal in the CharSequence + * @param literal the expected literal + * @throws IllegalArgumentException if the literal is not present + */ + private static void checkLiteral(CharSequence string, int index, String literal) { + assert index <= string.length() - literal.length() : "pre-checked invariant error"; + if (literal.isEmpty() || + (literal.length() == 1 && literal.charAt(0) == string.charAt(index))) { + return; + } + for (int i = 0; i < literal.length(); i++) { + if (string.charAt(index + i) != literal.charAt(i)) { + throw new IllegalArgumentException(escapeNL("found: \"" + + string.subSequence(index, index + literal.length()) + + "\", expected: \"" + literal + "\", index: " + index + + " ch: " + (int)string.charAt(index + i))); + } + } + } + + /** + * Expands new line characters to escaped newlines for display. + * + * @param string a string + * @return a string with newline characters escaped + */ + private static String escapeNL(String string) { + return string.replace("\n", "\\n") + .replace("\r", "\\r"); + } + + /** + * Returns the hexadecimal character for the low 4 bits of the value considering it to be a byte. + * If the parameter {@link #isUpperCase()} is {@code true} the + * character returned for values {@code 10-15} is uppercase {@code "A-F"}, + * otherwise the character returned is lowercase {@code "a-f"}. + * The values in the range {@code 0-9} are returned as {@code "0-9"}. + * + * @param value a value, only the low 4 bits {@code 0-3} of the value are used + * @return the hexadecimal character for the low 4 bits {@code 0-3} of the value + */ + public char toLowHexDigit(int value) { + return (char)digits[value & 0xf]; + } + + /** + * Returns the hexadecimal character for the high 4 bits of the value considering it to be a byte. + * If the parameter {@link #isUpperCase()} is {@code true} the + * character returned for values {@code 10-15} is uppercase {@code "A-F"}, + * otherwise the character returned is lowercase {@code "a-f"}. + * The values in the range {@code 0-9} are returned as {@code "0-9"}. + * + * @param value a value, only bits {@code 4-7} of the value are used + * @return the hexadecimal character for the bits {@code 4-7} of the value + */ + public char toHighHexDigit(int value) { + return (char)digits[(value >> 4) & 0xf]; + } + + /** + * Appends two hexadecimal characters for the byte value to the {@link Appendable}. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The hexadecimal characters are appended in one or more calls to the + * {@link Appendable} methods. The delimiter, prefix and suffix are not used. + * + * @param The type of {@code Appendable} + * @param out an {@code Appendable}, non-null + * @param value a byte value + * @return the {@code Appendable} + * @throws UncheckedIOException if an I/O exception occurs appending to the output + */ + public A toHexDigits(A out, byte value) { + Objects.requireNonNull(out, "out"); + try { + out.append(toHighHexDigit(value)); + out.append(toLowHexDigit(value)); + return out; + } catch (IOException ioe) { + throw new UncheckedIOException(ioe.getMessage(), ioe); + } + } + + /** + * Returns the two hexadecimal characters for the {@code byte} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a byte value + * @return the two hexadecimal characters for the byte value + */ + public String toHexDigits(byte value) { + byte[] rep = new byte[2]; + rep[0] = (byte)toHighHexDigit(value); + rep[1] = (byte)toLowHexDigit(value); + try { +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the four hexadecimal characters for the {@code char} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code char} value + * @return the four hexadecimal characters for the {@code char} value + */ + public String toHexDigits(char value) { + return toHexDigits((short)value); + } + + /** + * Returns the four hexadecimal characters for the {@code short} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code short} value + * @return the four hexadecimal characters for the {@code short} value + */ + public String toHexDigits(short value) { + byte[] rep = new byte[4]; + rep[0] = (byte)toHighHexDigit((byte)(value >> 8)); + rep[1] = (byte)toLowHexDigit((byte)(value >> 8)); + rep[2] = (byte)toHighHexDigit((byte)value); + rep[3] = (byte)toLowHexDigit((byte)value); + + try { +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the eight hexadecimal characters for the {@code int} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value an {@code int} value + * @return the eight hexadecimal characters for the {@code int} value + * @see Integer#toHexString + */ + public String toHexDigits(int value) { + byte[] rep = new byte[8]; + rep[0] = (byte)toHighHexDigit((byte)(value >> 24)); + rep[1] = (byte)toLowHexDigit((byte)(value >> 24)); + rep[2] = (byte)toHighHexDigit((byte)(value >> 16)); + rep[3] = (byte)toLowHexDigit((byte)(value >> 16)); + rep[4] = (byte)toHighHexDigit((byte)(value >> 8)); + rep[5] = (byte)toLowHexDigit((byte)(value >> 8)); + rep[6] = (byte)toHighHexDigit((byte)value); + rep[7] = (byte)toLowHexDigit((byte)value); + + try { +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns the sixteen hexadecimal characters for the {@code long} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code long} value + * @return the sixteen hexadecimal characters for the {@code long} value + * @see Long#toHexString + */ + public String toHexDigits(long value) { + byte[] rep = new byte[16]; + rep[0] = (byte)toHighHexDigit((byte)(value >>> 56)); + rep[1] = (byte)toLowHexDigit((byte)(value >>> 56)); + rep[2] = (byte)toHighHexDigit((byte)(value >>> 48)); + rep[3] = (byte)toLowHexDigit((byte)(value >>> 48)); + rep[4] = (byte)toHighHexDigit((byte)(value >>> 40)); + rep[5] = (byte)toLowHexDigit((byte)(value >>> 40)); + rep[6] = (byte)toHighHexDigit((byte)(value >>> 32)); + rep[7] = (byte)toLowHexDigit((byte)(value >>> 32)); + rep[8] = (byte)toHighHexDigit((byte)(value >>> 24)); + rep[9] = (byte)toLowHexDigit((byte)(value >>> 24)); + rep[10] = (byte)toHighHexDigit((byte)(value >>> 16)); + rep[11] = (byte)toLowHexDigit((byte)(value >>> 16)); + rep[12] = (byte)toHighHexDigit((byte)(value >>> 8)); + rep[13] = (byte)toLowHexDigit((byte)(value >>> 8)); + rep[14] = (byte)toHighHexDigit((byte)value); + rep[15] = (byte)toLowHexDigit((byte)value); + + try { +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns up to sixteen hexadecimal characters for the {@code long} value. + * Each nibble (4 bits) from most significant to least significant of the value + * is formatted as if by {@link #toLowHexDigit(int) toLowHexDigit(nibble)}. + * The delimiter, prefix and suffix are not used. + * + * @param value a {@code long} value + * @param digits the number of hexadecimal digits to return, 0 to 16 + * @return the hexadecimal characters for the {@code long} value + * @throws IllegalArgumentException if {@code digits} is negative or greater than 16 + */ + public String toHexDigits(long value, int digits) { + if (digits < 0 || digits > 16) + throw new IllegalArgumentException("number of digits: " + digits); + if (digits == 0) + return ""; + byte[] rep = new byte[digits]; + for (int i = rep.length - 1; i >= 0; i--) { + rep[i] = (byte)toLowHexDigit((byte)(value)); + value = value >>> 4; + } + try { +// return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1); + return SharedSecretsUtil.langNewStringNoRepl(rep, StandardCharsets.ISO_8859_1); + } catch (CharacterCodingException cce) { + throw new AssertionError(cce); + } + } + + /** + * Returns a byte array containing the parsed hex digits. + * A valid string consists only of an even number of hex digits. + * + * @param string a string containing an even number of only hex digits + * @return a byte array + * @throws IllegalArgumentException if the string length is not valid or + * the string contains non-hexadecimal characters + */ + private static byte[] parseNoDelimiter(CharSequence string) { + if ((string.length() & 1) != 0) + throw new IllegalArgumentException("string length not even: " + + string.length()); + + byte[] bytes = new byte[string.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) fromHexDigits(string, i * 2); + } + + return bytes; + } + + /** + * Check the number of requested digits against a limit. + * + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @param limit the maximum allowed + * @return the length of the range + */ + private static int checkDigitCount(int fromIndex, int toIndex, int limit) { + int length = toIndex - fromIndex; + if (length > limit) + throw new IllegalArgumentException("string length greater than " + + limit + ": " + length); + return length; + } + + /** + * Returns {@code true} if the character is a valid hexadecimal character or codepoint. + * The valid hexadecimal characters are: + *

+ * @param ch a codepoint + * @return {@code true} if the character is valid a hexadecimal character, + * otherwise {@code false} + */ + public static boolean isHexDigit(int ch) { + return ((ch >>> 8) == 0 && DIGITS[ch] >= 0); + } + + /** + * Returns the value for the hexadecimal character or codepoint. + * The value is: + *
    + *
  • {@code (ch - '0')} for {@code '0'} through {@code '9'} inclusive, + *
  • {@code (ch - 'A' + 10)} for {@code 'A'} through {@code 'F'} inclusive, and + *
  • {@code (ch - 'a' + 10)} for {@code 'a'} through {@code 'f'} inclusive. + *
+ * + * @param ch a character or codepoint + * @return the value {@code 0-15} + * @throws NumberFormatException if the codepoint is not a hexadecimal character + */ + public static int fromHexDigit(int ch) { + int value; + if ((ch >>> 8) == 0 && (value = DIGITS[ch]) >= 0) { + return value; + } + throw new NumberFormatException("not a hexadecimal digit: \"" + (char) ch + "\" = " + ch); + } + + /** + * Returns a value parsed from two hexadecimal characters in a string. + * The characters in the range from {@code index} to {@code index + 1}, + * inclusive, must be valid hex digits according to {@link #fromHexDigit(int)}. + * + * @param string a CharSequence containing the characters + * @param index the index of the first character of the range + * @return the value parsed from the string range + * @throws NumberFormatException if any of the characters in the range + * is not a hexadecimal character + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + */ + private static int fromHexDigits(CharSequence string, int index) { + int high = fromHexDigit(string.charAt(index)); + int low = fromHexDigit(string.charAt(index + 1)); + return (high << 4) | low; + } + + /** + * Returns the {@code int} value parsed from a string of up to eight hexadecimal characters. + * The hexadecimal characters are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 32 bits and is returned as an {@code int}. + * + * @apiNote + * {@link Integer#parseInt(String, int) Integer.parseInt(s, 16)} and + * {@link Integer#parseUnsignedInt(String, int) Integer.parseUnsignedInt(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters + * {@code "0-9"}, {@code "A-F"} and {@code "a-f"}. + * Signed hexadecimal strings can be parsed with {@link Integer#parseInt(String, int)}. + * + * @param string a CharSequence containing up to eight hexadecimal characters + * @return the value parsed from the string + * @throws IllegalArgumentException if the string length is greater than eight (8) or + * if any of the characters is not a hexadecimal character + */ + public static int fromHexDigits(CharSequence string) { + return fromHexDigits(string, 0, string.length()); + } + + /** + * Returns the {@code int} value parsed from a string range of up to eight hexadecimal + * characters. + * The characters in the range {@code fromIndex} to {@code toIndex}, exclusive, + * are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 32 bits and is returned as an {@code int}. + * + * @apiNote + * {@link Integer#parseInt(String, int) Integer.parseInt(s, 16)} and + * {@link Integer#parseUnsignedInt(String, int) Integer.parseUnsignedInt(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters + * {@code "0-9"}, {@code "A-F"} and {@code "a-f"}. + * Signed hexadecimal strings can be parsed with {@link Integer#parseInt(String, int)}. + * + * @param string a CharSequence containing the characters + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the value parsed from the string range + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + * @throws IllegalArgumentException if length of the range is greater than eight (8) or + * if any of the characters is not a hexadecimal character + */ + public static int fromHexDigits(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Preconditions.checkFromToIndex(fromIndex, toIndex, string.length(), null); + int length = checkDigitCount(fromIndex, toIndex, 8); + int value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(fromIndex + i)); + } + return value; + } + + /** + * Returns the long value parsed from a string of up to sixteen hexadecimal characters. + * The hexadecimal characters are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 64 bits and is returned as a {@code long}. + * + * @apiNote + * {@link Long#parseLong(String, int) Long.parseLong(s, 16)} and + * {@link Long#parseUnsignedLong(String, int) Long.parseUnsignedLong(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters + * {@code "0-9"}, {@code "A-F"} and {@code "a-f"}. + * Signed hexadecimal strings can be parsed with {@link Long#parseLong(String, int)}. + * + * @param string a CharSequence containing up to sixteen hexadecimal characters + * @return the value parsed from the string + * @throws IllegalArgumentException if the string length is greater than sixteen (16) or + * if any of the characters is not a hexadecimal character + */ + public static long fromHexDigitsToLong(CharSequence string) { + return fromHexDigitsToLong(string, 0, string.length()); + } + + /** + * Returns the long value parsed from a string range of up to sixteen hexadecimal + * characters. + * The characters in the range {@code fromIndex} to {@code toIndex}, exclusive, + * are parsed from most significant to least significant + * using {@link #fromHexDigit(int)} to form an unsigned value. + * The value is zero extended to 64 bits and is returned as a {@code long}. + * + * @apiNote + * {@link Long#parseLong(String, int) Long.parseLong(s, 16)} and + * {@link Long#parseUnsignedLong(String, int) Long.parseUnsignedLong(s, 16)} + * are similar but allow all Unicode hexadecimal digits defined by + * {@link Character#digit(char, int) Character.digit(ch, 16)}. + * {@code HexFormat} uses only hexadecimal characters + * {@code "0-9"}, {@code "A-F"} and {@code "a-f"}. + * Signed hexadecimal strings can be parsed with {@link Long#parseLong(String, int)}. + * + * @param string a CharSequence containing the characters + * @param fromIndex the initial index of the range, inclusive + * @param toIndex the final index of the range, exclusive. + * @return the value parsed from the string range + * @throws IndexOutOfBoundsException if the range is out of bounds + * for the {@code CharSequence} + * @throws IllegalArgumentException if the length of the range is greater than sixteen (16) or + * if any of the characters is not a hexadecimal character + */ + public static long fromHexDigitsToLong(CharSequence string, int fromIndex, int toIndex) { + Objects.requireNonNull(string, "string"); + Preconditions.checkFromToIndex(fromIndex, toIndex, string.length(), null); + int length = checkDigitCount(fromIndex, toIndex, 16); + long value = 0L; + for (int i = 0; i < length; i++) { + value = (value << 4) + fromHexDigit(string.charAt(fromIndex + i)); + } + return value; + } + + /** + * Returns {@code true} if the other object is a {@code HexFormat} + * with the same parameters. + * + * @param o an object, may be null + * @return {@code true} if the other object is a {@code HexFormat} and the parameters + * uppercase, delimiter, prefix, and suffix are equal; + * otherwise {@code false} + */ + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HexFormat otherHex = (HexFormat) o; + return Arrays.equals(digits, otherHex.digits) && + delimiter.equals(otherHex.delimiter) && + prefix.equals(otherHex.prefix) && + suffix.equals(otherHex.suffix); + } + + /** + * Returns a hashcode for this {@code HexFormat}. + * + * @return a hashcode for this {@code HexFormat} + */ + @Override + public int hashCode() { + int result = Objects.hash(delimiter, prefix, suffix); + result = 31 * result + Boolean.hashCode(Arrays.equals(digits, UPPERCASE_DIGITS)); + return result; + } + + /** + * Returns a description of the formatter parameters for uppercase, + * delimiter, prefix, and suffix. + * + * @return a description of this {@code HexFormat} + */ + @Override + public String toString() { + return escapeNL("uppercase: " + Arrays.equals(digits, UPPERCASE_DIGITS) + + ", delimiter: \"" + delimiter + + "\", prefix: \"" + prefix + + "\", suffix: \"" + suffix + "\""); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/jdk/internal/StaticProperty.java b/openjdk/src/main/java/net/tongsuo/jdk/internal/StaticProperty.java new file mode 100644 index 000000000..7b7e6d5fb --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/jdk/internal/StaticProperty.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.jdk.internal.util; + +import java.util.Properties; +import java.nio.charset.Charset; + +/** + * System Property access for internal use only. + * Read-only access to System property values initialized during Phase 1 + * are cached. Setting, clearing, or modifying the value using + * {@link System#setProperty} or {@link System#getProperties()} is ignored. + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in these access methods. The caller of these methods should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ +public final class StaticProperty { + + // The class static initialization is triggered to initialize these final + // fields during init Phase 1 and before a security manager is set. + private static final String JAVA_HOME; + private static final String USER_HOME; + private static final String USER_DIR; + private static final String USER_NAME; + private static final String JAVA_LIBRARY_PATH; + private static final String SUN_BOOT_LIBRARY_PATH; + private static final String JDK_SERIAL_FILTER; + private static final String JDK_SERIAL_FILTER_FACTORY; + private static final String JAVA_IO_TMPDIR; +// private static final String NATIVE_ENCODING; + private static final String FILE_ENCODING; + private static final String JAVA_PROPERTIES_DATE; +// private static final String SUN_JNU_ENCODING; +// private static final Charset jnuCharset; + + private StaticProperty() {} + + static { + Properties props = System.getProperties(); + JAVA_HOME = getProperty(props, "java.home"); + USER_HOME = getProperty(props, "user.home"); + USER_DIR = getProperty(props, "user.dir"); + USER_NAME = getProperty(props, "user.name"); + JAVA_IO_TMPDIR = getProperty(props, "java.io.tmpdir"); + JAVA_LIBRARY_PATH = getProperty(props, "java.library.path", ""); + SUN_BOOT_LIBRARY_PATH = getProperty(props, "sun.boot.library.path", ""); + JDK_SERIAL_FILTER = getProperty(props, "jdk.serialFilter", null); + JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null); +// NATIVE_ENCODING = getProperty(props, "native.encoding"); + FILE_ENCODING = getProperty(props, "file.encoding"); + JAVA_PROPERTIES_DATE = getProperty(props, "java.properties.date", null); +// SUN_JNU_ENCODING = getProperty(props, "sun.jnu.encoding"); +// jnuCharset = Charset.forName(SUN_JNU_ENCODING, Charset.defaultCharset()); + } + + private static String getProperty(Properties props, String key) { + String v = props.getProperty(key); + if (v == null) { + throw new InternalError("null property: " + key); + } + return v; + } + + private static String getProperty(Properties props, String key, + String defaultVal) { + String v = props.getProperty(key); + return (v == null) ? defaultVal : v; + } + + /** + * {@return the {@code java.home} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String javaHome() { + return JAVA_HOME; + } + + /** + * {@return the {@code user.home} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String userHome() { + return USER_HOME; + } + + /** + * {@return the {@code user.dir} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String userDir() { + return USER_DIR; + } + + /** + * {@return the {@code user.name} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String userName() { + return USER_NAME; + } + + /** + * {@return the {@code java.library.path} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String javaLibraryPath() { + return JAVA_LIBRARY_PATH; + } + + /** + * {@return the {@code java.io.tmpdir} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String javaIoTmpDir() { + return JAVA_IO_TMPDIR; + } + + /** + * {@return the {@code sun.boot.library.path} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String sunBootLibraryPath() { + return SUN_BOOT_LIBRARY_PATH; + } + + + /** + * {@return the {@code jdk.serialFilter} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String jdkSerialFilter() { + return JDK_SERIAL_FILTER; + } + + + /** + * {@return the {@code jdk.serialFilterFactory} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String jdkSerialFilterFactory() { + return JDK_SERIAL_FILTER_FACTORY; + } + +// /** +// * {@return the {@code native.encoding} system property} +// * +// * {@link SecurityManager#checkPropertyAccess} is NOT checked +// * in this method. The caller of this method should take care to ensure +// * that the returned property is not made accessible to untrusted code. +// */ +// public static String nativeEncoding() { +// return NATIVE_ENCODING; +// } + + /** + * {@return the {@code file.encoding} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. The caller of this method should take care to ensure + * that the returned property is not made accessible to untrusted code. + */ + public static String fileEncoding() { + return FILE_ENCODING; + } + + /** + * {@return the {@code java.properties.date} system property} + * + * {@link SecurityManager#checkPropertyAccess} is NOT checked + * in this method. + */ + public static String javaPropertiesDate() { + return JAVA_PROPERTIES_DATE; + } + +// /** +// * {@return the {@code sun.jnu.encoding} system property} +// * +// * {@link SecurityManager#checkPropertyAccess} is NOT checked +// * in this method. The caller of this method should take care to ensure +// * that the returned property is not made accessible to untrusted code. +// */ +// public static String jnuEncoding() { +// return SUN_JNU_ENCODING; +// } + +// /** +// * {@return {@code Charset} for {@code sun.jnu.encoding} system property} +// * +// * If {@code sun.jnu.encoding} system property has invalid +// * encoding name, {@link Charset#defaultCharset()} is returned. +// */ +// public static Charset jnuCharset() { +// return jnuCharset; +// } +} diff --git a/openjdk/src/main/java/net/tongsuo/jdk/internal/misc/SharedSecretsUtil.java b/openjdk/src/main/java/net/tongsuo/jdk/internal/misc/SharedSecretsUtil.java new file mode 100644 index 000000000..d7c05ddcd --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/jdk/internal/misc/SharedSecretsUtil.java @@ -0,0 +1,283 @@ +package net.tongsuo.jdk.internal.misc; + +import static net.tongsuo.crypto.CryptoUtils.*; + +import javax.crypto.spec.SecretKeySpec; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.EncodedKeySpec; + +/** + * The utilities for operating SharedSecrets cross JDK 8, 11 and 17. + */ +public class SharedSecretsUtil { + + private static final boolean USE_SHARED_SECRETS + = privilegedGetBoolProperty("com.tencent.misc.useSharedSecrets", "false"); + + /* JavaLangAccess */ + private static final Method langNewStringNoRepl; + private static final Object langAccess; + + /* JavaxCryptoSpecAccess */ + private static final Method cryptoSpecClearSecretKeySpec; + private static final Object cryptoSpecAccess; + + /* JavaNetInetAddressAccess */ + private static final Method netInetAddressGetOriginalHostName; + private static final Object netInetAddressAccess; + + /* JavaSecuritySignatureAccess */ + private static final Method secSignatureInitVerifyWithPubKey; + private static final Method secSignatureInitVerifyWithCert; + private static final Method secSignatureInitSign; + private static final Object secSignatureAccess; + + /* JavaSecuritySpecAccess */ + private static final Method secSpecClearEncodedKeySpec; + private static final Object secSpecAccess; + + static { + Class sharedSecretsClass = null; + + Class javaLangAccessClass = null; + Class javaxCryptoSpecAccessClass = null; + Class inetAddressAccessClass = null; + Class secSignatureAccessClass = null; + Class secSpecAccessClass = null; + + if (useSharedSecrets()) { + try { + if (isJdk8()) { + sharedSecretsClass = Class.forName("sun.misc.SharedSecrets"); + + javaLangAccessClass = Class.forName("sun.misc.JavaLangAccess"); + inetAddressAccessClass = Class.forName("sun.misc.JavaNetAccess"); + secSignatureAccessClass = Class.forName("sun.misc.JavaSecuritySignatureAccess"); + } else if (isJdk11()) { + sharedSecretsClass = Class.forName("jdk.internal.misc.SharedSecrets"); + + javaLangAccessClass = Class.forName("jdk.internal.misc.JavaLangAccess"); + inetAddressAccessClass = Class.forName("jdk.internal.misc.JavaNetInetAddressAccess"); + secSignatureAccessClass = Class.forName("jdk.internal.misc.JavaSecuritySignatureAccess"); + } else if (isJdk17()) { + sharedSecretsClass = Class.forName("jdk.internal.access.SharedSecrets"); + + javaLangAccessClass = Class.forName("jdk.internal.access.JavaLangAccess"); + javaxCryptoSpecAccessClass = Class.forName("jdk.internal.access.JavaxCryptoSpecAccess"); + inetAddressAccessClass = Class.forName("jdk.internal.access.JavaNetInetAddressAccess"); + secSignatureAccessClass = Class.forName("jdk.internal.access.JavaSecuritySignatureAccess"); + secSpecAccessClass = Class.forName("jdk.internal.access.JavaSecuritySpecAccess"); + } + } catch (ClassNotFoundException e) { + throw new InternalError("Cannot get SharedSecrets class", e); + } + } + + if (sharedSecretsClass != null) { + try { + langNewStringNoRepl = isJdk8() + ? null : javaLangAccessClass.getMethod( + "newStringNoRepl", byte[].class, Charset.class); + + cryptoSpecClearSecretKeySpec = javaxCryptoSpecAccessClass != null + ? javaxCryptoSpecAccessClass.getMethod( + "clearSecretKeySpec", SecretKeySpec.class) + : null; + + netInetAddressGetOriginalHostName = inetAddressAccessClass.getMethod( + "getOriginalHostName", InetAddress.class); + + secSignatureInitVerifyWithPubKey = secSignatureAccessClass.getMethod( + "initVerify", Signature.class, PublicKey.class, + AlgorithmParameterSpec.class); + secSignatureInitVerifyWithCert = secSignatureAccessClass.getMethod( + "initVerify", Signature.class, Certificate.class, + AlgorithmParameterSpec.class); + secSignatureInitSign = secSignatureAccessClass.getMethod( + "initSign", Signature.class, PrivateKey.class, + AlgorithmParameterSpec.class, SecureRandom.class); + + secSpecClearEncodedKeySpec = secSpecAccessClass == null + ? null : secSpecAccessClass.getMethod( + "clearEncodedKeySpec", EncodedKeySpec.class); + } catch (NoSuchMethodException e) { + throw new InternalError("Cannot get method", e); + } + + langAccess = getAccessObject(sharedSecretsClass, "getJavaLangAccess"); + cryptoSpecAccess = isJdk17() + ? getAccessObject(sharedSecretsClass, "getJavaxCryptoSpecAccess") + : null; + netInetAddressAccess = isJdk8() + ? getAccessObject(sharedSecretsClass, "getJavaNetAccess") + : getAccessObject(sharedSecretsClass, "getJavaNetInetAddressAccess"); + secSignatureAccess = getAccessObject(sharedSecretsClass, "getJavaSecuritySignatureAccess"); + secSpecAccess = isJdk17() + ? getAccessObject(sharedSecretsClass, "getJavaSecuritySpecAccess") + : null; + } else { + langNewStringNoRepl = null; + langAccess = null; + + cryptoSpecClearSecretKeySpec = null; + cryptoSpecAccess = null; + + netInetAddressGetOriginalHostName = null; + netInetAddressAccess = null; + + secSignatureInitVerifyWithPubKey = null; + secSignatureInitVerifyWithCert = null; + secSignatureInitSign = null; + secSignatureAccess = null; + + secSpecClearEncodedKeySpec = null; + secSpecAccess = null; + } + } + + private static boolean useSharedSecrets() { + return USE_SHARED_SECRETS && !isAndroid(); + } + + /* JavaLangAccess Start */ + public static String langNewStringNoRepl(byte[] bytes, Charset cs) + throws CharacterCodingException { + if (langNewStringNoRepl == null) { + return new String(bytes, cs); + } + + try { + return (String) langNewStringNoRepl.invoke( + langAccess, bytes, cs); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("getOriginalHostName failed", e); + } + } + /* JavaLangAccess End */ + + /* JavaxCryptoSpecAccess Start */ + public static void cryptoSpecClearSecretKeySpec(SecretKeySpec keySpec) + throws CharacterCodingException { + if (cryptoSpecClearSecretKeySpec == null) { + return; + } + + try { + cryptoSpecClearSecretKeySpec.invoke(cryptoSpecAccess, keySpec); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("specAccessClearEncodedKeySpec failed", e); + } + } + /* JavaxCryptoSpecAccess End */ + + /* JavaNetInetAddressAccess Start */ + public static String netInetAddressGetOriginalHostName( + InetAddress inetAddress) { + if (netInetAddressGetOriginalHostName == null) { + return inetAddress.getHostName(); + } else { + try { + return (String) netInetAddressGetOriginalHostName.invoke( + netInetAddressAccess, inetAddress); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("getOriginalHostName failed", e); + } + } + } + /* JavaNetInetAddressAccess End */ + + /* JavaSecuritySignatureAccess Start */ + public static void secSignatureInitVerify(Signature signature, + PublicKey publicKey, AlgorithmParameterSpec params) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (secSignatureInitVerifyWithPubKey == null) { + if (params != null) { + signature.setParameter(params); + } + signature.initVerify(publicKey); + } else { + try { + secSignatureInitVerifyWithPubKey.invoke( + secSignatureAccess, signature, publicKey, params); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("signatureAccessInitVerify failed", e); + } + } + } + + public static void secSignatureInitVerify(Signature signature, + Certificate certificate, AlgorithmParameterSpec params) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (secSignatureInitVerifyWithCert == null) { + if (params != null) { + signature.setParameter(params); + } + signature.initVerify(certificate); + } else { + try { + secSignatureInitVerifyWithCert.invoke( + secSignatureAccess, signature, certificate, params); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("signatureAccessInitVerify failed", e); + } + } + } + + public static void secSignatureInitSign(Signature signature, + PrivateKey privateKey, AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + if (secSignatureInitSign == null) { + if (params != null) { + signature.setParameter(params); + } + signature.initSign(privateKey, random); + } else { + try { + secSignatureInitSign.invoke( + secSignatureAccess, signature, privateKey, params, random); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("signatureAccessInitSign failed", e); + } + } + } + /* JavaSecuritySignatureAccess End */ + + /* JavaSecuritySpecAccess Start */ + public static void secSpecClearEncodedKeySpec(EncodedKeySpec keySpec) { + if (secSpecClearEncodedKeySpec == null) { + return; + } + + try { + secSpecClearEncodedKeySpec.invoke(secSpecAccess, keySpec); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("specAccessClearEncodedKeySpec failed", e); + } + } + /* JavaSecuritySpecAccess End */ + + private static Object getAccessObject(Class sharedSecretsClass, + String sharedSecretedMethodName) { + try { + Method sharedSecretedMethod = sharedSecretsClass.getDeclaredMethod( + sharedSecretedMethodName); + sharedSecretedMethod.setAccessible(true); + return sharedSecretedMethod.invoke(null); + } catch (IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new InternalError("Cannot get access object", e); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/jdk/internal/util/Preconditions.java b/openjdk/src/main/java/net/tongsuo/jdk/internal/util/Preconditions.java new file mode 100644 index 000000000..07a02e596 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/jdk/internal/util/Preconditions.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.jdk.internal.util; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Utility methods to check if state or arguments are correct. + */ +public class Preconditions { + + /** + * Utility exception formatters which can be used in {@code Preconditions} + * check functions below. + * + * These anonymous inner classes can be syntactically replaced by lambda + * expression or method reference, but it's not feasible in practices, + * because {@code Preconditions} is used in many fundamental classes such + * as {@code java.lang.String}, lambda expressions or method references + * exercise many other code at VM startup, this could lead a recursive + * calls when fundamental classes is used in lambda expressions or method + * references. + */ + public static final BiFunction, StringIndexOutOfBoundsException> + SIOOBE_FORMATTER = outOfBoundsExceptionFormatter(StringIndexOutOfBoundsException::new); + + public static final BiFunction, ArrayIndexOutOfBoundsException> + AIOOBE_FORMATTER = outOfBoundsExceptionFormatter(ArrayIndexOutOfBoundsException::new); + + public static final BiFunction, IndexOutOfBoundsException> + IOOBE_FORMATTER = outOfBoundsExceptionFormatter(IndexOutOfBoundsException::new); + + + /** + * Maps out-of-bounds values to a runtime exception. + * + * @param checkKind the kind of bounds check, whose name may correspond + * to the name of one of the range check methods, checkIndex, + * checkFromToIndex, checkFromIndexSize + * @param args the out-of-bounds arguments that failed the range check. + * If the checkKind corresponds to the name of a range check method + * then the bounds arguments are those that can be passed in order + * to the method. + * @param oobef the exception formatter that when applied with a checkKind + * and a list out-of-bounds arguments returns a runtime exception. + * If {@code null} then, it is as if an exception formatter was + * supplied that returns {@link IndexOutOfBoundsException} for any + * given arguments. + * @return the runtime exception + */ + private static RuntimeException outOfBounds( + BiFunction, ? extends RuntimeException> oobef, + String checkKind, + Number... args) { + List largs = Arrays.asList(args); + RuntimeException e = oobef == null + ? null : oobef.apply(checkKind, largs); + return e == null + ? new IndexOutOfBoundsException(outOfBoundsMessage(checkKind, largs)) : e; + } + + private static RuntimeException outOfBoundsCheckIndex( + BiFunction, ? extends RuntimeException> oobe, + int index, int length) { + return outOfBounds(oobe, "checkIndex", index, length); + } + + private static RuntimeException outOfBoundsCheckFromToIndex( + BiFunction, ? extends RuntimeException> oobe, + int fromIndex, int toIndex, int length) { + return outOfBounds(oobe, "checkFromToIndex", fromIndex, toIndex, length); + } + + private static RuntimeException outOfBoundsCheckFromIndexSize( + BiFunction, ? extends RuntimeException> oobe, + int fromIndex, int size, int length) { + return outOfBounds(oobe, "checkFromIndexSize", fromIndex, size, length); + } + + private static RuntimeException outOfBoundsCheckIndex( + BiFunction, ? extends RuntimeException> oobe, + long index, long length) { + return outOfBounds(oobe, "checkIndex", index, length); + } + + private static RuntimeException outOfBoundsCheckFromToIndex( + BiFunction, ? extends RuntimeException> oobe, + long fromIndex, long toIndex, long length) { + return outOfBounds(oobe, "checkFromToIndex", fromIndex, toIndex, length); + } + + private static RuntimeException outOfBoundsCheckFromIndexSize( + BiFunction, ? extends RuntimeException> oobe, + long fromIndex, long size, long length) { + return outOfBounds(oobe, "checkFromIndexSize", fromIndex, size, length); + } + + /** + * Returns an out-of-bounds exception formatter from an given exception + * factory. The exception formatter is a function that formats an + * out-of-bounds message from its arguments and applies that message to the + * given exception factory to produce and relay an exception. + * + *

The exception formatter accepts two arguments: a {@code String} + * describing the out-of-bounds range check that failed, referred to as the + * check kind; and a {@code List} containing the + * out-of-bound integral values that failed the check. The list of + * out-of-bound values is not modified. + * + *

Three check kinds are supported {@code checkIndex}, + * {@code checkFromToIndex} and {@code checkFromIndexSize} corresponding + * respectively to the specified application of an exception formatter as an + * argument to the out-of-bounds range check methods + * {@link #checkIndex(int, int, BiFunction) checkIndex}, + * {@link #checkFromToIndex(int, int, int, BiFunction) checkFromToIndex}, and + * {@link #checkFromIndexSize(int, int, int, BiFunction) checkFromIndexSize}. + * Thus a supported check kind corresponds to a method name and the + * out-of-bound integral values correspond to method argument values, in + * order, preceding the exception formatter argument (similar in many + * respects to the form of arguments required for a reflective invocation of + * such a range check method). + * + *

Formatter arguments conforming to such supported check kinds will + * produce specific exception messages describing failed out-of-bounds + * checks. Otherwise, more generic exception messages will be produced in + * any of the following cases: the check kind is supported but fewer + * or more out-of-bounds values are supplied, the check kind is not + * supported, the check kind is {@code null}, or the list of out-of-bound + * values is {@code null}. + * + * @apiNote + * This method produces an out-of-bounds exception formatter that can be + * passed as an argument to any of the supported out-of-bounds range check + * methods declared by {@code Objects}. For example, a formatter producing + * an {@code ArrayIndexOutOfBoundsException} may be produced and stored on a + * {@code static final} field as follows: + *

{@code
+     * static final
+     * BiFunction, ArrayIndexOutOfBoundsException> AIOOBEF =
+     *     outOfBoundsExceptionFormatter(ArrayIndexOutOfBoundsException::new);
+     * }
+ * The formatter instance {@code AIOOBEF} may be passed as an argument to an + * out-of-bounds range check method, such as checking if an {@code index} + * is within the bounds of a {@code limit}: + *
{@code
+     * checkIndex(index, limit, AIOOBEF);
+     * }
+ * If the bounds check fails then the range check method will throw an + * {@code ArrayIndexOutOfBoundsException} with an appropriate exception + * message that is a produced from {@code AIOOBEF} as follows: + *
{@code
+     * AIOOBEF.apply("checkIndex", List.of(index, limit));
+     * }
+ * + * @param f the exception factory, that produces an exception from a message + * where the message is produced and formatted by the returned + * exception formatter. If this factory is stateless and side-effect + * free then so is the returned formatter. + * Exceptions thrown by the factory are relayed to the caller + * of the returned formatter. + * @param the type of runtime exception to be returned by the given + * exception factory and relayed by the exception formatter + * @return the out-of-bounds exception formatter + */ + public static + BiFunction, X> outOfBoundsExceptionFormatter(Function f) { + // Use anonymous class to avoid bootstrap issues if this method is + // used early in startup + return new BiFunction, X>() { + @Override + public X apply(String checkKind, List args) { + return f.apply(outOfBoundsMessage(checkKind, args)); + } + }; + } + + private static String outOfBoundsMessage(String checkKind, List args) { + if (checkKind == null && args == null) { + return String.format("Range check failed"); + } else if (checkKind == null) { + return String.format("Range check failed: %s", args); + } else if (args == null) { + return String.format("Range check failed: %s", checkKind); + } + + int argSize = 0; + switch (checkKind) { + case "checkIndex": + argSize = 2; + break; + case "checkFromToIndex": + case "checkFromIndexSize": + argSize = 3; + break; + default: + } + + // Switch to default if fewer or more arguments than required are supplied + switch ((args.size() != argSize) ? "" : checkKind) { + case "checkIndex": + return String.format("Index %s out of bounds for length %s", + args.get(0), args.get(1)); + case "checkFromToIndex": + return String.format("Range [%s, %s) out of bounds for length %s", + args.get(0), args.get(1), args.get(2)); + case "checkFromIndexSize": + return String.format("Range [%s, %The {@code index} is defined to be out of bounds if any of the + * following inequalities is true: + *
    + *
  • {@code index < 0}
  • + *
  • {@code index >= length}
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the {@code index} is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkIndex}; + * and an unmodifiable list of integers whose values are, in order, the + * out-of-bounds arguments {@code index} and {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param index the index + * @param length the upper-bound (exclusive) of the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code index} if it is within bounds of the range + * @throws X if the {@code index} is out of bounds and the exception + * formatter is non-{@code null} + * @throws IndexOutOfBoundsException if the {@code index} is out of bounds + * and the exception formatter is {@code null} + * @since 9 + * + * @implNote + * This method is made intrinsic in optimizing compilers to guide them to + * perform unsigned comparisons of the index and length when it is known the + * length is a non-negative value (such as that of an array length or from + * the upper bound of a loop) + */ + public static + int checkIndex(int index, int length, + BiFunction, X> oobef) { + if (index < 0 || index >= length) + throw outOfBoundsCheckIndex(oobef, index, length); + return index; + } + + /** + * Checks if the sub-range from {@code fromIndex} (inclusive) to + * {@code toIndex} (exclusive) is within the bounds of range from {@code 0} + * (inclusive) to {@code length} (exclusive). + * + *

The sub-range is defined to be out of bounds if any of the following + * inequalities is true: + *

    + *
  • {@code fromIndex < 0}
  • + *
  • {@code fromIndex > toIndex}
  • + *
  • {@code toIndex > length}
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the sub-range is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkFromToIndex}; + * and an unmodifiable list of integers whose values are, in order, the + * out-of-bounds arguments {@code fromIndex}, {@code toIndex}, and {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param fromIndex the lower-bound (inclusive) of the sub-range + * @param toIndex the upper-bound (exclusive) of the sub-range + * @param length the upper-bound (exclusive) the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code fromIndex} if the sub-range within bounds of the range + * @throws X if the sub-range is out of bounds and the exception factory + * function is non-{@code null} + * @throws IndexOutOfBoundsException if the sub-range is out of bounds and + * the exception factory function is {@code null} + * @since 9 + */ + public static + int checkFromToIndex(int fromIndex, int toIndex, int length, + BiFunction, X> oobef) { + if (fromIndex < 0 || fromIndex > toIndex || toIndex > length) + throw outOfBoundsCheckFromToIndex(oobef, fromIndex, toIndex, length); + return fromIndex; + } + + /** + * Checks if the sub-range from {@code fromIndex} (inclusive) to + * {@code fromIndex + size} (exclusive) is within the bounds of range from + * {@code 0} (inclusive) to {@code length} (exclusive). + * + *

The sub-range is defined to be out of bounds if any of the following + * inequalities is true: + *

    + *
  • {@code fromIndex < 0}
  • + *
  • {@code size < 0}
  • + *
  • {@code fromIndex + size > length}, taking into account integer overflow
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the sub-range is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkFromIndexSize}; + * and an unmodifiable list of integers whose values are, in order, the + * out-of-bounds arguments {@code fromIndex}, {@code size}, and + * {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param fromIndex the lower-bound (inclusive) of the sub-interval + * @param size the size of the sub-range + * @param length the upper-bound (exclusive) of the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code fromIndex} if the sub-range within bounds of the range + * @throws X if the sub-range is out of bounds and the exception factory + * function is non-{@code null} + * @throws IndexOutOfBoundsException if the sub-range is out of bounds and + * the exception factory function is {@code null} + * @since 9 + */ + public static + int checkFromIndexSize(int fromIndex, int size, int length, + BiFunction, X> oobef) { + if ((length | fromIndex | size) < 0 || size > length - fromIndex) + throw outOfBoundsCheckFromIndexSize(oobef, fromIndex, size, length); + return fromIndex; + } + + /** + * Checks if the {@code index} is within the bounds of the range from + * {@code 0} (inclusive) to {@code length} (exclusive). + * + *

The {@code index} is defined to be out of bounds if any of the + * following inequalities is true: + *

    + *
  • {@code index < 0}
  • + *
  • {@code index >= length}
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the {@code index} is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkIndex}; + * and an unmodifiable list of longs whose values are, in order, the + * out-of-bounds arguments {@code index} and {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param index the index + * @param length the upper-bound (exclusive) of the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code index} if it is within bounds of the range + * @throws X if the {@code index} is out of bounds and the exception + * formatter is non-{@code null} + * @throws IndexOutOfBoundsException if the {@code index} is out of bounds + * and the exception formatter is {@code null} + * @since 16 + * + * @implNote + * This method is made intrinsic in optimizing compilers to guide them to + * perform unsigned comparisons of the index and length when it is known the + * length is a non-negative value (such as that of an array length or from + * the upper bound of a loop) + */ + public static + long checkIndex(long index, long length, + BiFunction, X> oobef) { + if (index < 0 || index >= length) + throw outOfBoundsCheckIndex(oobef, index, length); + return index; + } + + /** + * Checks if the sub-range from {@code fromIndex} (inclusive) to + * {@code toIndex} (exclusive) is within the bounds of range from {@code 0} + * (inclusive) to {@code length} (exclusive). + * + *

The sub-range is defined to be out of bounds if any of the following + * inequalities is true: + *

    + *
  • {@code fromIndex < 0}
  • + *
  • {@code fromIndex > toIndex}
  • + *
  • {@code toIndex > length}
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the sub-range is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkFromToIndex}; + * and an unmodifiable list of longs whose values are, in order, the + * out-of-bounds arguments {@code fromIndex}, {@code toIndex}, and {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param fromIndex the lower-bound (inclusive) of the sub-range + * @param toIndex the upper-bound (exclusive) of the sub-range + * @param length the upper-bound (exclusive) the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code fromIndex} if the sub-range within bounds of the range + * @throws X if the sub-range is out of bounds and the exception factory + * function is non-{@code null} + * @throws IndexOutOfBoundsException if the sub-range is out of bounds and + * the exception factory function is {@code null} + * @since 16 + */ + public static + long checkFromToIndex(long fromIndex, long toIndex, long length, + BiFunction, X> oobef) { + if (fromIndex < 0 || fromIndex > toIndex || toIndex > length) + throw outOfBoundsCheckFromToIndex(oobef, fromIndex, toIndex, length); + return fromIndex; + } + + /** + * Checks if the sub-range from {@code fromIndex} (inclusive) to + * {@code fromIndex + size} (exclusive) is within the bounds of range from + * {@code 0} (inclusive) to {@code length} (exclusive). + * + *

The sub-range is defined to be out of bounds if any of the following + * inequalities is true: + *

    + *
  • {@code fromIndex < 0}
  • + *
  • {@code size < 0}
  • + *
  • {@code fromIndex + size > length}, taking into account integer overflow
  • + *
  • {@code length < 0}, which is implied from the former inequalities
  • + *
+ * + *

If the sub-range is out of bounds, then a runtime exception is + * thrown that is the result of applying the following arguments to the + * exception formatter: the name of this method, {@code checkFromIndexSize}; + * and an unmodifiable list of longs whose values are, in order, the + * out-of-bounds arguments {@code fromIndex}, {@code size}, and + * {@code length}. + * + * @param the type of runtime exception to throw if the arguments are + * out of bounds + * @param fromIndex the lower-bound (inclusive) of the sub-interval + * @param size the size of the sub-range + * @param length the upper-bound (exclusive) of the range + * @param oobef the exception formatter that when applied with this + * method name and out-of-bounds arguments returns a runtime + * exception. If {@code null} or returns {@code null} then, it is as + * if an exception formatter produced from an invocation of + * {@code outOfBoundsExceptionFormatter(IndexOutOfBounds::new)} is used + * instead (though it may be more efficient). + * Exceptions thrown by the formatter are relayed to the caller. + * @return {@code fromIndex} if the sub-range within bounds of the range + * @throws X if the sub-range is out of bounds and the exception factory + * function is non-{@code null} + * @throws IndexOutOfBoundsException if the sub-range is out of bounds and + * the exception factory function is {@code null} + * @since 16 + */ + public static + long checkFromIndexSize(long fromIndex, long size, long length, + BiFunction, X> oobef) { + if ((length | fromIndex | size) < 0 || size > length - fromIndex) + throw outOfBoundsCheckFromIndexSize(oobef, fromIndex, size, length); + return fromIndex; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/pkix/PKIXInsts.java b/openjdk/src/main/java/net/tongsuo/pkix/PKIXInsts.java new file mode 100644 index 000000000..eb6f3223e --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/pkix/PKIXInsts.java @@ -0,0 +1,114 @@ +package net.tongsuo.pkix; + +import net.tongsuo.crypto.CryptoUtils; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathValidator; +import java.security.cert.CertStore; +import java.security.cert.CertStoreParameters; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class PKIXInsts { + + static final String PROV_NAME = "Tongsuo_Security_Provider"; + + private static final Set CERTIFICATE_FACTORY_TYPES + = new HashSet<>(Collections.singletonList("X.509")); + + public static CertificateFactory getCertificateFactory(String type) + throws CertificateException { + CertificateFactory certificateFactory = null; + if (CERTIFICATE_FACTORY_TYPES.contains(type)) { + try { + certificateFactory = CertificateFactory.getInstance(type, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + certificateFactory = CertificateFactory.getInstance(type); + } + return certificateFactory; + } + + private static final Set KEY_STORE_TYPES + = new HashSet<>(Arrays.asList("PKCS12")); + + public static KeyStore getKeyStore(String type) + throws KeyStoreException { + KeyStore keyStore = null; + if (KEY_STORE_TYPES.contains(type)) { + try { + keyStore = KeyStore.getInstance(type, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + keyStore = KeyStore.getInstance(type); + } + return keyStore; + } + + private static final Set CERT_PATH_VALIDATOR_ALGOS + = new HashSet<>(Collections.singletonList("PKIX")); + + public static CertPathValidator getCertPathValidator(String algorithm) + throws NoSuchAlgorithmException { + CertPathValidator certPathValidator = null; + if (CERT_PATH_VALIDATOR_ALGOS.contains(algorithm)) { + try { + certPathValidator = CertPathValidator.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + certPathValidator = CertPathValidator.getInstance(algorithm); + } + return certPathValidator; + } + + private static final Set CERT_PATH_BUILDER_ALGOS + = new HashSet<>(Collections.singletonList("PKIX")); + + public static CertPathBuilder getCertPathBuilder(String algorithm) + throws NoSuchAlgorithmException { + CertPathBuilder certPathBuilder = null; + if (CERT_PATH_BUILDER_ALGOS.contains(algorithm)) { + try { + certPathBuilder = CertPathBuilder.getInstance(algorithm, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + certPathBuilder = CertPathBuilder.getInstance(algorithm); + } + return certPathBuilder; + } + + private static final Set CERT_STORE_TYPES + = new HashSet<>(Collections.singletonList("Collection")); + + public static CertStore getCertStore(String type, CertStoreParameters params) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + CertStore certStore = null; + if (CERT_STORE_TYPES.contains(type)) { + try { + certStore = CertStore.getInstance(type, params, PROV_NAME); + } catch (NoSuchProviderException e) { + throw new IllegalStateException("No provider: " + PROV_NAME, e); + } + } else { + certStore = CertStore.getInstance(type, params); + } + return certStore; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/pkix/PKIXUtils.java b/openjdk/src/main/java/net/tongsuo/pkix/PKIXUtils.java new file mode 100644 index 000000000..54c81bff0 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/pkix/PKIXUtils.java @@ -0,0 +1,153 @@ +package net.tongsuo.pkix; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.NamedCurve; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Set; + +/** + * The utilities for this provider. + */ +public class PKIXUtils { + + private static final String PRIVATE_KEY_BEGIN = "-----BEGIN PRIVATE KEY-----"; + private static final String PRIVATE_KEY_END = "-----END PRIVATE KEY-----"; + + private static final String EC_PARAMS_BEGIN = "-----BEGIN EC PARAMETERS-----"; + private static final String EC_PARAMS_END = "-----END EC PARAMETERS-----"; + + private static final String PUBLIC_KEY_BEGIN = "-----BEGIN PUBLIC KEY-----"; + private static final String PUBLIC_KEY_END = "-----END PUBLIC KEY-----"; + + public static boolean isSM3withSM2(String algName) { + return "SM3withSM2".equalsIgnoreCase(algName) + || "sm2sig_sm3".equalsIgnoreCase(algName); + } + + // Parse named curve from an EC parameter PEM. + // This PEM contains an object identifier representing a named curve, + // e.g. 06 08 2A 81 1C CF 55 01 82 2D -- 1.2.156.10197.1.301, SM2 curve + public static String getNamedCurveId(String ecParams) + throws IOException { + String keyPem = ecParams.replace(EC_PARAMS_BEGIN, "") + .replace(EC_PARAMS_END, ""); + DerInputStream derIn = new DerInputStream( + Base64.getMimeDecoder().decode(keyPem)); + return derIn.getOID().toString(); + } + + // Create a PrivateKey from a PKCS#8-encoded PEM. + public static PrivateKey getPrivateKey(String keyAlgo, String pkcs8Key) + throws NoSuchAlgorithmException, InvalidKeySpecException { + String keyPem = pkcs8Key.replace(PRIVATE_KEY_BEGIN, "") + .replace(PRIVATE_KEY_END, ""); + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec( + Base64.getMimeDecoder().decode(keyPem)); + KeyFactory keyFactory = CryptoInsts.getKeyFactory(keyAlgo); + return keyFactory.generatePrivate(privateKeySpec); + } + + // Create a PublicKey from an X.509-encoded PEM. + public static PublicKey getPublicKey(String keyAlgo, String x509Key) + throws NoSuchAlgorithmException, InvalidKeySpecException { + String keyPem = x509Key.replace(PUBLIC_KEY_BEGIN, "") + .replace(PUBLIC_KEY_END, ""); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec( + Base64.getMimeDecoder().decode(keyPem)); + KeyFactory keyFactory = CryptoInsts.getKeyFactory(keyAlgo); + return keyFactory.generatePublic(publicKeySpec); + } + + public static PublicKey getPublicKey(Certificate cert) + throws InvalidKeyException { + // If the certificate is of type X509Certificate, + // we should check whether it has a Key Usage + // extension marked as critical. + if (cert instanceof X509Certificate) { + // Check whether the cert has a key usage extension + // marked as a critical extension. + // The OID for KeyUsage extension is 2.5.29.15. + X509Certificate c = (X509Certificate)cert; + Set criticalExtSet = c.getCriticalExtensionOIDs(); + + if (criticalExtSet != null && !criticalExtSet.isEmpty() + && criticalExtSet.contains("2.5.29.15")) { + boolean[] keyUsageInfo = c.getKeyUsage(); + // keyUsageInfo[0] is for digitalSignature. + if ((keyUsageInfo != null) && !keyUsageInfo[0]) { + throw new InvalidKeyException("Wrong key usage"); + } + } + } + return cert.getPublicKey(); + } + + // Create a PublicKey from an X.509 PEM. + public static PublicKey getPublicKey(String certPEM) + throws InvalidKeyException, CertificateException { + return getPublicKey(getCertificate(certPEM)); + } + + public static X509Certificate getCertificate(String certPEM) + throws CertificateException { + return (X509Certificate) PKIXInsts.getCertificateFactory("X.509") + .generateCertificate(new ByteArrayInputStream( + certPEM.getBytes(StandardCharsets.UTF_8))); + } + + // An SM certificate must use curveSM2 as ECC curve + // and SM2withSM3 as signature scheme. + public static boolean isSMCert(X509Certificate cert) { + if (!(cert.getPublicKey() instanceof ECPublicKey)) { + return false; + } + + NamedCurve curve = (NamedCurve) ((ECPublicKey) cert.getPublicKey()).getParams(); + return KnownOIDs.curveSM2.value().equals(curve.getObjectId()) + && KnownOIDs.SM3withSM2.value().equals(cert.getSigAlgOID()); + } + + // CA has basic constraints extension. + public static boolean isCA(X509Certificate certificate) { + return certificate.getBasicConstraints() != -1; + } + + // If the key usage is critical, it must contain digitalSignature. + public static boolean isSignCert(X509Certificate certificate) { + if (certificate == null) { + return false; + } + + boolean[] keyUsage = certificate.getKeyUsage(); + return keyUsage == null || keyUsage[0]; + } + + // If the key usage is critical, it must contain one or more of + // keyEncipherment, dataEncipherment and keyAgreement. + public static boolean isEncCert(X509Certificate certificate) { + if (certificate == null) { + return false; + } + + boolean[] keyUsage = certificate.getKeyUsage(); + return keyUsage == null || keyUsage[2] || keyUsage[3] || keyUsage[4]; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/net/util/IPAddressUtil.java b/openjdk/src/main/java/net/tongsuo/sun/net/util/IPAddressUtil.java new file mode 100644 index 000000000..2a7385777 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/net/util/IPAddressUtil.java @@ -0,0 +1,846 @@ +/* + * Copyright (c) 2004, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.net.util; + +import net.tongsuo.sun.security.action.GetPropertyAction; + +import java.io.UncheckedIOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.nio.CharBuffer; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class IPAddressUtil { + private static final int INADDR4SZ = 4; + private static final int INADDR16SZ = 16; + private static final int INT16SZ = 2; + + /* + * Converts IPv4 address in its textual presentation form + * into its numeric binary form. + * + * @param src a String representing an IPv4 address in standard format + * @return a byte array representing the IPv4 numeric address + */ + @SuppressWarnings("fallthrough") + public static byte[] textToNumericFormatV4(String src) + { + byte[] res = new byte[INADDR4SZ]; + + long tmpValue = 0; + int currByte = 0; + boolean newOctet = true; + + int len = src.length(); + if (len == 0 || len > 15) { + return null; + } + /* + * When only one part is given, the value is stored directly in + * the network address without any byte rearrangement. + * + * When a two part address is supplied, the last part is + * interpreted as a 24-bit quantity and placed in the right + * most three bytes of the network address. This makes the + * two part address format convenient for specifying Class A + * network addresses as net.host. + * + * When a three part address is specified, the last part is + * interpreted as a 16-bit quantity and placed in the right + * most two bytes of the network address. This makes the + * three part address format convenient for specifying + * Class B net- work addresses as 128.net.host. + * + * When four parts are specified, each is interpreted as a + * byte of data and assigned, from left to right, to the + * four bytes of an IPv4 address. + * + * We determine and parse the leading parts, if any, as single + * byte values in one pass directly into the resulting byte[], + * then the remainder is treated as a 8-to-32-bit entity and + * translated into the remaining bytes in the array. + */ + for (int i = 0; i < len; i++) { + char c = src.charAt(i); + if (c == '.') { + if (newOctet || tmpValue < 0 || tmpValue > 0xff || currByte == 3) { + return null; + } + res[currByte++] = (byte) (tmpValue & 0xff); + tmpValue = 0; + newOctet = true; + } else { + int digit = digit(c, 10); + if (digit < 0) { + return null; + } + tmpValue *= 10; + tmpValue += digit; + newOctet = false; + } + } + if (newOctet || tmpValue < 0 || tmpValue >= (1L << ((4 - currByte) * 8))) { + return null; + } + switch (currByte) { + case 0: + res[0] = (byte) ((tmpValue >> 24) & 0xff); + case 1: + res[1] = (byte) ((tmpValue >> 16) & 0xff); + case 2: + res[2] = (byte) ((tmpValue >> 8) & 0xff); + case 3: + res[3] = (byte) ((tmpValue >> 0) & 0xff); + } + return res; + } + + /** + * Validates if input string is a valid IPv4 address literal. + * If the "jdk.net.allowAmbiguousIPAddressLiterals" system property is set + * to {@code false}, or is not set then validation of the address string is performed as follows: + * If string can't be parsed by following IETF IPv4 address string literals + * formatting style rules (default one), but can be parsed by following BSD formatting + * style rules, the IPv4 address string content is treated as ambiguous and + * {@code IllegalArgumentException} is thrown. + * + * @param src input string + * @return bytes array if string is a valid IPv4 address string + * @throws IllegalArgumentException if "jdk.net.allowAmbiguousIPAddressLiterals" SP is set to + * "false" and IPv4 address string {@code "src"} is ambiguous + */ + public static byte[] validateNumericFormatV4(String src) { + byte[] parsedBytes = textToNumericFormatV4(src); + if (!ALLOW_AMBIGUOUS_IPADDRESS_LITERALS_SP_VALUE + && parsedBytes == null && isBsdParsableV4(src)) { + throw new IllegalArgumentException("Invalid IP address literal: " + src); + } + return parsedBytes; + } + + /* + * Convert IPv6 presentation level address to network order binary form. + * credit: + * Converted from C code from Solaris 8 (inet_pton) + * + * Any component of the string following a per-cent % is ignored. + * + * @param src a String representing an IPv6 address in textual format + * @return a byte array representing the IPv6 numeric address + */ + public static byte[] textToNumericFormatV6(String src) + { + // Shortest valid string is "::", hence at least 2 chars + if (src.length() < 2) { + return null; + } + + int colonp; + char ch; + boolean saw_xdigit; + int val; + char[] srcb = src.toCharArray(); + byte[] dst = new byte[INADDR16SZ]; + + int srcb_length = srcb.length; + int pc = src.indexOf ('%'); + if (pc == srcb_length -1) { + return null; + } + + if (pc != -1) { + srcb_length = pc; + } + + colonp = -1; + int i = 0, j = 0; + /* Leading :: requires some special handling. */ + if (srcb[i] == ':') + if (srcb[++i] != ':') + return null; + int curtok = i; + saw_xdigit = false; + val = 0; + while (i < srcb_length) { + ch = srcb[i++]; + int chval = digit(ch, 16); + if (chval != -1) { + val <<= 4; + val |= chval; + if (val > 0xffff) + return null; + saw_xdigit = true; + continue; + } + if (ch == ':') { + curtok = i; + if (!saw_xdigit) { + if (colonp != -1) + return null; + colonp = j; + continue; + } else if (i == srcb_length) { + return null; + } + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + saw_xdigit = false; + val = 0; + continue; + } + if (ch == '.' && ((j + INADDR4SZ) <= INADDR16SZ)) { + String ia4 = src.substring(curtok, srcb_length); + /* check this IPv4 address has 3 dots, i.e. A.B.C.D */ + int dot_count = 0, index=0; + while ((index = ia4.indexOf ('.', index)) != -1) { + dot_count ++; + index ++; + } + if (dot_count != 3) { + return null; + } + byte[] v4addr = textToNumericFormatV4(ia4); + if (v4addr == null) { + return null; + } + for (int k = 0; k < INADDR4SZ; k++) { + dst[j++] = v4addr[k]; + } + saw_xdigit = false; + break; /* '\0' was seen by inet_pton4(). */ + } + return null; + } + if (saw_xdigit) { + if (j + INT16SZ > INADDR16SZ) + return null; + dst[j++] = (byte) ((val >> 8) & 0xff); + dst[j++] = (byte) (val & 0xff); + } + + if (colonp != -1) { + int n = j - colonp; + + if (j == INADDR16SZ) + return null; + for (i = 1; i <= n; i++) { + dst[INADDR16SZ - i] = dst[colonp + n - i]; + dst[colonp + n - i] = 0; + } + j = INADDR16SZ; + } + if (j != INADDR16SZ) + return null; + byte[] newdst = convertFromIPv4MappedAddress(dst); + if (newdst != null) { + return newdst; + } else { + return dst; + } + } + + /** + * @param src a String representing an IPv4 address in textual format + * @return a boolean indicating whether src is an IPv4 literal address + */ + public static boolean isIPv4LiteralAddress(String src) { + return textToNumericFormatV4(src) != null; + } + + /** + * @param src a String representing an IPv6 address in textual format + * @return a boolean indicating whether src is an IPv6 literal address + */ + public static boolean isIPv6LiteralAddress(String src) { + return textToNumericFormatV6(src) != null; + } + + /* + * Convert IPv4-Mapped address to IPv4 address. Both input and + * returned value are in network order binary form. + * + * @param src a String representing an IPv4-Mapped address in textual format + * @return a byte array representing the IPv4 numeric address + */ + public static byte[] convertFromIPv4MappedAddress(byte[] addr) { + if (isIPv4MappedAddress(addr)) { + byte[] newAddr = new byte[INADDR4SZ]; + System.arraycopy(addr, 12, newAddr, 0, INADDR4SZ); + return newAddr; + } + return null; + } + + /** + * Utility routine to check if the InetAddress is an + * IPv4 mapped IPv6 address. + * + * @return a boolean indicating if the InetAddress is + * an IPv4 mapped IPv6 address; or false if address is IPv4 address. + */ + private static boolean isIPv4MappedAddress(byte[] addr) { + if (addr.length < INADDR16SZ) { + return false; + } + if ((addr[0] == 0x00) && (addr[1] == 0x00) && + (addr[2] == 0x00) && (addr[3] == 0x00) && + (addr[4] == 0x00) && (addr[5] == 0x00) && + (addr[6] == 0x00) && (addr[7] == 0x00) && + (addr[8] == 0x00) && (addr[9] == 0x00) && + (addr[10] == (byte)0xff) && + (addr[11] == (byte)0xff)) { + return true; + } + return false; + } + /** + * Mapping from unscoped local Inet(6)Address to the same address + * including the correct scope-id, determined from NetworkInterface. + */ + private static final ConcurrentHashMap + cache = new ConcurrentHashMap<>(); + + /** + * Returns a scoped version of the supplied local, link-local ipv6 address + * if that scope-id can be determined from local NetworkInterfaces. + * If the address already has a scope-id or if the address is not local, ipv6 + * or link local, then the original address is returned. + * + * @param address + * @exception SocketException if the given ipv6 link local address is found + * on more than one local interface + * @return + */ + public static InetAddress toScopedAddress(InetAddress address) + throws SocketException { + + if (address instanceof Inet6Address && address.isLinkLocalAddress() + && ((Inet6Address) address).getScopeId() == 0) { + + InetAddress cached = null; + try { + cached = cache.computeIfAbsent(address, k -> findScopedAddress(k)); + } catch (UncheckedIOException e) { + throw (SocketException)e.getCause(); + } + return cached != null ? cached : address; + } else { + return address; + } + } + + /** + * Same as above for InetSocketAddress + */ + public static InetSocketAddress toScopedAddress(InetSocketAddress address) + throws SocketException { + InetAddress addr; + InetAddress orig = address.getAddress(); + if ((addr = toScopedAddress(orig)) == orig) { + return address; + } else { + return new InetSocketAddress(addr, address.getPort()); + } + } + + @SuppressWarnings("removal") + private static InetAddress findScopedAddress(InetAddress address) { +// PrivilegedExceptionAction> pa = () -> NetworkInterface.networkInterfaces() +// .flatMap(NetworkInterface::inetAddresses) +// .filter(a -> (a instanceof Inet6Address) +// && address.equals(a) +// && ((Inet6Address) a).getScopeId() != 0) +// .collect(Collectors.toList()); + + PrivilegedExceptionAction> pa = () -> { + List result = new ArrayList<>(); + + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + while(enumeration.hasMoreElements()) { + NetworkInterface ni = enumeration.nextElement(); + Enumeration inetAddresses = ni.getInetAddresses(); + while(inetAddresses.hasMoreElements()) { + InetAddress inetAddress = inetAddresses.nextElement(); + if (inetAddress instanceof Inet6Address + && address.equals(inetAddress) + && ((Inet6Address) inetAddress).getScopeId() != 0) { + result.add(inetAddress); + } + } + } + return result; + }; + + List result; + try { + result = AccessController.doPrivileged(pa); + int sz = result.size(); + if (sz == 0) + return null; + if (sz > 1) + throw new UncheckedIOException(new SocketException( + "Duplicate link local addresses: must specify scope-id")); + return result.get(0); + } catch (PrivilegedActionException pae) { + return null; + } + } + + // See java.net.URI for more details on how to generate these + // masks. + // + // square brackets + private static final long L_IPV6_DELIMS = 0x0L; // "[]" + private static final long H_IPV6_DELIMS = 0x28000000L; // "[]" + // RFC 3986 gen-delims + private static final long L_GEN_DELIMS = 0x8400800800000000L; // ":/?#[]@" + private static final long H_GEN_DELIMS = 0x28000001L; // ":/?#[]@" + // These gen-delims can appear in authority + private static final long L_AUTH_DELIMS = 0x400000000000000L; // "@[]:" + private static final long H_AUTH_DELIMS = 0x28000001L; // "@[]:" + // colon is allowed in userinfo + private static final long L_COLON = 0x400000000000000L; // ":" + private static final long H_COLON = 0x0L; // ":" + // slash should be encoded in authority + private static final long L_SLASH = 0x800000000000L; // "/" + private static final long H_SLASH = 0x0L; // "/" + // backslash should always be encoded + private static final long L_BACKSLASH = 0x0L; // "\" + private static final long H_BACKSLASH = 0x10000000L; // "\" + // ASCII chars 0-31 + 127 - various controls + CRLF + TAB + private static final long L_NON_PRINTABLE = 0xffffffffL; + private static final long H_NON_PRINTABLE = 0x8000000000000000L; + // All of the above + private static final long L_EXCLUDE = 0x84008008ffffffffL; + private static final long H_EXCLUDE = 0x8000000038000001L; + + private static final char[] OTHERS = { + 8263,8264,8265,8448,8449,8453,8454,10868, + 65109,65110,65119,65131,65283,65295,65306,65311,65312 + }; + + // Tell whether the given character is found by the given mask pair + public static boolean match(char c, long lowMask, long highMask) { + if (c < 64) + return ((1L << c) & lowMask) != 0; + if (c < 128) + return ((1L << (c - 64)) & highMask) != 0; + return false; // other non ASCII characters are not filtered + } + + // returns -1 if the string doesn't contain any characters + // from the mask, the index of the first such character found + // otherwise. + public static int scan(String s, long lowMask, long highMask) { + int i = -1, len; + if (s == null || (len = s.length()) == 0) return -1; + boolean match = false; + while (++i < len && !(match = match(s.charAt(i), lowMask, highMask))); + if (match) return i; + return -1; + } + + public static int scan(String s, long lowMask, long highMask, char[] others) { + int i = -1, len; + if (s == null || (len = s.length()) == 0) return -1; + boolean match = false; + char c, c0 = others[0]; + while (++i < len && !(match = match((c=s.charAt(i)), lowMask, highMask))) { + if (c >= c0 && (Arrays.binarySearch(others, c) > -1)) { + match = true; break; + } + } + if (match) return i; + + return -1; + } + + private static String describeChar(char c) { + if (c < 32 || c == 127) { + if (c == '\n') return "LF"; + if (c == '\r') return "CR"; + return "control char (code=" + (int)c + ")"; + } + if (c == '\\') return "'\\'"; + return "'" + c + "'"; + } + + private static String checkUserInfo(String str) { + // colon is permitted in user info + int index = scan(str, L_EXCLUDE & ~L_COLON, + H_EXCLUDE & ~H_COLON); + if (index >= 0) { + return "Illegal character found in user-info: " + + describeChar(str.charAt(index)); + } + return null; + } + + private static String checkHost(String str) { + int index; + if (str.startsWith("[") && str.endsWith("]")) { + str = str.substring(1, str.length() - 1); + if (isIPv6LiteralAddress(str)) { + index = str.indexOf('%'); + if (index >= 0) { + index = scan(str = str.substring(index), + L_NON_PRINTABLE | L_IPV6_DELIMS, + H_NON_PRINTABLE | H_IPV6_DELIMS); + if (index >= 0) { + return "Illegal character found in IPv6 scoped address: " + + describeChar(str.charAt(index)); + } + } + return null; + } + return "Unrecognized IPv6 address format"; + } else { + index = scan(str, L_EXCLUDE, H_EXCLUDE); + if (index >= 0) { + return "Illegal character found in host: " + + describeChar(str.charAt(index)); + } + } + return null; + } + + private static String checkAuth(String str) { + int index = scan(str, + L_EXCLUDE & ~L_AUTH_DELIMS, + H_EXCLUDE & ~H_AUTH_DELIMS); + if (index >= 0) { + return "Illegal character found in authority: " + + describeChar(str.charAt(index)); + } + return null; + } + + // check authority of hierarchical URL. Appropriate for + // HTTP-like protocol handlers + public static String checkAuthority(URL url) { + String s, u, h; + if (url == null) return null; + if ((s = checkUserInfo(u = url.getUserInfo())) != null) { + return s; + } + if ((s = checkHost(h = url.getHost())) != null) { + return s; + } + if (h == null && u == null) { + return checkAuth(url.getAuthority()); + } + return null; + } + + // minimal syntax checks - deeper check may be performed + // by the appropriate protocol handler + public static String checkExternalForm(URL url) { + String s; + if (url == null) return null; + int index = scan(s = url.getUserInfo(), + L_NON_PRINTABLE | L_SLASH, + H_NON_PRINTABLE | H_SLASH); + if (index >= 0) { + return "Illegal character found in authority: " + + describeChar(s.charAt(index)); + } + if ((s = checkHostString(url.getHost())) != null) { + return s; + } + return null; + } + + public static String checkHostString(String host) { + if (host == null) return null; + int index = scan(host, + L_NON_PRINTABLE | L_SLASH, + H_NON_PRINTABLE | H_SLASH, + OTHERS); + if (index >= 0) { + return "Illegal character found in host: " + + describeChar(host.charAt(index)); + } + return null; + } + + /** + * Returns the numeric value of the character {@code ch} in the + * specified radix. + * + * @param ch the character to be converted. + * @param radix the radix. + * @return the numeric value represented by the character in the + * specified radix. + */ + public static int digit(char ch, int radix) { + if (ALLOW_AMBIGUOUS_IPADDRESS_LITERALS_SP_VALUE) { + return Character.digit(ch, radix); + } else { + return parseAsciiDigit(ch, radix); + } + } + + /** + * Try to parse String as IPv4 address literal by following + * BSD-style formatting rules. + * + * @param input input string + * @return {@code true} if input string is parsable as IPv4 address literal, + * {@code false} otherwise. + */ + public static boolean isBsdParsableV4(String input) { + char firstSymbol = input.charAt(0); + // Check if first digit is not a decimal digit + if (parseAsciiDigit(firstSymbol, DECIMAL) == -1) { + return false; + } + + // Last character is dot OR is not a supported digit: [0-9,A-F,a-f] + char lastSymbol = input.charAt(input.length() - 1); + if (lastSymbol == '.' || parseAsciiHexDigit(lastSymbol) == -1) { + return false; + } + + // Parse IP address fields + CharBuffer charBuffer = CharBuffer.wrap(input); + int fieldNumber = 0; + while (charBuffer.hasRemaining()) { + long fieldValue = -1L; + // Try to parse fields in all supported radixes + for (int radix : SUPPORTED_RADIXES) { + fieldValue = parseV4FieldBsd(radix, charBuffer, fieldNumber); + if (fieldValue >= 0) { + fieldNumber++; + break; + } else if (fieldValue == TERMINAL_PARSE_ERROR) { + return false; + } + } + // If field can't be parsed as one of supported radixes stop + // parsing + if (fieldValue < 0) { + return false; + } + } + return true; + } + + /** + * Method tries to parse IP address field that starts from {@linkplain CharBuffer#position() + * current position} of the provided character buffer. + *

+ * This method supports three {@code "radix"} values to decode field values in + * {@code "HEXADECIMAL (radix=16)"}, {@code "DECIMAL (radix=10)"} and + * {@code "OCTAL (radix=8)"} radixes. + *

+ * If {@code -1} value is returned the char buffer position is reset to the value + * it was before it was called. + *

+ * Method returns {@code -2} if formatting illegal for all supported {@code radix} + * values is observed, and there is no point in checking other radix values. + * That includes the following cases:

    + *
  • Two subsequent dots are observer + *
  • Number of dots more than 3 + *
  • Field value exceeds max allowed + *
  • Character is not a valid digit for the requested {@code radix} value, given + * that a field has the radix specific prefix + *
+ * + * @param radix digits encoding radix to use for parsing. Valid values: 8, 10, 16. + * @param buffer {@code CharBuffer} with position set to the field's fist character + * @param fieldNumber parsed field number + * @return {@code CANT_PARSE_IN_RADIX} if field can not be parsed in requested {@code radix}. + * {@code TERMINAL_PARSE_ERROR} if field can't be parsed and the whole parse process should be terminated. + * Parsed field value otherwise. + */ + private static long parseV4FieldBsd(int radix, CharBuffer buffer, int fieldNumber) { + int initialPos = buffer.position(); + long val = 0; + int digitsCount = 0; + if (!checkPrefix(buffer, radix)) { + val = CANT_PARSE_IN_RADIX; + } + boolean dotSeen = false; + while (buffer.hasRemaining() && val != CANT_PARSE_IN_RADIX && !dotSeen) { + char c = buffer.get(); + if (c == '.') { + dotSeen = true; + // Fail if 4 dots in IP address string. + // fieldNumber counter starts from 0, therefore 3 + if (fieldNumber == 3) { + // Terminal state, can stop parsing: too many fields + return TERMINAL_PARSE_ERROR; + } + // Check for literals with two dots, like '1.2..3', '1.2.3..' + if (digitsCount == 0) { + // Terminal state, can stop parsing: dot with no digits + return TERMINAL_PARSE_ERROR; + } + if (val > 255) { + // Terminal state, can stop parsing: too big value for an octet + return TERMINAL_PARSE_ERROR; + } + } else { + int dv = parseAsciiDigit(c, radix); + if (dv >= 0) { + digitsCount++; + val *= radix; + val += dv; + } else { + // Spotted digit can't be parsed in the requested 'radix'. + // The order in which radixes are checked - hex, octal, decimal: + // - if symbol is not a valid digit in hex radix - terminal + // - if symbol is not a valid digit in octal radix, and given + // that octal prefix was observed before - terminal + // - if symbol is not a valid digit in decimal radix - terminal + return TERMINAL_PARSE_ERROR; + } + } + } + if (val == CANT_PARSE_IN_RADIX) { + buffer.position(initialPos); + } else if (!dotSeen) { + // It is the last field - check its value + // This check will ensure that address strings with less + // than 4 fields, i.e. A, A.B and A.B.C address types + // contain value less then the allowed maximum for the last field. + long maxValue = (1L << ((4 - fieldNumber) * 8)) - 1; + if (val > maxValue) { + // Terminal state, can stop parsing: last field value exceeds its + // allowed value + return TERMINAL_PARSE_ERROR; + } + } + return val; + } + + // This method moves the position of the supplied CharBuffer by analysing the digit prefix + // symbols if any. + // The caller should reset the position when method returns false. + private static boolean checkPrefix(CharBuffer buffer, int radix) { + switch (radix) { + case OCTAL: + return isOctalFieldStart(buffer); + case DECIMAL: + return isDecimalFieldStart(buffer); + case HEXADECIMAL: + return isHexFieldStart(buffer); + default: + throw new AssertionError("Not supported radix"); + } + } + + // This method always moves the position of the supplied CharBuffer + // removing the octal prefix symbols '0'. + // The caller should reset the position when method returns false. + private static boolean isOctalFieldStart(CharBuffer cb) { + // .0 is not treated as octal field + if (cb.remaining() < 2) { + return false; + } + + // Fetch two first characters + int position = cb.position(); + char first = cb.get(); + char second = cb.get(); + + // Return false if the first char is not octal prefix '0' or second is a + // field separator - parseV4FieldBsd will reset position to start of the field. + // '.0.' fields will be successfully parsed in decimal radix. + boolean isOctalPrefix = first == '0' && second != '.'; + + // If the prefix looks like octal - consume '0', otherwise 'false' is returned + // and caller will reset the buffer position. + if (isOctalPrefix) { + cb.position(position + 1); + } + return isOctalPrefix; + } + + // This method doesn't move the position of the supplied CharBuffer + private static boolean isDecimalFieldStart(CharBuffer cb) { + return cb.hasRemaining(); + } + + // This method always moves the position of the supplied CharBuffer + // removing the hexadecimal prefix symbols '0x'. + // The caller should reset the position when method returns false. + private static boolean isHexFieldStart(CharBuffer cb) { + if (cb.remaining() < 2) { + return false; + } + char first = cb.get(); + char second = cb.get(); + return first == '0' && (second == 'x' || second == 'X'); + } + + // Parse ASCII digit in given radix + public static int parseAsciiDigit(char c, int radix) { + assert radix == OCTAL || radix == DECIMAL || radix == HEXADECIMAL; + if (radix == HEXADECIMAL) { + return parseAsciiHexDigit(c); + } + int val = c - '0'; + return (val < 0 || val >= radix) ? -1 : val; + } + + // Parse ASCII digit in hexadecimal radix + private static int parseAsciiHexDigit(char digit) { + char c = Character.toLowerCase(digit); + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + return parseAsciiDigit(c, DECIMAL); + } + + // Supported radixes + private static final int HEXADECIMAL = 16; + private static final int DECIMAL = 10; + private static final int OCTAL = 8; + // Order in which field formats are exercised to parse one IP address textual field + private static final int[] SUPPORTED_RADIXES = new int[]{HEXADECIMAL, OCTAL, DECIMAL}; + + // BSD parser's return values + private final static long CANT_PARSE_IN_RADIX = -1L; + private final static long TERMINAL_PARSE_ERROR = -2L; + + private static final String ALLOW_AMBIGUOUS_IPADDRESS_LITERALS_SP = "jdk.net.allowAmbiguousIPAddressLiterals"; + private static final boolean ALLOW_AMBIGUOUS_IPADDRESS_LITERALS_SP_VALUE = Boolean.valueOf( + GetPropertyAction.privilegedGetProperty(ALLOW_AMBIGUOUS_IPADDRESS_LITERALS_SP, "false")); +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/net/www/ParseUtil.java b/openjdk/src/main/java/net/tongsuo/sun/net/www/ParseUtil.java new file mode 100644 index 000000000..ac11b980f --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/net/www/ParseUtil.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 1998, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.net.www; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +import net.tongsuo.java.util.HexFormat; + +/** + * A class that contains useful routines common to sun.net.www + * @author Mike McCloskey + */ + +public final class ParseUtil { + + private static final HexFormat HEX_UPPERCASE = HexFormat.of().withUpperCase(); + + private ParseUtil() {} + + /** + * Constructs an encoded version of the specified path string suitable + * for use in the construction of a URL. + * + * A path separator is replaced by a forward slash. The string is UTF8 + * encoded. The % escape sequence is used for characters that are above + * 0x7F or those defined in RFC2396 as reserved or excluded in the path + * component of a URL. + */ + public static String encodePath(String path) { + return encodePath(path, true); + } + /* + * flag indicates whether path uses platform dependent + * File.separatorChar or not. True indicates path uses platform + * dependent File.separatorChar. + */ + public static String encodePath(String path, boolean flag) { + if (flag && File.separatorChar != '/') { + return encodePath(path, 0, File.separatorChar); + } else { + int index = firstEncodeIndex(path); + if (index > -1) { + return encodePath(path, index, '/'); + } else { + return path; + } + } + } + + private static int firstEncodeIndex(String path) { + int len = path.length(); + for (int i = 0; i < len; i++) { + char c = path.charAt(i); + // Ordering in the following test is performance sensitive, + // and typically paths have most chars in the a-z range, then + // in the symbol range '&'-':' (includes '.', '/' and '0'-'9') + // and more rarely in the A-Z range. + if (c >= 'a' && c <= 'z' || + c >= '&' && c <= ':' || + c >= 'A' && c <= 'Z') { + continue; + } else if (c > 0x007F || match(c, L_ENCODED, H_ENCODED)) { + return i; + } + } + return -1; + } + + private static String encodePath(String path, int index, char sep) { + char[] pathCC = path.toCharArray(); + char[] retCC = new char[pathCC.length * 2 + 16 - index]; + if (index > 0) { + System.arraycopy(pathCC, 0, retCC, 0, index); + } + int retLen = index; + + for (int i = index; i < pathCC.length; i++) { + char c = pathCC[i]; + if (c == sep) + retCC[retLen++] = '/'; + else { + if (c <= 0x007F) { + if (c >= 'a' && c <= 'z' || + c >= 'A' && c <= 'Z' || + c >= '0' && c <= '9') { + retCC[retLen++] = c; + } else if (match(c, L_ENCODED, H_ENCODED)) { + retLen = escape(retCC, c, retLen); + } else { + retCC[retLen++] = c; + } + } else if (c > 0x07FF) { + retLen = escape(retCC, (char)(0xE0 | ((c >> 12) & 0x0F)), retLen); + retLen = escape(retCC, (char)(0x80 | ((c >> 6) & 0x3F)), retLen); + retLen = escape(retCC, (char)(0x80 | ((c >> 0) & 0x3F)), retLen); + } else { + retLen = escape(retCC, (char)(0xC0 | ((c >> 6) & 0x1F)), retLen); + retLen = escape(retCC, (char)(0x80 | ((c >> 0) & 0x3F)), retLen); + } + } + //worst case scenario for character [0x7ff-] every single + //character will be encoded into 9 characters. + if (retLen + 9 > retCC.length) { + int newLen = retCC.length * 2 + 16; + if (newLen < 0) { + newLen = Integer.MAX_VALUE; + } + char[] buf = new char[newLen]; + System.arraycopy(retCC, 0, buf, 0, retLen); + retCC = buf; + } + } + return new String(retCC, 0, retLen); + } + + /** + * Appends the URL escape sequence for the specified char to the + * specified character array. + */ + private static int escape(char[] cc, char c, int index) { + cc[index++] = '%'; + cc[index++] = Character.forDigit((c >> 4) & 0xF, 16); + cc[index++] = Character.forDigit(c & 0xF, 16); + return index; + } + + /** + * Un-escape and return the character at position i in string s. + */ + private static byte unescape(String s, int i) { + return (byte) Integer.parseInt(s.substring(i+1,i+3), 16); + } + + + /** + * Returns a new String constructed from the specified String by replacing + * the URL escape sequences and UTF8 encoding with the characters they + * represent. + */ + public static String decode(String s) { + int n = s.length(); + if ((n == 0) || (s.indexOf('%') < 0)) + return s; + + StringBuilder sb = new StringBuilder(n); + ByteBuffer bb = ByteBuffer.allocate(n); + CharBuffer cb = CharBuffer.allocate(n); + CharsetDecoder dec = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + + char c = s.charAt(0); + for (int i = 0; i < n;) { + assert c == s.charAt(i); + if (c != '%') { + sb.append(c); + if (++i >= n) + break; + c = s.charAt(i); + continue; + } + bb.clear(); + int ui = i; + for (;;) { + assert (n - i >= 2); + try { + bb.put(unescape(s, i)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(); + } + i += 3; + if (i >= n) + break; + c = s.charAt(i); + if (c != '%') + break; + } + bb.flip(); + cb.clear(); + dec.reset(); + CoderResult cr = dec.decode(bb, cb, true); + if (cr.isError()) + throw new IllegalArgumentException("Error decoding percent encoded characters"); + cr = dec.flush(cb); + if (cr.isError()) + throw new IllegalArgumentException("Error decoding percent encoded characters"); + sb.append(cb.flip().toString()); + } + + return sb.toString(); + } + + public static URL fileToEncodedURL(File file) + throws MalformedURLException + { + String path = file.getAbsolutePath(); + path = ParseUtil.encodePath(path); + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/") && file.isDirectory()) { + path = path + "/"; + } + return new URL("file", "", path); + } + + public static java.net.URI toURI(URL url) { + String protocol = url.getProtocol(); + String auth = url.getAuthority(); + String path = url.getPath(); + String query = url.getQuery(); + String ref = url.getRef(); + if (path != null && !(path.startsWith("/"))) + path = "/" + path; + + // + // In java.net.URI class, a port number of -1 implies the default + // port number. So get it stripped off before creating URI instance. + // + if (auth != null && auth.endsWith(":-1")) + auth = auth.substring(0, auth.length() - 3); + + java.net.URI uri; + try { + uri = createURI(protocol, auth, path, query, ref); + } catch (java.net.URISyntaxException e) { + uri = null; + } + return uri; + } + + // + // createURI() and its auxiliary code are cloned from java.net.URI. + // Most of the code are just copy and paste, except that quote() + // has been modified to avoid double-escape. + // + // Usually it is unacceptable, but we're forced to do it because + // otherwise we need to change public API, namely java.net.URI's + // multi-argument constructors. It turns out that the changes cause + // incompatibilities so can't be done. + // + private static URI createURI(String scheme, + String authority, + String path, + String query, + String fragment) throws URISyntaxException + { + String s = toString(scheme, null, + authority, null, null, -1, + path, query, fragment); + checkPath(s, scheme, path); + return new URI(s); + } + + private static String toString(String scheme, + String opaquePart, + String authority, + String userInfo, + String host, + int port, + String path, + String query, + String fragment) + { + StringBuilder sb = new StringBuilder(); + if (scheme != null) { + sb.append(scheme); + sb.append(':'); + } + appendSchemeSpecificPart(sb, opaquePart, + authority, userInfo, host, port, + path, query); + appendFragment(sb, fragment); + return sb.toString(); + } + + private static void appendSchemeSpecificPart(StringBuilder sb, + String opaquePart, + String authority, + String userInfo, + String host, + int port, + String path, + String query) + { + if (opaquePart != null) { + /* check if SSP begins with an IPv6 address + * because we must not quote a literal IPv6 address + */ + if (opaquePart.startsWith("//[")) { + int end = opaquePart.indexOf(']'); + if (end != -1 && opaquePart.indexOf(':')!=-1) { + String doquote, dontquote; + if (end == opaquePart.length()) { + dontquote = opaquePart; + doquote = ""; + } else { + dontquote = opaquePart.substring(0,end+1); + doquote = opaquePart.substring(end+1); + } + sb.append (dontquote); + sb.append(quote(doquote, L_URIC, H_URIC)); + } + } else { + sb.append(quote(opaquePart, L_URIC, H_URIC)); + } + } else { + appendAuthority(sb, authority, userInfo, host, port); + if (path != null) + sb.append(quote(path, L_PATH, H_PATH)); + if (query != null) { + sb.append('?'); + sb.append(quote(query, L_URIC, H_URIC)); + } + } + } + + private static void appendAuthority(StringBuilder sb, + String authority, + String userInfo, + String host, + int port) + { + if (host != null) { + sb.append("//"); + if (userInfo != null) { + sb.append(quote(userInfo, L_USERINFO, H_USERINFO)); + sb.append('@'); + } + boolean needBrackets = ((host.indexOf(':') >= 0) + && !host.startsWith("[") + && !host.endsWith("]")); + if (needBrackets) sb.append('['); + sb.append(host); + if (needBrackets) sb.append(']'); + if (port != -1) { + sb.append(':'); + sb.append(port); + } + } else if (authority != null) { + sb.append("//"); + if (authority.startsWith("[")) { + int end = authority.indexOf(']'); + if (end != -1 && authority.indexOf(':')!=-1) { + String doquote, dontquote; + if (end == authority.length()) { + dontquote = authority; + doquote = ""; + } else { + dontquote = authority.substring(0,end+1); + doquote = authority.substring(end+1); + } + sb.append (dontquote); + sb.append(quote(doquote, + L_REG_NAME | L_SERVER, + H_REG_NAME | H_SERVER)); + } + } else { + sb.append(quote(authority, + L_REG_NAME | L_SERVER, + H_REG_NAME | H_SERVER)); + } + } + } + + private static void appendFragment(StringBuilder sb, String fragment) { + if (fragment != null) { + sb.append('#'); + sb.append(quote(fragment, L_URIC, H_URIC)); + } + } + + // Quote any characters in s that are not permitted + // by the given mask pair + // + private static String quote(String s, long lowMask, long highMask) { + int n = s.length(); + StringBuilder sb = null; + CharsetEncoder encoder = null; + boolean allowNonASCII = ((lowMask & L_ESCAPED) != 0); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '\u0080') { + if (!match(c, lowMask, highMask) && !isEscaped(s, i)) { + if (sb == null) { + sb = new StringBuilder(); + sb.append(s, 0, i); + } + appendEscape(sb, (byte)c); + } else { + if (sb != null) + sb.append(c); + } + } else if (allowNonASCII + && (Character.isSpaceChar(c) + || Character.isISOControl(c))) { + if (encoder == null) { + encoder = StandardCharsets.UTF_8.newEncoder(); + } + if (sb == null) { + sb = new StringBuilder(); + sb.append(s, 0, i); + } + appendEncoded(encoder, sb, c); + } else { + if (sb != null) + sb.append(c); + } + } + return (sb == null) ? s : sb.toString(); + } + + // + // To check if the given string has an escaped triplet + // at the given position + // + private static boolean isEscaped(String s, int pos) { + if (s == null || (s.length() <= (pos + 2))) + return false; + + return s.charAt(pos) == '%' + && match(s.charAt(pos + 1), L_HEX, H_HEX) + && match(s.charAt(pos + 2), L_HEX, H_HEX); + } + + private static void appendEncoded(CharsetEncoder encoder, + StringBuilder sb, char c) { + ByteBuffer bb = null; + try { + bb = encoder.encode(CharBuffer.wrap("" + c)); + } catch (CharacterCodingException x) { + assert false; + } + while (bb.hasRemaining()) { + int b = bb.get() & 0xff; + if (b >= 0x80) + appendEscape(sb, (byte)b); + else + sb.append((char)b); + } + } + + private static void appendEscape(StringBuilder sb, byte b) { + sb.append('%'); + HEX_UPPERCASE.toHexDigits(sb, b); + } + + // Tell whether the given character is permitted by the given mask pair + private static boolean match(char c, long lowMask, long highMask) { + if (c < 64) + return ((1L << c) & lowMask) != 0; + if (c < 128) + return ((1L << (c - 64)) & highMask) != 0; + return false; + } + + // If a scheme is given then the path, if given, must be absolute + // + private static void checkPath(String s, String scheme, String path) + throws URISyntaxException + { + if (scheme != null) { + if (path != null && !path.isEmpty() && path.charAt(0) != '/') + throw new URISyntaxException(s, + "Relative path in absolute URI"); + } + } + + + // -- Character classes for parsing -- + + // To save startup time, we manually calculate the low-/highMask constants. + // For reference, the following methods were used to calculate the values: + + // Compute a low-order mask for the characters + // between first and last, inclusive + // private static long lowMask(char first, char last) { + // long m = 0; + // int f = Math.max(Math.min(first, 63), 0); + // int l = Math.max(Math.min(last, 63), 0); + // for (int i = f; i <= l; i++) + // m |= 1L << i; + // return m; + // } + + // Compute the low-order mask for the characters in the given string + // private static long lowMask(String chars) { + // int n = chars.length(); + // long m = 0; + // for (int i = 0; i < n; i++) { + // char c = chars.charAt(i); + // if (c < 64) + // m |= (1L << c); + // } + // return m; + // } + + // Compute a high-order mask for the characters + // between first and last, inclusive + // private static long highMask(char first, char last) { + // long m = 0; + // int f = Math.max(Math.min(first, 127), 64) - 64; + // int l = Math.max(Math.min(last, 127), 64) - 64; + // for (int i = f; i <= l; i++) + // m |= 1L << i; + // return m; + // } + + // Compute the high-order mask for the characters in the given string + // private static long highMask(String chars) { + // int n = chars.length(); + // long m = 0; + // for (int i = 0; i < n; i++) { + // char c = chars.charAt(i); + // if ((c >= 64) && (c < 128)) + // m |= (1L << (c - 64)); + // } + // return m; + // } + + + // Character-class masks + + // digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | + // "8" | "9" + private static final long L_DIGIT = 0x3FF000000000000L; // lowMask('0', '9'); + private static final long H_DIGIT = 0L; + + // hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + // "a" | "b" | "c" | "d" | "e" | "f" + private static final long L_HEX = L_DIGIT; + private static final long H_HEX = 0x7E0000007EL; // highMask('A', 'F') | highMask('a', 'f'); + + // upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | + // "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | + // "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" + private static final long L_UPALPHA = 0L; + private static final long H_UPALPHA = 0x7FFFFFEL; // highMask('A', 'Z'); + + // lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | + // "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | + // "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" + private static final long L_LOWALPHA = 0L; + private static final long H_LOWALPHA = 0x7FFFFFE00000000L; // highMask('a', 'z'); + + // alpha = lowalpha | upalpha + private static final long L_ALPHA = L_LOWALPHA | L_UPALPHA; + private static final long H_ALPHA = H_LOWALPHA | H_UPALPHA; + + // alphanum = alpha | digit + private static final long L_ALPHANUM = L_DIGIT | L_ALPHA; + private static final long H_ALPHANUM = H_DIGIT | H_ALPHA; + + // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + // "(" | ")" + private static final long L_MARK = 0x678200000000L; // lowMask("-_.!~*'()"); + private static final long H_MARK = 0x4000000080000000L; // highMask("-_.!~*'()"); + + // unreserved = alphanum | mark + private static final long L_UNRESERVED = L_ALPHANUM | L_MARK; + private static final long H_UNRESERVED = H_ALPHANUM | H_MARK; + + // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + // "$" | "," | "[" | "]" + // Added per RFC2732: "[", "]" + private static final long L_RESERVED = 0xAC00985000000000L; // lowMask(";/?:@&=+$,[]"); + private static final long H_RESERVED = 0x28000001L; // highMask(";/?:@&=+$,[]"); + + // The zero'th bit is used to indicate that escape pairs and non-US-ASCII + // characters are allowed; this is handled by the scanEscape method below. + private static final long L_ESCAPED = 1L; + private static final long H_ESCAPED = 0L; + + // uric = reserved | unreserved | escaped + private static final long L_URIC = L_RESERVED | L_UNRESERVED | L_ESCAPED; + private static final long H_URIC = H_RESERVED | H_UNRESERVED | H_ESCAPED; + + // pchar = unreserved | escaped | + // ":" | "@" | "&" | "=" | "+" | "$" | "," + private static final long L_PCHAR + = L_UNRESERVED | L_ESCAPED | 0x2400185000000000L; // lowMask(":@&=+$,"); + private static final long H_PCHAR + = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask(":@&=+$,"); + + // All valid path characters + private static final long L_PATH = L_PCHAR | 0x800800000000000L; // lowMask(";/"); + private static final long H_PATH = H_PCHAR; // highMask(";/") == 0x0L; + + // Dash, for use in domainlabel and toplabel + private static final long L_DASH = 0x200000000000L; // lowMask("-"); + private static final long H_DASH = 0x0L; // highMask("-"); + + // userinfo = *( unreserved | escaped | + // ";" | ":" | "&" | "=" | "+" | "$" | "," ) + private static final long L_USERINFO + = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask(";:&=+$,"); + private static final long H_USERINFO + = H_UNRESERVED | H_ESCAPED; // | highMask(";:&=+$,") == 0L; + + // reg_name = 1*( unreserved | escaped | "$" | "," | + // ";" | ":" | "@" | "&" | "=" | "+" ) + private static final long L_REG_NAME + = L_UNRESERVED | L_ESCAPED | 0x2C00185000000000L; // lowMask("$,;:@&=+"); + private static final long H_REG_NAME + = H_UNRESERVED | H_ESCAPED | 0x1L; // highMask("$,;:@&=+"); + + // All valid characters for server-based authorities + private static final long L_SERVER + = L_USERINFO | L_ALPHANUM | L_DASH | 0x400400000000000L; // lowMask(".:@[]"); + private static final long H_SERVER + = H_USERINFO | H_ALPHANUM | H_DASH | 0x28000001L; // highMask(".:@[]"); + + // Characters that are encoded in the path component of a URI. + // + // These characters are reserved in the path segment as described in + // RFC2396 section 3.3: + // "=" | ";" | "?" | "/" + // + // These characters are defined as excluded in RFC2396 section 2.4.3 + // and must be escaped if they occur in the data part of a URI: + // "#" | " " | "<" | ">" | "%" | "\"" | "{" | "}" | "|" | "\\" | "^" | + // "[" | "]" | "`" + // + // Also US ASCII control characters 00-1F and 7F. + + // lowMask((char)0, (char)31) | lowMask("=;?/# <>%\"{}|\\^[]`"); + private static final long L_ENCODED = 0xF800802DFFFFFFFFL; + + // highMask((char)0x7F, (char)0x7F) | highMask("=;?/# <>%\"{}|\\^[]`"); + private static final long H_ENCODED = 0xB800000178000000L; + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/GetBooleanAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetBooleanAction.java new file mode 100644 index 000000000..89e1fd3d9 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetBooleanAction.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +import java.security.AccessController; + +/** + * A convenience class for retrieving the boolean value of a system property + * as a privileged action. + * + *

An instance of this class can be used as the argument of + * AccessController.doPrivileged. + * + *

The following code retrieves the boolean value of the system + * property named "prop" as a privileged action: + * + *

+ * boolean b = java.security.AccessController.doPrivileged
+ *              (new GetBooleanAction("prop")).booleanValue();
+ * 
+ * + * @author Roland Schemers + * @see java.security.PrivilegedAction + * @see java.security.AccessController + * @since 1.2 + */ + +public class GetBooleanAction + implements java.security.PrivilegedAction { + private final String theProp; + + /** + * Constructor that takes the name of the system property whose boolean + * value needs to be determined. + * + * @param theProp the name of the system property. + */ + public GetBooleanAction(String theProp) { + this.theProp = theProp; + } + + /** + * Determines the boolean value of the system property whose name was + * specified in the constructor. + * + * @return the Boolean value of the system property. + */ + public Boolean run() { + return Boolean.getBoolean(theProp); + } + + /** + * Convenience method to get a property without going through doPrivileged + * if no security manager is present. This is unsafe for inclusion in a + * public API but allowable here since this class is now encapsulated. + * + * Note that this method performs a privileged action using caller-provided + * inputs. The caller of this method should take care to ensure that the + * inputs are not tainted and the returned property is not made accessible + * to untrusted code if it contains sensitive information. + * + * @param theProp the name of the system property. + */ + @SuppressWarnings("removal") + public static boolean privilegedGetProperty(String theProp) { + if (System.getSecurityManager() == null) { + return Boolean.getBoolean(theProp); + } else { + return AccessController.doPrivileged( + new GetBooleanAction(theProp)); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/GetIntegerAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetIntegerAction.java new file mode 100644 index 000000000..e7f2183f4 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetIntegerAction.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +import java.security.AccessController; + +/** + * A convenience class for retrieving the integer value of a system property + * as a privileged action. + * + *

An instance of this class can be used as the argument of + * AccessController.doPrivileged. + * + *

The following code retrieves the integer value of the system + * property named "prop" as a privileged action. Since it does + * not pass a default value to be used in case the property + * "prop" is not defined, it has to check the result for + * null: + * + *

+ * Integer tmp = java.security.AccessController.doPrivileged
+ *     (new sun.security.action.GetIntegerAction("prop"));
+ * int i;
+ * if (tmp != null) {
+ *     i = tmp.intValue();
+ * }
+ * 
+ * + *

The following code retrieves the integer value of the system + * property named "prop" as a privileged action, and also passes + * a default value to be used in case the property "prop" is not + * defined: + * + *

+ * int i = ((Integer)java.security.AccessController.doPrivileged(
+ *                         new GetIntegerAction("prop", 3))).intValue();
+ * 
+ * + * @author Roland Schemers + * @see java.security.PrivilegedAction + * @see java.security.AccessController + * @since 1.2 + */ + +public class GetIntegerAction + implements java.security.PrivilegedAction { + private final String theProp; + private final int defaultVal; + private final boolean defaultSet; + + /** + * Constructor that takes the name of the system property whose integer + * value needs to be determined. + * + * @param theProp the name of the system property. + */ + public GetIntegerAction(String theProp) { + this.theProp = theProp; + this.defaultVal = 0; + this.defaultSet = false; + } + + /** + * Constructor that takes the name of the system property and the default + * value of that property. + * + * @param theProp the name of the system property. + * @param defaultVal the default value. + */ + public GetIntegerAction(String theProp, int defaultVal) { + this.theProp = theProp; + this.defaultVal = defaultVal; + this.defaultSet = true; + } + + /** + * Determines the integer value of the system property whose name was + * specified in the constructor. + * + *

If there is no property of the specified name, or if the property + * does not have the correct numeric format, then an Integer + * object representing the default value that was specified in the + * constructor is returned, or null if no default value was + * specified. + * + * @return the Integer value of the property. + */ + public Integer run() { + Integer value = Integer.getInteger(theProp); + if ((value == null) && defaultSet) + return defaultVal; + return value; + } + + /** + * Convenience method to get a property without going through doPrivileged + * if no security manager is present. This is unsafe for inclusion in a + * public API but allowable here since this class is now encapsulated. + * + * Note that this method performs a privileged action using caller-provided + * inputs. The caller of this method should take care to ensure that the + * inputs are not tainted and the returned property is not made accessible + * to untrusted code if it contains sensitive information. + * + * @param theProp the name of the system property. + */ + @SuppressWarnings("removal") + public static Integer privilegedGetProperty(String theProp) { + if (System.getSecurityManager() == null) { + return Integer.getInteger(theProp); + } else { + return AccessController.doPrivileged( + new GetIntegerAction(theProp)); + } + } + + /** + * Convenience method to get a property without going through doPrivileged + * if no security manager is present. This is unsafe for inclusion in a + * public API but allowable here since this class is now encapsulated. + * + * Note that this method performs a privileged action using caller-provided + * inputs. The caller of this method should take care to ensure that the + * inputs are not tainted and the returned property is not made accessible + * to untrusted code if it contains sensitive information. + * + * @param theProp the name of the system property. + * @param defaultVal the default value. + */ + @SuppressWarnings("removal") + public static Integer privilegedGetProperty(String theProp, + int defaultVal) { + Integer value; + if (System.getSecurityManager() == null) { + value = Integer.getInteger(theProp); + } else { + value = AccessController.doPrivileged( + new GetIntegerAction(theProp)); + } + return (value != null) ? value : defaultVal; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/GetLongAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetLongAction.java new file mode 100644 index 000000000..0ff1f1bcc --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetLongAction.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +/** + * A convenience class for retrieving the Long value of a system + * property as a privileged action. + * + *

An instance of this class can be used as the argument of + * AccessController.doPrivileged. + * + *

The following code retrieves the Long value of the system + * property named "prop" as a privileged action. Since it does + * not pass a default value to be used in case the property + * "prop" is not defined, it has to check the result for + * null: + * + *

+ * Long tmp = java.security.AccessController.doPrivileged
+ *     (new sun.security.action.GetLongAction("prop"));
+ * long l;
+ * if (tmp != null) {
+ *     l = tmp.longValue();
+ * }
+ * 
+ * + *

The following code retrieves the Long value of the system + * property named "prop" as a privileged action, and also passes + * a default value to be used in case the property "prop" is not + * defined: + * + *

+ * long l = java.security.AccessController.doPrivileged
+ *      (new GetLongAction("prop")).longValue();
+ * 
+ * + * @author Roland Schemers + * @see java.security.PrivilegedAction + * @see java.security.AccessController + * @since 1.2 + */ + +public class GetLongAction implements java.security.PrivilegedAction { + private final String theProp; + private final long defaultVal; + private final boolean defaultSet; + + /** + * Constructor that takes the name of the system property whose + * Long value needs to be determined. + * + * @param theProp the name of the system property. + */ + public GetLongAction(String theProp) { + this.theProp = theProp; + this.defaultVal = 0; + this.defaultSet = false; + } + + /** + * Constructor that takes the name of the system property and the default + * value of that property. + * + * @param theProp the name of the system property. + * @param defaultVal the default value. + */ + public GetLongAction(String theProp, long defaultVal) { + this.theProp = theProp; + this.defaultVal = defaultVal; + this.defaultSet = true; + } + + /** + * Determines the Long value of the system property whose + * name was specified in the constructor. + * + *

If there is no property of the specified name, or if the property + * does not have the correct numeric format, then a Long + * object representing the default value that was specified in the + * constructor is returned, or null if no default value was + * specified. + * + * @return the Long value of the property. + */ + public Long run() { + Long value = Long.getLong(theProp); + if ((value == null) && defaultSet) + return defaultVal; + return value; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/GetPropertyAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetPropertyAction.java new file mode 100644 index 000000000..da2553119 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/GetPropertyAction.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Properties; + +/** + * A convenience class for retrieving the string value of a system + * property as a privileged action. + * + *

An instance of this class can be used as the argument of + * AccessController.doPrivileged. + * + *

The following code retrieves the value of the system + * property named "prop" as a privileged action: + * + *

+ * String s = java.security.AccessController.doPrivileged
+ *                      (new GetPropertyAction("prop"));
+ * 
+ * + * @author Roland Schemers + * @see java.security.PrivilegedAction + * @see java.security.AccessController + * @since 1.2 + */ + +public class GetPropertyAction implements PrivilegedAction { + private final String theProp; + private final String defaultVal; + + /** + * Constructor that takes the name of the system property whose + * string value needs to be determined. + * + * @param theProp the name of the system property. + */ + public GetPropertyAction(String theProp) { + this.theProp = theProp; + this.defaultVal = null; + } + + /** + * Constructor that takes the name of the system property and the default + * value of that property. + * + * @param theProp the name of the system property. + * @param defaultVal the default value. + */ + public GetPropertyAction(String theProp, String defaultVal) { + this.theProp = theProp; + this.defaultVal = defaultVal; + } + + /** + * Determines the string value of the system property whose + * name was specified in the constructor. + * + * @return the string value of the system property, + * or the default value if there is no property with that key. + */ + public String run() { + String value = System.getProperty(theProp); + return (value == null) ? defaultVal : value; + } + + /** + * Convenience method to get a property without going through doPrivileged + * if no security manager is present. This is unsafe for inclusion in a + * public API but allowable here since this class is now encapsulated. + * + * Note that this method performs a privileged action using caller-provided + * inputs. The caller of this method should take care to ensure that the + * inputs are not tainted and the returned property is not made accessible + * to untrusted code if it contains sensitive information. + * + * @param theProp the name of the system property. + */ + @SuppressWarnings("removal") + public static String privilegedGetProperty(String theProp) { + if (System.getSecurityManager() == null) { + return System.getProperty(theProp); + } else { + return AccessController.doPrivileged( + new GetPropertyAction(theProp)); + } + } + + /** + * Convenience method to get a property without going through doPrivileged + * if no security manager is present. This is unsafe for inclusion in a + * public API but allowable here since this class is now encapsulated. + * + * Note that this method performs a privileged action using caller-provided + * inputs. The caller of this method should take care to ensure that the + * inputs are not tainted and the returned property is not made accessible + * to untrusted code if it contains sensitive information. + * + * @param theProp the name of the system property. + * @param defaultVal the default value. + */ + @SuppressWarnings("removal") + public static String privilegedGetProperty(String theProp, + String defaultVal) { + if (System.getSecurityManager() == null) { + return System.getProperty(theProp, defaultVal); + } else { + return AccessController.doPrivileged( + new GetPropertyAction(theProp, defaultVal)); + } + } + + /** + * Convenience method to call System.getProperties without + * having to go through doPrivileged if no security manager is present. + * This is unsafe for inclusion in a public API but allowable here since + * this class is now encapsulated. + * + * Note that this method performs a privileged action, and callers of + * this method should take care to ensure that the returned properties + * are not made accessible to untrusted code since it may contain + * sensitive information. + */ + @SuppressWarnings("removal") + public static Properties privilegedGetProperties() { + if (System.getSecurityManager() == null) { + return System.getProperties(); + } else { + return AccessController.doPrivileged( + new PrivilegedAction() { + public Properties run() { + return System.getProperties(); + } + } + ); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/OpenFileInputStreamAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/OpenFileInputStreamAction.java new file mode 100644 index 000000000..d47ad9631 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/OpenFileInputStreamAction.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2002, 2004, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +import java.io.*; + +import java.security.PrivilegedExceptionAction; + +/** + * A convenience class for opening a FileInputStream as a privileged action. + * + * @author Andreas Sterbenz + */ +public class OpenFileInputStreamAction + implements PrivilegedExceptionAction { + + private final File file; + + public OpenFileInputStreamAction(File file) { + this.file = file; + } + + public OpenFileInputStreamAction(String filename) { + this.file = new File(filename); + } + + public FileInputStream run() throws Exception { + return new FileInputStream(file); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/action/PutAllAction.java b/openjdk/src/main/java/net/tongsuo/sun/security/action/PutAllAction.java new file mode 100644 index 000000000..21611a61e --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/action/PutAllAction.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.action; + +import java.util.Map; + +import java.security.Provider; +import java.security.PrivilegedAction; + +/** + * A convenience PrivilegedAction class for setting the properties of + * a provider. See the SunRsaSign provider for a usage example. + * + * @see sun.security.rsa.SunRsaSign + * @author Andreas Sterbenz + * @since 1.5 + */ +public class PutAllAction implements PrivilegedAction { + + private final Provider provider; + private final Map map; + + public PutAllAction(Provider provider, Map map) { + this.provider = provider; + this.map = map; + } + + public Void run() { + provider.putAll(map); + return null; + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/cert/URICertStoreParameters.java b/openjdk/src/main/java/net/tongsuo/sun/security/cert/URICertStoreParameters.java new file mode 100644 index 000000000..83314a586 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/cert/URICertStoreParameters.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.cert; + +import java.net.URI; +import java.security.cert.CertStoreParameters; + +/** + * Parameters used as input for {@code CertStore} algorithms which use + * information contained in a URI to retrieve certificates and CRLs. + *

+ * This class is used to provide necessary configuration parameters + * through a URI as defined in RFC 5280 to implementations of + * {@code CertStore} algorithms. + *

+ * Concurrent Access + *

+ * Unless otherwise specified, the methods defined in this class are not + * thread-safe. Multiple threads that need to access a single + * object concurrently should synchronize amongst themselves and + * provide the necessary locking. Multiple threads each manipulating + * separate objects need not synchronize. + * + * @since 9 + * @see CertStore + * @see java.net.URI + */ +public final class URICertStoreParameters implements CertStoreParameters { + + /** + * The uri, cannot be null + */ + private final URI uri; + + /* + * Hash code for this parameters. + */ + private int myhash = -1; + + /** + * Creates an instance of {@code URICertStoreParameters} with the + * specified URI. + * + * @param uri the URI which contains configuration information. + * @throws NullPointerException if {@code uri} is null + */ + public URICertStoreParameters(URI uri) { + if (uri == null) { + throw new NullPointerException(); + } + this.uri = uri; + } + + /** + * Returns the URI used to construct this + * {@code URICertStoreParameters} object. + * + * @return the URI. + */ + public URI getURI() { + return uri; + } + + /** + * Returns a copy of this object. Changes to the copy will not affect + * the original and vice versa. + * + * @return the copy + */ + @Override + public URICertStoreParameters clone() { + try { + return new URICertStoreParameters(uri); + } catch (NullPointerException e) { + /* Cannot happen */ + throw new InternalError(e.toString(), e); + } + } + + /** + * Returns a hash code value for this parameters object. + * The hash code is generated using the URI supplied at construction. + * + * @return a hash code value for this parameters. + */ + @Override + public int hashCode() { + if (myhash == -1) { + myhash = uri.hashCode()*7; + } + return myhash; + } + + /** + * Compares the specified object with this parameters object for equality. + * Two URICertStoreParameters are considered equal if the URIs used + * to construct them are equal. + * + * @param p the object to test for equality with this parameters. + * + * @return true if the specified object is equal to this parameters object. + */ + @Override + public boolean equals(Object p) { + if (p == this) { + return true; + } + return p instanceof URICertStoreParameters + && uri.equals(((URICertStoreParameters) p).getURI()); + } + + /** + * Returns a formatted string describing the parameters + * including the URI used to construct this object. + * + * @return a formatted string describing the parameters + */ + @Override + public String toString() { + return "URICertStoreParameters: " + uri.toString(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/jca/JCAUtil.java b/openjdk/src/main/java/net/tongsuo/sun/security/jca/JCAUtil.java new file mode 100644 index 000000000..c89b149ed --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/jca/JCAUtil.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.jca; + +import java.security.SecureRandom; + +/** + * Collection of static utility methods used by the security framework. + * + * @author Andreas Sterbenz + * @since 1.5 + */ +public final class JCAUtil { + + private JCAUtil() { + // no instantiation + } + + // size of the temporary arrays we use. Should fit into the CPU's 1st + // level cache and could be adjusted based on the platform + private static final int ARRAY_SIZE = 4096; + + /** + * Get the size of a temporary buffer array to use in order to be + * cache efficient. totalSize indicates the total amount of data to + * be buffered. Used by the engineUpdate(ByteBuffer) methods. + */ + public static int getTempArraySize(int totalSize) { + return Math.min(ARRAY_SIZE, totalSize); + } + + // cached SecureRandom instance + private static class CachedSecureRandomHolder { + public static SecureRandom instance = new SecureRandom(); + } + + private static volatile SecureRandom def = null; + + /** + * Get a SecureRandom instance. This method should be used by JDK + * internal code in favor of calling "new SecureRandom()". That needs to + * iterate through the provider table to find the default SecureRandom + * implementation, which is fairly inefficient. + */ + public static SecureRandom getSecureRandom() { + return CachedSecureRandomHolder.instance; + } + + // called by sun.security.jca.Providers class when provider list is changed + static void clearDefSecureRandom() { + def = null; + } + + /** + * Get the default SecureRandom instance. This method is the + * optimized version of "new SecureRandom()" which re-uses the default + * SecureRandom impl if the provider table is the same. + */ + public static SecureRandom getDefSecureRandom() { + SecureRandom result = def; + if (result == null) { + synchronized (JCAUtil.class) { + result = def; + if (result == null) { + def = result = new SecureRandom(); + } + } + } + return result; + + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ContentInfo.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ContentInfo.java new file mode 100644 index 000000000..4721f527f --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ContentInfo.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.*; + +import net.tongsuo.sun.security.util.DerEncoder; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; + +/** + * A ContentInfo type, as defined in PKCS#7. + * + * @author Benjamin Renaud + */ + +public class ContentInfo implements DerEncoder { + + // pkcs7 pre-defined content types + public static final ObjectIdentifier PKCS7_OID = + Oid.of(KnownOIDs.PKCS7); + public static final ObjectIdentifier DATA_OID = + Oid.of(KnownOIDs.Data); + public static final ObjectIdentifier SIGNED_DATA_OID = + Oid.of(KnownOIDs.SignedData); + public static final ObjectIdentifier ENVELOPED_DATA_OID = + Oid.of(KnownOIDs.EnvelopedData); + public static final ObjectIdentifier SIGNED_AND_ENVELOPED_DATA_OID = + Oid.of(KnownOIDs.SignedAndEnvelopedData); + public static final ObjectIdentifier DIGESTED_DATA_OID = + Oid.of(KnownOIDs.DigestedData); + public static final ObjectIdentifier ENCRYPTED_DATA_OID = + Oid.of(KnownOIDs.EncryptedData); + + // this is for backwards-compatibility with JDK 1.1.x + public static final ObjectIdentifier OLD_SIGNED_DATA_OID = + Oid.of(KnownOIDs.JDK_OLD_SignedData); + public static final ObjectIdentifier OLD_DATA_OID = + Oid.of(KnownOIDs.JDK_OLD_Data); + + // The ASN.1 systax for the Netscape Certificate Sequence data type is + // defined at: + // http://wp.netscape.com/eng/security/comm4-cert-download.html + public static final ObjectIdentifier NETSCAPE_CERT_SEQUENCE_OID = + Oid.of(KnownOIDs.NETSCAPE_CertSequence); + + // timestamp token (id-ct-TSTInfo) from RFC 3161 + public static final ObjectIdentifier TIMESTAMP_TOKEN_INFO_OID = + Oid.of(KnownOIDs.TimeStampTokenInfo); + + ObjectIdentifier contentType; + DerValue content; // OPTIONAL + + public ContentInfo(ObjectIdentifier contentType, DerValue content) { + this.contentType = contentType; + this.content = content; + } + + /** + * Make a contentInfo of type data. + */ + public ContentInfo(byte[] bytes) { + DerValue octetString = new DerValue(DerValue.tag_OctetString, bytes); + this.contentType = DATA_OID; + this.content = octetString; + } + + /** + * Parses a PKCS#7 content info. + */ + public ContentInfo(DerInputStream derin) + throws IOException { + this(derin, false); + } + + /** + * Parses a PKCS#7 content info. + * + *

This constructor is used only for backwards compatibility with + * PKCS#7 blocks that were generated using JDK1.1.x. + * + * @param derin the ASN.1 encoding of the content info. + * @param oldStyle flag indicating whether the given content info + * is encoded according to JDK1.1.x. + */ + public ContentInfo(DerInputStream derin, boolean oldStyle) + throws IOException { + DerInputStream disType; + DerInputStream disTaggedContent; + DerValue type; + DerValue taggedContent; + DerValue[] typeAndContent; + DerValue[] contents; + + typeAndContent = derin.getSequence(2); + if (typeAndContent.length < 1 || typeAndContent.length > 2) { + throw new ParsingException("Invalid length for ContentInfo"); + } + + // Parse the content type + type = typeAndContent[0]; + disType = new DerInputStream(type.toByteArray()); + contentType = disType.getOID(); + + if (oldStyle) { + // JDK1.1.x-style encoding + if (typeAndContent.length > 1) { // content is OPTIONAL + content = typeAndContent[1]; + } + } else { + // This is the correct, standards-compliant encoding. + // Parse the content (OPTIONAL field). + // Skip the [0] EXPLICIT tag by pretending that the content is the + // one and only element in an implicitly tagged set + if (typeAndContent.length > 1) { // content is OPTIONAL + taggedContent = typeAndContent[1]; + disTaggedContent + = new DerInputStream(taggedContent.toByteArray()); + contents = disTaggedContent.getSet(1, true); + if (contents.length != 1) { + throw new ParsingException("ContentInfo encoding error"); + } + content = contents[0]; + } + } + } + + public DerValue getContent() { + return content; + } + + public ObjectIdentifier getContentType() { + return contentType; + } + + public byte[] getData() throws IOException { + if (contentType.equals((Object) DATA_OID) || + contentType.equals((Object) OLD_DATA_OID) || + contentType.equals((Object) TIMESTAMP_TOKEN_INFO_OID)) { + if (content == null) + return null; + else + return content.getOctetString(); + } + throw new IOException("content type is not DATA: " + contentType); + } + + @Override + public void encode(DerOutputStream out) { + DerOutputStream contentDerCode; + DerOutputStream seq; + + seq = new DerOutputStream(); + seq.putOID(contentType); + + // content is optional, it could be external + if (content != null) { + DerValue taggedContent; + contentDerCode = new DerOutputStream(); + content.encode(contentDerCode); + + // Add the [0] EXPLICIT tag in front of the content encoding + taggedContent = new DerValue((byte)0xA0, + contentDerCode.toByteArray()); + seq.putDerValue(taggedContent); + } + + out.write(DerValue.tag_Sequence, seq); + } + + /** + * Returns a byte array representation of the data held in + * the content field. + */ + public byte[] getContentBytes() throws IOException { + if (content == null) + return null; + + DerValue v = new DerValue(content.toByteArray()); + return v.getOctetString(); + } + + public String toString() { + String out = ""; + + out += "Content Info Sequence\n\tContent type: " + contentType + "\n"; + out += "\tContent: " + content; + return out; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/EncryptedPrivateKeyInfo.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/EncryptedPrivateKeyInfo.java new file mode 100644 index 000000000..bbbb3e974 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/EncryptedPrivateKeyInfo.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.*; + +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.util.DerValue; + +/** + * This class implements the EncryptedPrivateKeyInfo type, + * which is defined in PKCS #8 as follows: + * + *

+ * EncryptedPrivateKeyInfo ::=  SEQUENCE {
+ *     encryptionAlgorithm   AlgorithmIdentifier,
+ *     encryptedData   OCTET STRING }
+ * 
+ * + * @author Jan Luehe + * + */ + +public class EncryptedPrivateKeyInfo { + + // the "encryptionAlgorithm" field + private final AlgorithmId algid; + + // the "encryptedData" field + private final byte[] encryptedData; + + // the ASN.1 encoded contents of this class + private byte[] encoded; + + /** + * Constructs (i.e., parses) an EncryptedPrivateKeyInfo from + * its encoding. + */ + public EncryptedPrivateKeyInfo(byte[] encoded) + throws IOException + { + if (encoded == null) { + throw new IllegalArgumentException("encoding must not be null"); + } + + DerValue val = new DerValue(encoded); + + DerValue[] seq = new DerValue[2]; + + seq[0] = val.data.getDerValue(); + seq[1] = val.data.getDerValue(); + + if (val.data.available() != 0) { + throw new IOException("overrun, bytes = " + val.data.available()); + } + + this.algid = AlgorithmId.parse(seq[0]); + if (seq[0].data.available() != 0) { + throw new IOException("encryptionAlgorithm field overrun"); + } + + this.encryptedData = seq[1].getOctetString(); + if (seq[1].data.available() != 0) + throw new IOException("encryptedData field overrun"); + + this.encoded = encoded.clone(); + } + + /** + * Constructs an EncryptedPrivateKeyInfo from the + * encryption algorithm and the encrypted data. + */ + public EncryptedPrivateKeyInfo(AlgorithmId algid, byte[] encryptedData) { + this.algid = algid; + this.encryptedData = encryptedData.clone(); + } + + /** + * Returns the encryption algorithm. + */ + public AlgorithmId getAlgorithm() { + return this.algid; + } + + /** + * Returns the encrypted data. + */ + public byte[] getEncryptedData() { + return this.encryptedData.clone(); + } + + /** + * Returns the ASN.1 encoding of this class. + */ + public byte[] getEncoded() { + if (this.encoded != null) return this.encoded.clone(); + + DerOutputStream out = new DerOutputStream(); + DerOutputStream tmp = new DerOutputStream(); + + // encode encryption algorithm + algid.encode(tmp); + + // encode encrypted data + tmp.putOctetString(encryptedData); + + // wrap everything into a SEQUENCE + out.write(DerValue.tag_Sequence, tmp); + this.encoded = out.toByteArray(); + + return this.encoded.clone(); + } + + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof EncryptedPrivateKeyInfo)) + return false; + byte[] thisEncrInfo = this.getEncoded(); + byte[] otherEncrInfo + = ((EncryptedPrivateKeyInfo) other).getEncoded(); + + if (thisEncrInfo.length != otherEncrInfo.length) + return false; + for (int i = 0; i < thisEncrInfo.length; i++) + if (thisEncrInfo[i] != otherEncrInfo[i]) + return false; + return true; + } + + /** + * Returns a hashcode for this EncryptedPrivateKeyInfo. + * + * @return a hashcode for this EncryptedPrivateKeyInfo. + */ + public int hashCode() { + int retval = 0; + + for (int i = 0; i < this.encryptedData.length; i++) + retval += this.encryptedData[i] * i; + return retval; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS7.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS7.java new file mode 100644 index 000000000..7078a2a57 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS7.java @@ -0,0 +1,1084 @@ +/* + * Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.*; +import java.math.BigInteger; +import java.net.URI; +import java.util.*; +import java.security.cert.X509Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509CRL; +import java.security.cert.CRLException; +import java.security.cert.CertificateFactory; +import java.security.*; +import java.util.function.Function; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.timestamp.HttpTimestamper; +import net.tongsuo.sun.security.timestamp.TSRequest; +import net.tongsuo.sun.security.timestamp.TSResponse; +import net.tongsuo.sun.security.timestamp.TimestampToken; +import net.tongsuo.sun.security.timestamp.Timestamper; +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.util.SignatureUtil; +import net.tongsuo.sun.security.x509.AccessDescription; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.GeneralName; +import net.tongsuo.sun.security.x509.GeneralNameInterface; +import net.tongsuo.sun.security.x509.URIName; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CRLImpl; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.x509.X509CertInfo; + +/** + * PKCS7 as defined in RSA Laboratories PKCS7 Technical Note. Profile + * Supports only {@code SignedData} ContentInfo + * type, where to the type of data signed is plain Data. + * For signedData, {@code crls}, {@code attributes} and + * PKCS#6 Extended Certificates are not supported. + * + * @author Benjamin Renaud + */ +public class PKCS7 { + + // the ASN.1 members for a signedData (and other) contentTypes + private BigInteger version = null; + private AlgorithmId[] digestAlgorithmIds = null; + private ContentInfo contentInfo = null; + private X509Certificate[] certificates = null; + private X509CRL[] crls = null; + private SignerInfo[] signerInfos = null; + + private boolean oldStyle = false; // Is this JDK1.1.x-style? + + private Principal[] certIssuerNames; + + /* + * Random number generator for creating nonce values + * (Lazy initialization) + */ + private static class SecureRandomHolder { + static final SecureRandom RANDOM; + static { + SecureRandom tmp = null; + try { + tmp = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + // should not happen + } + RANDOM = tmp; + } + } + + /** + * Unmarshals a PKCS7 block from its encoded form, parsing the + * encoded bytes from the InputStream. + * + * @param in an input stream holding at least one PKCS7 block. + * @exception ParsingException on parsing errors. + * @exception IOException on other errors. + */ + public PKCS7(InputStream in) throws IOException { + DataInputStream dis = new DataInputStream(in); + byte[] data = new byte[dis.available()]; + dis.readFully(data); + + parse(new DerInputStream(data)); + } + + /** + * Unmarshals a PKCS7 block from its encoded form, parsing the + * encoded bytes from the DerInputStream. + * + * @param derin a DerInputStream holding at least one PKCS7 block. + * @exception ParsingException on parsing errors. + */ + public PKCS7(DerInputStream derin) throws ParsingException { + parse(derin); + } + + /** + * Unmarshals a PKCS7 block from its encoded form, parsing the + * encoded bytes. + * + * @param bytes the encoded bytes. + * @exception ParsingException on parsing errors. + */ + public PKCS7(byte[] bytes) throws ParsingException { + try { + DerInputStream derin = new DerInputStream(bytes); + parse(derin); + } catch (IOException ioe1) { + ParsingException pe = new ParsingException( + "Unable to parse the encoded bytes"); + pe.initCause(ioe1); + throw pe; + } + } + + /* + * Parses a PKCS#7 block. + */ + private void parse(DerInputStream derin) + throws ParsingException + { + try { + derin.mark(derin.available()); + // try new (i.e., JDK1.2) style + parse(derin, false); + } catch (IOException ioe) { + try { + derin.reset(); + // try old (i.e., JDK1.1.x) style + parse(derin, true); + oldStyle = true; + } catch (IOException ioe1) { + ParsingException pe = new ParsingException( + ioe1.getMessage()); + pe.initCause(ioe); + pe.addSuppressed(ioe1); + throw pe; + } + } + } + + /** + * Parses a PKCS#7 block. + * + * @param derin the ASN.1 encoding of the PKCS#7 block. + * @param oldStyle flag indicating whether the given PKCS#7 block + * is encoded according to JDK1.1.x. + */ + private void parse(DerInputStream derin, boolean oldStyle) + throws IOException + { + ContentInfo block = new ContentInfo(derin, oldStyle); + ObjectIdentifier contentType = block.contentType; + DerValue content = block.getContent(); + + if (contentType.equals((Object) ContentInfo.SIGNED_DATA_OID)) { + parseSignedData(content); + } else if (contentType.equals((Object) ContentInfo.OLD_SIGNED_DATA_OID)) { + // This is for backwards compatibility with JDK 1.1.x + parseOldSignedData(content); + } else if (contentType.equals((Object) ContentInfo.NETSCAPE_CERT_SEQUENCE_OID)){ + parseNetscapeCertChain(content); + contentInfo = block; // Maybe useless, just do not let it be null + } else { + throw new ParsingException("content type " + contentType + + " not supported."); + } + } + + /** + * Construct an initialized PKCS7 block. + * + * @param digestAlgorithmIds the message digest algorithm identifiers. + * @param contentInfo the content information. + * @param certificates an array of X.509 certificates. + * @param crls an array of CRLs + * @param signerInfos an array of signer information. + */ + public PKCS7(AlgorithmId[] digestAlgorithmIds, + ContentInfo contentInfo, + X509Certificate[] certificates, + X509CRL[] crls, + SignerInfo[] signerInfos) { + + version = BigInteger.ONE; + this.digestAlgorithmIds = digestAlgorithmIds; + this.contentInfo = contentInfo; + this.certificates = certificates; + this.crls = crls; + this.signerInfos = signerInfos; + } + + public PKCS7(AlgorithmId[] digestAlgorithmIds, + ContentInfo contentInfo, + X509Certificate[] certificates, + SignerInfo[] signerInfos) { + this(digestAlgorithmIds, contentInfo, certificates, null, signerInfos); + } + + private void parseNetscapeCertChain(DerValue val) throws IOException { + DerInputStream dis = new DerInputStream(val.toByteArray()); + DerValue[] contents = dis.getSequence(2); + certificates = new X509Certificate[contents.length]; + + CertificateFactory certfac = null; + try { + certfac = PKIXInsts.getCertificateFactory("X.509");; + } catch (CertificateException ce) { + // do nothing + } + + for (int i=0; i < contents.length; i++) { + ByteArrayInputStream bais = null; + try { + if (certfac == null) + certificates[i] = new X509CertImpl(contents[i]); + else { + byte[] encoded = contents[i].toByteArray(); + bais = new ByteArrayInputStream(encoded); + certificates[i] = + (X509Certificate)certfac.generateCertificate(bais); + bais.close(); + bais = null; + } + } catch (CertificateException | IOException ce) { + ParsingException pe = new ParsingException(ce.getMessage()); + pe.initCause(ce); + throw pe; + } finally { + if (bais != null) + bais.close(); + } + } + } + + // SignedData ::= SEQUENCE { + // version Version, + // digestAlgorithms DigestAlgorithmIdentifiers, + // contentInfo ContentInfo, + // certificates + // [0] IMPLICIT ExtendedCertificatesAndCertificates + // OPTIONAL, + // crls + // [1] IMPLICIT CertificateRevocationLists OPTIONAL, + // signerInfos SignerInfos } + private void parseSignedData(DerValue val) throws IOException { + DerInputStream dis = val.toDerInputStream(); + + // Version + version = dis.getBigInteger(); + + // digestAlgorithmIds + DerValue[] digestAlgorithmIdVals = dis.getSet(1); + int len = digestAlgorithmIdVals.length; + digestAlgorithmIds = new AlgorithmId[len]; + try { + for (int i = 0; i < len; i++) { + DerValue oid = digestAlgorithmIdVals[i]; + digestAlgorithmIds[i] = AlgorithmId.parse(oid); + } + + } catch (IOException e) { + ParsingException pe = + new ParsingException("Error parsing digest AlgorithmId IDs: " + + e.getMessage()); + pe.initCause(e); + throw pe; + } + // contentInfo + contentInfo = new ContentInfo(dis); + + CertificateFactory certfac = null; + try { + certfac = PKIXInsts.getCertificateFactory("X.509"); + } catch (CertificateException ce) { + // do nothing + } + + /* + * check if certificates (implicit tag) are provided + * (certificates are OPTIONAL) + */ + if ((byte)(dis.peekByte()) == (byte)0xA0) { + DerValue[] certVals = dis.getSet(2, true); + + len = certVals.length; + certificates = new X509Certificate[len]; + int count = 0; + + for (int i = 0; i < len; i++) { + ByteArrayInputStream bais = null; + try { + byte tag = certVals[i].getTag(); + // We only parse the normal certificate. Other types of + // CertificateChoices ignored. + if (tag == DerValue.tag_Sequence) { + if (certfac == null) { + certificates[count] = new X509CertImpl(certVals[i]); + } else { + byte[] encoded = certVals[i].toByteArray(); + bais = new ByteArrayInputStream(encoded); + certificates[count] = + (X509Certificate)certfac.generateCertificate(bais); + bais.close(); + bais = null; + } + count++; + } + } catch (CertificateException | IOException ce) { + ParsingException pe = new ParsingException(ce.getMessage()); + pe.initCause(ce); + throw pe; + } finally { + if (bais != null) + bais.close(); + } + } + if (count != len) { + certificates = Arrays.copyOf(certificates, count); + } + } + + // check if crls (implicit tag) are provided (crls are OPTIONAL) + if ((byte)(dis.peekByte()) == (byte)0xA1) { + DerValue[] crlVals = dis.getSet(1, true); + + len = crlVals.length; + crls = new X509CRL[len]; + + for (int i = 0; i < len; i++) { + ByteArrayInputStream bais = null; + try { + if (certfac == null) + crls[i] = new X509CRLImpl(crlVals[i]); + else { + byte[] encoded = crlVals[i].toByteArray(); + bais = new ByteArrayInputStream(encoded); + crls[i] = (X509CRL) certfac.generateCRL(bais); + bais.close(); + bais = null; + } + } catch (CRLException e) { + ParsingException pe = + new ParsingException(e.getMessage()); + pe.initCause(e); + throw pe; + } finally { + if (bais != null) + bais.close(); + } + } + } + + // signerInfos + DerValue[] signerInfoVals = dis.getSet(1); + + len = signerInfoVals.length; + signerInfos = new SignerInfo[len]; + + for (int i = 0; i < len; i++) { + DerInputStream in = signerInfoVals[i].toDerInputStream(); + signerInfos[i] = new SignerInfo(in); + } + } + + /* + * Parses an old-style SignedData encoding (for backwards + * compatibility with JDK1.1.x). + */ + private void parseOldSignedData(DerValue val) throws IOException { + DerInputStream dis = val.toDerInputStream(); + + // Version + version = dis.getBigInteger(); + + // digestAlgorithmIds + DerValue[] digestAlgorithmIdVals = dis.getSet(1); + int len = digestAlgorithmIdVals.length; + + digestAlgorithmIds = new AlgorithmId[len]; + try { + for (int i = 0; i < len; i++) { + DerValue oid = digestAlgorithmIdVals[i]; + digestAlgorithmIds[i] = AlgorithmId.parse(oid); + } + } catch (IOException e) { + throw new ParsingException("Error parsing digest AlgorithmId IDs"); + } + + // contentInfo + contentInfo = new ContentInfo(dis, true); + + // certificates + CertificateFactory certfac = null; + try { + certfac = PKIXInsts.getCertificateFactory("X.509"); + } catch (CertificateException ce) { + // do nothing + } + DerValue[] certVals = dis.getSet(2); + len = certVals.length; + certificates = new X509Certificate[len]; + + for (int i = 0; i < len; i++) { + ByteArrayInputStream bais = null; + try { + if (certfac == null) + certificates[i] = new X509CertImpl(certVals[i]); + else { + byte[] encoded = certVals[i].toByteArray(); + bais = new ByteArrayInputStream(encoded); + certificates[i] = + (X509Certificate)certfac.generateCertificate(bais); + bais.close(); + bais = null; + } + } catch (CertificateException | IOException ce) { + ParsingException pe = new ParsingException(ce.getMessage()); + pe.initCause(ce); + throw pe; + } finally { + if (bais != null) + bais.close(); + } + } + + // crls are ignored. + dis.getSet(0); + + // signerInfos + DerValue[] signerInfoVals = dis.getSet(1); + len = signerInfoVals.length; + signerInfos = new SignerInfo[len]; + for (int i = 0; i < len; i++) { + DerInputStream in = signerInfoVals[i].toDerInputStream(); + signerInfos[i] = new SignerInfo(in, true); + } + } + + /** + * Encodes the signed data to a DerOutputStream. + * + * @param out the DerOutputStream to write the encoded data to. + * @exception IOException on encoding errors. + */ + public void encodeSignedData(DerOutputStream out) + throws IOException + { + DerOutputStream signedData = new DerOutputStream(); + + // version + signedData.putInteger(version); + + // digestAlgorithmIds + signedData.putOrderedSetOf(DerValue.tag_Set, digestAlgorithmIds); + + // contentInfo + contentInfo.encode(signedData); + + // certificates (optional) + if (certificates != null && certificates.length != 0) { + // cast to X509CertImpl[] since X509CertImpl implements DerEncoder + X509CertImpl[] implCerts = new X509CertImpl[certificates.length]; + for (int i = 0; i < certificates.length; i++) { + if (certificates[i] instanceof X509CertImpl) + implCerts[i] = (X509CertImpl) certificates[i]; + else { + try { + byte[] encoded = certificates[i].getEncoded(); + implCerts[i] = new X509CertImpl(encoded); + } catch (CertificateException ce) { + throw new IOException(ce); + } + } + } + + // Add the certificate set (tagged with [0] IMPLICIT) + // to the signed data + signedData.putOrderedSetOf((byte)0xA0, implCerts); + } + + // CRLs (optional) + if (crls != null && crls.length != 0) { + // cast to X509CRLImpl[] since X509CRLImpl implements DerEncoder + Set implCRLs = new HashSet<>(crls.length); + for (X509CRL crl: crls) { + if (crl instanceof X509CRLImpl) + implCRLs.add((X509CRLImpl) crl); + else { + try { + byte[] encoded = crl.getEncoded(); + implCRLs.add(new X509CRLImpl(encoded)); + } catch (CRLException ce) { + throw new IOException(ce); + } + } + } + + // Add the CRL set (tagged with [1] IMPLICIT) + // to the signed data + signedData.putOrderedSetOf((byte)0xA1, + implCRLs.toArray(new X509CRLImpl[0])); + } + + // signerInfos + signedData.putOrderedSetOf(DerValue.tag_Set, signerInfos); + + // making it a signed data block + DerValue signedDataSeq = new DerValue(DerValue.tag_Sequence, + signedData.toByteArray()); + + // making it a content info sequence + ContentInfo block = new ContentInfo(ContentInfo.SIGNED_DATA_OID, + signedDataSeq); + + // writing out the contentInfo sequence + block.encode(out); + } + + /** + * This verifies a given SignerInfo. + * + * @param info the signer information. + * @param bytes the DER encoded content information. + * + * @exception NoSuchAlgorithmException on unrecognized algorithms. + * @exception SignatureException on signature handling errors. + */ + public SignerInfo verify(SignerInfo info, byte[] bytes) + throws NoSuchAlgorithmException, SignatureException { + return info.verify(this, bytes); + } + + /** + * Returns all signerInfos which self-verify. + * + * @param bytes the DER encoded content information. + * + * @exception NoSuchAlgorithmException on unrecognized algorithms. + * @exception SignatureException on signature handling errors. + */ + public SignerInfo[] verify(byte[] bytes) + throws NoSuchAlgorithmException, SignatureException { + + ArrayList intResult = new ArrayList<>(); + for (int i = 0; i < signerInfos.length; i++) { + + SignerInfo signerInfo = verify(signerInfos[i], bytes); + if (signerInfo != null) { + intResult.add(signerInfo); + } + } + if (!intResult.isEmpty()) { + + SignerInfo[] result = new SignerInfo[intResult.size()]; + return intResult.toArray(result); + } + return null; + } + + /** + * Returns all signerInfos which self-verify. + * + * @exception NoSuchAlgorithmException on unrecognized algorithms. + * @exception SignatureException on signature handling errors. + */ + public SignerInfo[] verify() + throws NoSuchAlgorithmException, SignatureException { + return verify(null); + } + + /** + * Returns the version number of this PKCS7 block. + * @return the version or null if version is not specified + * for the content type. + */ + public BigInteger getVersion() { + return version; + } + + /** + * Returns the message digest algorithms specified in this PKCS7 block. + * @return the array of Digest Algorithms or null if none are specified + * for the content type. + */ + public AlgorithmId[] getDigestAlgorithmIds() { + return digestAlgorithmIds; + } + + /** + * Returns the content information specified in this PKCS7 block. + */ + public ContentInfo getContentInfo() { + return contentInfo; + } + + /** + * Returns the X.509 certificates listed in this PKCS7 block. + * @return a clone of the array of X.509 certificates or null if + * none are specified for the content type. + */ + public X509Certificate[] getCertificates() { + if (certificates != null) + return certificates.clone(); + else + return null; + } + + /** + * Returns the X.509 crls listed in this PKCS7 block. + * @return a clone of the array of X.509 crls or null if none + * are specified for the content type. + */ + public X509CRL[] getCRLs() { + if (crls != null) + return crls.clone(); + else + return null; + } + + /** + * Returns the signer's information specified in this PKCS7 block. + * @return the array of Signer Infos or null if none are specified + * for the content type. + */ + public SignerInfo[] getSignerInfos() { + return signerInfos; + } + + /** + * Returns the X.509 certificate listed in this PKCS7 block + * which has a matching serial number and Issuer name, or + * null if one is not found. + * + * @param serial the serial number of the certificate to retrieve. + * @param issuerName the Distinguished Name of the Issuer. + */ + public X509Certificate getCertificate(BigInteger serial, X500Name issuerName) { + if (certificates != null) { + if (certIssuerNames == null) + populateCertIssuerNames(); + for (int i = 0; i < certificates.length; i++) { + X509Certificate cert = certificates[i]; + BigInteger thisSerial = cert.getSerialNumber(); + if (serial.equals(thisSerial) + && issuerName.equals(certIssuerNames[i])) + { + return cert; + } + } + } + return null; + } + + /** + * Populate array of Issuer DNs from certificates and convert + * each Principal to type X500Name if necessary. + */ + @SuppressWarnings("deprecation") + private void populateCertIssuerNames() { + if (certificates == null) + return; + + certIssuerNames = new Principal[certificates.length]; + for (int i = 0; i < certificates.length; i++) { + X509Certificate cert = certificates[i]; + Principal certIssuerName = cert.getIssuerDN(); + if (!(certIssuerName instanceof X500Name)) { + // must extract the original encoded form of DN for + // subsequent name comparison checks (converting to a + // String and back to an encoded DN could cause the + // types of String attribute values to be changed) + try { + X509CertInfo tbsCert = + new X509CertInfo(cert.getTBSCertificate()); + certIssuerName = tbsCert.getIssuer(); + } catch (Exception e) { + // error generating X500Name object from the cert's + // issuer DN, leave name as is. + } + } + certIssuerNames[i] = certIssuerName; + } + } + + /** + * Returns the PKCS7 block in a printable string form. + */ + public String toString() { + String out = ""; + + out += contentInfo + "\n"; + if (version != null) + out += "PKCS7 :: version: " + Debug.toHexString(version) + "\n"; + if (digestAlgorithmIds != null) { + out += "PKCS7 :: digest AlgorithmIds: \n"; + for (int i = 0; i < digestAlgorithmIds.length; i++) + out += "\t" + digestAlgorithmIds[i] + "\n"; + } + if (certificates != null) { + out += "PKCS7 :: certificates: \n"; + for (int i = 0; i < certificates.length; i++) + out += "\t" + i + ". " + certificates[i] + "\n"; + } + if (crls != null) { + out += "PKCS7 :: crls: \n"; + for (int i = 0; i < crls.length; i++) + out += "\t" + i + ". " + crls[i] + "\n"; + } + if (signerInfos != null) { + out += "PKCS7 :: signer infos: \n"; + for (int i = 0; i < signerInfos.length; i++) + out += ("\t" + i + ". " + signerInfos[i] + "\n"); + } + return out; + } + + /** + * Returns true if this is a JDK1.1.x-style PKCS#7 block, and false + * otherwise. + */ + public boolean isOldStyle() { + return this.oldStyle; + } + + /** + * Generate a PKCS7 data block. + * + * @param sigalg signature algorithm to be used + * @param sigProvider (optional) provider + * @param privateKey signer's private ky + * @param signerChain signer's certificate chain + * @param content the content to sign + * @param internalsf whether the content should be included in output + * @param directsign if the content is signed directly or through authattrs + * @param ts (optional) timestamper + * @return the pkcs7 output in an array + * @throws SignatureException if signing failed + * @throws InvalidKeyException if key cannot be used + * @throws IOException should not happen here, all byte array + * @throws NoSuchAlgorithmException if siglag is bad + */ + public static byte[] generateNewSignedData( + String sigalg, Provider sigProvider, + PrivateKey privateKey, X509Certificate[] signerChain, + byte[] content, boolean internalsf, boolean directsign, + Function ts) + throws SignatureException, InvalidKeyException, IOException, + NoSuchAlgorithmException { + + Signature signer = SignatureUtil.fromKey(sigalg, privateKey, sigProvider); + + AlgorithmId digAlgID = SignatureUtil.getDigestAlgInPkcs7SignerInfo( + signer, sigalg, privateKey, directsign); + AlgorithmId sigAlgID = SignatureUtil.fromSignature(signer, privateKey); + + PKCS9Attributes authAttrs = null; + if (!directsign) { + // MessageDigest + byte[] md = CryptoInsts.getMessageDigest(digAlgID.getName()) + .digest(content); + // CMSAlgorithmProtection (RFC6211) + DerOutputStream derAp = new DerOutputStream(); + DerOutputStream derAlgs = new DerOutputStream(); + digAlgID.encode(derAlgs); + DerOutputStream derSigAlg = new DerOutputStream(); + sigAlgID.encode(derSigAlg); + derAlgs.writeImplicit((byte)0xA1, derSigAlg); + derAp.write(DerValue.tag_Sequence, derAlgs); + authAttrs = new PKCS9Attributes(new PKCS9Attribute[]{ + new PKCS9Attribute(PKCS9Attribute.CONTENT_TYPE_OID, + ContentInfo.DATA_OID), + new PKCS9Attribute(PKCS9Attribute.SIGNING_TIME_OID, + new Date()), + new PKCS9Attribute(PKCS9Attribute.CMS_ALGORITHM_PROTECTION_OID, + derAp.toByteArray()), + new PKCS9Attribute(PKCS9Attribute.MESSAGE_DIGEST_OID, + md) + }); + signer.update(authAttrs.getDerEncoding()); + } else { + signer.update(content); + } + + byte[] signature = signer.sign(); + + return constructToken(signature, signerChain, + internalsf ? content : null, + authAttrs, + ts == null ? null : ts.apply(signature), + digAlgID, + sigAlgID); + } + + /** + * Assemble a PKCS7 token from its components + * @param signature the signature + * @param signerChain the signer's certificate chain + * @param content (optional) encapsulated content + * @param authAttrs (optional) authenticated attributes + * @param unauthAttrs (optional) unauthenticated attributes + * @param digAlgID digest algorithm identifier + * @param encAlgID encryption algorithm identifier + * @return the token in a byte array + * @throws IOException should not happen here, all byte array + */ + private static byte[] constructToken(byte[] signature, + X509Certificate[] signerChain, + byte[] content, + PKCS9Attributes authAttrs, + PKCS9Attributes unauthAttrs, + AlgorithmId digAlgID, + AlgorithmId encAlgID) + throws IOException { + // Create the SignerInfo + X500Name issuerName = + X500Name.asX500Name(signerChain[0].getIssuerX500Principal()); + BigInteger serialNumber = signerChain[0].getSerialNumber(); + SignerInfo signerInfo = new SignerInfo(issuerName, serialNumber, + digAlgID, authAttrs, + encAlgID, + signature, unauthAttrs); + + // Create the PKCS #7 signed data message + SignerInfo[] signerInfos = {signerInfo}; + AlgorithmId[] algorithms = {signerInfo.getDigestAlgorithmId()}; + // Include or exclude content + ContentInfo contentInfo = (content == null) + ? new ContentInfo(ContentInfo.DATA_OID, null) + : new ContentInfo(content); + PKCS7 pkcs7 = new PKCS7(algorithms, contentInfo, + signerChain, signerInfos); + DerOutputStream p7out = new DerOutputStream(); + pkcs7.encodeSignedData(p7out); + + return p7out.toByteArray(); + } + + /** + * Assembles a PKCS #7 signed data message that optionally includes a + * signature timestamp. + * + * @param signature the signature bytes + * @param signerChain the signer's X.509 certificate chain + * @param content the content that is signed; specify null to not include + * it in the PKCS7 data + * @param signatureAlgorithm the name of the signature algorithm + * @param tsaURI the URI of the Timestamping Authority; or null if no + * timestamp is requested + * @param tSAPolicyID the TSAPolicyID of the Timestamping Authority as a + * numerical object identifier; or null if we leave the TSA server + * to choose one. This argument is only used when tsaURI is provided + * @return the bytes of the encoded PKCS #7 signed data message + * @throws NoSuchAlgorithmException The exception is thrown if the signature + * algorithm is unrecognised. + * @throws CertificateException The exception is thrown if an error occurs + * while processing the signer's certificate or the TSA's + * certificate. + * @throws IOException The exception is thrown if an error occurs while + * generating the signature timestamp or while generating the signed + * data message. + */ + public static byte[] generateSignedData(byte[] signature, + X509Certificate[] signerChain, + byte[] content, + String signatureAlgorithm, + URI tsaURI, + String tSAPolicyID, + String tSADigestAlg) + throws CertificateException, IOException, NoSuchAlgorithmException + { + + // Generate the timestamp token + PKCS9Attributes unauthAttrs = null; + if (tsaURI != null) { + // Timestamp the signature + HttpTimestamper tsa = new HttpTimestamper(tsaURI); + byte[] tsToken = generateTimestampToken( + tsa, tSAPolicyID, tSADigestAlg, signature); + + // Insert the timestamp token into the PKCS #7 signer info element + // (as an unsigned attribute) + unauthAttrs = + new PKCS9Attributes(new PKCS9Attribute[]{ + new PKCS9Attribute( + PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_OID, + tsToken)}); + } + + return constructToken(signature, signerChain, content, + null, + unauthAttrs, + AlgorithmId.get(SignatureUtil.extractDigestAlgFromDwithE(signatureAlgorithm)), + AlgorithmId.get(signatureAlgorithm)); + } + + /** + * Examine the certificate for a Subject Information Access extension + * (
RFC 5280). + * The extension's {@code accessMethod} field should contain the object + * identifier defined for timestamping: 1.3.6.1.5.5.7.48.3 and its + * {@code accessLocation} field should contain an HTTP or HTTPS URL. + * + * @param tsaCertificate (optional) X.509 certificate for the TSA. + * @return An HTTP or HTTPS URI or null if none was found. + */ + public static URI getTimestampingURI(X509Certificate tsaCertificate) { + + if (tsaCertificate == null) { + return null; + } + // Parse the extensions + try { + byte[] extensionValue = tsaCertificate.getExtensionValue + (KnownOIDs.SubjectInfoAccess.value()); + if (extensionValue == null) { + return null; + } + DerInputStream der = new DerInputStream(extensionValue); + der = new DerInputStream(der.getOctetString()); + DerValue[] derValue = der.getSequence(5); + AccessDescription description; + GeneralName location; + URIName uri; + for (int i = 0; i < derValue.length; i++) { + description = new AccessDescription(derValue[i]); + if (description.getAccessMethod() + .equals((Object) Oid.of(KnownOIDs.AD_TimeStamping))) { + location = description.getAccessLocation(); + if (location.getType() == GeneralNameInterface.NAME_URI) { + uri = (URIName) location.getName(); + if (uri.getScheme().equalsIgnoreCase("http") || + uri.getScheme().equalsIgnoreCase("https")) { + return uri.getURI(); + } + } + } + } + } catch (IOException ioe) { + // ignore + } + return null; + } + + /** + * Requests, processes and validates a timestamp token from a TSA using + * common defaults. Uses the following defaults in the timestamp request: + * SHA-1 for the hash algorithm, a 64-bit nonce, and request certificate + * set to true. + * + * @param tsa the timestamping authority to use + * @param tSAPolicyID the TSAPolicyID of the Timestamping Authority as a + * numerical object identifier; or null if we leave the TSA server + * to choose one + * @param toBeTimestamped the token that is to be timestamped + * @return the encoded timestamp token + * @throws IOException The exception is thrown if an error occurs while + * communicating with the TSA, or a non-null + * TSAPolicyID is specified in the request but it + * does not match the one in the reply + * @throws CertificateException The exception is thrown if the TSA's + * certificate is not permitted for timestamping. + */ + public static byte[] generateTimestampToken(Timestamper tsa, + String tSAPolicyID, + String tSADigestAlg, + byte[] toBeTimestamped) + throws IOException, CertificateException + { + // Generate a timestamp + MessageDigest messageDigest; + TSRequest tsQuery; + try { + messageDigest = CryptoInsts.getMessageDigest(tSADigestAlg); + tsQuery = new TSRequest(tSAPolicyID, toBeTimestamped, messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + + // Generate a nonce + BigInteger nonce = null; + if (SecureRandomHolder.RANDOM != null) { + nonce = new BigInteger(64, SecureRandomHolder.RANDOM); + tsQuery.setNonce(nonce); + } + tsQuery.requestCertificate(true); + + TSResponse tsReply = tsa.generateTimestamp(tsQuery); + int status = tsReply.getStatusCode(); + // Handle TSP error + if (status != 0 && status != 1) { + throw new IOException("Error generating timestamp: " + + tsReply.getStatusCodeAsText() + " " + + tsReply.getFailureCodeAsText()); + } + + if (tSAPolicyID != null && + !tSAPolicyID.equals(tsReply.getTimestampToken().getPolicyID())) { + throw new IOException("TSAPolicyID changed in " + + "timestamp token"); + } + PKCS7 tsToken = tsReply.getToken(); + + TimestampToken tst = tsReply.getTimestampToken(); + try { + if (!tst.getHashAlgorithm().equals(AlgorithmId.get(tSADigestAlg))) { + throw new IOException("Digest algorithm not " + tSADigestAlg + " in " + + "timestamp token"); + } + } catch (NoSuchAlgorithmException nase) { + throw new IllegalArgumentException(); // should have been caught before + } + if (!MessageDigest.isEqual(tst.getHashedMessage(), + tsQuery.getHashedMessage())) { + throw new IOException("Digest octets changed in timestamp token"); + } + + BigInteger replyNonce = tst.getNonce(); + if (replyNonce == null && nonce != null) { + throw new IOException("Nonce missing in timestamp token"); + } + if (replyNonce != null && !replyNonce.equals(nonce)) { + throw new IOException("Nonce changed in timestamp token"); + } + + // Examine the TSA's certificate (if present) + for (SignerInfo si: tsToken.getSignerInfos()) { + X509Certificate cert = si.getCertificate(tsToken); + if (cert == null) { + // Error, we've already set tsRequestCertificate = true + throw new CertificateException( + "Certificate not included in timestamp token"); + } else { + if (!cert.getCriticalExtensionOIDs().contains( + KnownOIDs.extendedKeyUsage.value())) { + throw new CertificateException( + "Certificate is not valid for timestamping"); + } + List keyPurposes = cert.getExtendedKeyUsage(); + if (keyPurposes == null || + !keyPurposes.contains(KnownOIDs.KP_TimeStamping.value())) { + throw new CertificateException( + "Certificate is not valid for timestamping"); + } + } + } + return tsReply.getEncodedToken(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS8Key.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS8Key.java new file mode 100644 index 000000000..453f652e4 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS8Key.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.jdk.internal.misc.SharedSecretsUtil; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.InternalPrivateKey; +import net.tongsuo.sun.security.x509.AlgorithmId; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyRep; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +/** + * Holds a PKCS#8 key, for example a private key + * + * According to https://tools.ietf.org/html/rfc5958: + * + * OneAsymmetricKey ::= SEQUENCE { + * version Version, + * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + * privateKey PrivateKey, + * attributes [0] Attributes OPTIONAL, + * ..., + * [[2: publicKey [1] PublicKey OPTIONAL ]], + * ... + * } + * + * We support this format but do not parse attributes and publicKey now. + */ +public class PKCS8Key implements PrivateKey, InternalPrivateKey { + + /** use serialVersionUID from JDK 1.1. for interoperability */ + private static final long serialVersionUID = -3836890099307167124L; + + /* The algorithm information (name, parameters, etc). */ + protected AlgorithmId algid; + + /* The key bytes, without the algorithm information */ + protected byte[] key; + + /* The encoded for the key. Created on demand by encode(). */ + protected byte[] encodedKey; + + /* The version for this key */ + private static final int V1 = 0; + private static final int V2 = 1; + + /** + * Default constructor. Constructors in subclasses that create a new key + * from its components require this. These constructors must initialize + * {@link #algid} and {@link #key}. + */ + protected PKCS8Key() { } + + /** + * Another constructor. Constructors in subclasses that create a new key + * from an encoded byte array require this. We do not assign this + * encoding to {@link #encodedKey} directly. + * + * This method is also used by {@link #parseKey} to create a raw key. + */ + protected PKCS8Key(byte[] input) throws InvalidKeyException { + decode(new ByteArrayInputStream(input)); + } + + private void decode(InputStream is) throws InvalidKeyException { + DerValue val = null; + try { + val = new DerValue(is); + if (val.tag != DerValue.tag_Sequence) { + throw new InvalidKeyException("invalid key format"); + } + + int version = val.data.getInteger(); + if (version != V1 && version != V2) { + throw new InvalidKeyException("unknown version: " + version); + } + algid = AlgorithmId.parse(val.data.getDerValue ()); + key = val.data.getOctetString(); + + DerValue next; + if (val.data.available() == 0) { + return; + } + next = val.data.getDerValue(); + if (next.isContextSpecific((byte)0)) { + if (val.data.available() == 0) { + return; + } + next = val.data.getDerValue(); + } + + if (next.isContextSpecific((byte)1)) { + if (version == V1) { + throw new InvalidKeyException("publicKey seen in v1"); + } + if (val.data.available() == 0) { + return; + } + } + throw new InvalidKeyException("Extra bytes"); + } catch (IOException e) { + throw new InvalidKeyException("IOException : " + e.getMessage()); + } finally { + if (val != null) { + val.clear(); + } + } + } + + /** + * Construct PKCS#8 subject public key from a DER encoding. If a + * security provider supports the key algorithm with a specific class, + * a PrivateKey from the provider is returned. Otherwise, a raw + * PKCS8Key object is returned. + * + *

This mechanism guarantees that keys (and algorithms) may be + * freely manipulated and transferred, without risk of losing + * information. Also, when a key (or algorithm) needs some special + * handling, that specific need can be accommodated. + * + * @param encoded the DER-encoded SubjectPublicKeyInfo value + * @exception IOException on data format errors + */ + public static PrivateKey parseKey(byte[] encoded) throws IOException { + try { + PKCS8Key rawKey = new PKCS8Key(encoded); + byte[] internal = rawKey.getEncodedInternal(); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(internal); + PrivateKey result = null; + try { + result = CryptoInsts.getKeyFactory(rawKey.algid.getName()) + .generatePrivate(pkcs8KeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + // Ignore and return raw key + result = rawKey; + } finally { + if (result != rawKey) { + rawKey.clear(); + } + SharedSecretsUtil.secSpecClearEncodedKeySpec(pkcs8KeySpec); + } + return result; + } catch (InvalidKeyException e) { + throw new IOException("corrupt private key", e); + } + } + + /** + * Returns the algorithm to be used with this key. + */ + public String getAlgorithm() { + return algid.getName(); + } + + /** + * Returns the algorithm ID to be used with this key. + */ + public AlgorithmId getAlgorithmId () { + return algid; + } + + /** + * Returns the DER-encoded form of the key as a byte array, + * or {@code null} if an encoding error occurs. + */ + public byte[] getEncoded() { + return getEncodedInternal().clone(); + } + + /** + * Returns the format for this key: "PKCS#8" + */ + public String getFormat() { + return "PKCS#8"; + } + + /** + * DER-encodes this key as a byte array stored inside this object + * and return it. + * + * @return the encoding, or null if there is an I/O error. + */ + private synchronized byte[] getEncodedInternal() { + if (encodedKey == null) { + DerOutputStream tmp = new DerOutputStream(); + tmp.putInteger(V1); + algid.encode(tmp); + tmp.putOctetString(key); + DerValue out = DerValue.wrap(DerValue.tag_Sequence, tmp); + encodedKey = out.toByteArray(); + out.clear(); + } + return encodedKey; + } + + protected Object writeReplace() throws ObjectStreamException { + return new KeyRep(KeyRep.Type.PRIVATE, + getAlgorithm(), + getFormat(), + getEncodedInternal()); + } + + /** + * We used to serialize a PKCS8Key as itself (instead of a KeyRep). + */ + private void readObject(ObjectInputStream stream) throws IOException { + try { + decode(stream); + } catch (InvalidKeyException e) { + throw new IOException("deserialized key is invalid: " + + e.getMessage()); + } + } + + /** + * Compares two private keys. This returns false if the object with which + * to compare is not of type Key. + * Otherwise, the encoding of this key object is compared with the + * encoding of the given key object. + * + * @param object the object with which to compare + * @return {@code true} if this key has the same encoding as the + * object argument; {@code false} otherwise. + */ + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof PKCS8Key) { + // time-constant comparison + return MessageDigest.isEqual( + getEncodedInternal(), + ((PKCS8Key)object).getEncodedInternal()); + } else if (object instanceof Key) { + // time-constant comparison + byte[] otherEncoded = ((Key)object).getEncoded(); + try { + return MessageDigest.isEqual( + getEncodedInternal(), + otherEncoded); + } finally { + if (otherEncoded != null) { + Arrays.fill(otherEncoded, (byte) 0); + } + } + } + return false; + } + + /** + * Calculates a hash code value for this object. Objects + * which are equal will also have the same hashcode. + */ + public int hashCode() { + return Arrays.hashCode(getEncodedInternal()); + } + + public void clear() { + if (encodedKey != null) { + Arrays.fill(encodedKey, (byte)0); + } + Arrays.fill(key, (byte)0); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attribute.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attribute.java new file mode 100644 index 000000000..fe763d7fe --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attribute.java @@ -0,0 +1,816 @@ +/* + * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.IOException; +import java.util.Date; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerEncoder; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.x509.CertificateExtensions; + +/** + * Class supporting any PKCS9 attributes. + * Supports DER decoding/encoding and access to attribute values. + * + *

Type/Class Table

+ * The following table shows the correspondence between + * PKCS9 attribute types and value component classes. + * For types not listed here, its name is the OID + * in string form, its value is a (single-valued) + * byte array that is the SET's encoding. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Object IdentifierAttribute NameTypeValue Class
1.2.840.113549.1.9.1EmailAddressMulti-valuedString[]
1.2.840.113549.1.9.2UnstructuredNameMulti-valuedString[]
1.2.840.113549.1.9.3ContentTypeSingle-valuedObjectIdentifier
1.2.840.113549.1.9.4MessageDigestSingle-valuedbyte[]
1.2.840.113549.1.9.5SigningTimeSingle-valuedDate
1.2.840.113549.1.9.6CountersignatureMulti-valuedSignerInfo[]
1.2.840.113549.1.9.7ChallengePasswordSingle-valuedString
1.2.840.113549.1.9.8UnstructuredAddressSingle-valuedString
1.2.840.113549.1.9.9ExtendedCertificateAttributesMulti-valued(not supported)
1.2.840.113549.1.9.10IssuerAndSerialNumberSingle-valued(not supported)
1.2.840.113549.1.9.{11,12}RSA DSI proprietarySingle-valued(not supported)
1.2.840.113549.1.9.13S/MIME unused assignmentSingle-valued(not supported)
1.2.840.113549.1.9.14ExtensionRequestSingle-valuedCertificateExtensions
1.2.840.113549.1.9.15SMIMECapabilitySingle-valued(not supported)
1.2.840.113549.1.9.16.2.12SigningCertificateSingle-valuedSigningCertificateInfo
1.2.840.113549.1.9.16.2.14SignatureTimestampTokenSingle-valuedbyte[]
1.2.840.113549.1.9.16.2.52CMSAlgorithmProtectionSingle-valuedbyte[]
+ * + * @author Douglas Hoover + */ +public class PKCS9Attribute implements DerEncoder { + + /* Are we debugging ? */ + private static final Debug debug = Debug.getInstance("jar"); + + /** + * Array of attribute OIDs defined in PKCS9, by number. + */ + static final ObjectIdentifier[] PKCS9_OIDS = new ObjectIdentifier[19]; + + private static final Class BYTE_ARRAY_CLASS; + + static { + // set unused PKCS9_OIDS entries to null + // rest are initialized with public constants + PKCS9_OIDS[0] = PKCS9_OIDS[11] = PKCS9_OIDS[12] = PKCS9_OIDS[13] = + PKCS9_OIDS[15] = null; + try { + BYTE_ARRAY_CLASS = Class.forName("[B"); + } catch (ClassNotFoundException e) { + throw new ExceptionInInitializerError(e.toString()); + } + } + + public static final ObjectIdentifier EMAIL_ADDRESS_OID = PKCS9_OIDS[1] = + Oid.of(KnownOIDs.EmailAddress); + public static final ObjectIdentifier UNSTRUCTURED_NAME_OID = PKCS9_OIDS[2] = + Oid.of(KnownOIDs.UnstructuredName); + public static final ObjectIdentifier CONTENT_TYPE_OID = PKCS9_OIDS[3] = + Oid.of(KnownOIDs.ContentType); + public static final ObjectIdentifier MESSAGE_DIGEST_OID = PKCS9_OIDS[4] = + Oid.of(KnownOIDs.MessageDigest); + public static final ObjectIdentifier SIGNING_TIME_OID = PKCS9_OIDS[5] = + Oid.of(KnownOIDs.SigningTime); + public static final ObjectIdentifier COUNTERSIGNATURE_OID = PKCS9_OIDS[6] = + Oid.of(KnownOIDs.CounterSignature); + public static final ObjectIdentifier CHALLENGE_PASSWORD_OID = + PKCS9_OIDS[7] = Oid.of(KnownOIDs.ChallengePassword); + public static final ObjectIdentifier UNSTRUCTURED_ADDRESS_OID = + PKCS9_OIDS[8] = Oid.of(KnownOIDs.UnstructuredAddress); + public static final ObjectIdentifier EXTENDED_CERTIFICATE_ATTRIBUTES_OID = + PKCS9_OIDS[9] = + Oid.of(KnownOIDs.ExtendedCertificateAttributes); + public static final ObjectIdentifier ISSUER_SERIALNUMBER_OID = + PKCS9_OIDS[10] = + Oid.of(KnownOIDs.IssuerAndSerialNumber); + // [11], [12] are RSA DSI proprietary + // [13] ==> signingDescription, S/MIME, not used anymore + public static final ObjectIdentifier EXTENSION_REQUEST_OID = + PKCS9_OIDS[14] = Oid.of(KnownOIDs.ExtensionRequest); + public static final ObjectIdentifier SIGNING_CERTIFICATE_OID = + PKCS9_OIDS[16] = Oid.of(KnownOIDs.SigningCertificate); + public static final ObjectIdentifier SIGNATURE_TIMESTAMP_TOKEN_OID = + PKCS9_OIDS[17] = + Oid.of(KnownOIDs.SignatureTimestampToken); + public static final ObjectIdentifier CMS_ALGORITHM_PROTECTION_OID = + PKCS9_OIDS[18] = + Oid.of(KnownOIDs.CMSAlgorithmProtection); + + /** + * Acceptable ASN.1 tags for DER encodings of values of PKCS9 + * attributes, by index in PKCS9_OIDS. + * Sets of acceptable tags are represented as arrays. + */ + private static final Byte[][] PKCS9_VALUE_TAGS = { + null, + {DerValue.tag_IA5String}, // EMailAddress + {DerValue.tag_IA5String, + DerValue.tag_PrintableString, + DerValue.tag_T61String, + DerValue.tag_BMPString, + DerValue.tag_UniversalString, + DerValue.tag_UTF8String}, // UnstructuredName + {DerValue.tag_ObjectId}, // ContentType + {DerValue.tag_OctetString}, // MessageDigest + {DerValue.tag_UtcTime, + DerValue.tag_GeneralizedTime}, // SigningTime + {DerValue.tag_Sequence}, // Countersignature + {DerValue.tag_PrintableString, + DerValue.tag_T61String, + DerValue.tag_BMPString, + DerValue.tag_UniversalString, + DerValue.tag_UTF8String}, // ChallengePassword + {DerValue.tag_PrintableString, + DerValue.tag_T61String, + DerValue.tag_BMPString, + DerValue.tag_UniversalString, + DerValue.tag_UTF8String}, // UnstructuredAddress + {DerValue.tag_SetOf}, // ExtendedCertificateAttributes + {DerValue.tag_Sequence}, // issuerAndSerialNumber + null, + null, + null, + {DerValue.tag_Sequence}, // extensionRequest + {DerValue.tag_Sequence}, // SMIMECapability + {DerValue.tag_Sequence}, // SigningCertificate + {DerValue.tag_Sequence}, // SignatureTimestampToken + {DerValue.tag_Sequence} // CMSAlgorithmProtection + }; + + private static final Class[] VALUE_CLASSES = new Class[19]; + + static { + try { + Class str = Class.forName("[Ljava.lang.String;"); + + VALUE_CLASSES[0] = null; // not used + VALUE_CLASSES[1] = str; // EMailAddress + VALUE_CLASSES[2] = str; // UnstructuredName + VALUE_CLASSES[3] = // ContentType + Class.forName("sun.security.util.ObjectIdentifier"); + VALUE_CLASSES[4] = BYTE_ARRAY_CLASS; // MessageDigest (byte[]) + VALUE_CLASSES[5] = Class.forName("java.util.Date"); // SigningTime + VALUE_CLASSES[6] = // Countersignature + Class.forName("[Lsun.security.pkcs.SignerInfo;"); + VALUE_CLASSES[7] = // ChallengePassword + Class.forName("java.lang.String"); + VALUE_CLASSES[8] = str; // UnstructuredAddress + VALUE_CLASSES[9] = null; // ExtendedCertificateAttributes + VALUE_CLASSES[10] = null; // IssuerAndSerialNumber + VALUE_CLASSES[11] = null; // not used + VALUE_CLASSES[12] = null; // not used + VALUE_CLASSES[13] = null; // not used + VALUE_CLASSES[14] = // ExtensionRequest + Class.forName("net.tongsuo.sun.security.x509.CertificateExtensions"); + VALUE_CLASSES[15] = null; // not supported yet + VALUE_CLASSES[16] = null; // not supported yet + VALUE_CLASSES[17] = BYTE_ARRAY_CLASS; // SignatureTimestampToken + VALUE_CLASSES[18] = BYTE_ARRAY_CLASS; // CMSAlgorithmProtection + } catch (ClassNotFoundException e) { + throw new ExceptionInInitializerError(e.toString()); + } + } + + /** + * Array indicating which PKCS9 attributes are single-valued, + * by index in PKCS9_OIDS. + */ + private static final boolean[] SINGLE_VALUED = { + false, + false, // EMailAddress + false, // UnstructuredName + true, // ContentType + true, // MessageDigest + true, // SigningTime + false, // Countersignature + true, // ChallengePassword + false, // UnstructuredAddress + false, // ExtendedCertificateAttributes + true, // IssuerAndSerialNumber - not supported yet + false, // not used + false, // not used + false, // not used + true, // ExtensionRequest + true, // SMIMECapability - not supported yet + true, // SigningCertificate + true, // SignatureTimestampToken + true, // CMSAlgorithmProtection + }; + + /** + * The OID of this attribute. + */ + private ObjectIdentifier oid; + + /** + * The index of the OID of this attribute in PKCS9_OIDS, + * or -1 if it's unknown. + */ + private int index; + + /** + * Value set of this attribute. Its class is given by + * VALUE_CLASSES[index]. The SET itself + * as byte[] if unknown. + */ + private Object value; + + /** + * Construct an attribute object from the attribute's OID and + * value. If the attribute is single-valued, provide only one + * value. If the attribute is multi-valued, provide an array + * containing all the values. + * Arrays of length zero are accepted, though probably useless. + * + *

The + * table gives the class that value + * must have for a given attribute. + * + * @exception IllegalArgumentException + * if the value has the wrong type. + */ + public PKCS9Attribute(ObjectIdentifier oid, Object value) + throws IllegalArgumentException { + init(oid, value); + } + + private void init(ObjectIdentifier oid, Object value) + throws IllegalArgumentException { + + this.oid = oid; + index = indexOf(oid, PKCS9_OIDS, 1); + Class clazz = index == -1 ? BYTE_ARRAY_CLASS: VALUE_CLASSES[index]; + if (clazz == null) { + throw new IllegalArgumentException( + "No value class supported " + + " for attribute " + oid + + " constructing PKCS9Attribute"); + } + if (!clazz.isInstance(value)) { + throw new IllegalArgumentException( + "Wrong value class " + + " for attribute " + oid + + " constructing PKCS9Attribute; was " + + value.getClass().toString() + ", should be " + + clazz.toString()); + } + this.value = value; + } + + + /** + * Construct a PKCS9Attribute from its encoding on an input + * stream. + * + * @param derVal the DerValue representing the DER encoding of the attribute. + * @exception IOException on parsing error. + */ + public PKCS9Attribute(DerValue derVal) throws IOException { + + DerInputStream derIn = new DerInputStream(derVal.toByteArray()); + DerValue[] val = derIn.getSequence(2); + + if (derIn.available() != 0) + throw new IOException("Excess data parsing PKCS9Attribute"); + + if (val.length != 2) + throw new IOException("PKCS9Attribute doesn't have two components"); + + // get the oid + oid = val[0].getOID(); + byte[] content = val[1].toByteArray(); + DerValue[] elems = new DerInputStream(content).getSet(1); + + index = indexOf(oid, PKCS9_OIDS, 1); + if (index == -1) { + if (debug != null) { + debug.println("Unsupported signer attribute: " + oid); + } + value = content; + return; + } + + // check single valued have only one value + if (SINGLE_VALUED[index] && elems.length > 1) + throwSingleValuedException(); + + // check for illegal element tags + Byte tag; + for (DerValue elem : elems) { + tag = elem.tag; + if (indexOf(tag, PKCS9_VALUE_TAGS[index], 0) == -1) + throwTagException(tag); + } + + switch (index) { + case 1: // email address + case 2: // unstructured name + case 8: // unstructured address + { // open scope + String[] values = new String[elems.length]; + + for (int i=0; i < elems.length; i++) + values[i] = elems[i].getAsString(); + value = values; + } // close scope + break; + + case 3: // content type + value = elems[0].getOID(); + break; + + case 4: // message digest + value = elems[0].getOctetString(); + break; + + case 5: // signing time + byte elemTag = elems[0].getTag(); + DerInputStream dis = new DerInputStream(elems[0].toByteArray()); + value = (elemTag == DerValue.tag_GeneralizedTime) ? + dis.getGeneralizedTime() : dis.getUTCTime(); + break; + + case 6: // countersignature + { // open scope + SignerInfo[] values = new SignerInfo[elems.length]; + for (int i=0; i < elems.length; i++) + values[i] = + new SignerInfo(elems[i].toDerInputStream()); + value = values; + } // close scope + break; + + case 7: // challenge password + value = elems[0].getAsString(); + break; + + case 9: // extended-certificate attribute -- not supported + throw new IOException("PKCS9 extended-certificate " + + "attribute not supported."); + // break unnecessary + case 10: // issuerAndserialNumber attribute -- not supported + throw new IOException("PKCS9 IssuerAndSerialNumber " + + "attribute not supported."); + // break unnecessary + case 11: // RSA DSI proprietary + case 12: // RSA DSI proprietary + throw new IOException("PKCS9 RSA DSI attributes " + + "11 and 12, not supported."); + // break unnecessary + case 13: // S/MIME unused attribute + throw new IOException("PKCS9 attribute #13 not supported."); + // break unnecessary + + case 14: // ExtensionRequest + value = new CertificateExtensions( + new DerInputStream(elems[0].toByteArray())); + break; + + case 15: // SMIME-capability attribute -- not supported + throw new IOException("PKCS9 SMIMECapability " + + "attribute not supported."); + // break unnecessary + case 16: // SigningCertificate attribute + value = new SigningCertificateInfo(elems[0].toByteArray()); + break; + + case 17: // SignatureTimestampToken attribute + case 18: // CMSAlgorithmProtection + value = elems[0].toByteArray(); + break; + + default: // can't happen + } + } + + /** + * Write the DER encoding of this attribute to an output stream. + * + *

N.B.: This method always encodes values of + * ChallengePassword and UnstructuredAddress attributes as ASN.1 + * PrintableStrings, without checking whether they + * should be encoded as T61Strings. + */ + @Override + public void encode(DerOutputStream out) { + DerOutputStream temp = new DerOutputStream(); + temp.putOID(oid); + switch (index) { + case -1: // Unknown + byte[] bytes = (byte[]) value; + temp.write(bytes, 0, bytes.length); + break; + case 1: // email address + case 2: // unstructured name + { // open scope + String[] values = (String[]) value; + DerOutputStream[] temps = new + DerOutputStream[values.length]; + + for (int i=0; i < values.length; i++) { + temps[i] = new DerOutputStream(); + temps[i].putIA5String( values[i]); + } + temp.putOrderedSetOf(DerValue.tag_Set, temps); + } // close scope + break; + + case 3: // content type + { + DerOutputStream temp2 = new DerOutputStream(); + temp2.putOID((ObjectIdentifier) value); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + + case 4: // message digest + { + DerOutputStream temp2 = new DerOutputStream(); + temp2.putOctetString((byte[]) value); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + + case 5: // signing time + { + DerOutputStream temp2 = new DerOutputStream(); + temp2.putUTCTime((Date) value); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + + case 6: // countersignature + temp.putOrderedSetOf(DerValue.tag_Set, (DerEncoder[]) value); + break; + + case 7: // challenge password + { + DerOutputStream temp2 = new DerOutputStream(); + temp2.putPrintableString((String) value); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + + case 8: // unstructured address + { // open scope + String[] values = (String[]) value; + DerOutputStream[] temps = new + DerOutputStream[values.length]; + + for (int i=0; i < values.length; i++) { + temps[i] = new DerOutputStream(); + temps[i].putPrintableString(values[i]); + } + temp.putOrderedSetOf(DerValue.tag_Set, temps); + } // close scope + break; + + case 9: // extended-certificate attribute -- not supported + throw new IllegalArgumentException("PKCS9 extended-certificate " + + "attribute not supported."); + // break unnecessary + case 10: // issuerAndserialNumber attribute -- not supported + throw new IllegalArgumentException("PKCS9 IssuerAndSerialNumber " + + "attribute not supported."); + // break unnecessary + case 11: // RSA DSI proprietary + case 12: // RSA DSI proprietary + throw new IllegalArgumentException("PKCS9 RSA DSI attributes " + + "11 and 12, not supported."); + // break unnecessary + case 13: // S/MIME unused attribute + throw new IllegalArgumentException("PKCS9 attribute #13 not supported."); + // break unnecessary + + case 14: // ExtensionRequest + { + DerOutputStream temp2 = new DerOutputStream(); + CertificateExtensions exts = (CertificateExtensions)value; + exts.encode(temp2, true); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + case 15: // SMIMECapability + throw new IllegalArgumentException("PKCS9 attribute #15 not supported."); + // break unnecessary + + case 16: // SigningCertificate + { + DerOutputStream temp2 = new DerOutputStream(); + SigningCertificateInfo info = (SigningCertificateInfo)value; + byte[] infoBytes = info.toByteArray(); + temp2.write(infoBytes, 0, infoBytes.length); + temp.write(DerValue.tag_Set, temp2.toByteArray()); + } + break; + case 17: // SignatureTimestampToken + case 18: // CMSAlgorithmProtection + temp.write(DerValue.tag_Set, (byte[])value); + break; + + default: // can't happen + } + + out.write(DerValue.tag_Sequence, temp.toByteArray()); + } + + /** + * Returns if the attribute is known. Unknown attributes can be created + * from DER encoding with unknown OIDs. + */ + public boolean isKnown() { + return index != -1; + } + + /** + * Get the value of this attribute. If the attribute is + * single-valued, return just the one value. If the attribute is + * multi-valued, return an array containing all the values. + * It is possible for this array to be of length 0. + * + *

The + * table gives the class of the value returned, + * depending on the type of this attribute. + */ + public Object getValue() { + return value; + } + + /** + * Show whether this attribute is single-valued. + */ + public boolean isSingleValued() { + return index == -1 || SINGLE_VALUED[index]; + } + + /** + * Return the OID of this attribute. + */ + public ObjectIdentifier getOID() { + return oid; + } + + /** + * Return the name of this attribute. + */ + public String getName() { + String n = oid.toString(); + KnownOIDs os = KnownOIDs.findMatch(n); + return os == null ? n : os.stdName(); + } + + /** + * Return the OID for a given attribute name or null if we don't recognize + * the name. + */ + public static ObjectIdentifier getOID(String name) { + KnownOIDs o = KnownOIDs.findMatch(name); + if (o != null) { + return Oid.of(o); + } else { + return null; + } + } + + /** + * Return the attribute name for a given OID or null if we don't recognize + * the oid. + */ + public static String getName(ObjectIdentifier oid) { + return KnownOIDs.findMatch(oid.toString()).stdName(); + } + + /** + * Returns a string representation of this attribute. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(100); + + sb.append("["); + + if (index == -1) { + sb.append(oid.toString()); + } else { + sb.append(getName(oid)); + } + sb.append(": "); + + if (index == -1 || SINGLE_VALUED[index]) { + if (value instanceof byte[]) { // special case for octet string + HexDumpEncoder hexDump = new HexDumpEncoder(); + sb.append(hexDump.encodeBuffer((byte[]) value)); + } else { + sb.append(value.toString()); + } + sb.append("]"); + } else { // multi-valued + boolean first = true; + Object[] values = (Object[]) value; + + for (Object curVal : values) { + if (first) + first = false; + else + sb.append(", "); + sb.append(curVal.toString()); + } + } + return sb.toString(); + } + + /** + * Beginning the search at start, find the first + * index i such that a[i] = obj. + * + * @return the index, if found, and -1 otherwise. + */ + static int indexOf(Object obj, Object[] a, int start) { + for (int i=start; i < a.length; i++) { + if (obj.equals(a[i])) return i; + } + return -1; + } + + /** + * Throw an exception when there are multiple values for + * a single-valued attribute. + */ + private void throwSingleValuedException() throws IOException { + throw new IOException("Single-value attribute " + + oid + " (" + getName() + ")" + + " has multiple values."); + } + + /** + * Throw an exception when the tag on a value encoding is + * wrong for the attribute whose value it is. This method + * will only be called for known tags. + */ + private void throwTagException(Byte tag) + throws IOException { + Byte[] expectedTags = PKCS9_VALUE_TAGS[index]; + StringBuilder msg = new StringBuilder(100); + msg.append("Value of attribute "); + msg.append(oid.toString()); + msg.append(" ("); + msg.append(getName()); + msg.append(") has wrong tag: "); + msg.append(tag.toString()); + msg.append(". Expected tags: "); + + msg.append(expectedTags[0].toString()); + + for (int i = 1; i < expectedTags.length; i++) { + msg.append(", "); + msg.append(expectedTags[i].toString()); + } + msg.append("."); + throw new IOException(msg.toString()); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attributes.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attributes.java new file mode 100644 index 000000000..30bdb144a --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/PKCS9Attributes.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.IOException; +import java.util.Hashtable; + +import net.tongsuo.sun.security.util.DerEncoder; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.ObjectIdentifier; + +/** + * A set of attributes of class PKCS9Attribute. + * + * @author Douglas Hoover + */ +public class PKCS9Attributes { + /** + * Attributes in this set indexed by OID. + */ + private final Hashtable attributes = + new Hashtable<>(3); + + /** + * The keys of this hashtable are the OIDs of permitted attributes. + */ + private final Hashtable permittedAttributes; + + /** + * The DER encoding of this attribute set. The tag byte must be + * DerValue.tag_SetOf. + */ + private final byte[] derEncoding; + + /* + * Contols how attributes, which are not recognized by the PKCS9Attribute + * class, are handled during parsing. + */ + private boolean ignoreUnsupportedAttributes = false; + + /** + * Construct a set of PKCS9 Attributes from its + * DER encoding on a DerInputStream, accepting only attributes + * with OIDs on the given + * list. If the array is null, accept all attributes supported by + * class PKCS9Attribute. + * + * @param permittedAttributes + * Array of attribute OIDs that will be accepted. + * @param in + * the contents of the DER encoding of the attribute set. + * + * @exception IOException + * on i/o error, encoding syntax error, unacceptable or + * unsupported attribute, or duplicate attribute. + * + * @see PKCS9Attribute + */ + public PKCS9Attributes(ObjectIdentifier[] permittedAttributes, + DerInputStream in) throws IOException { + if (permittedAttributes != null) { + this.permittedAttributes = + new Hashtable<>(permittedAttributes.length); + + for (int i = 0; i < permittedAttributes.length; i++) + this.permittedAttributes.put(permittedAttributes[i], + permittedAttributes[i]); + } else { + this.permittedAttributes = null; + } + + // derEncoding initialized in decode() + derEncoding = decode(in); + } + + /** + * Construct a set of PKCS9 Attributes from the contents of its + * DER encoding on a DerInputStream. Accept all attributes + * supported by class PKCS9Attribute and reject any unsupported + * attributes. + * + * @param in the contents of the DER encoding of the attribute set. + * @exception IOException + * on i/o error, encoding syntax error, or unsupported or + * duplicate attribute. + * + * @see PKCS9Attribute + */ + public PKCS9Attributes(DerInputStream in) throws IOException { + this(in, false); + } + + /** + * Construct a set of PKCS9 Attributes from the contents of its + * DER encoding on a DerInputStream. Accept all attributes + * supported by class PKCS9Attribute and ignore any unsupported + * attributes, if directed. + * + * @param in the contents of the DER encoding of the attribute set. + * @param ignoreUnsupportedAttributes If true then any attributes + * not supported by the PKCS9Attribute class are ignored. Otherwise, + * unsupported attributes cause an exception to be thrown. + * @exception IOException + * on i/o error, encoding syntax error, or unsupported or + * duplicate attribute. + * + * @see PKCS9Attribute + */ + public PKCS9Attributes(DerInputStream in, + boolean ignoreUnsupportedAttributes) throws IOException { + + this.ignoreUnsupportedAttributes = ignoreUnsupportedAttributes; + // derEncoding initialized in decode() + derEncoding = decode(in); + permittedAttributes = null; + } + + /** + * Construct a set of PKCS9 Attributes from the given array of + * PKCS9 attributes. + * DER encoding on a DerInputStream. All attributes in + * attribs must be + * supported by class PKCS9Attribute. + * + * @exception IOException + * on i/o error, encoding syntax error, or unsupported or + * duplicate attribute. + * + * @see PKCS9Attribute + */ + public PKCS9Attributes(PKCS9Attribute[] attribs) + throws IllegalArgumentException, IOException { + ObjectIdentifier oid; + for (int i=0; i < attribs.length; i++) { + oid = attribs[i].getOID(); + if (attributes.containsKey(oid)) + throw new IllegalArgumentException( + "PKCSAttribute " + attribs[i].getOID() + + " duplicated while constructing " + + "PKCS9Attributes."); + + attributes.put(oid, attribs[i]); + } + derEncoding = generateDerEncoding(); + permittedAttributes = null; + } + + + /** + * Decode this set of PKCS9 attributes from the contents of its + * DER encoding. Ignores unsupported attributes when directed. + * + * @param in + * the contents of the DER encoding of the attribute set. + * + * @exception IOException + * on i/o error, encoding syntax error, unacceptable or + * unsupported attribute, or duplicate attribute. + */ + private byte[] decode(DerInputStream in) throws IOException { + + DerValue val = in.getDerValue(); + + // save the DER encoding with its proper tag byte. + byte[] derEncoding = val.toByteArray(); + derEncoding[0] = DerValue.tag_SetOf; + + DerInputStream derIn = new DerInputStream(derEncoding); + DerValue[] derVals = derIn.getSet(3,true); + + PKCS9Attribute attrib; + ObjectIdentifier oid; + boolean reuseEncoding = true; + + for (int i=0; i < derVals.length; i++) { + + try { + attrib = new PKCS9Attribute(derVals[i]); + + } catch (ParsingException e) { + if (ignoreUnsupportedAttributes) { + reuseEncoding = false; // cannot reuse supplied DER encoding + continue; // skip + } else { + throw e; + } + } + oid = attrib.getOID(); + + if (attributes.get(oid) != null) + throw new IOException("Duplicate PKCS9 attribute: " + oid); + + if (permittedAttributes != null && + !permittedAttributes.containsKey(oid)) + throw new IOException("Attribute " + oid + + " not permitted in this attribute set"); + + attributes.put(oid, attrib); + } + return reuseEncoding ? derEncoding : generateDerEncoding(); + } + + /** + * Put the DER encoding of this PKCS9 attribute set on an + * DerOutputStream, tagged with the given implicit tag. + * + * @param tag the implicit tag to use in the DER encoding. + * @param out the output stream on which to put the DER encoding. + */ + public void encode(byte tag, DerOutputStream out) { + out.write(tag); + out.write(derEncoding, 1, derEncoding.length -1); + } + + private byte[] generateDerEncoding() { + DerOutputStream out = new DerOutputStream(); + DerEncoder[] attribVals = attributes.values().toArray(new DerEncoder[0]); + out.putOrderedSetOf(DerValue.tag_SetOf, attribVals); + return out.toByteArray(); + } + + /** + * Return the DER encoding of this attribute set, tagged with + * DerValue.tag_SetOf. + */ + public byte[] getDerEncoding() { + return derEncoding.clone(); + + } + + /** + * Get an attribute from this set. + */ + public PKCS9Attribute getAttribute(ObjectIdentifier oid) { + return attributes.get(oid); + } + + /** + * Get an attribute from this set. + */ + public PKCS9Attribute getAttribute(String name) { + return attributes.get(PKCS9Attribute.getOID(name)); + } + + + /** + * Get an array of all attributes in this set, in order of OID. + */ + public PKCS9Attribute[] getAttributes() { + PKCS9Attribute[] attribs = new PKCS9Attribute[attributes.size()]; + + int j = 0; + for (int i=1; i < PKCS9Attribute.PKCS9_OIDS.length && + j < attribs.length; i++) { + if (PKCS9Attribute.PKCS9_OIDS[i] == null) { + continue; + } + attribs[j] = getAttribute(PKCS9Attribute.PKCS9_OIDS[i]); + + if (attribs[j] != null) + j++; + } + return attribs; + } + + /** + * Get an attribute value by OID. + */ + public Object getAttributeValue(ObjectIdentifier oid) + throws IOException { + try { + return getAttribute(oid).getValue(); + } catch (NullPointerException ex) { + throw new IOException("No value found for attribute " + oid); + } + + } + + /** + * Get an attribute value by type name. + */ + public Object getAttributeValue(String name) throws IOException { + ObjectIdentifier oid = PKCS9Attribute.getOID(name); + + if (oid == null) + throw new IOException("Attribute name " + name + + " not recognized or not supported."); + + return getAttributeValue(oid); + } + + + /** + * Returns the PKCS9 block in a printable string form. + */ + public String toString() { + StringBuilder sb = new StringBuilder(200); + sb.append("PKCS9 Attributes: [\n\t"); + + PKCS9Attribute value; + + boolean first = true; + for (int i = 1; i < PKCS9Attribute.PKCS9_OIDS.length; i++) { + if (PKCS9Attribute.PKCS9_OIDS[i] == null) { + continue; + } + value = getAttribute(PKCS9Attribute.PKCS9_OIDS[i]); + + if (value == null) continue; + + // we have a value; print it + if (first) + first = false; + else + sb.append(";\n\t"); + + sb.append(value); + } + + sb.append("\n\t] (end PKCS9 Attributes)"); + + return sb.toString(); + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ParsingException.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ParsingException.java new file mode 100644 index 000000000..9091cd98a --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/ParsingException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * Generic PKCS Parsing exception. + * + * @author Benjamin Renaud + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.IOException; + +public class ParsingException extends IOException { + + private static final long serialVersionUID = -6316569918966181883L; + + public ParsingException() { + super(); + } + + public ParsingException(String s) { + super(s); + } +} + diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SignerInfo.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SignerInfo.java new file mode 100644 index 000000000..19b430e36 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SignerInfo.java @@ -0,0 +1,756 @@ +/* + * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertPath; +import java.security.cert.X509Certificate; +import java.security.*; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.timestamp.TimestampToken; +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerEncoder; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.DisabledAlgorithmConstraints; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.util.SignatureUtil; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.KeyUsageExtension; +import net.tongsuo.sun.security.x509.X500Name; + +/** + * A SignerInfo, as defined in PKCS#7's signedData type. + * + * @author Benjamin Renaud + */ +public class SignerInfo implements DerEncoder { + + private static final DisabledAlgorithmConstraints JAR_DISABLED_CHECK = + DisabledAlgorithmConstraints.jarConstraints(); + + BigInteger version; + X500Name issuerName; + BigInteger certificateSerialNumber; + AlgorithmId digestAlgorithmId; + AlgorithmId digestEncryptionAlgorithmId; + byte[] encryptedDigest; + Timestamp timestamp; + private boolean hasTimestamp = true; + private static final Debug debug = Debug.getInstance("jar"); + + PKCS9Attributes authenticatedAttributes; + PKCS9Attributes unauthenticatedAttributes; + + /** + * A map containing the algorithms in this SignerInfo. This is used to + * avoid checking algorithms to see if they are disabled more than once. + * The key is the AlgorithmId of the algorithm, and the value is a record + * containing the name of the field or attribute and whether the key + * should also be checked (ex: if it is a signature algorithm). + */ + private static class AlgorithmInfo { + + private final String field; + private final boolean checkKey; + + AlgorithmInfo(String field, boolean checkKey) { + this.field = field; + this.checkKey = checkKey; + } + } + private final Map algorithms = new HashMap<>(); + + public SignerInfo(X500Name issuerName, + BigInteger serial, + AlgorithmId digestAlgorithmId, + AlgorithmId digestEncryptionAlgorithmId, + byte[] encryptedDigest) { + this(issuerName, serial, digestAlgorithmId, null, + digestEncryptionAlgorithmId, encryptedDigest, null); + } + + public SignerInfo(X500Name issuerName, + BigInteger serial, + AlgorithmId digestAlgorithmId, + PKCS9Attributes authenticatedAttributes, + AlgorithmId digestEncryptionAlgorithmId, + byte[] encryptedDigest, + PKCS9Attributes unauthenticatedAttributes) { + this.version = BigInteger.ONE; + this.issuerName = issuerName; + this.certificateSerialNumber = serial; + this.digestAlgorithmId = digestAlgorithmId; + this.authenticatedAttributes = authenticatedAttributes; + this.digestEncryptionAlgorithmId = digestEncryptionAlgorithmId; + this.encryptedDigest = encryptedDigest; + this.unauthenticatedAttributes = unauthenticatedAttributes; + } + + /** + * Parses a PKCS#7 signer info. + */ + public SignerInfo(DerInputStream derin) throws IOException { + this(derin, false); + } + + /** + * Parses a PKCS#7 signer info. + * + *

This constructor is used only for backwards compatibility with + * PKCS#7 blocks that were generated using JDK1.1.x. + * + * @param derin the ASN.1 encoding of the signer info. + * @param oldStyle flag indicating whether the given signer info + * is encoded according to JDK1.1.x. + */ + public SignerInfo(DerInputStream derin, boolean oldStyle) + throws IOException { + // version + version = derin.getBigInteger(); + + // issuerAndSerialNumber + DerValue[] issuerAndSerialNumber = derin.getSequence(2); + if (issuerAndSerialNumber.length != 2) { + throw new ParsingException("Invalid length for IssuerAndSerialNumber"); + } + byte[] issuerBytes = issuerAndSerialNumber[0].toByteArray(); + issuerName = new X500Name(new DerValue(DerValue.tag_Sequence, + issuerBytes)); + certificateSerialNumber = issuerAndSerialNumber[1].getBigInteger(); + + // digestAlgorithmId + DerValue tmp = derin.getDerValue(); + + digestAlgorithmId = AlgorithmId.parse(tmp); + + // authenticatedAttributes + if (oldStyle) { + // In JDK1.1.x, the authenticatedAttributes are always present, + // encoded as an empty Set (Set of length zero) + derin.getSet(0); + } else { + // check if set of auth attributes (implicit tag) is provided + // (auth attributes are OPTIONAL) + if ((byte)(derin.peekByte()) == (byte)0xA0) { + authenticatedAttributes = new PKCS9Attributes(derin); + } + } + + // digestEncryptionAlgorithmId - little RSA naming scheme - + // signature == encryption... + tmp = derin.getDerValue(); + + digestEncryptionAlgorithmId = AlgorithmId.parse(tmp); + + // encryptedDigest + encryptedDigest = derin.getOctetString(); + + // unauthenticatedAttributes + if (oldStyle) { + // In JDK1.1.x, the unauthenticatedAttributes are always present, + // encoded as an empty Set (Set of length zero) + derin.getSet(0); + } else { + // check if set of unauth attributes (implicit tag) is provided + // (unauth attributes are OPTIONAL) + if (derin.available() != 0 + && (byte)(derin.peekByte()) == (byte)0xA1) { + unauthenticatedAttributes = + new PKCS9Attributes(derin, true);// ignore unsupported attrs + } + } + + // all done + if (derin.available() != 0) { + throw new ParsingException("extra data at the end"); + } + + // verify CMSAlgorithmProtection + checkCMSAlgorithmProtection(); + } + + // CMSAlgorithmProtection verification as described in RFC 6211 + private void checkCMSAlgorithmProtection() throws IOException { + if (authenticatedAttributes == null) { + return; + } + PKCS9Attribute ap = authenticatedAttributes.getAttribute( + PKCS9Attribute.CMS_ALGORITHM_PROTECTION_OID); + if (ap == null) { + return; + } + DerValue dv = new DerValue((byte[])ap.getValue()); + DerInputStream data = dv.getData(); + AlgorithmId d = AlgorithmId.parse(data.getDerValue()); + DerValue ds = data.getDerValue(); + if (data.available() > 0) { + throw new IOException("Unknown field in CMSAlgorithmProtection"); + } + if (!ds.isContextSpecific((byte)1)) { + throw new IOException("No signature algorithm in CMSAlgorithmProtection"); + } + AlgorithmId s = AlgorithmId.parse(ds.withTag(DerValue.tag_Sequence)); + if (!s.equals(digestEncryptionAlgorithmId) + || !d.equals(digestAlgorithmId)) { + throw new IOException("CMSAlgorithmProtection check failed"); + } + } + + /** + * DER encode this object onto an output stream. + * Implements the {@code DerEncoder} interface. + * + * @param out + * the output stream on which to write the DER encoding. + */ + @Override + public void encode(DerOutputStream out) { + DerOutputStream seq = new DerOutputStream(); + seq.putInteger(version); + DerOutputStream issuerAndSerialNumber = new DerOutputStream(); + issuerName.encode(issuerAndSerialNumber); + issuerAndSerialNumber.putInteger(certificateSerialNumber); + seq.write(DerValue.tag_Sequence, issuerAndSerialNumber); + + digestAlgorithmId.encode(seq); + + // encode authenticated attributes if there are any + if (authenticatedAttributes != null) + authenticatedAttributes.encode((byte)0xA0, seq); + + digestEncryptionAlgorithmId.encode(seq); + + seq.putOctetString(encryptedDigest); + + // encode unauthenticated attributes if there are any + if (unauthenticatedAttributes != null) + unauthenticatedAttributes.encode((byte)0xA1, seq); + + out.write(DerValue.tag_Sequence, seq); + } + + /* + * Returns the (user) certificate pertaining to this SignerInfo. + */ + public X509Certificate getCertificate(PKCS7 block) + throws IOException + { + return block.getCertificate(certificateSerialNumber, issuerName); + } + + /* + * Returns the certificate chain pertaining to this SignerInfo. + */ + public ArrayList getCertificateChain(PKCS7 block) + throws IOException + { + X509Certificate userCert; + userCert = block.getCertificate(certificateSerialNumber, issuerName); + if (userCert == null) + return null; + + ArrayList certList = new ArrayList<>(); + certList.add(userCert); + + X509Certificate[] pkcsCerts = block.getCertificates(); + if (pkcsCerts == null + || userCert.getSubjectX500Principal().equals(userCert.getIssuerX500Principal())) { + return certList; + } + + Principal issuer = userCert.getIssuerX500Principal(); + int start = 0; + while (true) { + boolean match = false; + int i = start; + while (i < pkcsCerts.length) { + if (issuer.equals(pkcsCerts[i].getSubjectX500Principal())) { + // next cert in chain found + certList.add(pkcsCerts[i]); + // if selected cert is self-signed, we're done + // constructing the chain + if (pkcsCerts[i].getSubjectX500Principal().equals( + pkcsCerts[i].getIssuerX500Principal())) { + start = pkcsCerts.length; + } else { + issuer = pkcsCerts[i].getIssuerX500Principal(); + X509Certificate tmpCert = pkcsCerts[start]; + pkcsCerts[start] = pkcsCerts[i]; + pkcsCerts[i] = tmpCert; + start++; + } + match = true; + break; + } else { + i++; + } + } + if (!match) + break; + } + + return certList; + } + + /* Returns null if verify fails, this signerInfo if + verify succeeds. */ + SignerInfo verify(PKCS7 block, byte[] data) + throws NoSuchAlgorithmException, SignatureException { + + try { + Timestamp timestamp = null; + try { + timestamp = getTimestamp(); + } catch (Exception e) { + // Log exception and continue. This allows for the case + // where, if there are no other errors, the code is + // signed but w/o a timestamp. + if (debug != null) { + debug.println("Unexpected exception while getting" + + " timestamp: " + e); + } + } + + ContentInfo content = block.getContentInfo(); + if (data == null) { + data = content.getContentBytes(); + } + + String digestAlgName = digestAlgorithmId.getName(); + algorithms.put(digestAlgorithmId, + new AlgorithmInfo("SignerInfo digestAlgorithm field", false)); + + byte[] dataSigned; + + // if there are authenticated attributes, get the message + // digest and compare it with the digest of data + if (authenticatedAttributes == null) { + dataSigned = data; + } else { + + // first, check content type + ObjectIdentifier contentType = (ObjectIdentifier) + authenticatedAttributes.getAttributeValue( + PKCS9Attribute.CONTENT_TYPE_OID); + if (contentType == null || + !contentType.equals((Object) content.contentType)) + return null; // contentType does not match, bad SignerInfo + + // now, check message digest + byte[] messageDigest = (byte[]) + authenticatedAttributes.getAttributeValue( + PKCS9Attribute.MESSAGE_DIGEST_OID); + + if (messageDigest == null) // fail if there is no message digest + return null; + + MessageDigest md = CryptoInsts.getMessageDigest(digestAlgName); + byte[] computedMessageDigest = md.digest(data); + + if (!MessageDigest.isEqual(messageDigest, computedMessageDigest)) { + return null; + } + + // message digest attribute matched + // digest of original data + + // the data actually signed is the DER encoding of + // the authenticated attributes (tagged with + // the "SET OF" tag, not 0xA0). + dataSigned = authenticatedAttributes.getDerEncoding(); + } + + // put together digest algorithm and encryption algorithm + // to form signing algorithm. See makeSigAlg for details. + String sigAlgName = makeSigAlg( + digestAlgorithmId, + digestEncryptionAlgorithmId, + authenticatedAttributes == null); + + KnownOIDs oid = KnownOIDs.findMatch(sigAlgName); + if (oid != null) { + AlgorithmId sigAlgId = + new AlgorithmId(Oid.of(oid), + digestEncryptionAlgorithmId.getParameters()); + algorithms.put(sigAlgId, + new AlgorithmInfo( + "SignerInfo digestEncryptionAlgorithm field", true)); + } + + X509Certificate cert = getCertificate(block); + if (cert == null) { + return null; + } + PublicKey key = cert.getPublicKey(); + + if (cert.hasUnsupportedCriticalExtension()) { + throw new SignatureException("Certificate has unsupported " + + "critical extension(s)"); + } + + // Make sure that if the usage of the key in the certificate is + // restricted, it can be used for digital signatures. + // XXX We may want to check for additional extensions in the + // future. + boolean[] keyUsageBits = cert.getKeyUsage(); + if (keyUsageBits != null) { + KeyUsageExtension keyUsage; + // We don't care whether this extension was marked + // critical in the certificate. + // We're interested only in its value (i.e., the bits set) + // and treat the extension as critical. + keyUsage = new KeyUsageExtension(keyUsageBits); + + boolean digSigAllowed + = keyUsage.get(KeyUsageExtension.DIGITAL_SIGNATURE); + + boolean nonRepuAllowed + = keyUsage.get(KeyUsageExtension.NON_REPUDIATION); + + if (!digSigAllowed && !nonRepuAllowed) { + throw new SignatureException("Key usage restricted: " + + "cannot be used for " + + "digital signatures"); + } + } + + Signature sig = CryptoInsts.getSignature(sigAlgName); + + AlgorithmParameters ap = + digestEncryptionAlgorithmId.getParameters(); + try { + SignatureUtil.initVerifyWithParam(sig, key, + SignatureUtil.getParamSpec(sigAlgName, ap)); + } catch (ProviderException | InvalidAlgorithmParameterException | + InvalidKeyException e) { + throw new SignatureException(e.getMessage(), e); + } + + sig.update(dataSigned); + if (sig.verify(encryptedDigest)) { + return this; + } + } catch (IOException e) { + throw new SignatureException("Error verifying signature", e); + } + return null; + } + + /** + * Derives the signature algorithm name from the digest algorithm + * and the encryption algorithm inside a PKCS7 SignerInfo. + * + * The digest algorithm is in the form "DIG", and the encryption + * algorithm can be in any of the 3 forms: + * + * 1. Old style key algorithm like RSA, DSA, EC, this method returns + * DIGwithKEY. + * 2. New style signature algorithm in the form of HASHwithKEY, this + * method returns DIGwithKEY. Please note this is not HASHwithKEY. + * 3. Modern signature algorithm like RSASSA-PSS and EdDSA, this method + * returns the signature algorithm itself but ensures digAlgId is + * compatible with the algorithm as described in RFC 4056 and 8419. + * + * @param digAlgId the digest algorithm + * @param encAlgId the encryption algorithm + * @param directSign whether the signature is calculated on the content + * directly. This makes difference for Ed448. + */ + public static String makeSigAlg(AlgorithmId digAlgId, AlgorithmId encAlgId, + boolean directSign) throws NoSuchAlgorithmException { + String encAlg = encAlgId.getName(); + switch (encAlg) { + case "RSASSA-PSS": + PSSParameterSpec spec = (PSSParameterSpec) + SignatureUtil.getParamSpec(encAlg, encAlgId.getParameters()); + /* + * RFC 4056 section 3 for Signed-data: + * signatureAlgorithm MUST contain id-RSASSA-PSS. The algorithm + * parameters field MUST contain RSASSA-PSS-params. + */ + if (spec == null) { + throw new NoSuchAlgorithmException("Missing PSSParameterSpec for RSASSA-PSS algorithm"); + } + + if (!AlgorithmId.get(spec.getDigestAlgorithm()).equals(digAlgId)) { + throw new NoSuchAlgorithmException("Incompatible digest algorithm"); + } + return encAlg; +// case "Ed25519": +// if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.sha512)) { +// throw new NoSuchAlgorithmException("Incompatible digest algorithm"); +// } +// return encAlg; +// case "Ed448": +// if (directSign) { +// if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256)) { +// throw new NoSuchAlgorithmException("Incompatible digest algorithm"); +// } +// } else { +// if (!digAlgId.equals(SignatureUtil.EdDSADigestAlgHolder.shake256$512)) { +// throw new NoSuchAlgorithmException("Incompatible digest algorithm"); +// } +// } +// return encAlg; + default: + String digAlg = digAlgId.getName(); + String keyAlg = SignatureUtil.extractKeyAlgFromDwithE(encAlg); + if (keyAlg == null) { + // The encAlg used to be only the key alg + keyAlg = encAlg; + } + if (digAlg.startsWith("SHA-")) { + digAlg = "SHA" + digAlg.substring(4); + } + if (keyAlg.equals("EC")) keyAlg = "ECDSA"; + String sigAlg = digAlg + "with" + keyAlg; + try { + Signature.getInstance(sigAlg); + return sigAlg; + } catch (NoSuchAlgorithmException e) { + // Possibly an unknown modern signature algorithm, + // in this case, encAlg should already be a signature + // algorithm. + return encAlg; + } + } + } + + /* Verify the content of the pkcs7 block. */ + SignerInfo verify(PKCS7 block) + throws NoSuchAlgorithmException, SignatureException { + return verify(block, null); + } + + public BigInteger getVersion() { + return version; + } + + public X500Name getIssuerName() { + return issuerName; + } + + public BigInteger getCertificateSerialNumber() { + return certificateSerialNumber; + } + + public AlgorithmId getDigestAlgorithmId() { + return digestAlgorithmId; + } + + public PKCS9Attributes getAuthenticatedAttributes() { + return authenticatedAttributes; + } + + public AlgorithmId getDigestEncryptionAlgorithmId() { + return digestEncryptionAlgorithmId; + } + + public byte[] getEncryptedDigest() { + return encryptedDigest; + } + + public PKCS9Attributes getUnauthenticatedAttributes() { + return unauthenticatedAttributes; + } + + /** + * Returns the timestamp PKCS7 data unverified. + * @return a PKCS7 object + */ + public PKCS7 getTsToken() throws IOException { + if (unauthenticatedAttributes == null) { + return null; + } + PKCS9Attribute tsTokenAttr = + unauthenticatedAttributes.getAttribute( + PKCS9Attribute.SIGNATURE_TIMESTAMP_TOKEN_OID); + if (tsTokenAttr == null) { + return null; + } + return new PKCS7((byte[])tsTokenAttr.getValue()); + } + + /* + * Extracts a timestamp from a PKCS7 SignerInfo. + * + * Examines the signer's unsigned attributes for a + * {@code signatureTimestampToken} attribute. If present, + * then it is parsed to extract the date and time at which the + * timestamp was generated. + * + * @param info A signer information element of a PKCS 7 block. + * + * @return A timestamp token or null if none is present. + * @throws IOException if an error is encountered while parsing the + * PKCS7 data. + * @throws NoSuchAlgorithmException if an error is encountered while + * verifying the PKCS7 object. + * @throws SignatureException if an error is encountered while + * verifying the PKCS7 object. + * @throws CertificateException if an error is encountered while generating + * the TSA's certpath. + */ + public Timestamp getTimestamp() + throws IOException, NoSuchAlgorithmException, SignatureException, + CertificateException + { + if (timestamp != null || !hasTimestamp) + return timestamp; + + PKCS7 tsToken = getTsToken(); + if (tsToken == null) { + hasTimestamp = false; + return null; + } + + // Extract the content (an encoded timestamp token info) + byte[] encTsTokenInfo = tsToken.getContentInfo().getData(); + // Extract the signer (the Timestamping Authority) + // while verifying the content + SignerInfo[] tsa = tsToken.verify(encTsTokenInfo); + if (tsa == null || tsa.length == 0) { + throw new SignatureException("Unable to verify timestamp"); + } + // Expect only one signer + ArrayList chain = tsa[0].getCertificateChain(tsToken); + CertificateFactory cf = PKIXInsts.getCertificateFactory("X.509"); + CertPath tsaChain = cf.generateCertPath(chain); + // Create a timestamp token info object + TimestampToken tsTokenInfo = new TimestampToken(encTsTokenInfo); + // Check that the signature timestamp applies to this signature + verifyTimestamp(tsTokenInfo); + algorithms.putAll(tsa[0].algorithms); + // Create a timestamp object + timestamp = new Timestamp(tsTokenInfo.getDate(), tsaChain); + return timestamp; + } + + /* + * Check that the signature timestamp applies to this signature. + * Match the hash present in the signature timestamp token against the hash + * of this signature. + */ + private void verifyTimestamp(TimestampToken token) + throws NoSuchAlgorithmException, SignatureException { + + AlgorithmId digestAlgId = token.getHashAlgorithm(); + algorithms.put(digestAlgId, + new AlgorithmInfo("TimestampToken digestAlgorithm field", false)); + + MessageDigest md = CryptoInsts.getMessageDigest(digestAlgId.getName()); + + if (!MessageDigest.isEqual(token.getHashedMessage(), + md.digest(encryptedDigest))) { + + throw new SignatureException("Signature timestamp (#" + + token.getSerialNumber() + ") generated on " + token.getDate() + + " is inapplicable"); + } + + if (debug != null) { + debug.println(); + debug.println("Detected signature timestamp (#" + + token.getSerialNumber() + ") generated on " + token.getDate()); + debug.println(); + } + } + + public String toString() { + HexDumpEncoder hexDump = new HexDumpEncoder(); + + String out = ""; + + out += "Signer Info for (issuer): " + issuerName + "\n"; + out += "\tversion: " + Debug.toHexString(version) + "\n"; + out += "\tcertificateSerialNumber: " + + Debug.toHexString(certificateSerialNumber) + "\n"; + out += "\tdigestAlgorithmId: " + digestAlgorithmId + "\n"; + if (authenticatedAttributes != null) { + out += "\tauthenticatedAttributes: " + authenticatedAttributes + + "\n"; + } + out += "\tdigestEncryptionAlgorithmId: " + digestEncryptionAlgorithmId + + "\n"; + + out += "\tencryptedDigest: " + "\n" + + hexDump.encodeBuffer(encryptedDigest) + "\n"; + if (unauthenticatedAttributes != null) { + out += "\tunauthenticatedAttributes: " + + unauthenticatedAttributes + "\n"; + } + return out; + } + + // sun.security.util.SignatureFileVerifier invokes this method + // from sun.security.pkcs.SignerInfo, but not here. +// /** +// * Verify all the algorithms in the array of SignerInfos against the +// * constraints in the jdk.jar.disabledAlgorithms security property. +// * +// * @param infos array of SignerInfos +// * @param params constraint parameters +// * @param name the name of the signer's PKCS7 file +// * @return a set of algorithms that passed the checks and are not disabled +// */ +// public static Set verifyAlgorithms(SignerInfo[] infos, +// JarConstraintsParameters params, String name) throws SignatureException { +// Map algorithms = new HashMap<>(); +// for (SignerInfo info : infos) { +// algorithms.putAll(info.algorithms); +// } +// +// Set enabledAlgorithms = new HashSet<>(); +// try { +// for (Map.Entry algEntry +// : algorithms.entrySet()) { +// AlgorithmInfo info = algEntry.getValue(); +// params.setExtendedExceptionMsg(name, info.field); +// AlgorithmId algId = algEntry.getKey(); +// JAR_DISABLED_CHECK.permits(algId.getName(), +// algId.getParameters(), params, info.checkKey); +// enabledAlgorithms.add(algId.getName()); +// } +// } catch (CertPathValidatorException e) { +// throw new SignatureException(e); +// } +// return enabledAlgorithms; +// } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SigningCertificateInfo.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SigningCertificateInfo.java new file mode 100644 index 000000000..a90c5af83 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs/SigningCertificateInfo.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs; + +import java.io.IOException; + +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.x509.GeneralNames; +import net.tongsuo.sun.security.x509.SerialNumber; + +/** + * This class represents a signing certificate attribute. + * Its attribute value is defined by the following ASN.1 definition. + *

+ *
+ *   id-aa-signingCertificate OBJECT IDENTIFIER ::= { iso(1)
+ *     member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs9(9)
+ *     smime(16) id-aa(2) 12 }
+ *
+ *   SigningCertificate ::=  SEQUENCE {
+ *       certs       SEQUENCE OF ESSCertID,
+ *       policies    SEQUENCE OF PolicyInformation OPTIONAL
+ *   }
+ *
+ *   ESSCertID ::=  SEQUENCE {
+ *       certHash        Hash,
+ *       issuerSerial    IssuerSerial OPTIONAL
+ *   }
+ *
+ *   Hash ::= OCTET STRING -- SHA1 hash of entire certificate
+ *
+ *   IssuerSerial ::= SEQUENCE {
+ *       issuer         GeneralNames,
+ *       serialNumber   CertificateSerialNumber
+ *   }
+ *
+ *   PolicyInformation ::= SEQUENCE {
+ *       policyIdentifier   CertPolicyId,
+ *       policyQualifiers   SEQUENCE SIZE (1..MAX) OF
+ *               PolicyQualifierInfo OPTIONAL }
+ *
+ *   CertPolicyId ::= OBJECT IDENTIFIER
+ *
+ *   PolicyQualifierInfo ::= SEQUENCE {
+ *       policyQualifierId  PolicyQualifierId,
+ *       qualifier        ANY DEFINED BY policyQualifierId }
+ *
+ *   -- Implementations that recognize additional policy qualifiers MUST
+ *   -- augment the following definition for PolicyQualifierId
+ *
+ *   PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
+ *
+ * 
+ * + * @since 1.5 + * @author Vincent Ryan + */ +class SigningCertificateInfo { + + private byte[] ber; + private ESSCertId[] certId = null; + + SigningCertificateInfo(byte[] ber) throws IOException { + parse(ber); + this.ber = ber; + } + + byte[] toByteArray() { + return ber; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < certId.length; i++) { + sb.append(certId[i].toString()); + } + // format policies as a string + sb.append("\n]"); + + return sb.toString(); + } + + private void parse(byte[] bytes) throws IOException { + + // Parse signingCertificate + DerValue derValue = new DerValue(bytes); + if (derValue.tag != DerValue.tag_Sequence) { + throw new IOException("Bad encoding for signingCertificate"); + } + + // Parse certs + DerValue[] certs = derValue.data.getSequence(1); + certId = new ESSCertId[certs.length]; + for (int i = 0; i < certs.length; i++) { + certId[i] = new ESSCertId(certs[i]); + } + + // Parse policies, if present + if (derValue.data.available() > 0) { + DerValue[] policies = derValue.data.getSequence(1); + for (int i = 0; i < policies.length; i++) { + // parse PolicyInformation + } + } + } + + static class ESSCertId { + + private static volatile HexDumpEncoder hexDumper; + + private final byte[] certHash; + private final GeneralNames issuer; + private final SerialNumber serialNumber; + + ESSCertId(DerValue certId) throws IOException { + // Parse certHash + certHash = certId.data.getDerValue().toByteArray(); + + // Parse issuerSerial, if present + if (certId.data.available() > 0) { + DerValue issuerSerial = certId.data.getDerValue(); + // Parse issuer + issuer = new GeneralNames(issuerSerial.data.getDerValue()); + // Parse serialNumber + serialNumber = new SerialNumber(issuerSerial.data.getDerValue()); + } else { + issuer = null; + serialNumber = null; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[\n\tCertificate hash (SHA-1):\n"); + if (hexDumper == null) { + hexDumper = new HexDumpEncoder(); + } + sb.append(hexDumper.encode(certHash)); + if (issuer != null && serialNumber != null) { + sb.append("\n\tIssuer: " + issuer + "\n"); + sb.append("\t" + serialNumber); + } + sb.append("\n]"); + return sb.toString(); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/MacData.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/MacData.java new file mode 100644 index 000000000..0eb30959d --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/MacData.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 1999, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy i included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs12; + +import java.io.*; +import java.security.*; + +import net.tongsuo.sun.security.pkcs.ParsingException; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.x509.AlgorithmId; + + +/** + * A MacData type, as defined in PKCS#12. + * + * @author Sharon Liu + */ + +class MacData { + + private final String digestAlgorithmName; + private AlgorithmParameters digestAlgorithmParams; + private final byte[] digest; + private final byte[] macSalt; + private final int iterations; + + // the ASN.1 encoded contents of this class + private byte[] encoded = null; + + /** + * Parses a PKCS#12 MAC data. + */ + MacData(DerInputStream derin) throws IOException { + DerValue[] macData = derin.getSequence(2); + if (macData.length < 2 || macData.length > 3) { + throw new ParsingException("Invalid length for MacData"); + } + + // Parse the digest info + DerInputStream digestIn = new DerInputStream(macData[0].toByteArray()); + DerValue[] digestInfo = digestIn.getSequence(2); + if (digestInfo.length != 2) { + throw new ParsingException("Invalid length for DigestInfo"); + } + + // Parse the DigestAlgorithmIdentifier. + AlgorithmId digestAlgorithmId = AlgorithmId.parse(digestInfo[0]); + this.digestAlgorithmName = digestAlgorithmId.getName(); + this.digestAlgorithmParams = digestAlgorithmId.getParameters(); + // Get the digest. + this.digest = digestInfo[1].getOctetString(); + + // Get the salt. + this.macSalt = macData[1].getOctetString(); + + // Iterations is optional. The default value is 1. + if (macData.length > 2) { + this.iterations = macData[2].getInteger(); + } else { + this.iterations = 1; + } + } + + MacData(String algName, byte[] digest, byte[] salt, int iterations) + throws NoSuchAlgorithmException + { + if (algName == null) + throw new NullPointerException("the algName parameter " + + "must be non-null"); + + AlgorithmId algid = AlgorithmId.get(algName); + this.digestAlgorithmName = algid.getName(); + this.digestAlgorithmParams = algid.getParameters(); + + if (digest == null) { + throw new NullPointerException("the digest " + + "parameter must be non-null"); + } else if (digest.length == 0) { + throw new IllegalArgumentException("the digest " + + "parameter must not be empty"); + } else { + this.digest = digest.clone(); + } + + this.macSalt = salt; + this.iterations = iterations; + + // delay the generation of ASN.1 encoding until + // getEncoded() is called + this.encoded = null; + + } + + String getDigestAlgName() { + return digestAlgorithmName; + } + + byte[] getSalt() { + return macSalt; + } + + int getIterations() { + return iterations; + } + + byte[] getDigest() { + return digest; + } + + /** + * Returns the ASN.1 encoding of this object. + * @return the ASN.1 encoding. + * @exception IOException if error occurs when constructing its + * ASN.1 encoding. + */ + public byte[] getEncoded() throws NoSuchAlgorithmException + { + if (this.encoded != null) + return this.encoded.clone(); + + DerOutputStream out = new DerOutputStream(); + DerOutputStream tmp = new DerOutputStream(); + + DerOutputStream tmp2 = new DerOutputStream(); + // encode encryption algorithm + AlgorithmId algid = AlgorithmId.get(digestAlgorithmName); + algid.encode(tmp2); + + // encode digest data + tmp2.putOctetString(digest); + + tmp.write(DerValue.tag_Sequence, tmp2); + + // encode salt + tmp.putOctetString(macSalt); + + // encode iterations + tmp.putInteger(iterations); + + // wrap everything into a SEQUENCE + out.write(DerValue.tag_Sequence, tmp); + this.encoded = out.toByteArray(); + + return this.encoded.clone(); + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/PKCS12KeyStore.java b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/PKCS12KeyStore.java new file mode 100644 index 000000000..34b7cb4a6 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/pkcs12/PKCS12KeyStore.java @@ -0,0 +1,2760 @@ +/* + * Copyright (c) 1999, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.pkcs12; + +import java.io.*; +import java.security.AccessController; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreSpi; +import java.security.KeyStoreException; +import java.security.PKCS12Attribute; +import java.security.PrivateKey; +import java.security.PrivilegedAction; +import java.security.UnrecoverableEntryException; +import java.security.UnrecoverableKeyException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.cert.CertificateException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import javax.crypto.spec.PBEParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.SecretKeyFactory; +import javax.crypto.SecretKey; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.security.auth.DestroyFailedException; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.crypto.CryptoUtils; +import net.tongsuo.jdk.internal.misc.SharedSecretsUtil; +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.provider.KeyStoreDelegator; +import net.tongsuo.sun.security.provider.JavaKeyStore; +import net.tongsuo.sun.security.pkcs.ContentInfo; +import net.tongsuo.sun.security.pkcs.EncryptedPrivateKeyInfo; +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.util.SecurityProperties; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.AuthorityKeyIdentifierExtension; +import net.tongsuo.sun.security.action.GetPropertyAction; +import net.tongsuo.sun.security.tools.KeyStoreUtil; + + +/** + * This class provides the keystore implementation referred to as "PKCS12". + * Implements the PKCS#12 PFX protected using the Password privacy mode. + * The contents are protected using Password integrity mode. + * + * NOTE: In a PKCS12 keystore, entries are identified by the alias, and + * a localKeyId is required to match the private key with the certificate. + * Trusted certificate entries are identified by the presence of an + * trustedKeyUsage attribute. + * + * @author Seema Malkani + * @author Jeff Nisewanger + * @author Jan Luehe + * + * @see java.security.KeyStoreSpi + */ +public final class PKCS12KeyStore extends KeyStoreSpi { + + // Hardcoded defaults. They should be the same with commented out + // lines inside the java.security file. + private static final String DEFAULT_CERT_PBE_ALGORITHM + = "PBEWithHmacSHA256AndAES_256"; + private static final String DEFAULT_KEY_PBE_ALGORITHM + = "PBEWithHmacSHA256AndAES_256"; + private static final String DEFAULT_MAC_ALGORITHM = "HmacPBESHA256"; + private static final int DEFAULT_CERT_PBE_ITERATION_COUNT = 10000; + private static final int DEFAULT_KEY_PBE_ITERATION_COUNT = 10000; + private static final int DEFAULT_MAC_ITERATION_COUNT = 10000; + + // Legacy settings. Used when "keystore.pkcs12.legacy" is set. + private static final String LEGACY_CERT_PBE_ALGORITHM + = "PBEWithSHA1AndRC2_40"; + private static final String LEGACY_KEY_PBE_ALGORITHM + = "PBEWithSHA1AndDESede"; + private static final String LEGACY_MAC_ALGORITHM = "HmacPBESHA1"; + private static final int LEGACY_PBE_ITERATION_COUNT = 50000; + private static final int LEGACY_MAC_ITERATION_COUNT = 100000; + + // Big switch. When this system property is set. Legacy settings + // are used no matter what other keystore.pkcs12.* properties are set. + // Note: This is only a system property, there's no same-name + // security property defined. + private static final String USE_LEGACY_PROP = "keystore.pkcs12.legacy"; + + // special PKCS12 keystore that supports PKCS12 and JKS file formats + public static final class DualFormatPKCS12 extends KeyStoreDelegator { + public DualFormatPKCS12() { + super("PKCS12", PKCS12KeyStore.class, "JKS", JavaKeyStore.JKS.class); + } + } + + public static final int VERSION_3 = 3; + + private static final int MAX_ITERATION_COUNT = 5000000; + private static final int SALT_LEN = 20; + + private static final KnownOIDs[] CORE_ATTRIBUTES = { + KnownOIDs.FriendlyName, + KnownOIDs.LocalKeyID, + KnownOIDs.ORACLE_TrustedKeyUsage + }; + + private static final Debug debug = Debug.getInstance("pkcs12"); + + private static final ObjectIdentifier PKCS8ShroudedKeyBag_OID = + Oid.of(KnownOIDs.PKCS8ShroudedKeyBag); + private static final ObjectIdentifier CertBag_OID = + Oid.of(KnownOIDs.CertBag); + private static final ObjectIdentifier SecretBag_OID = + Oid.of(KnownOIDs.SecretBag); + + private static final ObjectIdentifier PKCS9FriendlyName_OID = + Oid.of(KnownOIDs.FriendlyName); + private static final ObjectIdentifier PKCS9LocalKeyId_OID = + Oid.of(KnownOIDs.LocalKeyID); + private static final ObjectIdentifier PKCS9CertType_OID = + Oid.of(KnownOIDs.CertTypeX509); + private static final ObjectIdentifier pbes2_OID = + Oid.of(KnownOIDs.PBES2); + + /* + * Temporary Oracle OID + * + * {joint-iso-itu-t(2) country(16) us(840) organization(1) + * oracle(113894) jdk(746875) crypto(1) id-at-trustedKeyUsage(1)} + */ + private static final ObjectIdentifier TrustedKeyUsage_OID = + Oid.of(KnownOIDs.ORACLE_TrustedKeyUsage); + + private static final ObjectIdentifier[] AnyUsage = new ObjectIdentifier[] { + Oid.of(KnownOIDs.anyExtendedKeyUsage) + }; + + private int counter = 0; + + // private key count + // Note: This is a workaround to allow null localKeyID attribute + // in pkcs12 with one private key entry and associated cert-chain + private int privateKeyCount = 0; + + // secret key count + private int secretKeyCount = 0; + + // certificate count + private int certificateCount = 0; + + // Alg/params used for *this* keystore. Initialized as -1 for ic and + // null for algorithm names. When an existing file is read, they will be + // assigned inside engineLoad() so storing an existing keystore uses the + // old alg/params. This makes sure if a keystore is created password-less + // it will be password-less forever. Otherwise, engineStore() will read + // the default values. These fields are always reset when load() is called. + private String certProtectionAlgorithm = null; + private int certPbeIterationCount = -1; + private String macAlgorithm = null; + private int macIterationCount = -1; + + // the source of randomness + private SecureRandom random; + + // A keystore entry and associated attributes + private static class Entry { + Date date; // the creation date of this entry + String alias; + byte[] keyId; + Set attributes; + } + + // A key entry + private static class KeyEntry extends Entry { + } + + // A private key entry and its supporting certificate chain + private static class PrivateKeyEntry extends KeyEntry { + byte[] protectedPrivKey; + Certificate[] chain; + } + + // A secret key + private static class SecretKeyEntry extends KeyEntry { + byte[] protectedSecretKey; + } + + // A certificate entry + private static class CertEntry extends Entry { + final X509Certificate cert; + ObjectIdentifier[] trustedKeyUsage; + + CertEntry(X509Certificate cert, byte[] keyId, String alias) { + this(cert, keyId, alias, null, null); + } + + CertEntry(X509Certificate cert, byte[] keyId, String alias, + ObjectIdentifier[] trustedKeyUsage, + Set attributes) { + this.date = new Date(); + this.cert = cert; + this.keyId = keyId; + this.alias = alias; + this.trustedKeyUsage = trustedKeyUsage; + this.attributes = new HashSet<>(); + if (attributes != null) { + this.attributes.addAll(attributes); + } + } + } + + /** + * Retries an action with password "\0" if "" fails. + * @param the return type + */ + @FunctionalInterface + private interface RetryWithZero { + + T tryOnce(char[] password) throws Exception; + + static S run(RetryWithZero f, char[] password) throws Exception { + try { + return f.tryOnce(password); + } catch (Exception e) { + if (password.length == 0) { + // Retry using an empty password with a NUL terminator. + if (debug != null) { + debug.println("Retry with a NUL password"); + } + return f.tryOnce(new char[1]); + } + throw e; + } + } + } + + /** + * Private keys and certificates are stored in a map. + * Map entries are keyed by alias names. + */ + private final Map entries = + Collections.synchronizedMap(new LinkedHashMap<>()); + + // The cache for entries + private final Map entryCache = + Collections.synchronizedMap(new LinkedHashMap<>()); + + private final ArrayList keyList = new ArrayList<>(); + private final List allCerts = new ArrayList<>(); + private final ArrayList certEntries = new ArrayList<>(); + + /** + * Returns the key associated with the given alias, using the given + * password to recover it. + * + * @param alias the alias name + * @param password the password for recovering the key + * + * @return the requested key, or null if the given alias does not exist + * or does not identify a key entry. + * + * @exception NoSuchAlgorithmException if the algorithm for recovering the + * key cannot be found + * @exception UnrecoverableKeyException if the key cannot be recovered + * (e.g., the given password is wrong). + */ + public Key engineGetKey(String alias, char[] password) + throws NoSuchAlgorithmException, UnrecoverableKeyException + { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + Key key; + + if (!(entry instanceof KeyEntry)) { + return null; + } + + // get the encoded private key or secret key + byte[] encrBytes; + if (entry instanceof PrivateKeyEntry) { + encrBytes = ((PrivateKeyEntry) entry).protectedPrivKey; + } else if (entry instanceof SecretKeyEntry) { + encrBytes = ((SecretKeyEntry) entry).protectedSecretKey; + } else { + throw new UnrecoverableKeyException("Error locating key"); + } + + byte[] encryptedKey; + AlgorithmParameters algParams; + AlgorithmId aid; + + try { + // get the encrypted private key + EncryptedPrivateKeyInfo encrInfo = + new EncryptedPrivateKeyInfo(encrBytes); + encryptedKey = encrInfo.getEncryptedData(); + + // parse Algorithm parameters + DerValue val = new DerValue(encrInfo.getAlgorithm().encode()); + aid = AlgorithmId.parse(val); + algParams = aid.getParameters(); + + } catch (IOException ioe) { + UnrecoverableKeyException uke = + new UnrecoverableKeyException("Private key not stored as " + + "PKCS#8 EncryptedPrivateKeyInfo: " + ioe); + uke.initCause(ioe); + throw uke; + } + + try { + PBEParameterSpec pbeSpec; + int ic; + + if (algParams != null) { + try { + pbeSpec = + algParams.getParameterSpec(PBEParameterSpec.class); + } catch (InvalidParameterSpecException ipse) { + throw new IOException("Invalid PBE algorithm parameters"); + } + ic = pbeSpec.getIterationCount(); + + if (ic > MAX_ITERATION_COUNT) { + throw new IOException("key PBE iteration count too large"); + } + } else { + ic = 0; + } + + key = RetryWithZero.run(pass -> { + // Use JCE + Cipher cipher = CryptoInsts.getCipher(aid.getName()); + SecretKey skey = getPBEKey(pass); + try { + cipher.init(Cipher.DECRYPT_MODE, skey, algParams); + } finally { + destroyPBEKey(skey); + } + byte[] keyInfo = cipher.doFinal(encryptedKey); + /* + * Parse the key algorithm and then use a JCA key factory + * to re-create the key. + */ + DerValue val = new DerValue(keyInfo); + try { + DerInputStream in = val.toDerInputStream(); + int i = in.getInteger(); + DerValue[] value = in.getSequence(2); + if (value.length < 1 || value.length > 2) { + throw new IOException("Invalid length for AlgorithmIdentifier"); + } + AlgorithmId algId = new AlgorithmId(value[0].getOID()); + String keyAlgo = algId.getName(); + + // decode private key + if (entry instanceof PrivateKeyEntry) { + KeyFactory kfac = CryptoInsts.getKeyFactory(keyAlgo); + PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(keyInfo); + try { + Key tmp = kfac.generatePrivate(kspec); + + if (debug != null) { + debug.println("Retrieved a protected private key at alias" + + " '" + alias + "' (" + + aid.getName() + + " iterations: " + ic + ")"); + } + return tmp; + } finally { + SharedSecretsUtil.secSpecClearEncodedKeySpec(kspec); + } + // decode secret key + } else { + byte[] keyBytes = in.getOctetString(); + SecretKeySpec secretKeySpec = + new SecretKeySpec(keyBytes, keyAlgo); + + try { + // Special handling required for PBE: needs a PBEKeySpec + Key tmp; + if (keyAlgo.startsWith("PBE")) { + SecretKeyFactory sKeyFactory = + SecretKeyFactory.getInstance(keyAlgo); + KeySpec pbeKeySpec = + sKeyFactory.getKeySpec(secretKeySpec, PBEKeySpec.class); + try { + tmp = sKeyFactory.generateSecret(pbeKeySpec); + } finally { + ((PBEKeySpec)pbeKeySpec).clearPassword(); + SharedSecretsUtil.cryptoSpecClearSecretKeySpec(secretKeySpec); + } + } else { + tmp = secretKeySpec; + } + + if (debug != null) { + debug.println("Retrieved a protected secret key at alias " + + "'" + alias + "' (" + + aid.getName() + + " iterations: " + ic + ")"); + } + return tmp; + } finally { + Arrays.fill(keyBytes, (byte)0); + } + } + } finally { + val.clear(); + Arrays.fill(keyInfo, (byte) 0); + } + }, password); + + } catch (Exception e) { + UnrecoverableKeyException uke = + new UnrecoverableKeyException("Get Key failed: " + + e.getMessage()); + uke.initCause(e); + throw uke; + } + return key; + } + + /** + * Returns the certificate chain associated with the given alias. + * + * @param alias the alias name + * + * @return the certificate chain (ordered with the user's certificate first + * and the root certificate authority last), or null if the given alias + * does not exist or does not contain a certificate chain (i.e., the given + * alias identifies either a trusted certificate entry or a + * key entry without a certificate chain). + */ + public Certificate[] engineGetCertificateChain(String alias) { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entry instanceof PrivateKeyEntry) { + if (((PrivateKeyEntry) entry).chain == null) { + return null; + } else { + + if (debug != null) { + debug.println("Retrieved a " + + ((PrivateKeyEntry) entry).chain.length + + "-certificate chain at alias '" + alias + "'"); + } + + return ((PrivateKeyEntry) entry).chain.clone(); + } + } else { + return null; + } + } + + /** + * Returns the certificate associated with the given alias. + * + *

If the given alias name identifies a + * trusted certificate entry, the certificate associated with that + * entry is returned. If the given alias name identifies a + * key entry, the first element of the certificate chain of that + * entry is returned, or null if that entry does not have a certificate + * chain. + * + * @param alias the alias name + * + * @return the certificate, or null if the given alias does not exist or + * does not contain a certificate. + */ + public Certificate engineGetCertificate(String alias) { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entry == null) { + return null; + } + if (entry instanceof CertEntry && + ((CertEntry) entry).trustedKeyUsage != null) { + + if (debug != null) { + if (Arrays.equals(AnyUsage, + ((CertEntry) entry).trustedKeyUsage)) { + debug.println("Retrieved a certificate at alias '" + alias + + "' (trusted for any purpose)"); + } else { + debug.println("Retrieved a certificate at alias '" + alias + + "' (trusted for limited purposes)"); + } + } + + return ((CertEntry) entry).cert; + + } else if (entry instanceof PrivateKeyEntry) { + if (((PrivateKeyEntry) entry).chain == null) { + return null; + } else { + + if (debug != null) { + debug.println("Retrieved a certificate at alias '" + alias + + "'"); + } + + return ((PrivateKeyEntry) entry).chain[0]; + } + + } else { + return null; + } + } + + /** + * Returns the creation date of the entry identified by the given alias. + * + * @param alias the alias name + * + * @return the creation date of this entry, or null if the given alias does + * not exist + */ + public Date engineGetCreationDate(String alias) { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entry != null) { + return new Date(entry.date.getTime()); + } else { + return null; + } + } + + /** + * Assigns the given key to the given alias, protecting it with the given + * password. + * + *

If the given key is of type java.security.PrivateKey, + * it must be accompanied by a certificate chain certifying the + * corresponding public key. + * + *

If the given alias already exists, the keystore information + * associated with it is overridden by the given key (and possibly + * certificate chain). + * + * @param alias the alias name + * @param key the key to be associated with the alias + * @param password the password to protect the key + * @param chain the certificate chain for the corresponding public + * key (only required if the given key is of type + * java.security.PrivateKey). + * + * @exception KeyStoreException if the given key cannot be protected, or + * this operation fails for some other reason + */ + public synchronized void engineSetKeyEntry(String alias, Key key, + char[] password, Certificate[] chain) + throws KeyStoreException + { + KeyStore.PasswordProtection passwordProtection = + new KeyStore.PasswordProtection(password); + + try { + setKeyEntry(alias, key, passwordProtection, chain, null); + + } finally { + try { + passwordProtection.destroy(); + } catch (DestroyFailedException dfe) { + // ignore + } + } + } + + /* + * Sets a key entry (with attributes, when present) + */ + private void setKeyEntry(String alias, Key key, + KeyStore.PasswordProtection passwordProtection, Certificate[] chain, + Set attributes) + throws KeyStoreException + { + try { + Entry entry; + + if (key instanceof PrivateKey) { + // Check that all the certs are X.509 certs + checkX509Certs(chain); + + PrivateKeyEntry keyEntry = new PrivateKeyEntry(); + keyEntry.date = new Date(); + + if ((key.getFormat().equals("PKCS#8")) || + (key.getFormat().equals("PKCS8"))) { + + if (debug != null) { + debug.println( + "Setting a protected private key at alias '" + + alias + "'"); + } + + // Encrypt the private key + byte[] encoded = key.getEncoded(); + try { + keyEntry.protectedPrivKey = + encryptPrivateKey(encoded, passwordProtection); + } finally { + if (encoded != null) { + Arrays.fill(encoded, (byte) 0); + } + } + } else { + throw new KeyStoreException("Private key is not encoded " + + "as PKCS#8"); + } + + // clone the chain + if (chain != null) { + // validate cert-chain + if ((chain.length > 1) && (!validateChain(chain))) + throw new KeyStoreException("Certificate chain is " + + "not valid"); + keyEntry.chain = chain.clone(); + certificateCount += chain.length; + + if (debug != null) { + debug.println("Setting a " + chain.length + + "-certificate chain at alias '" + alias + "'"); + } + } + privateKeyCount++; + entry = keyEntry; + + } else if (key instanceof SecretKey) { + SecretKeyEntry keyEntry = new SecretKeyEntry(); + keyEntry.date = new Date(); + + // Encode secret key in a PKCS#8 + DerOutputStream secretKeyInfo = new DerOutputStream(); + secretKeyInfo.putInteger(0); + AlgorithmId algId = AlgorithmId.get(key.getAlgorithm()); + algId.encode(secretKeyInfo); + + byte[] encoded = key.getEncoded(); + secretKeyInfo.putOctetString(encoded); + Arrays.fill(encoded, (byte)0); + + DerValue pkcs8 = DerValue.wrap(DerValue.tag_Sequence, secretKeyInfo); + byte[] p8Array = pkcs8.toByteArray(); + pkcs8.clear(); + try { + // Encrypt the secret key (using same PBE as for private keys) + keyEntry.protectedSecretKey = + encryptPrivateKey(p8Array, passwordProtection); + } finally { + Arrays.fill(p8Array, (byte)0); + } + + if (debug != null) { + debug.println("Setting a protected secret key at alias '" + + alias + "'"); + } + secretKeyCount++; + entry = keyEntry; + + } else { + throw new KeyStoreException("Unsupported Key type"); + } + + entry.attributes = new HashSet<>(); + if (attributes != null) { + entry.attributes.addAll(attributes); + } + // set the keyId to current date + entry.keyId = ("Time " + (entry.date).getTime()).getBytes(UTF_8); + // set the alias + entry.alias = alias.toLowerCase(Locale.ENGLISH); + // add the entry + entries.put(alias.toLowerCase(Locale.ENGLISH), entry); + + } catch (KeyStoreException kse) { + throw kse; + } catch (Exception nsae) { + throw new KeyStoreException("Key protection" + + " algorithm not found: " + nsae, nsae); + } + } + + /** + * Assigns the given key (that has already been protected) to the given + * alias. + * + *

If the protected key is of type + * java.security.PrivateKey, it must be accompanied by a + * certificate chain certifying the corresponding public key. If the + * underlying keystore implementation is of type jks, + * key must be encoded as an + * EncryptedPrivateKeyInfo as defined in the PKCS #8 standard. + * + *

If the given alias already exists, the keystore information + * associated with it is overridden by the given key (and possibly + * certificate chain). + * + * @param alias the alias name + * @param key the key (in protected format) to be associated with the alias + * @param chain the certificate chain for the corresponding public + * key (only useful if the protected key is of type + * java.security.PrivateKey). + * + * @exception KeyStoreException if this operation fails. + */ + public synchronized void engineSetKeyEntry(String alias, byte[] key, + Certificate[] chain) + throws KeyStoreException + { + // Check that all the certs are X.509 certs + checkX509Certs(chain); + + // Private key must be encoded as EncryptedPrivateKeyInfo + // as defined in PKCS#8 + try { + new EncryptedPrivateKeyInfo(key); + } catch (IOException ioe) { + throw new KeyStoreException("Private key is not stored" + + " as PKCS#8 EncryptedPrivateKeyInfo: " + ioe, ioe); + } + + PrivateKeyEntry entry = new PrivateKeyEntry(); + entry.date = new Date(); + + if (debug != null) { + debug.println("Setting a protected private key at alias '" + + alias + "'"); + } + + // set the keyId to current date + entry.keyId = ("Time " + (entry.date).getTime()).getBytes(UTF_8); + // set the alias + entry.alias = alias.toLowerCase(Locale.ENGLISH); + + entry.protectedPrivKey = key.clone(); + if (chain != null) { + // validate cert-chain + if ((chain.length > 1) && (!validateChain(chain))) { + throw new KeyStoreException("Certificate chain is " + + "not valid"); + } + entry.chain = chain.clone(); + certificateCount += chain.length; + + if (debug != null) { + debug.println("Setting a " + entry.chain.length + + "-certificate chain at alias '" + alias + "'"); + } + } + + // add the entry + privateKeyCount++; + entries.put(alias.toLowerCase(Locale.ENGLISH), entry); + } + + + /* + * Generate random salt + */ + private byte[] getSalt() + { + // Generate a random salt. + byte[] salt = new byte[SALT_LEN]; + if (random == null) { + random = new SecureRandom(); + } + random.nextBytes(salt); + return salt; + } + + /* + * Generate PBE Algorithm Parameters + */ + private AlgorithmParameters getPBEAlgorithmParameters( + String algorithm, int iterationCount) throws IOException { + AlgorithmParameters algParams; + + byte[] salt = getSalt(); + if (KnownOIDs.findMatch(algorithm) == KnownOIDs.PBEWithMD5AndDES) { + // PBES1 scheme such as PBEWithMD5AndDES requires a 8-byte salt + salt = Arrays.copyOf(salt, 8); + } + + // create PBE parameters from salt and iteration count + PBEParameterSpec paramSpec = + new PBEParameterSpec(salt, iterationCount); + try { + algParams = CryptoInsts.getAlgorithmParameters(algorithm); + algParams.init(paramSpec); + } catch (Exception e) { + throw new IOException("getPBEAlgorithmParameters failed: " + + e.getMessage(), e); + } + return algParams; + } + + /* + * Generate PBE key + */ + private SecretKey getPBEKey(char[] password) throws IOException + { + SecretKey skey; + + PBEKeySpec keySpec = new PBEKeySpec(password); + try { + SecretKeyFactory skFac = SecretKeyFactory.getInstance("PBE"); + skey = skFac.generateSecret(keySpec); + } catch (Exception e) { + throw new IOException("getSecretKey failed: " + e.getMessage(), e); + } finally { + keySpec.clearPassword(); + } + + return skey; + } + + /* + * Destroy the key obtained from getPBEKey(). + */ + private void destroyPBEKey(SecretKey key) { + try { + key.destroy(); + } catch (DestroyFailedException e) { + // Accept this + } + } + + /* + * Encrypt private key or secret key using Password-based encryption (PBE) + * as defined in PKCS#5. + * + * @return encrypted private key or secret key encoded as + * EncryptedPrivateKeyInfo + */ + private byte[] encryptPrivateKey(byte[] data, + KeyStore.PasswordProtection passwordProtection) + throws UnrecoverableKeyException + { + byte[] key; + + try { + String algorithm; + AlgorithmParameters algParams; + AlgorithmId algid; + + // Initialize PBE algorithm and parameters + algorithm = passwordProtection.getProtectionAlgorithm(); + if (algorithm != null) { + AlgorithmParameterSpec algParamSpec = + passwordProtection.getProtectionParameters(); + if (algParamSpec != null) { + algParams = CryptoInsts.getAlgorithmParameters(algorithm); + algParams.init(algParamSpec); + } else { + algParams = getPBEAlgorithmParameters(algorithm, + defaultKeyPbeIterationCount()); + } + } else { + // Check default key protection algorithm for PKCS12 keystores + algorithm = defaultKeyProtectionAlgorithm(); + algParams = getPBEAlgorithmParameters(algorithm, + defaultKeyPbeIterationCount()); + } + + ObjectIdentifier pbeOID = mapPBEAlgorithmToOID(algorithm); + if (pbeOID == null) { + throw new IOException("PBE algorithm '" + algorithm + + " 'is not supported for key entry protection"); + } + + // Use JCE + Cipher cipher = CryptoInsts.getCipher(algorithm); + SecretKey skey = getPBEKey(passwordProtection.getPassword()); + try { + cipher.init(Cipher.ENCRYPT_MODE, skey, algParams); + } finally { + destroyPBEKey(skey); + } + byte[] encryptedKey = cipher.doFinal(data); + algid = new AlgorithmId(pbeOID, cipher.getParameters()); + + if (debug != null) { + debug.println(" (Cipher algorithm: " + cipher.getAlgorithm() + + ")"); + } + + // wrap encrypted private key in EncryptedPrivateKeyInfo + // as defined in PKCS#8 + EncryptedPrivateKeyInfo encrInfo = + new EncryptedPrivateKeyInfo(algid, encryptedKey); + key = encrInfo.getEncoded(); + } catch (Exception e) { + UnrecoverableKeyException uke = + new UnrecoverableKeyException("Encrypt Private Key failed: " + + e.getMessage()); + uke.initCause(e); + throw uke; + } + + return key; + } + + /* + * Map a PBE algorithm name onto its object identifier + */ + private static ObjectIdentifier mapPBEAlgorithmToOID(String algorithm) + throws NoSuchAlgorithmException { + // Check for PBES2 algorithms + if (algorithm.toLowerCase(Locale.ENGLISH).startsWith("pbewithhmacsha")) { + return pbes2_OID; + } + return AlgorithmId.get(algorithm).getOID(); + } + + /** + * Assigns the given certificate to the given alias. + * + *

If the given alias already exists in this keystore and identifies a + * trusted certificate entry, the certificate associated with it is + * overridden by the given certificate. + * + * @param alias the alias name + * @param cert the certificate + * + * @exception KeyStoreException if the given alias already exists and does + * not identify a trusted certificate entry, or this operation fails + * for some other reason. + */ + public synchronized void engineSetCertificateEntry(String alias, + Certificate cert) throws KeyStoreException + { + setCertEntry(alias, cert, null); + } + + /* + * Sets a trusted cert entry (with attributes, when present) + */ + private void setCertEntry(String alias, Certificate cert, + Set attributes) throws KeyStoreException { + + // Check that the cert is an X.509 cert + if (cert != null && (!(cert instanceof X509Certificate))) { + throw new KeyStoreException( + "Only X.509 certificates are supported - rejecting class: " + + cert.getClass().getName()); + } + + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entry instanceof KeyEntry) { + throw new KeyStoreException("Cannot overwrite own certificate"); + } + + CertEntry certEntry = + new CertEntry((X509Certificate) cert, null, alias, AnyUsage, + attributes); + certificateCount++; + entries.put(alias.toLowerCase(Locale.ENGLISH), certEntry); + + if (debug != null) { + debug.println("Setting a trusted certificate at alias '" + alias + + "'"); + } + } + + /** + * Deletes the entry identified by the given alias from this keystore. + * + * @param alias the alias name + * + * @exception KeyStoreException if the entry cannot be removed. + */ + public synchronized void engineDeleteEntry(String alias) + throws KeyStoreException + { + if (debug != null) { + debug.println("Removing entry at alias '" + alias + "'"); + } + + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry keyEntry = (PrivateKeyEntry) entry; + if (keyEntry.chain != null) { + certificateCount -= keyEntry.chain.length; + } + privateKeyCount--; + } else if (entry instanceof CertEntry) { + certificateCount--; + } else if (entry instanceof SecretKeyEntry) { + secretKeyCount--; + } + entries.remove(alias.toLowerCase(Locale.ENGLISH)); + } + + /** + * Lists all the alias names of this keystore. + * + * @return enumeration of the alias names + */ + public Enumeration engineAliases() { + return Collections.enumeration(entries.keySet()); + } + + /** + * Checks if the given alias exists in this keystore. + * + * @param alias the alias name + * + * @return true if the alias exists, false otherwise + */ + public boolean engineContainsAlias(String alias) { + return entries.containsKey(alias.toLowerCase(Locale.ENGLISH)); + } + + /** + * Retrieves the number of entries in this keystore. + * + * @return the number of entries in this keystore + */ + public int engineSize() { + return entries.size(); + } + + /** + * Returns true if the entry identified by the given alias is a + * key entry, and false otherwise. + * + * @return true if the entry identified by the given alias is a + * key entry, false otherwise. + */ + public boolean engineIsKeyEntry(String alias) { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + return entry instanceof KeyEntry; + } + + /** + * Returns true if the entry identified by the given alias is a + * trusted certificate entry, and false otherwise. + * + * @return true if the entry identified by the given alias is a + * trusted certificate entry, false otherwise. + */ + public boolean engineIsCertificateEntry(String alias) { + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + return entry instanceof CertEntry && + ((CertEntry) entry).trustedKeyUsage != null; + } + + /** + * Determines if the keystore {@code Entry} for the specified + * {@code alias} is an instance or subclass of the specified + * {@code entryClass}. + * + * @param alias the alias name + * @param entryClass the entry class + * + * @return true if the keystore {@code Entry} for the specified + * {@code alias} is an instance or subclass of the + * specified {@code entryClass}, false otherwise + * + * @since 1.5 + */ + @Override + public boolean + engineEntryInstanceOf(String alias, + Class entryClass) + { + if (entryClass == KeyStore.TrustedCertificateEntry.class) { + return engineIsCertificateEntry(alias); + } + + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (entryClass == KeyStore.PrivateKeyEntry.class) { + return (entry instanceof PrivateKeyEntry); + } + if (entryClass == KeyStore.SecretKeyEntry.class) { + return (entry instanceof SecretKeyEntry); + } + return false; + } + + /** + * Returns the (alias) name of the first keystore entry whose certificate + * matches the given certificate. + * + *

This method attempts to match the given certificate with each + * keystore entry. If the entry being considered + * is a trusted certificate entry, the given certificate is + * compared to that entry's certificate. If the entry being considered is + * a key entry, the given certificate is compared to the first + * element of that entry's certificate chain (if a chain exists). + * + * @param cert the certificate to match with. + * + * @return the (alias) name of the first entry with matching certificate, + * or null if no such entry exists in this keystore. + */ + public String engineGetCertificateAlias(Certificate cert) { + Certificate certElem = null; + + for (Enumeration e = engineAliases(); e.hasMoreElements(); ) { + String alias = e.nextElement(); + Entry entry = entries.get(alias); + if (entry instanceof PrivateKeyEntry) { + if (((PrivateKeyEntry) entry).chain != null) { + certElem = ((PrivateKeyEntry) entry).chain[0]; + } + } else if (entry instanceof CertEntry && + ((CertEntry) entry).trustedKeyUsage != null) { + certElem = ((CertEntry) entry).cert; + } else { + continue; + } + if (certElem != null && certElem.equals(cert)) { + return alias; + } + } + return null; + } + + /** + * Stores this keystore to the given output stream, and protects its + * integrity with the given password. + * + * @param stream the output stream to which this keystore is written. + * @param password the password to generate the keystore integrity check + * + * @exception IOException if there was an I/O problem with data + * @exception NoSuchAlgorithmException if the appropriate data integrity + * algorithm could not be found + * @exception CertificateException if any of the certificates included in + * the keystore data could not be stored + */ + public synchronized void engineStore(OutputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException + { + + // -- Create PFX + DerOutputStream pfx = new DerOutputStream(); + + // PFX version (always write the latest version) + DerOutputStream version = new DerOutputStream(); + version.putInteger(VERSION_3); + byte[] pfxVersion = version.toByteArray(); + pfx.write(pfxVersion, 0, pfxVersion.length); + + // -- Create AuthSafe + DerOutputStream authSafe = new DerOutputStream(); + + // -- Create ContentInfos + DerOutputStream authSafeContentInfo = new DerOutputStream(); + + // -- create safeContent Data ContentInfo + if (privateKeyCount > 0 || secretKeyCount > 0) { + + if (debug != null) { + debug.println("Storing " + (privateKeyCount + secretKeyCount) + + " protected key(s) in a PKCS#7 data"); + } + + byte[] safeContentData = createSafeContent(); + ContentInfo dataContentInfo = new ContentInfo(safeContentData); + dataContentInfo.encode(authSafeContentInfo); + } + + // -- create EncryptedContentInfo + if (certificateCount > 0) { + + if (certProtectionAlgorithm == null) { + certProtectionAlgorithm = defaultCertProtectionAlgorithm(); + } + if (certPbeIterationCount < 0) { + certPbeIterationCount = defaultCertPbeIterationCount(); + } + + if (debug != null) { + debug.println("Storing " + certificateCount + + " certificate(s) in a PKCS#7 encryptedData"); + } + + byte[] certsData = getCertificateData(); + if (password != null && !certProtectionAlgorithm.equalsIgnoreCase("NONE")) { + // -- SEQUENCE of EncryptedData + DerOutputStream encrData = new DerOutputStream(); + encrData.putInteger(0); + encrData.write(encryptContent(certsData, password)); + DerOutputStream encrDataContent = new DerOutputStream(); + encrDataContent.write(DerValue.tag_Sequence, encrData); + ContentInfo encrContentInfo = + new ContentInfo(ContentInfo.ENCRYPTED_DATA_OID, + new DerValue(encrDataContent.toByteArray())); + encrContentInfo.encode(authSafeContentInfo); + } else { + ContentInfo dataContentInfo = new ContentInfo(certsData); + dataContentInfo.encode(authSafeContentInfo); + } + } + + // wrap as SequenceOf ContentInfos + DerOutputStream cInfo = new DerOutputStream(); + cInfo.write(DerValue.tag_SequenceOf, authSafeContentInfo); + byte[] authenticatedSafe = cInfo.toByteArray(); + + // Create Encapsulated ContentInfo + ContentInfo contentInfo = new ContentInfo(authenticatedSafe); + contentInfo.encode(authSafe); + byte[] authSafeData = authSafe.toByteArray(); + pfx.write(authSafeData); + + // -- MAC + if (macAlgorithm == null) { + macAlgorithm = defaultMacAlgorithm(); + } + if (macIterationCount < 0) { + macIterationCount = defaultMacIterationCount(); + } + if (password != null && !macAlgorithm.equalsIgnoreCase("NONE")) { + byte[] macData = calculateMac(password, authenticatedSafe); + pfx.write(macData); + } + // write PFX to output stream + DerOutputStream pfxout = new DerOutputStream(); + pfxout.write(DerValue.tag_Sequence, pfx); + byte[] pfxData = pfxout.toByteArray(); + stream.write(pfxData); + stream.flush(); + } + + /** + * Gets a KeyStore.Entry for the specified alias + * with the specified protection parameter. + * + * @param alias get the KeyStore.Entry for this alias + * @param protParam the ProtectionParameter + * used to protect the Entry, + * which may be null + * + * @return the KeyStore.Entry for the specified alias, + * or null if there is no such entry + * + * @exception KeyStoreException if the operation failed + * @exception NoSuchAlgorithmException if the algorithm for recovering the + * entry cannot be found + * @exception UnrecoverableEntryException if the specified + * protParam were insufficient or invalid + * @exception UnrecoverableKeyException if the entry is a + * PrivateKeyEntry or SecretKeyEntry + * and the specified protParam does not contain + * the information needed to recover the key (e.g. wrong password) + * + * @since 1.5 + */ + @Override + public KeyStore.Entry engineGetEntry(String alias, + KeyStore.ProtectionParameter protParam) + throws KeyStoreException, NoSuchAlgorithmException, + UnrecoverableEntryException { + + if (!engineContainsAlias(alias)) { + return null; + } + + Entry entry = entries.get(alias.toLowerCase(Locale.ENGLISH)); + if (protParam == null) { + if (engineIsCertificateEntry(alias)) { + if (entryCache.containsKey(alias)) { + return entryCache.get(alias); + } + + if (entry instanceof CertEntry && + ((CertEntry) entry).trustedKeyUsage != null) { + + if (debug != null) { + debug.println("Retrieved a trusted certificate at " + + "alias '" + alias + "'"); + } + + return new KeyStore.TrustedCertificateEntry( + ((CertEntry)entry).cert, getAttributes(entry)); + } + } else { + throw new UnrecoverableKeyException + ("requested entry requires a password"); + } + } + + if (protParam instanceof KeyStore.PasswordProtection) { + if (engineIsCertificateEntry(alias)) { + throw new UnsupportedOperationException + ("trusted certificate entries are not password-protected"); + } else if (engineIsKeyEntry(alias)) { + if (entryCache.containsKey(alias)) { + return entryCache.get(alias); + } + + KeyStore.PasswordProtection pp = + (KeyStore.PasswordProtection)protParam; + char[] password = pp.getPassword(); + + Key key = engineGetKey(alias, password); + if (key instanceof PrivateKey) { + Certificate[] chain = engineGetCertificateChain(alias); + + KeyStore.PrivateKeyEntry privateKeyEntry + = new KeyStore.PrivateKeyEntry( + (PrivateKey)key, chain, getAttributes(entry)); + entryCache.put(alias, privateKeyEntry); + return privateKeyEntry; + } else if (key instanceof SecretKey) { + KeyStore.SecretKeyEntry secretKeyEntry + = new KeyStore.SecretKeyEntry( + (SecretKey)key, getAttributes(entry)); + entryCache.put(alias, secretKeyEntry); + return secretKeyEntry; + } + } else if (!engineIsKeyEntry(alias)) { + throw new UnsupportedOperationException + ("untrusted certificate entries are not " + + "password-protected"); + } + } + + throw new UnsupportedOperationException(); + } + + /** + * Saves a KeyStore.Entry under the specified alias. + * The specified protection parameter is used to protect the + * Entry. + * + *

If an entry already exists for the specified alias, + * it is overridden. + * + * @param alias save the KeyStore.Entry under this alias + * @param entry the Entry to save + * @param protParam the ProtectionParameter + * used to protect the Entry, + * which may be null + * + * @exception KeyStoreException if this operation fails + * + * @since 1.5 + */ + @Override + public synchronized void engineSetEntry(String alias, KeyStore.Entry entry, + KeyStore.ProtectionParameter protParam) throws KeyStoreException { + + // get password + if (protParam != null && + !(protParam instanceof KeyStore.PasswordProtection)) { + throw new KeyStoreException("unsupported protection parameter"); + } + KeyStore.PasswordProtection pProtect = null; + if (protParam != null) { + pProtect = (KeyStore.PasswordProtection)protParam; + } + + // set entry + if (entry instanceof KeyStore.TrustedCertificateEntry) { + if (protParam != null && pProtect.getPassword() != null) { + // pre-1.5 style setCertificateEntry did not allow password + throw new KeyStoreException + ("trusted certificate entries are not password-protected"); + } else { + KeyStore.TrustedCertificateEntry tce = + (KeyStore.TrustedCertificateEntry)entry; + setCertEntry(alias, tce.getTrustedCertificate(), + tce.getAttributes()); + + return; + } + } else if (entry instanceof KeyStore.PrivateKeyEntry) { + if (pProtect == null || pProtect.getPassword() == null) { + // pre-1.5 style setKeyEntry required password + throw new KeyStoreException + ("non-null password required to create PrivateKeyEntry"); + } else { + KeyStore.PrivateKeyEntry pke = (KeyStore.PrivateKeyEntry)entry; + setKeyEntry(alias, pke.getPrivateKey(), pProtect, + pke.getCertificateChain(), pke.getAttributes()); + + return; + } + } else if (entry instanceof KeyStore.SecretKeyEntry) { + if (pProtect == null || pProtect.getPassword() == null) { + // pre-1.5 style setKeyEntry required password + throw new KeyStoreException + ("non-null password required to create SecretKeyEntry"); + } else { + KeyStore.SecretKeyEntry ske = (KeyStore.SecretKeyEntry)entry; + setKeyEntry(alias, ske.getSecretKey(), pProtect, + null, ske.getAttributes()); + + return; + } + } + + throw new KeyStoreException + ("unsupported entry type: " + entry.getClass().getName()); + } + + /* + * Assemble the entry attributes + */ + private Set getAttributes(Entry entry) { + + if (entry.attributes == null) { + entry.attributes = new HashSet<>(); + } + + // friendlyName + entry.attributes.add(new PKCS12Attribute( + PKCS9FriendlyName_OID.toString(), entry.alias)); + + // localKeyID + byte[] keyIdValue = entry.keyId; + if (keyIdValue != null) { + entry.attributes.add(new PKCS12Attribute( + PKCS9LocalKeyId_OID.toString(), Debug.toString(keyIdValue))); + } + + // trustedKeyUsage + if (entry instanceof CertEntry) { + ObjectIdentifier[] trustedKeyUsageValue = + ((CertEntry) entry).trustedKeyUsage; + if (trustedKeyUsageValue != null) { + if (trustedKeyUsageValue.length == 1) { // omit brackets + entry.attributes.add(new PKCS12Attribute( + TrustedKeyUsage_OID.toString(), + trustedKeyUsageValue[0].toString())); + } else { // multi-valued + entry.attributes.add(new PKCS12Attribute( + TrustedKeyUsage_OID.toString(), + Arrays.toString(trustedKeyUsageValue))); + } + } + } + + return entry.attributes; + } + + /* + * Calculate MAC using HMAC algorithm (required for password integrity) + * + * Hash-based MAC algorithm combines secret key with message digest to + * create a message authentication code (MAC) + */ + private byte[] calculateMac(char[] passwd, byte[] data) + throws IOException + { + byte[] mData; + String algName = macAlgorithm.substring(7); + + try { + // Generate a random salt. + byte[] salt = getSalt(); + + // generate MAC (MAC key is generated within JCE) + Mac m = CryptoInsts.getMac(macAlgorithm); + PBEParameterSpec params = + new PBEParameterSpec(salt, macIterationCount); + SecretKey key = getPBEKey(passwd); + try { + m.init(key, params); + } finally { + destroyPBEKey(key); + } + m.update(data); + byte[] macResult = m.doFinal(); + + // encode as MacData + MacData macData = new MacData(algName, macResult, salt, + macIterationCount); + DerOutputStream bytes = new DerOutputStream(); + bytes.write(macData.getEncoded()); + mData = bytes.toByteArray(); + } catch (Exception e) { + throw new IOException("calculateMac failed: " + e, e); + } + return mData; + } + + + /* + * Validate Certificate Chain + */ + private boolean validateChain(Certificate[] certChain) + { + for (int i = 0; i < certChain.length-1; i++) { + X500Principal issuerDN = + ((X509Certificate)certChain[i]).getIssuerX500Principal(); + X500Principal subjectDN = + ((X509Certificate)certChain[i+1]).getSubjectX500Principal(); + if (!(issuerDN.equals(subjectDN))) + return false; + } + + // Check for loops in the chain. If there are repeated certs, + // the Set of certs in the chain will contain fewer certs than + // the chain + Set set = new HashSet<>(Arrays.asList(certChain)); + return set.size() == certChain.length; + } + + /* + * Check that all the certificates are X.509 certificates + */ + private static void checkX509Certs(Certificate[] certs) + throws KeyStoreException { + if (certs != null) { + for (Certificate cert : certs) { + if (!(cert instanceof X509Certificate)) { + throw new KeyStoreException( + "Only X.509 certificates are supported - " + + "rejecting class: " + cert.getClass().getName()); + } + } + } + } + + /* + * Create PKCS#12 Attributes, friendlyName, localKeyId and trustedKeyUsage. + * + * Although attributes are optional, they could be required. + * For e.g. localKeyId attribute is required to match the + * private key with the associated end-entity certificate. + * The trustedKeyUsage attribute is used to denote a trusted certificate. + * + * PKCS8ShroudedKeyBags include unique localKeyID and friendlyName. + * CertBags may or may not include attributes depending on the type + * of Certificate. In end-entity certificates, localKeyID should be + * unique, and the corresponding private key should have the same + * localKeyID. For trusted CA certs in the cert-chain, localKeyID + * attribute is not required, hence most vendors don't include it. + * NSS/Netscape require it to be unique or null, whereas IE/OpenSSL + * ignore it. + * + * Here is a list of pkcs12 attribute values in CertBags. + * + * PKCS12 Attribute NSS/Netscape IE OpenSSL J2SE + * -------------------------------------------------------------- + * LocalKeyId + * (In EE cert only, + * NULL in CA certs) true true true true + * + * friendlyName unique same/ same/ unique + * unique unique/ + * null + * trustedKeyUsage - - - true + * + * Note: OpenSSL adds friendlyName for end-entity cert only, and + * removes the localKeyID and friendlyName for CA certs. + * If the CertBag did not have a friendlyName, most vendors will + * add it, and assign it to the DN of the cert. + */ + private byte[] getBagAttributes(String alias, byte[] keyId, + Set attributes) throws IOException { + return getBagAttributes(alias, keyId, null, attributes); + } + + private byte[] getBagAttributes(String alias, byte[] keyId, + ObjectIdentifier[] trustedUsage, + Set attributes) throws IOException { + + byte[] localKeyID = null; + byte[] friendlyName = null; + byte[] trustedKeyUsage = null; + + // return null if both attributes are null + if (alias == null && keyId == null) { + return null; + } + + // SafeBag Attributes + DerOutputStream bagAttrs = new DerOutputStream(); + + // Encode the friendlyname oid. + if (alias != null) { + DerOutputStream bagAttr1 = new DerOutputStream(); + bagAttr1.putOID(PKCS9FriendlyName_OID); + DerOutputStream bagAttrContent1 = new DerOutputStream(); + DerOutputStream bagAttrValue1 = new DerOutputStream(); + bagAttrContent1.putBMPString(alias); + bagAttr1.write(DerValue.tag_Set, bagAttrContent1); + bagAttrValue1.write(DerValue.tag_Sequence, bagAttr1); + friendlyName = bagAttrValue1.toByteArray(); + } + + // Encode the localkeyId oid. + if (keyId != null) { + DerOutputStream bagAttr2 = new DerOutputStream(); + bagAttr2.putOID(PKCS9LocalKeyId_OID); + DerOutputStream bagAttrContent2 = new DerOutputStream(); + DerOutputStream bagAttrValue2 = new DerOutputStream(); + bagAttrContent2.putOctetString(keyId); + bagAttr2.write(DerValue.tag_Set, bagAttrContent2); + bagAttrValue2.write(DerValue.tag_Sequence, bagAttr2); + localKeyID = bagAttrValue2.toByteArray(); + } + + // Encode the trustedKeyUsage oid. + if (trustedUsage != null) { + DerOutputStream bagAttr3 = new DerOutputStream(); + bagAttr3.putOID(TrustedKeyUsage_OID); + DerOutputStream bagAttrContent3 = new DerOutputStream(); + DerOutputStream bagAttrValue3 = new DerOutputStream(); + for (ObjectIdentifier usage : trustedUsage) { + bagAttrContent3.putOID(usage); + } + bagAttr3.write(DerValue.tag_Set, bagAttrContent3); + bagAttrValue3.write(DerValue.tag_Sequence, bagAttr3); + trustedKeyUsage = bagAttrValue3.toByteArray(); + } + + DerOutputStream attrs = new DerOutputStream(); + if (friendlyName != null) { + attrs.write(friendlyName); + } + if (localKeyID != null) { + attrs.write(localKeyID); + } + if (trustedKeyUsage != null) { + attrs.write(trustedKeyUsage); + } + + if (attributes != null) { + for (KeyStore.Entry.Attribute attribute : attributes) { + String attributeName = attribute.getName(); + // skip friendlyName, localKeyId and trustedKeyUsage + if (CORE_ATTRIBUTES[0].value().equals(attributeName) || + CORE_ATTRIBUTES[1].value().equals(attributeName) || + CORE_ATTRIBUTES[2].value().equals(attributeName)) { + continue; + } + attrs.write(((PKCS12Attribute) attribute).getEncoded()); + } + } + + bagAttrs.write(DerValue.tag_Set, attrs); + return bagAttrs.toByteArray(); + } + + /* + * Create Data content type, includes certificates in individual + * SafeBags of type CertBag. Each CertBag may include pkcs12 attributes + * (see comments in getBagAttributes) + */ + private byte[] getCertificateData() + throws CertificateException, IOException + { + DerOutputStream out = new DerOutputStream(); + for (Enumeration e = engineAliases(); e.hasMoreElements(); ) { + + String alias = e.nextElement(); + Entry entry = entries.get(alias); + + // certificate chain + Certificate[] certs; + + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry keyEntry = (PrivateKeyEntry) entry; + certs = (keyEntry.chain != null) ? + keyEntry.chain : new Certificate[0]; + } else if (entry instanceof CertEntry) { + certs = new Certificate[]{((CertEntry) entry).cert}; + } else { + certs = new Certificate[0]; + } + + for (int i = 0; i < certs.length; i++) { + // create SafeBag of Type CertBag + DerOutputStream safeBag = new DerOutputStream(); + safeBag.putOID(CertBag_OID); + + // create a CertBag + DerOutputStream certBag = new DerOutputStream(); + certBag.putOID(PKCS9CertType_OID); + + // write encoded certs in a context-specific tag + DerOutputStream certValue = new DerOutputStream(); + X509Certificate cert = (X509Certificate) certs[i]; + certValue.putOctetString(cert.getEncoded()); + certBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte) 0), certValue); + + // wrap CertBag in a Sequence + DerOutputStream certout = new DerOutputStream(); + certout.write(DerValue.tag_Sequence, certBag); + byte[] certBagValue = certout.toByteArray(); + + // Wrap the CertBag encoding in a context-specific tag. + DerOutputStream bagValue = new DerOutputStream(); + bagValue.write(certBagValue); + // write SafeBag Value + safeBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte) 0), bagValue); + + // write SafeBag Attributes + // All Certs should have a unique friendlyName. + // This change is made to meet NSS requirements. + byte[] bagAttrs; + if (i == 0) { + // Only End-Entity Cert should have a localKeyId. + if (entry instanceof KeyEntry) { + KeyEntry keyEntry = (KeyEntry) entry; + bagAttrs = + getBagAttributes(keyEntry.alias, keyEntry.keyId, + keyEntry.attributes); + } else { + CertEntry certEntry = (CertEntry) entry; + bagAttrs = + getBagAttributes(certEntry.alias, certEntry.keyId, + certEntry.trustedKeyUsage, + certEntry.attributes); + } + } else { + // Trusted root CA certs and Intermediate CA certs do not + // need to have a localKeyId, and hence localKeyId is null + // This change is made to meet NSS/Netscape requirements. + // NSS pkcs12 library requires trusted CA certs in the + // certificate chain to have unique or null localKeyID. + // However, IE/OpenSSL do not impose this restriction. + bagAttrs = getBagAttributes( + cert.getSubjectX500Principal().getName(), null, + entry.attributes); + } + if (bagAttrs != null) { + safeBag.write(bagAttrs); + } + + // wrap as Sequence + out.write(DerValue.tag_Sequence, safeBag); + } // for cert-chain + } + + // wrap as SequenceOf SafeBag + DerOutputStream safeBagValue = new DerOutputStream(); + safeBagValue.write(DerValue.tag_SequenceOf, out); + return safeBagValue.toByteArray(); + } + + /* + * Create SafeContent Data content type. + * Includes encrypted secret key in a SafeBag of type SecretBag. + * Includes encrypted private key in a SafeBag of type PKCS8ShroudedKeyBag. + * Each PKCS8ShroudedKeyBag includes pkcs12 attributes + * (see comments in getBagAttributes) + */ + private byte[] createSafeContent() + throws CertificateException, IOException { + + DerOutputStream out = new DerOutputStream(); + for (Enumeration e = engineAliases(); e.hasMoreElements(); ) { + + String alias = e.nextElement(); + Entry entry = entries.get(alias); + if ((!(entry instanceof KeyEntry))) { + continue; + } + DerOutputStream safeBag = new DerOutputStream(); + KeyEntry keyEntry = (KeyEntry) entry; + + // DER encode the private key + if (keyEntry instanceof PrivateKeyEntry) { + // Create SafeBag of type pkcs8ShroudedKeyBag + safeBag.putOID(PKCS8ShroudedKeyBag_OID); + + // get the encrypted private key + byte[] encrBytes = ((PrivateKeyEntry)keyEntry).protectedPrivKey; + EncryptedPrivateKeyInfo encrInfo; + try { + encrInfo = new EncryptedPrivateKeyInfo(encrBytes); + + } catch (IOException ioe) { + throw new IOException("Private key not stored as " + + "PKCS#8 EncryptedPrivateKeyInfo" + + ioe.getMessage()); + } + + // Wrap the EncryptedPrivateKeyInfo in a context-specific tag. + DerOutputStream bagValue = new DerOutputStream(); + bagValue.write(encrInfo.getEncoded()); + safeBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte) 0), bagValue); + + // DER encode the secret key + } else if (keyEntry instanceof SecretKeyEntry) { + // Create SafeBag of type SecretBag + safeBag.putOID(SecretBag_OID); + + // Create a SecretBag + DerOutputStream secretBag = new DerOutputStream(); + secretBag.putOID(PKCS8ShroudedKeyBag_OID); + + // Write secret key in a context-specific tag + DerOutputStream secretKeyValue = new DerOutputStream(); + secretKeyValue.putOctetString( + ((SecretKeyEntry) keyEntry).protectedSecretKey); + secretBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte) 0), secretKeyValue); + + // Wrap SecretBag in a Sequence + DerOutputStream secretBagSeq = new DerOutputStream(); + secretBagSeq.write(DerValue.tag_Sequence, secretBag); + byte[] secretBagValue = secretBagSeq.toByteArray(); + + // Wrap the secret bag in a context-specific tag. + DerOutputStream bagValue = new DerOutputStream(); + bagValue.write(secretBagValue); + + // Write SafeBag value + safeBag.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte) 0), bagValue); + } else { + continue; // skip this entry + } + + // write SafeBag Attributes + byte[] bagAttrs = + getBagAttributes(alias, entry.keyId, entry.attributes); + safeBag.write(bagAttrs); + + // wrap as Sequence + out.write(DerValue.tag_Sequence, safeBag); + } + + // wrap as Sequence + DerOutputStream safeBagValue = new DerOutputStream(); + safeBagValue.write(DerValue.tag_Sequence, out); + return safeBagValue.toByteArray(); + } + + + /* + * Encrypt the contents using Password-based (PBE) encryption + * as defined in PKCS #5. + * + * @return encrypted contents encoded as EncryptedContentInfo + */ + private byte[] encryptContent(byte[] data, char[] password) + throws IOException { + + byte[] encryptedData; + + + try { + // create AlgorithmParameters + AlgorithmParameters algParams = getPBEAlgorithmParameters( + certProtectionAlgorithm, certPbeIterationCount); + DerOutputStream bytes = new DerOutputStream(); + + // Use JCE + Cipher cipher = CryptoInsts.getCipher(certProtectionAlgorithm); + SecretKey skey = getPBEKey(password); + try { + cipher.init(Cipher.ENCRYPT_MODE, skey, algParams); + } finally { + destroyPBEKey(skey); + } + encryptedData = cipher.doFinal(data); + + AlgorithmId algId = new AlgorithmId( + mapPBEAlgorithmToOID(certProtectionAlgorithm), + cipher.getParameters()); + // cipher.getParameters() now has IV + algId.encode(bytes); + byte[] encodedAlgId = bytes.toByteArray(); + + if (debug != null) { + debug.println(" (Cipher algorithm: " + cipher.getAlgorithm() + + ")"); + } + + // create EncryptedContentInfo + DerOutputStream bytes2 = new DerOutputStream(); + bytes2.putOID(ContentInfo.DATA_OID); + bytes2.write(encodedAlgId); + + // Wrap encrypted data in a context-specific tag. + DerOutputStream tmpout2 = new DerOutputStream(); + tmpout2.putOctetString(encryptedData); + bytes2.writeImplicit(DerValue.createTag(DerValue.TAG_CONTEXT, + false, (byte) 0), tmpout2); + + // wrap EncryptedContentInfo in a Sequence + DerOutputStream out = new DerOutputStream(); + out.write(DerValue.tag_Sequence, bytes2); + return out.toByteArray(); + } catch (IOException ioe) { + throw ioe; + } catch (Exception e) { + throw new IOException("Failed to encrypt" + + " safe contents entry: " + e, e); + } + } + + /** + * Loads the keystore from the given input stream. + * + *

If a password is given, it is used to check the integrity of the + * keystore data. Otherwise, the integrity of the keystore is not checked. + * + * @param stream the input stream from which the keystore is loaded + * @param password the (optional) password used to check the integrity of + * the keystore. + * + * @exception IOException if there is an I/O or format problem with the + * keystore data + * @exception NoSuchAlgorithmException if the algorithm used to check + * the integrity of the keystore cannot be found + * @exception CertificateException if any of the certificates in the + * keystore could not be loaded + */ + public synchronized void engineLoad(InputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException + { + + // Reset config when loading a different keystore. + certProtectionAlgorithm = null; + certPbeIterationCount = -1; + macAlgorithm = null; + macIterationCount = -1; + + if (stream == null) + return; + + // reset the counter + counter = 0; + + DerValue val = new DerValue(stream); + DerInputStream s = val.toDerInputStream(); + int version = s.getInteger(); + + if (version != VERSION_3) { + throw new IOException("PKCS12 keystore not in version 3 format"); + } + + entries.clear(); + + /* + * Read the authSafe. + */ + byte[] authSafeData; + ContentInfo authSafe = new ContentInfo(s); + ObjectIdentifier contentType = authSafe.getContentType(); + + if (contentType.equals((Object) ContentInfo.DATA_OID)) { + authSafeData = authSafe.getData(); + } else /* signed data */ { + throw new IOException("public key protected PKCS12 not supported"); + } + + DerInputStream as = new DerInputStream(authSafeData); + DerValue[] safeContentsArray = as.getSequence(2); + int count = safeContentsArray.length; + + // reset the counters at the start + privateKeyCount = 0; + secretKeyCount = 0; + certificateCount = 0; + + boolean seeEncBag = false; + + /* + * Spin over the ContentInfos. + */ + for (int i = 0; i < count; i++) { + ContentInfo safeContents; + DerInputStream sci; + byte[] eAlgId = null; + + sci = new DerInputStream(safeContentsArray[i].toByteArray()); + safeContents = new ContentInfo(sci); + contentType = safeContents.getContentType(); + if (contentType.equals((Object) ContentInfo.DATA_OID)) { + + if (debug != null) { + debug.println("Loading PKCS#7 data"); + } + + loadSafeContents(new DerInputStream(safeContents.getData())); + } else if (contentType.equals((Object) ContentInfo.ENCRYPTED_DATA_OID)) { + if (password == null) { + + if (debug != null) { + debug.println("Warning: skipping PKCS#7 encryptedData" + + " - no password was supplied"); + } + // No password to decrypt ENCRYPTED_DATA_OID. *Skip it*. + // This means user will see a PrivateKeyEntry without + // certificates and a whole TrustedCertificateEntry will + // be lost. This is not a perfect solution but alternative + // solutions are more disruptive: + // + // We cannot just fail, since KeyStore.load(is, null) + // has been known to never fail because of a null password. + // + // We cannot just throw away the whole PrivateKeyEntry, + // this is too silent and no one will notice anything. + // + // We also cannot fail when getCertificate() on such a + // PrivateKeyEntry is called, since the method has not + // specified this behavior. + continue; + } + + DerInputStream edi = + safeContents.getContent().toDerInputStream(); + int edVersion = edi.getInteger(); + DerValue[] seq = edi.getSequence(3); + if (seq.length != 3) { + // We require the encryptedContent field, even though + // it is optional + throw new IOException("Invalid length for EncryptedContentInfo"); + } + ObjectIdentifier edContentType = seq[0].getOID(); + eAlgId = seq[1].toByteArray(); + if (!seq[2].isContextSpecific((byte)0)) { + throw new IOException("unsupported encrypted content type " + + seq[2].tag); + } + byte newTag = DerValue.tag_OctetString; + if (seq[2].isConstructed()) + newTag |= 0x20; + seq[2].resetTag(newTag); + byte[] rawData = seq[2].getOctetString(); + + // parse Algorithm parameters + AlgorithmId aid = AlgorithmId.parse(seq[1]); + AlgorithmParameters algParams = aid.getParameters(); + + PBEParameterSpec pbeSpec; + int ic = 0; + + if (algParams != null) { + try { + pbeSpec = + algParams.getParameterSpec(PBEParameterSpec.class); + } catch (InvalidParameterSpecException ipse) { + throw new IOException( + "Invalid PBE algorithm parameters"); + } + ic = pbeSpec.getIterationCount(); + + if (ic > MAX_ITERATION_COUNT) { + throw new IOException("cert PBE iteration count too large"); + } + + certProtectionAlgorithm = aid.getName(); + certPbeIterationCount = ic; + seeEncBag = true; + } + + if (debug != null) { + debug.println("Loading PKCS#7 encryptedData " + + "(" + certProtectionAlgorithm + + " iterations: " + ic + ")"); + } + + try { + RetryWithZero.run(pass -> { + // Use JCE + Cipher cipher = CryptoInsts.getCipher(certProtectionAlgorithm); + SecretKey skey = getPBEKey(pass); + try { + cipher.init(Cipher.DECRYPT_MODE, skey, algParams); + } finally { + destroyPBEKey(skey); + } + loadSafeContents(new DerInputStream(cipher.doFinal(rawData))); + return null; + }, password); + } catch (Exception e) { + throw new IOException("keystore password was incorrect", + new UnrecoverableKeyException( + "failed to decrypt safe contents entry: " + e)); + } + } else { + throw new IOException("public key protected PKCS12" + + " not supported"); + } + } + + // No ENCRYPTED_DATA_OID but see certificate. Must be passwordless. + if (!seeEncBag && certificateCount > 0) { + certProtectionAlgorithm = "NONE"; + } + + // The MacData is optional. + if (s.available() > 0) { + // If there is no password, we cannot fail. KeyStore.load(is, null) + // has been known to never fail because of a null password. + if (password != null) { + MacData macData = new MacData(s); + int ic = macData.getIterations(); + + try { + if (ic > MAX_ITERATION_COUNT) { + throw new InvalidAlgorithmParameterException( + "MAC iteration count too large: " + ic); + } + + String algName = + macData.getDigestAlgName().toUpperCase(Locale.ENGLISH); + + // Change SHA-1 to SHA1 + algName = algName.replace("-", ""); + + macAlgorithm = "HmacPBE" + algName; + macIterationCount = ic; + + // generate MAC (MAC key is created within JCE) + Mac m = CryptoInsts.getMac(macAlgorithm); + PBEParameterSpec params = + new PBEParameterSpec(macData.getSalt(), ic); + + RetryWithZero.run(pass -> { + SecretKey key = getPBEKey(pass); + try { + m.init(key, params); + } finally { + destroyPBEKey(key); + } + m.update(authSafeData); + byte[] macResult = m.doFinal(); + + if (debug != null) { + debug.println("Checking keystore integrity " + + "(" + m.getAlgorithm() + " iterations: " + ic + ")"); + } + + if (!MessageDigest.isEqual(macData.getDigest(), macResult)) { + throw new UnrecoverableKeyException("Failed PKCS12" + + " integrity checking"); + } + return (Void) null; + }, password); + } catch (Exception e) { + throw new IOException("Integrity check failed: " + e, e); + } + } + } else { + macAlgorithm = "NONE"; + } + + /* + * Match up private keys with certificate chains. + */ + PrivateKeyEntry[] list = + keyList.toArray(new PrivateKeyEntry[0]); + for (int m = 0; m < list.length; m++) { + PrivateKeyEntry entry = list[m]; + if (entry.keyId != null) { + ArrayList chain = new ArrayList<>(); + X509Certificate cert = findMatchedCertificate(entry); + + mainloop: + while (cert != null) { + // Check for loops in the certificate chain + if (!chain.isEmpty()) { + for (X509Certificate chainCert : chain) { + if (cert.equals(chainCert)) { + if (debug != null) { + debug.println("Loop detected in " + + "certificate chain. Skip adding " + + "repeated cert to chain. Subject: " + + cert.getSubjectX500Principal() + .toString()); + } + break mainloop; + } + } + } + chain.add(cert); + if (KeyStoreUtil.isSelfSigned(cert)) { + break; + } + cert = findIssuer(cert); + } + /* Update existing KeyEntry in entries table */ + if (chain.size() > 0) { + entry.chain = chain.toArray(new Certificate[0]); + } + } + } + + if (debug != null) { + debug.println("PKCS12KeyStore load: private key count: " + + privateKeyCount + ". secret key count: " + secretKeyCount + + ". certificate count: " + certificateCount); + } + + certEntries.clear(); + allCerts.clear(); + keyList.clear(); + } + + /** + * Find the issuer of input in allCerts. If the input has an + * AuthorityKeyIdentifier extension and the keyId inside matches + * the keyId of the SubjectKeyIdentifier of a cert. This cert is + * returned. Otherwise, a cert whose subjectDN is the same as the + * input's issuerDN is returned. + * + * @param input the input certificate + * @return the isssuer, or null if none matches + */ + private X509Certificate findIssuer(X509Certificate input) { + + X509Certificate fallback = null; // the DN match + X500Principal issuerPrinc = input.getIssuerX500Principal(); + + // AuthorityKeyIdentifier value encoded as an OCTET STRING + byte[] issuerIdExtension = input.getExtensionValue( + KnownOIDs.AuthorityKeyID.value()); + byte[] issuerId = null; + + if (issuerIdExtension != null) { + try { + issuerId = new AuthorityKeyIdentifierExtension( + false, + new DerValue(issuerIdExtension).getOctetString()) + .getEncodedKeyIdentifier(); + } catch (IOException e) { + // ignored. issuerId is still null + } + } + + for (X509Certificate cert : allCerts) { + if (cert.getSubjectX500Principal().equals(issuerPrinc)) { + if (issuerId != null) { + // SubjectKeyIdentifier value encoded as an OCTET STRING + byte[] subjectIdExtension = cert.getExtensionValue( + KnownOIDs.SubjectKeyID.value()); + byte[] subjectId = null; + if (subjectIdExtension != null) { + try { + subjectId = new DerValue(subjectIdExtension) + .getOctetString(); + } catch (IOException e) { + // ignored. issuerId is still null + } + } + if (subjectId != null) { + if (Arrays.equals(issuerId, subjectId)) { + // keyId exact match! + return cert; + } else { + // Different keyId. Not a fallback. + continue; + } + } else { + // A DN match with no subjectId + fallback = cert; + } + } else { // if there is no issuerId, return the 1st DN match + return cert; + } + } + } + return fallback; + } + + /** + * Returns if a pkcs12 file is password-less. This means no cert is + * encrypted and there is no Mac. Please note that the private key + * can be encrypted. + * + * This is a simplified version of {@link #engineLoad} that only looks + * at the ContentInfo types. + * + * @param f the pkcs12 file + * @return if it's password-less + * @throws IOException + */ + public static boolean isPasswordless(File f) throws IOException { + + try (FileInputStream stream = new FileInputStream(f)) { + DerValue val = new DerValue(stream); + DerInputStream s = val.toDerInputStream(); + + s.getInteger(); // skip version + + ContentInfo authSafe = new ContentInfo(s); + DerInputStream as = new DerInputStream(authSafe.getData()); + for (DerValue seq : as.getSequence(2)) { + DerInputStream sci = new DerInputStream(seq.toByteArray()); + ContentInfo safeContents = new ContentInfo(sci); + if (safeContents.getContentType() + .equals((Object) ContentInfo.ENCRYPTED_DATA_OID)) { + // Certificate encrypted + return false; + } + } + + if (s.available() > 0) { + // The MacData exists. + return false; + } + } + return true; + } + + /** + * Locates a matched CertEntry from certEntries, and returns its cert. + * @param entry the KeyEntry to match + * @return a certificate, null if not found + */ + private X509Certificate findMatchedCertificate(PrivateKeyEntry entry) { + CertEntry keyIdMatch = null; + CertEntry aliasMatch = null; + for (CertEntry ce: certEntries) { + if (Arrays.equals(entry.keyId, ce.keyId)) { + keyIdMatch = ce; + if (entry.alias.equalsIgnoreCase(ce.alias)) { + // Full match! + return ce.cert; + } + } else if (entry.alias.equalsIgnoreCase(ce.alias)) { + aliasMatch = ce; + } + } + // keyId match first, for compatibility + if (keyIdMatch != null) return keyIdMatch.cert; + else if (aliasMatch != null) return aliasMatch.cert; + else return null; + } + + private void loadSafeContents(DerInputStream stream) + throws IOException, CertificateException { + DerValue[] safeBags = stream.getSequence(2); + int count = safeBags.length; + + /* + * Spin over the SafeBags. + */ + for (int i = 0; i < count; i++) { + ObjectIdentifier bagId; + DerInputStream sbi; + DerValue bagValue; + Object bagItem = null; + + sbi = safeBags[i].toDerInputStream(); + bagId = sbi.getOID(); + bagValue = sbi.getDerValue(); + if (!bagValue.isContextSpecific((byte)0)) { + throw new IOException("unsupported PKCS12 bag value type " + + bagValue.tag); + } + bagValue = bagValue.data.getDerValue(); + if (bagId.equals((Object) PKCS8ShroudedKeyBag_OID)) { + PrivateKeyEntry kEntry = new PrivateKeyEntry(); + kEntry.protectedPrivKey = bagValue.toByteArray(); + bagItem = kEntry; + privateKeyCount++; + } else if (bagId.equals((Object) CertBag_OID)) { + DerInputStream cs = new DerInputStream(bagValue.toByteArray()); + DerValue[] certValues = cs.getSequence(2); + if (certValues.length != 2) { + throw new IOException("Invalid length for CertBag"); + } + ObjectIdentifier certId = certValues[0].getOID(); + if (!certValues[1].isContextSpecific((byte)0)) { + throw new IOException("unsupported PKCS12 cert value type " + + certValues[1].tag); + } + DerValue certValue = certValues[1].data.getDerValue(); + CertificateFactory cf = PKIXInsts.getCertificateFactory("X.509"); + X509Certificate cert; + cert = (X509Certificate)cf.generateCertificate + (new ByteArrayInputStream(certValue.getOctetString())); + bagItem = cert; + certificateCount++; + } else if (bagId.equals((Object) SecretBag_OID)) { + DerInputStream ss = new DerInputStream(bagValue.toByteArray()); + DerValue[] secretValues = ss.getSequence(2); + if (secretValues.length != 2) { + throw new IOException("Invalid length for SecretBag"); + } + ObjectIdentifier secretId = secretValues[0].getOID(); + if (!secretValues[1].isContextSpecific((byte)0)) { + throw new IOException( + "unsupported PKCS12 secret value type " + + secretValues[1].tag); + } + DerValue secretValue = secretValues[1].data.getDerValue(); + SecretKeyEntry kEntry = new SecretKeyEntry(); + kEntry.protectedSecretKey = secretValue.getOctetString(); + bagItem = kEntry; + secretKeyCount++; + } else { + + if (debug != null) { + debug.println("Unsupported PKCS12 bag type: " + bagId); + } + } + + DerValue[] attrSet; + try { + attrSet = sbi.getSet(3); + } catch (IOException e) { + // entry does not have attributes + // Note: CA certs can have no attributes + // OpenSSL generates pkcs12 with no attr for CA certs. + attrSet = null; + } + + String alias = null; + byte[] keyId = null; + ObjectIdentifier[] trustedKeyUsage = null; + Set attributes = new HashSet<>(); + + if (attrSet != null) { + for (int j = 0; j < attrSet.length; j++) { + byte[] encoded = attrSet[j].toByteArray(); + DerInputStream as = new DerInputStream(encoded); + DerValue[] attrSeq = as.getSequence(2); + if (attrSeq.length != 2) { + throw new IOException("Invalid length for Attribute"); + } + ObjectIdentifier attrId = attrSeq[0].getOID(); + DerInputStream vs = + new DerInputStream(attrSeq[1].toByteArray()); + DerValue[] valSet; + try { + valSet = vs.getSet(1); + } catch (IOException e) { + throw new IOException("Attribute " + attrId + + " should have a value " + e.getMessage()); + } + if (attrId.equals((Object) PKCS9FriendlyName_OID)) { + alias = valSet[0].getBMPString(); + } else if (attrId.equals((Object) PKCS9LocalKeyId_OID)) { + keyId = valSet[0].getOctetString(); + } else if + (attrId.equals((Object) TrustedKeyUsage_OID)) { + trustedKeyUsage = new ObjectIdentifier[valSet.length]; + for (int k = 0; k < valSet.length; k++) { + trustedKeyUsage[k] = valSet[k].getOID(); + } + } else { + attributes.add(new PKCS12Attribute(encoded)); + } + } + } + + /* + * As per PKCS12 v1.0 friendlyname (alias) and localKeyId (keyId) + * are optional PKCS12 bagAttributes. But entries in the keyStore + * are identified by their alias. Hence, we need to have an + * Unfriendlyname in the alias, if alias is null. The keyId + * attribute is required to match the private key with the + * certificate. If we get a bagItem of type KeyEntry with a + * null keyId, we should skip it entirely. + */ + if (bagItem instanceof KeyEntry) { + KeyEntry entry = (KeyEntry)bagItem; + + if (keyId == null) { + if (bagItem instanceof PrivateKeyEntry) { + // Insert a localKeyID for the privateKey + // Note: This is a workaround to allow null localKeyID + // attribute in pkcs12 with one private key entry and + // associated cert-chain + if (privateKeyCount == 1) { + keyId = "01".getBytes(UTF_8); + } else { + continue; + } + } else { + // keyId in a SecretKeyEntry is not significant + keyId = "00".getBytes(UTF_8); + } + } + entry.keyId = keyId; + // restore date if it exists + String keyIdStr = new String(keyId, UTF_8); + Date date = null; + if (keyIdStr.startsWith("Time ")) { + try { + date = new Date( + Long.parseLong(keyIdStr.substring(5))); + } catch (Exception e) { + // date has been initialized to null + } + } + if (date == null) { + date = new Date(); + } + entry.date = date; + + if (bagItem instanceof PrivateKeyEntry) { + keyList.add(entry); + } + if (entry.attributes == null) { + entry.attributes = new HashSet<>(); + } + entry.attributes.addAll(attributes); + if (alias == null) { + alias = getUnfriendlyName(); + } + entry.alias = alias; + entries.put(alias.toLowerCase(Locale.ENGLISH), entry); + + } else if (bagItem instanceof X509Certificate) { + X509Certificate cert = (X509Certificate)bagItem; + // Insert a localKeyID for the corresponding cert + // Note: This is a workaround to allow null localKeyID + // attribute in pkcs12 with one private key entry and + // associated cert-chain + if ((keyId == null) && (privateKeyCount == 1)) { + // insert localKeyID only for EE cert or self-signed cert + if (i == 0) { + keyId = "01".getBytes(UTF_8); + } + } + // Trusted certificate + if (trustedKeyUsage != null) { + if (alias == null) { + alias = getUnfriendlyName(); + } + CertEntry certEntry = + new CertEntry(cert, keyId, alias, trustedKeyUsage, + attributes); + entries.put(alias.toLowerCase(Locale.ENGLISH), certEntry); + } else { + certEntries.add(new CertEntry(cert, keyId, alias)); + } + allCerts.add(cert); + } + } + } + + private String getUnfriendlyName() { + counter++; + return (String.valueOf(counter)); + } + + /* + * PKCS12 permitted first 24 bytes: + * + * 30 80 02 01 03 30 80 06 09 2A 86 48 86 F7 0D 01 07 01 A0 80 24 80 04 -- + * 30 82 -- -- 02 01 03 30 82 -- -- 06 09 2A 86 48 86 F7 0D 01 07 01 A0 8- + * 30 -- 02 01 03 30 -- 06 09 2A 86 48 86 F7 0D 01 07 01 A0 -- 04 -- -- -- + * 30 81 -- 02 01 03 30 81 -- 06 09 2A 86 48 86 F7 0D 01 07 01 A0 81 -- 04 + * 30 82 -- -- 02 01 03 30 81 -- 06 09 2A 86 48 86 F7 0D 01 07 01 A0 81 -- + * 30 83 -- -- -- 02 01 03 30 82 -- -- 06 09 2A 86 48 86 F7 0D 01 07 01 A0 + * 30 83 -- -- -- 02 01 03 30 83 -- -- -- 06 09 2A 86 48 86 F7 0D 01 07 01 + * 30 84 -- -- -- -- 02 01 03 30 83 -- -- -- 06 09 2A 86 48 86 F7 0D 01 07 + * 30 84 -- -- -- -- 02 01 03 30 84 -- -- -- -- 06 09 2A 86 48 86 F7 0D 01 + */ + + private static final long[][] PKCS12_HEADER_PATTERNS = { + { 0x3080020103308006L, 0x092A864886F70D01L, 0x0701A08024800400L }, + { 0x3082000002010330L, 0x82000006092A8648L, 0x86F70D010701A080L }, + { 0x3000020103300006L, 0x092A864886F70D01L, 0x0701A00004000000L }, + { 0x3081000201033081L, 0x0006092A864886F7L, 0x0D010701A0810004L }, + { 0x3082000002010330L, 0x810006092A864886L, 0xF70D010701A08100L }, + { 0x3083000000020103L, 0x3082000006092A86L, 0x4886F70D010701A0L }, + { 0x3083000000020103L, 0x308300000006092AL, 0x864886F70D010701L }, + { 0x3084000000000201L, 0x0330830000000609L, 0x2A864886F70D0107L }, + { 0x3084000000000201L, 0x0330840000000006L, 0x092A864886F70D01L } + }; + + private static final long[][] PKCS12_HEADER_MASKS = { + { 0xFFFFFFFFFFFFFFFFL, 0xFFFFFFFFFFFFFFFFL, 0xFFFFFFFFFFFFFF00L }, + { 0xFFFF0000FFFFFFFFL, 0xFF0000FFFFFFFFFFL, 0xFFFFFFFFFFFFFFF0L }, + { 0xFF00FFFFFFFF00FFL, 0xFFFFFFFFFFFFFFFFL, 0xFFFFFF00FF000000L }, + { 0xFFFF00FFFFFFFFFFL, 0x00FFFFFFFFFFFFFFL, 0xFFFFFFFFFFFF00FFL }, + { 0xFFFF0000FFFFFFFFL, 0xFF00FFFFFFFFFFFFL, 0xFFFFFFFFFFFFFF00L }, + { 0xFFFF000000FFFFFFL, 0xFFFF0000FFFFFFFFL, 0xFFFFFFFFFFFFFFFFL }, + { 0xFFFF000000FFFFFFL, 0xFFFF000000FFFFFFL, 0xFFFFFFFFFFFFFFFFL }, + { 0xFFFF00000000FFFFL, 0xFFFFFF000000FFFFL, 0xFFFFFFFFFFFFFFFFL }, + { 0xFFFF00000000FFFFL, 0xFFFFFF00000000FFL, 0xFFFFFFFFFFFFFFFFL } + }; + + /** + * Probe the first few bytes of the keystore data stream for a valid + * PKCS12 keystore encoding. + */ +// @Override + public boolean engineProbe(InputStream stream) throws IOException { + + DataInputStream dataStream; + if (stream instanceof DataInputStream) { + dataStream = (DataInputStream)stream; + } else { + dataStream = new DataInputStream(stream); + } + + long firstPeek = dataStream.readLong(); + long nextPeek = dataStream.readLong(); + long finalPeek = dataStream.readLong(); + boolean result = false; + + for (int i = 0; i < PKCS12_HEADER_PATTERNS.length; i++) { + if (PKCS12_HEADER_PATTERNS[i][0] == + (firstPeek & PKCS12_HEADER_MASKS[i][0]) && + (PKCS12_HEADER_PATTERNS[i][1] == + (nextPeek & PKCS12_HEADER_MASKS[i][1])) && + (PKCS12_HEADER_PATTERNS[i][2] == + (finalPeek & PKCS12_HEADER_MASKS[i][2]))) { + result = true; + break; + } + } + + return result; + } + + // The following methods are related to customizing + // the generation of a PKCS12 keystore or private/secret + // key entries. + + private static boolean useLegacy() { + // JDK 8 just uses the legacy MAC algorithm + return CryptoUtils.isJdk8() || GetPropertyAction.privilegedGetProperty( + USE_LEGACY_PROP) != null; + } + + private static String defaultCertProtectionAlgorithm() { + if (useLegacy()) { + return LEGACY_CERT_PBE_ALGORITHM; + } + String result = SecurityProperties.privilegedGetOverridable( + "keystore.pkcs12.certProtectionAlgorithm"); + return (result != null && !result.isEmpty()) + ? result : DEFAULT_CERT_PBE_ALGORITHM; + } + + private static int defaultCertPbeIterationCount() { + if (useLegacy()) { + return LEGACY_PBE_ITERATION_COUNT; + } + String result = SecurityProperties.privilegedGetOverridable( + "keystore.pkcs12.certPbeIterationCount"); + return (result != null && !result.isEmpty()) + ? string2IC("certPbeIterationCount", result) + : DEFAULT_CERT_PBE_ITERATION_COUNT; + } + + // Read both "keystore.pkcs12.keyProtectionAlgorithm" and + // "keystore.PKCS12.keyProtectionAlgorithm" for compatibility. + private static String defaultKeyProtectionAlgorithm() { + if (useLegacy()) { + return LEGACY_KEY_PBE_ALGORITHM; + } + @SuppressWarnings("removal") + String result = AccessController.doPrivileged(new PrivilegedAction() { + public String run() { + String result; + String name1 = "keystore.pkcs12.keyProtectionAlgorithm"; + String name2 = "keystore.PKCS12.keyProtectionAlgorithm"; + result = System.getProperty(name1); + if (result != null) { + return result; + } + result = System.getProperty(name2); + if (result != null) { + return result; + } + result = Security.getProperty(name1); + if (result != null) { + return result; + } + return Security.getProperty(name2); + } + }); + return (result != null && !result.isEmpty()) + ? result : DEFAULT_KEY_PBE_ALGORITHM; + } + + private static int defaultKeyPbeIterationCount() { + if (useLegacy()) { + return LEGACY_PBE_ITERATION_COUNT; + } + String result = SecurityProperties.privilegedGetOverridable( + "keystore.pkcs12.keyPbeIterationCount"); + return (result != null && !result.isEmpty()) + ? string2IC("keyPbeIterationCount", result) + : DEFAULT_KEY_PBE_ITERATION_COUNT; + } + + private static String defaultMacAlgorithm() { + if (useLegacy()) { + return LEGACY_MAC_ALGORITHM; + } + String result = SecurityProperties.privilegedGetOverridable( + "keystore.pkcs12.macAlgorithm"); + return (result != null && !result.isEmpty()) + ? result : DEFAULT_MAC_ALGORITHM; + } + + private static int defaultMacIterationCount() { + if (useLegacy()) { + return LEGACY_MAC_ITERATION_COUNT; + } + String result = SecurityProperties.privilegedGetOverridable( + "keystore.pkcs12.macIterationCount"); + return (result != null && !result.isEmpty()) + ? string2IC("macIterationCount", result) + : DEFAULT_MAC_ITERATION_COUNT; + } + + private static int string2IC(String type, String value) { + int number; + try { + number = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("keystore.pkcs12." + type + + " is not a number: " + value); + } + if (number <= 0 || number > MAX_ITERATION_COUNT) { + throw new IllegalArgumentException("Invalid keystore.pkcs12." + + type + ": " + value); + } + return number; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/JavaKeyStore.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/JavaKeyStore.java new file mode 100644 index 000000000..e86f1b4de --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/JavaKeyStore.java @@ -0,0 +1,821 @@ +/* + * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider; + +import java.io.*; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateException; +import java.util.*; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.pkcs12.PKCS12KeyStore; +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.IOUtils; +import net.tongsuo.sun.security.pkcs.EncryptedPrivateKeyInfo; + +/** + * This class provides the keystore implementation referred to as "JKS". + * + * @author Jan Luehe + * @author David Brownell + * + * + * @see KeyProtector + * @see java.security.KeyStoreSpi + * + * @since 1.2 + */ + +public abstract class JavaKeyStore extends KeyStoreSpi { + + // regular JKS + public static final class JKS extends JavaKeyStore { + String convertAlias(String alias) { + return alias.toLowerCase(Locale.ENGLISH); + } + } + + // special JKS that uses case-sensitive aliases + public static final class CaseExactJKS extends JavaKeyStore { + String convertAlias(String alias) { + return alias; + } + } + + // special JKS that supports JKS and PKCS12 file formats + public static final class DualFormatJKS extends KeyStoreDelegator { + public DualFormatJKS() { + super("JKS", JKS.class, "PKCS12", PKCS12KeyStore.class); + } + } + + private static final Debug debug = Debug.getInstance("keystore"); + private static final int MAGIC = 0xfeedfeed; + private static final int VERSION_1 = 0x01; + private static final int VERSION_2 = 0x02; + + // Private keys and their supporting certificate chains + private static class KeyEntry { + Date date; // the creation date of this entry + byte[] protectedPrivKey; + Certificate chain[]; + } + + // Trusted certificates + private static class TrustedCertEntry { + Date date; // the creation date of this entry + Certificate cert; + } + + /** + * Private keys and certificates are stored in a hashtable. + * Hash entries are keyed by alias names. + */ + private final Hashtable entries; + + JavaKeyStore() { + entries = new Hashtable<>(); + } + + // convert an alias to internal form, overridden in subclasses: + // lower case for regular JKS + // original string for CaseExactJKS + abstract String convertAlias(String alias); + + /** + * Returns the key associated with the given alias, using the given + * password to recover it. + * + * @param alias the alias name + * @param password the password for recovering the key + * + * @return the requested key, or null if the given alias does not exist + * or does not identify a key entry. + * + * @exception NoSuchAlgorithmException if the algorithm for recovering the + * key cannot be found + * @exception UnrecoverableKeyException if the key cannot be recovered + * (e.g., the given password is wrong). + */ + public Key engineGetKey(String alias, char[] password) + throws NoSuchAlgorithmException, UnrecoverableKeyException + { + Object entry = entries.get(convertAlias(alias)); + + if (!(entry instanceof KeyEntry)) { + return null; + } + if (password == null) { + throw new UnrecoverableKeyException("Password must not be null"); + } + + byte[] passwordBytes = convertToBytes(password); + KeyProtector keyProtector = new KeyProtector(passwordBytes); + byte[] encrBytes = ((KeyEntry)entry).protectedPrivKey; + EncryptedPrivateKeyInfo encrInfo; + try { + encrInfo = new EncryptedPrivateKeyInfo(encrBytes); + return keyProtector.recover(encrInfo); + } catch (IOException ioe) { + throw new UnrecoverableKeyException("Private key not stored as " + + "PKCS #8 " + + "EncryptedPrivateKeyInfo"); + } finally { + Arrays.fill(passwordBytes, (byte) 0x00); + } + } + + /** + * Returns the certificate chain associated with the given alias. + * + * @param alias the alias name + * + * @return the certificate chain (ordered with the user's certificate first + * and the root certificate authority last), or null if the given alias + * does not exist or does not contain a certificate chain (i.e., the given + * alias identifies either a trusted certificate entry or a + * key entry without a certificate chain). + */ + public Certificate[] engineGetCertificateChain(String alias) { + Object entry = entries.get(convertAlias(alias)); + + if (entry instanceof KeyEntry) { + if (((KeyEntry)entry).chain == null) { + return null; + } else { + return ((KeyEntry)entry).chain.clone(); + } + } else { + return null; + } + } + + /** + * Returns the certificate associated with the given alias. + * + *

If the given alias name identifies a + * trusted certificate entry, the certificate associated with that + * entry is returned. If the given alias name identifies a + * key entry, the first element of the certificate chain of that + * entry is returned, or null if that entry does not have a certificate + * chain. + * + * @param alias the alias name + * + * @return the certificate, or null if the given alias does not exist or + * does not contain a certificate. + */ + public Certificate engineGetCertificate(String alias) { + Object entry = entries.get(convertAlias(alias)); + + if (entry != null) { + if (entry instanceof TrustedCertEntry) { + return ((TrustedCertEntry)entry).cert; + } else { + if (((KeyEntry)entry).chain == null) { + return null; + } else { + return ((KeyEntry)entry).chain[0]; + } + } + } else { + return null; + } + } + + /** + * Returns the creation date of the entry identified by the given alias. + * + * @param alias the alias name + * + * @return the creation date of this entry, or null if the given alias does + * not exist + */ + public Date engineGetCreationDate(String alias) { + Object entry = entries.get(convertAlias(alias)); + + if (entry != null) { + if (entry instanceof TrustedCertEntry) { + return new Date(((TrustedCertEntry)entry).date.getTime()); + } else { + return new Date(((KeyEntry)entry).date.getTime()); + } + } else { + return null; + } + } + + /** + * Assigns the given private key to the given alias, protecting + * it with the given password as defined in PKCS8. + * + *

The given java.security.PrivateKey key must + * be accompanied by a certificate chain certifying the + * corresponding public key. + * + *

If the given alias already exists, the keystore information + * associated with it is overridden by the given key and certificate + * chain. + * + * @param alias the alias name + * @param key the private key to be associated with the alias + * @param password the password to protect the key + * @param chain the certificate chain for the corresponding public + * key (only required if the given key is of type + * java.security.PrivateKey). + * + * @exception KeyStoreException if the given key is not a private key, + * cannot be protected, or this operation fails for some other reason + */ + public void engineSetKeyEntry(String alias, Key key, char[] password, + Certificate[] chain) + throws KeyStoreException + { + KeyProtector keyProtector; + byte[] passwordBytes = null; + + if (!(key instanceof java.security.PrivateKey)) { + throw new KeyStoreException("Cannot store non-PrivateKeys"); + } + try { + synchronized(entries) { + KeyEntry entry = new KeyEntry(); + entry.date = new Date(); + + // Protect the encoding of the key + passwordBytes = convertToBytes(password); + keyProtector = new KeyProtector(passwordBytes); + entry.protectedPrivKey = keyProtector.protect(key); + + // clone the chain + if ((chain != null) && + (chain.length != 0)) { + entry.chain = chain.clone(); + } else { + entry.chain = null; + } + + entries.put(convertAlias(alias), entry); + } + } catch (NoSuchAlgorithmException nsae) { + throw new KeyStoreException("Key protection algorithm not found"); + } finally { + if (passwordBytes != null) + Arrays.fill(passwordBytes, (byte) 0x00); + } + } + + /** + * Assigns the given key (that has already been protected) to the given + * alias. + * + *

If the protected key is of type + * java.security.PrivateKey, it must be accompanied by a + * certificate chain certifying the corresponding public key. If the + * underlying keystore implementation is of type jks, + * key must be encoded as an + * EncryptedPrivateKeyInfo as defined in the PKCS #8 standard. + * + *

If the given alias already exists, the keystore information + * associated with it is overridden by the given key (and possibly + * certificate chain). + * + * @param alias the alias name + * @param key the key (in protected format) to be associated with the alias + * @param chain the certificate chain for the corresponding public + * key (only useful if the protected key is of type + * java.security.PrivateKey). + * + * @exception KeyStoreException if this operation fails. + */ + public void engineSetKeyEntry(String alias, byte[] key, + Certificate[] chain) + throws KeyStoreException + { + synchronized(entries) { + // key must be encoded as EncryptedPrivateKeyInfo as defined in + // PKCS#8 + try { + new EncryptedPrivateKeyInfo(key); + } catch (IOException ioe) { + throw new KeyStoreException("key is not encoded as " + + "EncryptedPrivateKeyInfo"); + } + + KeyEntry entry = new KeyEntry(); + entry.date = new Date(); + + entry.protectedPrivKey = key.clone(); + if ((chain != null) && + (chain.length != 0)) { + entry.chain = chain.clone(); + } else { + entry.chain = null; + } + + entries.put(convertAlias(alias), entry); + } + } + + /** + * Assigns the given certificate to the given alias. + * + *

If the given alias already exists in this keystore and identifies a + * trusted certificate entry, the certificate associated with it is + * overridden by the given certificate. + * + * @param alias the alias name + * @param cert the certificate + * + * @exception KeyStoreException if the given alias already exists and does + * not identify a trusted certificate entry, or this operation + * fails for some other reason. + */ + public void engineSetCertificateEntry(String alias, Certificate cert) + throws KeyStoreException + { + synchronized(entries) { + + Object entry = entries.get(convertAlias(alias)); + if ((entry instanceof KeyEntry)) { + throw new KeyStoreException + ("Cannot overwrite own certificate"); + } + + TrustedCertEntry trustedCertEntry = new TrustedCertEntry(); + trustedCertEntry.cert = cert; + trustedCertEntry.date = new Date(); + entries.put(convertAlias(alias), trustedCertEntry); + } + } + + /** + * Deletes the entry identified by the given alias from this keystore. + * + * @param alias the alias name + * + * @exception KeyStoreException if the entry cannot be removed. + */ + public void engineDeleteEntry(String alias) + throws KeyStoreException + { + synchronized(entries) { + entries.remove(convertAlias(alias)); + } + } + + /** + * Lists all the alias names of this keystore. + * + * @return enumeration of the alias names + */ + public Enumeration engineAliases() { + return entries.keys(); + } + + /** + * Checks if the given alias exists in this keystore. + * + * @param alias the alias name + * + * @return true if the alias exists, false otherwise + */ + public boolean engineContainsAlias(String alias) { + return entries.containsKey(convertAlias(alias)); + } + + /** + * Retrieves the number of entries in this keystore. + * + * @return the number of entries in this keystore + */ + public int engineSize() { + return entries.size(); + } + + /** + * Returns true if the entry identified by the given alias is a + * key entry, and false otherwise. + * + * @return true if the entry identified by the given alias is a + * key entry, false otherwise. + */ + public boolean engineIsKeyEntry(String alias) { + Object entry = entries.get(convertAlias(alias)); + return entry instanceof KeyEntry; + } + + /** + * Returns true if the entry identified by the given alias is a + * trusted certificate entry, and false otherwise. + * + * @return true if the entry identified by the given alias is a + * trusted certificate entry, false otherwise. + */ + public boolean engineIsCertificateEntry(String alias) { + Object entry = entries.get(convertAlias(alias)); + return entry instanceof TrustedCertEntry; + } + + /** + * Returns the (alias) name of the first keystore entry whose certificate + * matches the given certificate. + * + *

This method attempts to match the given certificate with each + * keystore entry. If the entry being considered + * is a trusted certificate entry, the given certificate is + * compared to that entry's certificate. If the entry being considered is + * a key entry, the given certificate is compared to the first + * element of that entry's certificate chain (if a chain exists). + * + * @param cert the certificate to match with. + * + * @return the (alias) name of the first entry with matching certificate, + * or null if no such entry exists in this keystore. + */ + public String engineGetCertificateAlias(Certificate cert) { + Certificate certElem; + + for (Map.Entry e : entries.entrySet()) { + String alias = e.getKey(); + Object entry = e.getValue(); + if (entry instanceof TrustedCertEntry) { + certElem = ((TrustedCertEntry)entry).cert; + } else if (((KeyEntry)entry).chain != null) { + certElem = ((KeyEntry)entry).chain[0]; + } else { + continue; + } + if (certElem.equals(cert)) { + return alias; + } + } + return null; + } + + /** + * Stores this keystore to the given output stream, and protects its + * integrity with the given password. + * + * @param stream the output stream to which this keystore is written. + * @param password the password to generate the keystore integrity check + * + * @exception IOException if there was an I/O problem with data + * @exception NoSuchAlgorithmException if the appropriate data integrity + * algorithm could not be found + * @exception CertificateException if any of the certificates included in + * the keystore data could not be stored + */ + public void engineStore(OutputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException + { + synchronized(entries) { + /* + * KEYSTORE FORMAT: + * + * Magic number (big-endian integer), + * Version of this file format (big-endian integer), + * + * Count (big-endian integer), + * followed by "count" instances of either: + * + * { + * tag=1 (big-endian integer), + * alias (UTF string) + * timestamp + * encrypted private-key info according to PKCS #8 + * (integer length followed by encoding) + * cert chain (integer count, then certs; for each cert, + * integer length followed by encoding) + * } + * + * or: + * + * { + * tag=2 (big-endian integer) + * alias (UTF string) + * timestamp + * cert (integer length followed by encoding) + * } + * + * ended by a keyed SHA1 hash (bytes only) of + * { password + whitener + preceding body } + */ + + // password is mandatory when storing + if (password == null) { + throw new IllegalArgumentException("password can't be null"); + } + + byte[] encoded; // the certificate encoding + + MessageDigest md = getPreKeyedHash(password); + DataOutputStream dos + = new DataOutputStream(new DigestOutputStream(stream, md)); + + dos.writeInt(MAGIC); + // always write the latest version + dos.writeInt(VERSION_2); + + dos.writeInt(entries.size()); + + for (Map.Entry e : entries.entrySet()) { + String alias = e.getKey(); + Object entry = e.getValue(); + + if (entry instanceof KeyEntry) { + + // Store this entry as a KeyEntry + dos.writeInt(1); + + // Write the alias + dos.writeUTF(alias); + + // Write the (entry creation) date + dos.writeLong(((KeyEntry)entry).date.getTime()); + + // Write the protected private key + dos.writeInt(((KeyEntry)entry).protectedPrivKey.length); + dos.write(((KeyEntry)entry).protectedPrivKey); + + // Write the certificate chain + int chainLen; + if (((KeyEntry)entry).chain == null) { + chainLen = 0; + } else { + chainLen = ((KeyEntry)entry).chain.length; + } + dos.writeInt(chainLen); + for (int i = 0; i < chainLen; i++) { + encoded = ((KeyEntry)entry).chain[i].getEncoded(); + dos.writeUTF(((KeyEntry)entry).chain[i].getType()); + dos.writeInt(encoded.length); + dos.write(encoded); + } + } else { + + // Store this entry as a certificate + dos.writeInt(2); + + // Write the alias + dos.writeUTF(alias); + + // Write the (entry creation) date + dos.writeLong(((TrustedCertEntry)entry).date.getTime()); + + // Write the trusted certificate + encoded = ((TrustedCertEntry)entry).cert.getEncoded(); + dos.writeUTF(((TrustedCertEntry)entry).cert.getType()); + dos.writeInt(encoded.length); + dos.write(encoded); + } + } + + /* + * Write the keyed hash which is used to detect tampering with + * the keystore (such as deleting or modifying key or + * certificate entries). + */ + byte digest[] = md.digest(); + + dos.write(digest); + dos.flush(); + } + } + + /** + * Loads the keystore from the given input stream. + * + *

If a password is given, it is used to check the integrity of the + * keystore data. Otherwise, the integrity of the keystore is not checked. + * + * @param stream the input stream from which the keystore is loaded + * @param password the (optional) password used to check the integrity of + * the keystore. + * + * @exception IOException if there is an I/O or format problem with the + * keystore data + * @exception NoSuchAlgorithmException if the algorithm used to check + * the integrity of the keystore cannot be found + * @exception CertificateException if any of the certificates in the + * keystore could not be loaded + */ + public void engineLoad(InputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException + { + synchronized(entries) { + DataInputStream dis; + MessageDigest md = null; + CertificateFactory cf = null; + Hashtable cfs = null; + ByteArrayInputStream bais; + byte[] encoded; + int trustedKeyCount = 0, privateKeyCount = 0; + + if (stream == null) + return; + + if (password != null) { + md = getPreKeyedHash(password); + dis = new DataInputStream(new DigestInputStream(stream, md)); + } else { + dis = new DataInputStream(stream); + } + + // Body format: see store method + + int xMagic = dis.readInt(); + int xVersion = dis.readInt(); + + if (xMagic!=MAGIC || + (xVersion!=VERSION_1 && xVersion!=VERSION_2)) { + throw new IOException("Invalid keystore format"); + } + + if (xVersion == VERSION_1) { + cf = PKIXInsts.getCertificateFactory("X509"); + } else { + // version 2 + cfs = new Hashtable<>(3); + } + + entries.clear(); + int count = dis.readInt(); + + for (int i = 0; i < count; i++) { + int tag; + String alias; + + tag = dis.readInt(); + + if (tag == 1) { // private key entry + privateKeyCount++; + KeyEntry entry = new KeyEntry(); + + // Read the alias + alias = dis.readUTF(); + + // Read the (entry creation) date + entry.date = new Date(dis.readLong()); + + // Read the private key + entry.protectedPrivKey = + IOUtils.readExactlyNBytes(dis, dis.readInt()); + + // Read the certificate chain + int numOfCerts = dis.readInt(); + if (numOfCerts > 0) { + List certs = new ArrayList<>( + Math.min(numOfCerts, 10)); + for (int j = 0; j < numOfCerts; j++) { + if (xVersion == 2) { + // read the certificate type, and instantiate a + // certificate factory of that type (reuse + // existing factory if possible) + String certType = dis.readUTF(); + if (cfs.containsKey(certType)) { + // reuse certificate factory + cf = cfs.get(certType); + } else { + // create new certificate factory + cf = PKIXInsts.getCertificateFactory(certType); + // store the certificate factory so we can + // reuse it later + cfs.put(certType, cf); + } + } + // instantiate the certificate + encoded = IOUtils.readExactlyNBytes(dis, dis.readInt()); + bais = new ByteArrayInputStream(encoded); + certs.add(cf.generateCertificate(bais)); + bais.close(); + } + // We can be sure now that numOfCerts of certs are read + entry.chain = certs.toArray(new Certificate[numOfCerts]); + } + + // Add the entry to the list + entries.put(alias, entry); + + } else if (tag == 2) { // trusted certificate entry + trustedKeyCount++; + TrustedCertEntry entry = new TrustedCertEntry(); + + // Read the alias + alias = dis.readUTF(); + + // Read the (entry creation) date + entry.date = new Date(dis.readLong()); + + // Read the trusted certificate + if (xVersion == 2) { + // read the certificate type, and instantiate a + // certificate factory of that type (reuse + // existing factory if possible) + String certType = dis.readUTF(); + if (cfs.containsKey(certType)) { + // reuse certificate factory + cf = cfs.get(certType); + } else { + // create new certificate factory + cf = PKIXInsts.getCertificateFactory(certType); + // store the certificate factory so we can + // reuse it later + cfs.put(certType, cf); + } + } + encoded = IOUtils.readExactlyNBytes(dis, dis.readInt()); + bais = new ByteArrayInputStream(encoded); + entry.cert = cf.generateCertificate(bais); + bais.close(); + + // Add the entry to the list + entries.put(alias, entry); + + } else { + throw new IOException("Unrecognized keystore entry: " + + tag); + } + } + + if (debug != null) { + debug.println("JavaKeyStore load: private key count: " + + privateKeyCount + ". trusted key count: " + trustedKeyCount); + } + + /* + * If a password has been provided, we check the keyed digest + * at the end. If this check fails, the store has been tampered + * with + */ + if (password != null) { + byte computed[], actual[]; + computed = md.digest(); + actual = IOUtils.readExactlyNBytes(dis, computed.length); + if (!MessageDigest.isEqual(computed, actual)) { + Throwable t = new UnrecoverableKeyException + ("Password verification failed"); + throw (IOException) new IOException + ("Keystore was tampered with, or " + + "password was incorrect").initCause(t); + } + } + } + } + + /** + * To guard against tampering with the keystore, we append a keyed + * hash with a bit of whitener. + */ + private MessageDigest getPreKeyedHash(char[] password) + throws NoSuchAlgorithmException, UnsupportedEncodingException + { + + MessageDigest md = CryptoInsts.getMessageDigest("SHA"); + byte[] passwdBytes = convertToBytes(password); + md.update(passwdBytes); + Arrays.fill(passwdBytes, (byte) 0x00); + md.update("Mighty Aphrodite".getBytes("UTF8")); + return md; + } + + /** + * Helper method to convert char[] to byte[] + */ + + private byte[] convertToBytes(char[] password) { + int i, j; + byte[] passwdBytes = new byte[password.length * 2]; + for (i=0, j=0; i> 8); + passwdBytes[j++] = (byte)password[i]; + } + return passwdBytes; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyProtector.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyProtector.java new file mode 100644 index 000000000..29441d7a8 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyProtector.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider; + +import java.io.IOException; +import java.security.Key; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.util.*; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.sun.security.pkcs.PKCS8Key; +import net.tongsuo.sun.security.pkcs.EncryptedPrivateKeyInfo; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.util.KnownOIDs; + +/** + * This is an implementation of a Sun proprietary, exportable algorithm + * intended for use when protecting (or recovering the cleartext version of) + * sensitive keys. + * This algorithm is not intended as a general purpose cipher. + * + * This is how the algorithm works for key protection: + * + * p - user password + * s - random salt + * X - xor key + * P - to-be-protected key + * Y - protected key + * R - what gets stored in the keystore + * + * Step 1: + * Take the user's password, append a random salt (of fixed size) to it, + * and hash it: d1 = digest(p, s) + * Store d1 in X. + * + * Step 2: + * Take the user's password, append the digest result from the previous step, + * and hash it: dn = digest(p, dn-1). + * Store dn in X (append it to the previously stored digests). + * Repeat this step until the length of X matches the length of the private key + * P. + * + * Step 3: + * XOR X and P, and store the result in Y: Y = X XOR P. + * + * Step 4: + * Store s, Y, and digest(p, P) in the result buffer R: + * R = s + Y + digest(p, P), where "+" denotes concatenation. + * (NOTE: digest(p, P) is stored in the result buffer, so that when the key is + * recovered, we can check if the recovered key indeed matches the original + * key.) R is stored in the keystore. + * + * The protected key is recovered as follows: + * + * Step1 and Step2 are the same as above, except that the salt is not randomly + * generated, but taken from the result R of step 4 (the first length(s) + * bytes). + * + * Step 3 (XOR operation) yields the plaintext key. + * + * Then concatenate the password with the recovered key, and compare with the + * last length(digest(p, P)) bytes of R. If they match, the recovered key is + * indeed the same key as the original key. + * + * @author Jan Luehe + * + * + * @see java.security.KeyStore + * @see JavaKeyStore + * + * @since 1.2 + */ + +final class KeyProtector { + + private static final int SALT_LEN = 20; // the salt length + private static final String DIGEST_ALG = "SHA"; + private static final int DIGEST_LEN = 20; + + // The password used for protecting/recovering keys passed through this + // key protector. We store it as a byte array, so that we can digest it. + private byte[] passwdBytes; + + private final MessageDigest md; + + + /** + * Creates an instance of this class, and initializes it with the given + * password. + */ + public KeyProtector(byte[] passwordBytes) + throws NoSuchAlgorithmException + { + if (passwordBytes == null) { + throw new IllegalArgumentException("password can't be null"); + } + md = CryptoInsts.getMessageDigest(DIGEST_ALG); + this.passwdBytes = passwordBytes; + } + + /* + * Protects the given plaintext key, using the password provided at + * construction time. + */ + public byte[] protect(Key key) throws KeyStoreException + { + int i; + int numRounds; + byte[] digest; + int xorOffset; // offset in xorKey where next digest will be stored + int encrKeyOffset = 0; + + if (key == null) { + throw new IllegalArgumentException("plaintext key can't be null"); + } + + if (!"PKCS#8".equalsIgnoreCase(key.getFormat())) { + throw new KeyStoreException( + "Cannot get key bytes, not PKCS#8 encoded"); + } + + byte[] plainKey = key.getEncoded(); + if (plainKey == null) { + throw new KeyStoreException( + "Cannot get key bytes, encoding not supported"); + } + + // Determine the number of digest rounds + numRounds = plainKey.length / DIGEST_LEN; + if ((plainKey.length % DIGEST_LEN) != 0) + numRounds++; + + // Create a random salt + byte[] salt = new byte[SALT_LEN]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + + // Set up the byte array which will be XORed with "plainKey" + byte[] xorKey = new byte[plainKey.length]; + + // Compute the digests, and store them in "xorKey" + for (i = 0, xorOffset = 0, digest = salt; + i < numRounds; + i++, xorOffset += DIGEST_LEN) { + md.update(passwdBytes); + md.update(digest); + digest = md.digest(); + md.reset(); + // Copy the digest into "xorKey" + if (i < numRounds - 1) { + System.arraycopy(digest, 0, xorKey, xorOffset, + digest.length); + } else { + System.arraycopy(digest, 0, xorKey, xorOffset, + xorKey.length - xorOffset); + } + } + + // XOR "plainKey" with "xorKey", and store the result in "tmpKey" + byte[] tmpKey = new byte[plainKey.length]; + for (i = 0; i < tmpKey.length; i++) { + tmpKey[i] = (byte)(plainKey[i] ^ xorKey[i]); + } + + // Store salt and "tmpKey" in "encrKey" + byte[] encrKey = new byte[salt.length + tmpKey.length + DIGEST_LEN]; + System.arraycopy(salt, 0, encrKey, encrKeyOffset, salt.length); + encrKeyOffset += salt.length; + System.arraycopy(tmpKey, 0, encrKey, encrKeyOffset, tmpKey.length); + encrKeyOffset += tmpKey.length; + + // Append digest(password, plainKey) as an integrity check to "encrKey" + md.update(passwdBytes); + Arrays.fill(passwdBytes, (byte)0x00); + passwdBytes = null; + md.update(plainKey); + digest = md.digest(); + md.reset(); + System.arraycopy(digest, 0, encrKey, encrKeyOffset, digest.length); + Arrays.fill(plainKey, (byte)0); + + // wrap the protected private key in a PKCS#8-style + // EncryptedPrivateKeyInfo, and returns its encoding + AlgorithmId encrAlg = new AlgorithmId(Oid.of + (KnownOIDs.JAVASOFT_JDKKeyProtector)); + return new EncryptedPrivateKeyInfo(encrAlg,encrKey).getEncoded(); + } + + /* + * Recovers the plaintext version of the given key (in protected format), + * using the password provided at construction time. + */ + public Key recover(EncryptedPrivateKeyInfo encrInfo) + throws UnrecoverableKeyException + { + int i; + byte[] digest; + int numRounds; + int xorOffset; // offset in xorKey where next digest will be stored + int encrKeyLen; // the length of the encrypted key + + // do we support the algorithm? + AlgorithmId encrAlg = encrInfo.getAlgorithm(); + if (!(encrAlg.getOID().toString().equals + (KnownOIDs.JAVASOFT_JDKKeyProtector.value()))) { + throw new UnrecoverableKeyException("Unsupported key protection " + + "algorithm"); + } + + byte[] protectedKey = encrInfo.getEncryptedData(); + + /* + * Get the salt associated with this key (the first SALT_LEN bytes of + * protectedKey) + */ + byte[] salt = new byte[SALT_LEN]; + System.arraycopy(protectedKey, 0, salt, 0, SALT_LEN); + + // Determine the number of digest rounds + encrKeyLen = protectedKey.length - SALT_LEN - DIGEST_LEN; + numRounds = encrKeyLen / DIGEST_LEN; + if ((encrKeyLen % DIGEST_LEN) != 0) numRounds++; + + // Get the encrypted key portion and store it in "encrKey" + byte[] encrKey = new byte[encrKeyLen]; + System.arraycopy(protectedKey, SALT_LEN, encrKey, 0, encrKeyLen); + + // Set up the byte array which will be XORed with "encrKey" + byte[] xorKey = new byte[encrKey.length]; + + // Compute the digests, and store them in "xorKey" + for (i = 0, xorOffset = 0, digest = salt; + i < numRounds; + i++, xorOffset += DIGEST_LEN) { + md.update(passwdBytes); + md.update(digest); + digest = md.digest(); + md.reset(); + // Copy the digest into "xorKey" + if (i < numRounds - 1) { + System.arraycopy(digest, 0, xorKey, xorOffset, + digest.length); + } else { + System.arraycopy(digest, 0, xorKey, xorOffset, + xorKey.length - xorOffset); + } + } + + // XOR "encrKey" with "xorKey", and store the result in "plainKey" + byte[] plainKey = new byte[encrKey.length]; + for (i = 0; i < plainKey.length; i++) { + plainKey[i] = (byte)(encrKey[i] ^ xorKey[i]); + } + + /* + * Check the integrity of the recovered key by concatenating it with + * the password, digesting the concatenation, and comparing the + * result of the digest operation with the digest provided at the end + * of protectedKey. If the two digest values are + * different, throw an exception. + */ + md.update(passwdBytes); + Arrays.fill(passwdBytes, (byte)0x00); + passwdBytes = null; + md.update(plainKey); + digest = md.digest(); + md.reset(); + for (i = 0; i < digest.length; i++) { + if (digest[i] != protectedKey[SALT_LEN + encrKeyLen + i]) { + throw new UnrecoverableKeyException("Cannot recover key"); + } + } + + // The parseKey() method of PKCS8Key parses the key + // algorithm and instantiates the appropriate key factory, + // which in turn parses the key material. + try { + return PKCS8Key.parseKey(plainKey); + } catch (IOException ioe) { + throw new UnrecoverableKeyException(ioe.getMessage()); + } finally { + Arrays.fill(plainKey, (byte)0); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyStoreDelegator.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyStoreDelegator.java new file mode 100644 index 000000000..62e3b3525 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/KeyStoreDelegator.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider; + +import java.io.*; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.*; + +import net.tongsuo.sun.security.util.Debug; + +/** + * This class delegates to a primary or secondary keystore implementation. + * + * @since 9 + */ + +public class KeyStoreDelegator extends KeyStoreSpi { + + private static final String KEYSTORE_TYPE_COMPAT = "keystore.type.compat"; + private static final Debug debug = Debug.getInstance("keystore"); + + private final String primaryType; // the primary keystore's type + private final String secondaryType; // the secondary keystore's type + private final Class primaryKeyStore; + // the primary keystore's class + private final Class secondaryKeyStore; + // the secondary keystore's class + private String type; // the delegate's type + private KeyStoreSpi keystore; // the delegate + private boolean compatModeEnabled = true; + + public KeyStoreDelegator( + String primaryType, + Class primaryKeyStore, + String secondaryType, + Class secondaryKeyStore) { + + // Check whether compatibility mode has been disabled + @SuppressWarnings("removal") + String prop = AccessController.doPrivileged((PrivilegedAction) () -> + Security.getProperty(KEYSTORE_TYPE_COMPAT)); + compatModeEnabled = "true".equalsIgnoreCase(prop); + + if (compatModeEnabled) { + this.primaryType = primaryType; + this.secondaryType = secondaryType; + this.primaryKeyStore = primaryKeyStore; + this.secondaryKeyStore = secondaryKeyStore; + } else { + this.primaryType = primaryType; + this.secondaryType = null; + this.primaryKeyStore = primaryKeyStore; + this.secondaryKeyStore = null; + + if (debug != null) { + debug.println("WARNING: compatibility mode disabled for " + + primaryType + " and " + secondaryType + " keystore types"); + } + } + } + + @Override + public Key engineGetKey(String alias, char[] password) + throws NoSuchAlgorithmException, UnrecoverableKeyException { + return keystore.engineGetKey(alias, password); + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return keystore.engineGetCertificateChain(alias); + } + + @Override + public Certificate engineGetCertificate(String alias) { + return keystore.engineGetCertificate(alias); + } + + @Override + public Date engineGetCreationDate(String alias) { + return keystore.engineGetCreationDate(alias); + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, + Certificate[] chain) throws KeyStoreException { + keystore.engineSetKeyEntry(alias, key, password, chain); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) + throws KeyStoreException { + keystore.engineSetKeyEntry(alias, key, chain); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) + throws KeyStoreException { + keystore.engineSetCertificateEntry(alias, cert); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + keystore.engineDeleteEntry(alias); + } + + @Override + public Enumeration engineAliases() { + return keystore.engineAliases(); + } + + @Override + public boolean engineContainsAlias(String alias) { + return keystore.engineContainsAlias(alias); + } + + @Override + public int engineSize() { + return keystore.engineSize(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return keystore.engineIsKeyEntry(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return keystore.engineIsCertificateEntry(alias); + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + return keystore.engineGetCertificateAlias(cert); + } + + @Override + public KeyStore.Entry engineGetEntry(String alias, + KeyStore.ProtectionParameter protParam) + throws KeyStoreException, NoSuchAlgorithmException, + UnrecoverableEntryException { + return keystore.engineGetEntry(alias, protParam); + } + + @Override + public void engineSetEntry(String alias, KeyStore.Entry entry, + KeyStore.ProtectionParameter protParam) + throws KeyStoreException { + keystore.engineSetEntry(alias, entry, protParam); + } + + @Override + public boolean engineEntryInstanceOf(String alias, + Class entryClass) { + return keystore.engineEntryInstanceOf(alias, entryClass); + } + + @Override + public void engineStore(OutputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException { + + if (debug != null) { + debug.println("Storing keystore in " + type + " format"); + } + keystore.engineStore(stream, password); + } + + @Override + public void engineLoad(InputStream stream, char[] password) + throws IOException, NoSuchAlgorithmException, CertificateException { + + // A new keystore is always created in the primary keystore format + if (stream == null) { + try { + @SuppressWarnings("deprecation") + KeyStoreSpi tmp = primaryKeyStore.newInstance(); + keystore = tmp; + } catch (InstantiationException | IllegalAccessException e) { + // can safely ignore + } + type = primaryType; + + if (debug != null) { + debug.println("Creating a new keystore in " + type + " format"); + } + keystore.engineLoad(stream, password); + + } else { + // First try the primary keystore then try the secondary keystore + InputStream bufferedStream = new BufferedInputStream(stream); + bufferedStream.mark(Integer.MAX_VALUE); + + try { + @SuppressWarnings("deprecation") + KeyStoreSpi tmp = primaryKeyStore.newInstance(); + tmp.engineLoad(bufferedStream, password); + keystore = tmp; + type = primaryType; + + } catch (Exception e) { + + // incorrect password + if (e instanceof IOException && + e.getCause() instanceof UnrecoverableKeyException) { + throw (IOException)e; + } + + try { + // Ignore secondary keystore when no compatibility mode + if (!compatModeEnabled) { + throw e; + } + + @SuppressWarnings("deprecation") + KeyStoreSpi tmp = secondaryKeyStore.newInstance(); + bufferedStream.reset(); + tmp.engineLoad(bufferedStream, password); + keystore = tmp; + type = secondaryType; + + if (debug != null) { + debug.println("WARNING: switching from " + + primaryType + " to " + secondaryType + + " keystore file format has altered the " + + "keystore security level"); + } + + } catch (InstantiationException | + IllegalAccessException e2) { + // can safely ignore + + } catch (IOException | + NoSuchAlgorithmException | + CertificateException e3) { + + // incorrect password + if (e3 instanceof IOException && + e3.getCause() instanceof UnrecoverableKeyException) { + throw (IOException)e3; + } + // rethrow the outer exception + if (e instanceof IOException) { + throw (IOException)e; + } else if (e instanceof CertificateException) { + throw (CertificateException)e; + } else if (e instanceof NoSuchAlgorithmException) { + throw (NoSuchAlgorithmException)e; + } else if (e instanceof RuntimeException){ + throw (RuntimeException)e; + } + } + } + + if (debug != null) { + debug.println("Loaded a keystore in " + type + " format"); + } + } + } + + /** + * Probe the first few bytes of the keystore data stream for a valid + * keystore encoding. Only the primary keystore implementation is probed. + */ +// @Override +// public boolean engineProbe(InputStream stream) throws IOException { +// +// boolean result = false; +// +// try { +// @SuppressWarnings("deprecation") +// KeyStoreSpi tmp = primaryKeyStore.newInstance(); +// keystore = tmp; +// type = primaryType; +// result = keystore.engineProbe(stream); +// +// } catch (Exception e) { +// throw new IOException(e); +// +// } finally { +// // reset +// if (!result) { +// type = null; +// keystore = null; +// } +// } +// +// return result; +// } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/X509Factory.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/X509Factory.java new file mode 100644 index 000000000..f027c550a --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/X509Factory.java @@ -0,0 +1,777 @@ +/* + * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider; + +import net.tongsuo.sun.security.provider.certpath.X509CertPath; +import net.tongsuo.sun.security.provider.certpath.X509CertificatePair; +import net.tongsuo.sun.security.pkcs.PKCS7; +import net.tongsuo.sun.security.pkcs.ParsingException; +import net.tongsuo.sun.security.util.Cache; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.Pem; +import net.tongsuo.sun.security.x509.X509CRLImpl; +import net.tongsuo.sun.security.x509.X509CertImpl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.security.cert.CRL; +import java.security.cert.CRLException; +import java.security.cert.CertPath; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactorySpi; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * This class defines a certificate factory for X.509 v3 certificates & + * certification paths, and X.509 v2 certificate revocation lists (CRLs). + * + * @author Jan Luehe + * @author Hemma Prafullchandra + * @author Sean Mullan + * + * + * @see CertificateFactorySpi + * @see Certificate + * @see CertPath + * @see CRL + * @see X509Certificate + * @see X509CRL + * @see X509CertImpl + * @see X509CRLImpl + */ + +public class X509Factory extends CertificateFactorySpi { + + public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + public static final String END_CERT = "-----END CERTIFICATE-----"; + + private static final int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX + + private static final Cache certCache + = Cache.newSoftMemoryCache(750); + private static final Cache crlCache + = Cache.newSoftMemoryCache(750); + + /** + * Generates an X.509 certificate object and initializes it with + * the data read from the input stream is. + * + * @param is an input stream with the certificate data. + * + * @return an X.509 certificate object initialized with the data + * from the input stream. + * + * @exception CertificateException on parsing errors. + */ + @Override + public Certificate engineGenerateCertificate(InputStream is) + throws CertificateException + { + if (is == null) { + // clear the caches (for debugging) + certCache.clear(); + X509CertificatePair.clearCache(); + throw new CertificateException("Missing input stream"); + } + try { + byte[] encoding = readOneBlock(is); + if (encoding != null) { + return cachedGetX509Cert(encoding); + } else { + throw new IOException("Empty input"); + } + } catch (IOException ioe) { + throw new CertificateException("Could not parse certificate: " + + ioe.toString(), ioe); + } + } + + public static X509CertImpl cachedGetX509Cert(byte[] encoding) + throws CertificateException { + X509CertImpl cert = getFromCache(certCache, encoding); + if (cert != null) { + return cert; + } + cert = new X509CertImpl(encoding); + addToCache(certCache, cert.getEncodedInternal(), cert); + return cert; + } + + /** + * Read from the stream until length bytes have been read or EOF has + * been reached. Return the number of bytes actually read. + */ + private static int readFully(InputStream in, ByteArrayOutputStream bout, + int length) throws IOException { + int read = 0; + byte[] buffer = new byte[2048]; + while (length > 0) { + int n = in.read(buffer, 0, Math.min(length, 2048)); + if (n <= 0) { + break; + } + bout.write(buffer, 0, n); + read += n; + length -= n; + } + return read; + } + + /** + * Return an interned X509CertImpl for the given certificate. + * If the given X509Certificate or X509CertImpl is already present + * in the cert cache, the cached object is returned. Otherwise, + * if it is a X509Certificate, it is first converted to a X509CertImpl. + * Then the X509CertImpl is added to the cache and returned. + * + * Note that all certificates created via generateCertificate(InputStream) + * are already interned and this method does not need to be called. + * It is useful for certificates that cannot be created via + * generateCertificate() and for converting other X509Certificate + * implementations to an X509CertImpl. + * + * @param c The source X509Certificate + * @return An X509CertImpl object that is either a cached certificate or a + * newly built X509CertImpl from the provided X509Certificate + * @throws CertificateException if failures occur while obtaining the DER + * encoding for certificate data. + */ + public static synchronized X509CertImpl intern(X509Certificate c) + throws CertificateException { + if (c == null) { + return null; + } + boolean isImpl = c instanceof X509CertImpl; + byte[] encoding; + if (isImpl) { + encoding = ((X509CertImpl)c).getEncodedInternal(); + } else { + encoding = c.getEncoded(); + } + X509CertImpl newC = getFromCache(certCache, encoding); + if (newC != null) { + return newC; + } + if (isImpl) { + newC = (X509CertImpl)c; + } else { + newC = new X509CertImpl(encoding); + encoding = newC.getEncodedInternal(); + } + addToCache(certCache, encoding, newC); + return newC; + } + + /** + * Return an interned X509CRLImpl for the given certificate. + * For more information, see intern(X509Certificate). + * + * @param c The source X509CRL + * @return An X509CRLImpl object that is either a cached CRL or a + * newly built X509CRLImpl from the provided X509CRL + * @throws CRLException if failures occur while obtaining the DER + * encoding for CRL data. + */ + public static synchronized X509CRLImpl intern(X509CRL c) + throws CRLException { + if (c == null) { + return null; + } + boolean isImpl = c instanceof X509CRLImpl; + byte[] encoding; + if (isImpl) { + encoding = ((X509CRLImpl)c).getEncodedInternal(); + } else { + encoding = c.getEncoded(); + } + X509CRLImpl newC = getFromCache(crlCache, encoding); + if (newC != null) { + return newC; + } + if (isImpl) { + newC = (X509CRLImpl)c; + } else { + newC = new X509CRLImpl(encoding); + encoding = newC.getEncodedInternal(); + } + addToCache(crlCache, encoding, newC); + return newC; + } + + /** + * Get the X509CertImpl or X509CRLImpl from the cache. + */ + private static synchronized V getFromCache(Cache cache, + byte[] encoding) { + Object key = new Cache.EqualByteArray(encoding); + return cache.get(key); + } + + /** + * Add the X509CertImpl or X509CRLImpl to the cache. + */ + private static synchronized void addToCache(Cache cache, + byte[] encoding, V value) { + if (encoding.length > ENC_MAX_LENGTH) { + return; + } + Object key = new Cache.EqualByteArray(encoding); + cache.put(key, value); + } + + /** + * Generates a CertPath object and initializes it with + * the data read from the InputStream inStream. The data + * is assumed to be in the default encoding. + * + * @param inStream an InputStream containing the data + * @return a CertPath initialized with the data from the + * InputStream + * @exception CertificateException if an exception occurs while decoding + * @since 1.4 + */ + @Override + public CertPath engineGenerateCertPath(InputStream inStream) + throws CertificateException + { + if (inStream == null) { + throw new CertificateException("Missing input stream"); + } + try { + byte[] encoding = readOneBlock(inStream); + if (encoding != null) { + return new X509CertPath(new ByteArrayInputStream(encoding)); + } else { + throw new IOException("Empty input"); + } + } catch (IOException ioe) { + throw new CertificateException(ioe.getMessage()); + } + } + + /** + * Generates a CertPath object and initializes it with + * the data read from the InputStream inStream. The data + * is assumed to be in the specified encoding. + * + * @param inStream an InputStream containing the data + * @param encoding the encoding used for the data + * @return a CertPath initialized with the data from the + * InputStream + * @exception CertificateException if an exception occurs while decoding or + * the encoding requested is not supported + * @since 1.4 + */ + @Override + public CertPath engineGenerateCertPath(InputStream inStream, + String encoding) throws CertificateException + { + if (inStream == null) { + throw new CertificateException("Missing input stream"); + } + try { + byte[] data = readOneBlock(inStream); + if (data != null) { + return new X509CertPath(new ByteArrayInputStream(data), encoding); + } else { + throw new IOException("Empty input"); + } + } catch (IOException ioe) { + throw new CertificateException(ioe.getMessage()); + } + } + + /** + * Generates a CertPath object and initializes it with + * a List of Certificates. + *

+ * The certificates supplied must be of a type supported by the + * CertificateFactory. They will be copied out of the supplied + * List object. + * + * @param certificates a List of Certificates + * @return a CertPath initialized with the supplied list of + * certificates + * @exception CertificateException if an exception occurs + * @since 1.4 + */ + @Override + public CertPath + engineGenerateCertPath(List certificates) + throws CertificateException + { + return(new X509CertPath(certificates)); + } + + /** + * Returns an iteration of the CertPath encodings supported + * by this certificate factory, with the default encoding first. + *

+ * Attempts to modify the returned Iterator via its + * remove method result in an + * UnsupportedOperationException. + * + * @return an Iterator over the names of the supported + * CertPath encodings (as Strings) + * @since 1.4 + */ + @Override + public Iterator engineGetCertPathEncodings() { + return(X509CertPath.getEncodingsStatic()); + } + + /** + * Returns a (possibly empty) collection view of X.509 certificates read + * from the given input stream is. + * + * @param is the input stream with the certificates. + * + * @return a (possibly empty) collection view of X.509 certificate objects + * initialized with the data from the input stream. + * + * @exception CertificateException on parsing errors. + */ + @Override + public Collection + engineGenerateCertificates(InputStream is) + throws CertificateException { + if (is == null) { + throw new CertificateException("Missing input stream"); + } + try { + return parseX509orPKCS7Cert(is); + } catch (IOException ioe) { + throw new CertificateException(ioe); + } + } + + /** + * Generates an X.509 certificate revocation list (CRL) object and + * initializes it with the data read from the given input stream + * is. + * + * @param is an input stream with the CRL data. + * + * @return an X.509 CRL object initialized with the data + * from the input stream. + * + * @exception CRLException on parsing errors. + */ + @Override + public CRL engineGenerateCRL(InputStream is) + throws CRLException + { + if (is == null) { + // clear the cache (for debugging) + crlCache.clear(); + throw new CRLException("Missing input stream"); + } + try { + byte[] encoding = readOneBlock(is); + if (encoding != null) { + X509CRLImpl crl = getFromCache(crlCache, encoding); + if (crl != null) { + return crl; + } + crl = new X509CRLImpl(encoding); + addToCache(crlCache, crl.getEncodedInternal(), crl); + return crl; + } else { + throw new IOException("Empty input"); + } + } catch (IOException ioe) { + throw new CRLException(ioe.getMessage()); + } + } + + /** + * Returns a (possibly empty) collection view of X.509 CRLs read + * from the given input stream is. + * + * @param is the input stream with the CRLs. + * + * @return a (possibly empty) collection view of X.509 CRL objects + * initialized with the data from the input stream. + * + * @exception CRLException on parsing errors. + */ + @Override + public Collection engineGenerateCRLs( + InputStream is) throws CRLException + { + if (is == null) { + throw new CRLException("Missing input stream"); + } + try { + return parseX509orPKCS7CRL(is); + } catch (IOException ioe) { + throw new CRLException(ioe.getMessage()); + } + } + + /* + * Parses the data in the given input stream as a sequence of DER + * encoded X.509 certificates (in binary or base 64 encoded format) OR + * as a single PKCS#7 encoded blob (in binary or base64 encoded format). + */ + private Collection + parseX509orPKCS7Cert(InputStream is) + throws CertificateException, IOException + { + int peekByte; + byte[] data; + PushbackInputStream pbis = new PushbackInputStream(is); + Collection coll = new ArrayList<>(); + + // Test the InputStream for end-of-stream. If the stream's + // initial state is already at end-of-stream then return + // an empty collection. Otherwise, push the byte back into the + // stream and let readOneBlock look for the first certificate. + peekByte = pbis.read(); + if (peekByte == -1) { + return new ArrayList<>(0); + } else { + pbis.unread(peekByte); + data = readOneBlock(pbis); + } + + // If we end up with a null value after reading the first block + // then we know the end-of-stream has been reached and no certificate + // data has been found. + if (data == null) { + throw new CertificateException("No certificate data found"); + } + + try { + PKCS7 pkcs7 = new PKCS7(data); + X509Certificate[] certs = pkcs7.getCertificates(); + // certs are optional in PKCS #7 + if (certs != null) { + return Arrays.asList(certs); + } else { + // no certificates provided + return new ArrayList<>(0); + } + } catch (ParsingException e) { + while (data != null) { + coll.add(new X509CertImpl(data)); + data = readOneBlock(pbis); + } + } + return coll; + } + + /* + * Parses the data in the given input stream as a sequence of DER encoded + * X.509 CRLs (in binary or base 64 encoded format) OR as a single PKCS#7 + * encoded blob (in binary or base 64 encoded format). + */ + private Collection + parseX509orPKCS7CRL(InputStream is) + throws CRLException, IOException + { + int peekByte; + byte[] data; + PushbackInputStream pbis = new PushbackInputStream(is); + Collection coll = new ArrayList<>(); + + // Test the InputStream for end-of-stream. If the stream's + // initial state is already at end-of-stream then return + // an empty collection. Otherwise, push the byte back into the + // stream and let readOneBlock look for the first CRL. + peekByte = pbis.read(); + if (peekByte == -1) { + return new ArrayList<>(0); + } else { + pbis.unread(peekByte); + data = readOneBlock(pbis); + } + + // If we end up with a null value after reading the first block + // then we know the end-of-stream has been reached and no CRL + // data has been found. + if (data == null) { + throw new CRLException("No CRL data found"); + } + + try { + PKCS7 pkcs7 = new PKCS7(data); + X509CRL[] crls = pkcs7.getCRLs(); + // CRLs are optional in PKCS #7 + if (crls != null) { + return Arrays.asList(crls); + } else { + // no crls provided + return new ArrayList<>(0); + } + } catch (ParsingException e) { + while (data != null) { + coll.add(new X509CRLImpl(data)); + data = readOneBlock(pbis); + } + } + return coll; + } + + /** + * Returns an ASN.1 SEQUENCE from a stream, which might be a BER-encoded + * binary block or a PEM-style BASE64-encoded ASCII data. In the latter + * case, it's de-BASE64'ed before return. + * + * After the reading, the input stream pointer is after the BER block, or + * after the newline character after the -----END SOMETHING----- line. + * + * @param is the InputStream + * @return byte block or null if end of stream + * @throws IOException If any parsing error + */ + private static byte[] readOneBlock(InputStream is) throws IOException { + + // The first character of a BLOCK. + int c = is.read(); + if (c == -1) { + return null; + } + if (c == DerValue.tag_Sequence) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(2048); + bout.write(c); + readBERInternal(is, bout, c); + return bout.toByteArray(); + } else { + // Read BASE64 encoded data, might skip info at the beginning + char[] data = new char[2048]; + int pos = 0; + + // Step 1: Read until header is found + int hyphen = (c=='-') ? 1: 0; // count of consequent hyphens + int last = (c=='-') ? -1: c; // the char before hyphen + while (true) { + int next = is.read(); + if (next == -1) { + // We accept useless data after the last block, + // say, empty lines. + return null; + } + if (next == '-') { + hyphen++; + } else { + hyphen = 0; + last = next; + } + if (hyphen == 5 && (last == -1 || last == '\r' || last == '\n')) { + break; + } + } + + // Step 2: Read the rest of header, determine the line end + int end; + StringBuilder header = new StringBuilder("-----"); + while (true) { + int next = is.read(); + if (next == -1) { + throw new IOException("Incomplete data"); + } + if (next == '\n') { + end = '\n'; + break; + } + if (next == '\r') { + next = is.read(); + if (next == -1) { + throw new IOException("Incomplete data"); + } + if (next == '\n') { + end = '\n'; + } else { + end = '\r'; + data[pos++] = (char)next; + } + break; + } + header.append((char)next); + } + + // Step 3: Read the data + while (true) { + int next = is.read(); + if (next == -1) { + throw new IOException("Incomplete data"); + } + if (next != '-') { + data[pos++] = (char)next; + if (pos >= data.length) { + data = Arrays.copyOf(data, data.length+1024); + } + } else { + break; + } + } + + // Step 4: Consume the footer + StringBuilder footer = new StringBuilder("-"); + while (true) { + int next = is.read(); + // Add next == '\n' for maximum safety, in case endline + // is not consistent. + if (next == -1 || next == end || next == '\n') { + break; + } + if (next != '\r') footer.append((char)next); + } + + checkHeaderFooter(header.toString(), footer.toString()); + + return Pem.decode(new String(data, 0, pos)); + } + } + + private static void checkHeaderFooter(String header, + String footer) throws IOException { + if (header.length() < 16 || !header.startsWith("-----BEGIN ") || + !header.endsWith("-----")) { + throw new IOException("Illegal header: " + header); + } + if (footer.length() < 14 || !footer.startsWith("-----END ") || + !footer.endsWith("-----")) { + throw new IOException("Illegal footer: " + footer); + } + String headerType = header.substring(11, header.length()-5); + String footerType = footer.substring(9, footer.length()-5); + if (!headerType.equals(footerType)) { + throw new IOException("Header and footer do not match: " + + header + " " + footer); + } + } + + /** + * Read one BER data block. This method is aware of indefinite-length BER + * encoding and will read all the subsections in a recursive way + * + * @param is Read from this InputStream + * @param bout Write into this OutputStream + * @param tag Tag already read (-1 mean not read) + * @return The current tag, used to check EOC in indefinite-length BER + * @throws IOException Any parsing error + */ + private static int readBERInternal(InputStream is, + ByteArrayOutputStream bout, int tag) throws IOException { + + if (tag == -1) { // Not read before the call, read now + tag = is.read(); + if (tag == -1) { + throw new IOException("BER/DER tag info absent"); + } + if ((tag & 0x1f) == 0x1f) { + throw new IOException("Multi octets tag not supported"); + } + bout.write(tag); + } + + int n = is.read(); + if (n == -1) { + throw new IOException("BER/DER length info absent"); + } + bout.write(n); + + int length; + + if (n == 0x80) { // Indefinite-length encoding + if ((tag & 0x20) != 0x20) { + throw new IOException( + "Non constructed encoding must have definite length"); + } + while (true) { + int subTag = readBERInternal(is, bout, -1); + if (subTag == 0) { // EOC, end of indefinite-length section + break; + } + } + } else { + if (n < 0x80) { + length = n; + } else if (n == 0x81) { + length = is.read(); + if (length == -1) { + throw new IOException("Incomplete BER/DER length info"); + } + bout.write(length); + } else if (n == 0x82) { + int highByte = is.read(); + int lowByte = is.read(); + if (lowByte == -1) { + throw new IOException("Incomplete BER/DER length info"); + } + bout.write(highByte); + bout.write(lowByte); + length = (highByte << 8) | lowByte; + } else if (n == 0x83) { + int highByte = is.read(); + int midByte = is.read(); + int lowByte = is.read(); + if (lowByte == -1) { + throw new IOException("Incomplete BER/DER length info"); + } + bout.write(highByte); + bout.write(midByte); + bout.write(lowByte); + length = (highByte << 16) | (midByte << 8) | lowByte; + } else if (n == 0x84) { + int highByte = is.read(); + int nextByte = is.read(); + int midByte = is.read(); + int lowByte = is.read(); + if (lowByte == -1) { + throw new IOException("Incomplete BER/DER length info"); + } + if (highByte > 127) { + throw new IOException("Invalid BER/DER data (a little huge?)"); + } + bout.write(highByte); + bout.write(nextByte); + bout.write(midByte); + bout.write(lowByte); + length = (highByte << 24 ) | (nextByte << 16) | + (midByte << 8) | lowByte; + } else { // ignore longer length forms + throw new IOException("Invalid BER/DER data (too huge?)"); + } + if (readFully(is, bout, length) != length) { + throw new IOException("Incomplete BER/DER data"); + } + } + return tag; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdaptableX509CertSelector.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdaptableX509CertSelector.java new file mode 100644 index 000000000..2812455b1 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdaptableX509CertSelector.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.cert.X509CertSelector; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Date; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.x509.AuthorityKeyIdentifierExtension; +import net.tongsuo.sun.security.x509.SerialNumber; + +/** + * An adaptable X509 certificate selector for forward certification path + * building. This selector overrides the default X509CertSelector matching + * rules for the subjectKeyIdentifier and serialNumber criteria, and adds + * additional rules for certificate validity. + * + * @since 1.7 + */ +class AdaptableX509CertSelector extends X509CertSelector { + + private static final Debug debug = Debug.getInstance("certpath"); + + // The start date of a validity period. + private Date startDate; + + // The end date of a validity period. + private Date endDate; + + // The subject key identifier + private byte[] ski; + + // The serial number + private BigInteger serial; + + /** + * Sets the criterion of the X509Certificate validity period. + * + * Normally, we may not have to check that a certificate validity period + * must fall within its issuer's certificate validity period. However, + * when we face root CA key updates for version 1 certificates, according + * to scheme of RFC 4210 or 2510, the validity periods should be checked + * to determine the right issuer's certificate. + * + * Conservatively, we will only check the validity periods for version + * 1 and version 2 certificates. For version 3 certificates, we can + * determine the right issuer by authority and subject key identifier + * extensions. + * + * @param startDate the start date of a validity period that must fall + * within the certificate validity period for the X509Certificate + * @param endDate the end date of a validity period that must fall + * within the certificate validity period for the X509Certificate + */ + void setValidityPeriod(Date startDate, Date endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + /** + * This selector overrides the subjectKeyIdentifier matching rules of + * X509CertSelector, so it throws IllegalArgumentException if this method + * is ever called. + */ + @Override + public void setSubjectKeyIdentifier(byte[] subjectKeyID) { + throw new IllegalArgumentException(); + } + + /** + * This selector overrides the serialNumber matching rules of + * X509CertSelector, so it throws IllegalArgumentException if this method + * is ever called. + */ + @Override + public void setSerialNumber(BigInteger serial) { + throw new IllegalArgumentException(); + } + + /** + * Sets the subjectKeyIdentifier and serialNumber criteria from the + * authority key identifier extension. + * + * The subjectKeyIdentifier criterion is set to the keyIdentifier field + * of the extension, or null if it is empty. The serialNumber criterion + * is set to the authorityCertSerialNumber field, or null if it is empty. + * + * Note that we do not set the subject criterion to the + * authorityCertIssuer field of the extension. The caller MUST set + * the subject criterion before calling match(). + * + * @param ext the authorityKeyIdentifier extension + * @throws IOException if there is an error parsing the extension + */ + void setSkiAndSerialNumber(AuthorityKeyIdentifierExtension ext) + throws IOException { + + ski = null; + serial = null; + + if (ext != null) { + ski = ext.getEncodedKeyIdentifier(); + SerialNumber asn = ext.getSerialNumber(); + if (asn != null) { + serial = asn.getNumber(); + } + // the subject criterion should be set by the caller + } + } + + /** + * Decides whether a Certificate should be selected. + * + * This method overrides the matching rules for the subjectKeyIdentifier + * and serialNumber criteria and adds additional rules for certificate + * validity. + * + * For the purpose of compatibility, when a certificate is of + * version 1 and version 2, or the certificate does not include + * a subject key identifier extension, the selection criterion + * of subjectKeyIdentifier will be disabled. + */ + @Override + public boolean match(Certificate cert) { + X509Certificate xcert = (X509Certificate)cert; + + // match subject key identifier + if (!matchSubjectKeyID(xcert)) { + return false; + } + + // In practice, a CA may replace its root certificate and require that + // the existing certificate is still valid, even if the AKID extension + // does not match the replacement root certificate fields. + // + // Conservatively, we only support the replacement for version 1 and + // version 2 certificate. As for version 3, the certificate extension + // may contain sensitive information (for example, policies), the + // AKID need to be respected to seek the exact certificate in case + // of key or certificate abuse. + int version = xcert.getVersion(); + if (serial != null && version > 2) { + if (!serial.equals(xcert.getSerialNumber())) { + return false; + } + } + + // Check the validity period for version 1 and 2 certificate. + if (version < 3) { + if (startDate != null) { + try { + xcert.checkValidity(startDate); + } catch (CertificateException ce) { + return false; + } + } + if (endDate != null) { + try { + xcert.checkValidity(endDate); + } catch (CertificateException ce) { + return false; + } + } + } + + return super.match(cert); + } + + /* + * Match on subject key identifier extension value. These matching rules + * are identical to X509CertSelector except that if the certificate does + * not have a subject key identifier extension, it returns true. + */ + private boolean matchSubjectKeyID(X509Certificate xcert) { + if (ski == null) { + return true; + } + try { + byte[] extVal = xcert.getExtensionValue( + KnownOIDs.SubjectKeyID.value()); + if (extVal == null) { + if (debug != null && Debug.isOn("verbose")) { + debug.println("AdaptableX509CertSelector.match: " + + "no subject key ID extension. Subject: " + + xcert.getSubjectX500Principal()); + } + return true; + } + DerInputStream in = new DerInputStream(extVal); + byte[] certSubjectKeyID = in.getOctetString(); + if (certSubjectKeyID == null || + !Arrays.equals(ski, certSubjectKeyID)) { + if (debug != null && Debug.isOn("verbose")) { + debug.println("AdaptableX509CertSelector.match: " + + "subject key IDs don't match. " + + "Expected: " + Arrays.toString(ski) + " " + + "Cert's: " + Arrays.toString(certSubjectKeyID)); + } + return false; + } + } catch (IOException ex) { + if (debug != null && Debug.isOn("verbose")) { + debug.println("AdaptableX509CertSelector.match: " + + "exception in subject key ID check"); + } + return false; + } + return true; + } + + @Override + public Object clone() { + AdaptableX509CertSelector copy = + (AdaptableX509CertSelector)super.clone(); + if (startDate != null) { + copy.startDate = (Date)startDate.clone(); + } + + if (endDate != null) { + copy.endDate = (Date)endDate.clone(); + } + + if (ski != null) { + copy.ski = ski.clone(); + } + return copy; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdjacencyList.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdjacencyList.java new file mode 100644 index 000000000..888bc4e58 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AdjacencyList.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package net.tongsuo.sun.security.provider.certpath; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * An AdjacencyList is used to store the history of certification paths + * attempted in constructing a path from an initiator to a target. The + * AdjacencyList is initialized with a List of + * Lists, where each sub-List contains objects of + * type Vertex. A Vertex describes one possible or + * actual step in the chain building process, and the associated + * Certificate. Specifically, a Vertex object + * contains a Certificate and an index value referencing the + * next sub-list in the process. If the index value is -1 then this + * Vertex doesn't continue the attempted build path. + *

+ * Example: + *

+ * Attempted Paths:

    + *
  • C1->C2->C3 + *
  • C1->C4->C5 + *
  • C1->C4->C6 + *
  • C1->C4->C7 + *
  • C1->C8->C9 + *
  • C1->C10->C11 + *
+ *

+ * AdjacencyList structure:

    + *
  • AL[0] = C1,1 + *
  • AL[1] = C2,2 =>C4,3 =>C8,4 =>C10,5 + *
  • AL[2] = C3,-1 + *
  • AL[3] = C5,-1 =>C6,-1 =>C7,-1 + *
  • AL[4] = C9,-1 + *
  • AL[5] = C11,-1 + *
+ *

+ * The iterator method returns objects of type BuildStep, not + * objects of type Vertex. + * A BuildStep contains a Vertex and a result code, + * accessible via getResult method. There are five result values. + * POSSIBLE denotes that the current step represents a + * Certificate that the builder is considering at this point in + * the build. FOLLOW denotes a Certificate (one of + * those noted as POSSIBLE) that the builder is using to try + * extending the chain. BACK represents that a + * FOLLOW was incorrect, and is being removed from the chain. + * There is exactly one FOLLOW for each BACK. The + * values SUCCEED and FAIL mean that we've come to + * the end of the build process, and there will not be any more entries in + * the list. + * + * @see BuildStep + * @see Vertex + * + * @author seth proctor + * @since 1.4 + */ +public class AdjacencyList { + + // the actual set of steps the AdjacencyList represents + private final ArrayList mStepList; + + // the original list, just for the toString method + private final List> mOrigList; + + /** + * Constructs a new AdjacencyList based on the specified + * List. See the example above. + * + * @param list a List of Lists of + * Vertex objects + */ + public AdjacencyList(List> list) { + mStepList = new ArrayList<>(); + mOrigList = list; + buildList(list, 0, null); + } + + /** + * Gets an Iterator to iterate over the set of + * BuildSteps in build-order. Any attempts to change + * the list through the remove method will fail. + * + * @return an Iterator over the BuildSteps + */ + public Iterator iterator() { + return Collections.unmodifiableList(mStepList).iterator(); + } + + /** + * Recursive, private method which actually builds the step list from + * the given adjacency list. Follow is the parent BuildStep + * that we followed to get here, and if it's null, it means that we're + * at the start. + */ + private boolean buildList(List> theList, int index, + BuildStep follow) { + + // Each time this method is called, we're examining a new list + // from the global list. So, we have to start by getting the list + // that contains the set of Vertexes we're considering. + List l = theList.get(index); + + // we're interested in the case where all indexes are -1... + boolean allNegOne = true; + // ...and in the case where every entry has a Throwable + boolean allXcps = true; + + for (Vertex v : l) { + if (v.getIndex() != -1) { + // count an empty list the same as an index of -1...this + // is to patch a bug somewhere in the builder + if (theList.get(v.getIndex()).size() != 0) + allNegOne = false; + } else { + if (v.getThrowable() == null) + allXcps = false; + } + // every entry, regardless of the final use for it, is always + // entered as a possible step before we take any actions + mStepList.add(new BuildStep(v, BuildStep.POSSIBLE)); + } + + if (allNegOne) { + // There are two cases that we could be looking at here. We + // may need to back up, or the build may have succeeded at + // this point. This is based on whether any + // exceptions were found in the list. + if (allXcps) { + // we need to go back...see if this is the last one + if (follow == null) + mStepList.add(new BuildStep(null, BuildStep.FAIL)); + else + mStepList.add(new BuildStep(follow.getVertex(), + BuildStep.BACK)); + + return false; + } else { + // we succeeded...now the only question is which is the + // successful step? If there's only one entry without + // a throwable, then that's the successful step. Otherwise, + // we'll have to make some guesses... + List possibles = new ArrayList<>(); + for (Vertex v : l) { + if (v.getThrowable() == null) + possibles.add(v); + } + + if (possibles.size() == 1) { + // real easy...we've found the final Vertex + mStepList.add(new BuildStep(possibles.get(0), + BuildStep.SUCCEED)); + } else { + // ok...at this point, there is more than one Cert + // which might be the succeed step...how do we know + // which it is? I'm going to assume that our builder + // algorithm is good enough to know which is the + // correct one, and put it first...but a FIXME goes + // here anyway, and we should be comparing to the + // target/initiator Cert... + mStepList.add(new BuildStep(possibles.get(0), + BuildStep.SUCCEED)); + } + + return true; + } + } else { + // There's at least one thing that we can try before we give + // up and go back. Run through the list now, and enter a new + // BuildStep for each path that we try to follow. If none of + // the paths we try produce a successful end, we're going to + // have to back out ourselves. + boolean success = false; + + for (Vertex v : l) { + + // Note that we'll only find a SUCCEED case when we're + // looking at the last possible path, so we don't need to + // consider success in the while loop + + if (v.getIndex() != -1) { + if (theList.get(v.getIndex()).size() != 0) { + // If the entry we're looking at doesn't have an + // index of -1, and doesn't lead to an empty list, + // then it's something we follow! + BuildStep bs = new BuildStep(v, BuildStep.FOLLOW); + mStepList.add(bs); + success = buildList(theList, v.getIndex(), bs); + } + } + } + + if (success) { + // We're already finished! + return true; + } else { + // We failed, and we've exhausted all the paths that we + // could take. The only choice is to back ourselves out. + if (follow == null) + mStepList.add(new BuildStep(null, BuildStep.FAIL)); + else + mStepList.add(new BuildStep(follow.getVertex(), + BuildStep.BACK)); + + return false; + } + } + } + + /** + * Prints out a string representation of this AdjacencyList. + * + * @return String representation + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[\n"); + + int i = 0; + for (List l : mOrigList) { + sb.append("LinkedList[").append(i++).append("]:\n"); + + for (Vertex step : l) { + sb.append(step.toString()).append("\n"); + } + } + sb.append("]\n"); + + return sb.toString(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AlgorithmChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AlgorithmChecker.java new file mode 100644 index 000000000..fbc8efe16 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/AlgorithmChecker.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2009, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.security.AlgorithmConstraints; +import java.security.CryptoPrimitive; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Set; +import java.util.EnumSet; +import java.math.BigInteger; +import java.security.PublicKey; +import java.security.KeyFactory; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.TrustAnchor; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertPathValidatorException.BasicReason; +import java.security.cert.PKIXReason; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPublicKeySpec; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DisabledAlgorithmConstraints; +import net.tongsuo.sun.security.util.KeyUtil; +import net.tongsuo.sun.security.validator.Validator; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.X509CertImpl; + +/** + * A {@code PKIXCertPathChecker} implementation to check whether a + * specified certificate contains the required algorithm constraints. + *

+ * Certificate fields such as the subject public key, the signature + * algorithm, key usage, extended key usage, etc. need to conform to + * the specified algorithm constraints. + * + * @see PKIXCertPathChecker + * @see PKIXParameters + */ +public final class AlgorithmChecker extends PKIXCertPathChecker { + private static final Debug debug = Debug.getInstance("certpath"); + + private final AlgorithmConstraints constraints; + private final Date date; + private final String variant; + private PublicKey trustedPubKey; + private PublicKey prevPubKey; + private TrustAnchor anchor; + + private static final Set SIGNATURE_PRIMITIVE_SET = + Collections.unmodifiableSet(EnumSet.of(CryptoPrimitive.SIGNATURE)); + + private static final Set KU_PRIMITIVE_SET = + Collections.unmodifiableSet(EnumSet.of( + CryptoPrimitive.SIGNATURE, + CryptoPrimitive.KEY_ENCAPSULATION, + CryptoPrimitive.PUBLIC_KEY_ENCRYPTION, + CryptoPrimitive.KEY_AGREEMENT)); + + /** + * Create a new {@code AlgorithmChecker} with the given + * {@code TrustAnchor} and {@code String} variant. + * + * @param anchor the trust anchor selected to validate the target + * certificate + * @param variant the Validator variant of the operation. A null value + * passed will set it to Validator.GENERIC. + */ + public AlgorithmChecker(TrustAnchor anchor, String variant) { + this(anchor, null, null, variant); + } + + /** + * Create a new {@code AlgorithmChecker} with the given + * {@code AlgorithmConstraints} and {@code String} variant. + * + * Note that this constructor can initialize a variation of situations where + * the AlgorithmConstraints or Variant maybe known. + * + * @param constraints the algorithm constraints (or null) + * @param variant the Validator variant of the operation. A null value + * passed will set it to Validator.GENERIC. + */ + public AlgorithmChecker(AlgorithmConstraints constraints, String variant) { + this(null, constraints, null, variant); + } + + /** + * Create a new {@code AlgorithmChecker} with the + * given {@code TrustAnchor}, {@code AlgorithmConstraints}, {@code Date}, + * and {@code String} variant. + * + * @param anchor the trust anchor selected to validate the target + * certificate + * @param constraints the algorithm constraints (or null) + * @param date the date specified by the PKIXParameters date, or the + * timestamp if JAR files are being validated and the + * JAR is timestamped. May be null if no timestamp or + * PKIXParameter date is set. + * @param variant the Validator variant of the operation. A null value + * passed will set it to Validator.GENERIC. + */ + public AlgorithmChecker(TrustAnchor anchor, + AlgorithmConstraints constraints, Date date, String variant) { + + if (anchor != null) { + setTrustAnchorAndKeys(anchor); + } + + this.constraints = constraints == null ? + DisabledAlgorithmConstraints.certPathConstraints() : constraints; + this.date = date; + this.variant = (variant == null ? Validator.VAR_GENERIC : variant); + } + + /** + * Create a new {@code AlgorithmChecker} with the given {@code TrustAnchor}, + * {@code PKIXParameter} date, and {@code variant}. + * + * @param anchor the trust anchor selected to validate the target + * certificate + * @param date the date specified by the PKIXParameters date, or the + * timestamp if JAR files are being validated and the + * JAR is timestamped. May be null if no timestamp or + * PKIXParameter date is set. + * @param variant the Validator variant of the operation. A null value + * passed will set it to Validator.GENERIC. + */ + public AlgorithmChecker(TrustAnchor anchor, Date date, String variant) { + this(anchor, null, date, variant); + } + + @Override + public void init(boolean forward) throws CertPathValidatorException { + // Note that this class does not support forward mode. + if (!forward) { + prevPubKey = trustedPubKey; + } else { + throw new + CertPathValidatorException("forward checking not supported"); + } + } + + @Override + public boolean isForwardCheckingSupported() { + // Note that as this class does not support forward mode, the method + // will always return false. + return false; + } + + @Override + public Set getSupportedExtensions() { + return null; + } + + @Override + public void check(Certificate cert, + Collection unresolvedCritExts) + throws CertPathValidatorException { + + if (!(cert instanceof X509Certificate)) { + // ignore the check for non-x.509 certificate + return; + } + + // check the key usage and key size + boolean[] keyUsage = ((X509Certificate) cert).getKeyUsage(); + if (keyUsage != null && keyUsage.length < 9) { + throw new CertPathValidatorException( + "incorrect KeyUsage extension", + null, null, -1, PKIXReason.INVALID_KEY_USAGE); + } + + X509CertImpl x509Cert; + AlgorithmId algorithmId; + try { + x509Cert = X509CertImpl.toImpl((X509Certificate)cert); + algorithmId = x509Cert.getSigAlg(); + } catch (CertificateException ce) { + throw new CertPathValidatorException(ce); + } + + AlgorithmParameters currSigAlgParams = algorithmId.getParameters(); + PublicKey currPubKey = cert.getPublicKey(); + String currSigAlg = x509Cert.getSigAlgName(); + + if (constraints instanceof DisabledAlgorithmConstraints) { + DisabledAlgorithmConstraints dac = + (DisabledAlgorithmConstraints)constraints; + if (prevPubKey != null && prevPubKey == trustedPubKey) { + // check constraints of trusted public key (make sure + // algorithm and size is not restricted) + CertPathConstraintsParameters cp = + new CertPathConstraintsParameters(trustedPubKey, variant, + anchor, date); + dac.permits(trustedPubKey.getAlgorithm(), cp, true); + } + // Check the signature algorithm and parameters against constraints + CertPathConstraintsParameters cp = + new CertPathConstraintsParameters(x509Cert, variant, + anchor, date); + dac.permits(currSigAlg, currSigAlgParams, cp, true); + } else { + if (prevPubKey != null) { + if (!constraints.permits(SIGNATURE_PRIMITIVE_SET, + currSigAlg, prevPubKey, currSigAlgParams)) { + throw new CertPathValidatorException( + "Algorithm constraints check failed on " + + currSigAlg + "signature and " + + currPubKey.getAlgorithm() + " key with size of " + + KeyUtil.getKeySize(currPubKey) + + "bits", + null, null, -1, BasicReason.ALGORITHM_CONSTRAINED); + } + } else { + if (!constraints.permits(SIGNATURE_PRIMITIVE_SET, + currSigAlg, currSigAlgParams)) { + throw new CertPathValidatorException( + "Algorithm constraints check failed on " + + "signature algorithm: " + currSigAlg, + null, null, -1, BasicReason.ALGORITHM_CONSTRAINED); + } + } + // Assume all key usage bits are set if key usage is not present + Set primitives = KU_PRIMITIVE_SET; + + if (keyUsage != null) { + primitives = EnumSet.noneOf(CryptoPrimitive.class); + + if (keyUsage[0] || keyUsage[1] || keyUsage[5] || keyUsage[6]) { + // keyUsage[0]: KeyUsage.digitalSignature + // keyUsage[1]: KeyUsage.nonRepudiation + // keyUsage[5]: KeyUsage.keyCertSign + // keyUsage[6]: KeyUsage.cRLSign + primitives.add(CryptoPrimitive.SIGNATURE); + } + + if (keyUsage[2]) { // KeyUsage.keyEncipherment + primitives.add(CryptoPrimitive.KEY_ENCAPSULATION); + } + + if (keyUsage[3]) { // KeyUsage.dataEncipherment + primitives.add(CryptoPrimitive.PUBLIC_KEY_ENCRYPTION); + } + + if (keyUsage[4]) { // KeyUsage.keyAgreement + primitives.add(CryptoPrimitive.KEY_AGREEMENT); + } + + // KeyUsage.encipherOnly and KeyUsage.decipherOnly are + // undefined in the absence of the keyAgreement bit. + + if (primitives.isEmpty()) { + throw new CertPathValidatorException( + "incorrect KeyUsage extension bits", + null, null, -1, PKIXReason.INVALID_KEY_USAGE); + } + } + if (!constraints.permits(primitives, currPubKey)) { + throw new CertPathValidatorException( + "Algorithm constraints check failed on " + + currPubKey.getAlgorithm() + " key with size of " + + KeyUtil.getKeySize(currPubKey) + + "bits", + null, null, -1, BasicReason.ALGORITHM_CONSTRAINED); + } + } + + if (prevPubKey != null) { + // Inherit key parameters from previous key + if (PKIX.isDSAPublicKeyWithoutParams(currPubKey)) { + // Inherit DSA parameters from previous key + if (!(prevPubKey instanceof DSAPublicKey)) { + throw new CertPathValidatorException("Input key is not " + + "of a appropriate type for inheriting parameters"); + } + + DSAParams params = ((DSAPublicKey)prevPubKey).getParams(); + if (params == null) { + throw new CertPathValidatorException( + "Key parameters missing from public key."); + } + + try { + BigInteger y = ((DSAPublicKey)currPubKey).getY(); + KeyFactory kf = KeyFactory.getInstance("DSA"); + DSAPublicKeySpec ks = new DSAPublicKeySpec(y, params.getP(), + params.getQ(), params.getG()); + currPubKey = kf.generatePublic(ks); + } catch (GeneralSecurityException e) { + throw new CertPathValidatorException("Unable to generate " + + "key with inherited parameters: " + + e.getMessage(), e); + } + } + } + + // reset the previous public key + prevPubKey = currPubKey; + } + + /** + * Sets the anchor, trustedPubKey and prevPubKey fields based on the + * specified trust anchor. + */ + private void setTrustAnchorAndKeys(TrustAnchor anchor) { + if (anchor.getTrustedCert() != null) { + this.trustedPubKey = anchor.getTrustedCert().getPublicKey(); + } else { + this.trustedPubKey = anchor.getCAPublicKey(); + } + this.anchor = anchor; + this.prevPubKey = this.trustedPubKey; + } + + /** + * Try to set the trust anchor of the checker. + *

+ * If there is no trust anchor specified and the checker has not started, + * set the trust anchor. + * + * @param anchor the trust anchor selected to validate the target + * certificate + */ + void trySetTrustAnchor(TrustAnchor anchor) { + // Only set if trust anchor has not already been set. + if (this.trustedPubKey == null) { + setTrustAnchorAndKeys(anchor); + } + } + + /** + * Check the signature algorithm with the specified public key. + * + * @param key the public key to verify the CRL signature + * @param algorithmId signature algorithm Algorithm ID + * @param variant the Validator variant of the operation. A null + * value passed will set it to Validator.GENERIC. + * @param anchor the trust anchor selected to validate the public key + */ + static void check(PublicKey key, AlgorithmId algorithmId, String variant, + TrustAnchor anchor) throws CertPathValidatorException { + + DisabledAlgorithmConstraints.certPathConstraints().permits(algorithmId.getName(), + algorithmId.getParameters(), + new CertPathConstraintsParameters(key, variant, anchor, null), true); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BasicChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BasicChecker.java new file mode 100644 index 000000000..83a47d1db --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BasicChecker.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.math.BigInteger; +import java.util.Collection; +import java.util.Date; +import java.util.Set; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertPathValidatorException.BasicReason; +import java.security.cert.X509Certificate; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.PKIXReason; +import java.security.cert.TrustAnchor; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.util.Debug; + +/** + * BasicChecker is a PKIXCertPathChecker that checks the basic information + * on a PKIX certificate, namely the signature, validity, and subject/issuer + * name chaining. + * + * @since 1.4 + * @author Yassir Elley + */ +class BasicChecker extends PKIXCertPathChecker { + + private static final Debug debug = Debug.getInstance("certpath"); + private final PublicKey trustedPubKey; + private final X500Principal caName; + private final Date date; + private final String sigProvider; + private final boolean sigOnly; + private X500Principal prevSubject; + private PublicKey prevPubKey; + + /** + * Constructor that initializes the input parameters. + * + * @param anchor the anchor selected to validate the target certificate + * @param date the time for which the validity of the certificate + * should be determined + * @param sigProvider the name of the signature provider + * @param sigOnly true if only signature checking is to be done; + * if false, all checks are done + */ + BasicChecker(TrustAnchor anchor, Date date, String sigProvider, + boolean sigOnly) { + if (anchor.getTrustedCert() != null) { + this.trustedPubKey = anchor.getTrustedCert().getPublicKey(); + this.caName = anchor.getTrustedCert().getSubjectX500Principal(); + } else { + this.trustedPubKey = anchor.getCAPublicKey(); + this.caName = anchor.getCA(); + } + this.date = date; + this.sigProvider = sigProvider; + this.sigOnly = sigOnly; + this.prevPubKey = trustedPubKey; + } + + /** + * Initializes the internal state of the checker from parameters + * specified in the constructor. + */ + @Override + public void init(boolean forward) throws CertPathValidatorException { + if (!forward) { + prevPubKey = trustedPubKey; + if (PKIX.isDSAPublicKeyWithoutParams(prevPubKey)) { + // If TrustAnchor is a DSA public key and it has no params, it + // cannot be used to verify the signature of the first cert, + // so throw exception + throw new CertPathValidatorException("Key parameters missing"); + } + prevSubject = caName; + } else { + throw new + CertPathValidatorException("forward checking not supported"); + } + } + + @Override + public boolean isForwardCheckingSupported() { + return false; + } + + @Override + public Set getSupportedExtensions() { + return null; + } + + /** + * Performs the signature, validity, and subject/issuer name chaining + * checks on the certificate using its internal state. This method does + * not remove any critical extensions from the Collection. + * + * @param cert the Certificate + * @param unresolvedCritExts a Collection of the unresolved critical + * extensions + * @throws CertPathValidatorException if certificate does not verify + */ + @Override + public void check(Certificate cert, Collection unresolvedCritExts) + throws CertPathValidatorException + { + X509Certificate currCert = (X509Certificate)cert; + + if (!sigOnly) { + verifyValidity(currCert); + verifyNameChaining(currCert); + } + verifySignature(currCert); + + updateState(currCert); + } + + /** + * Verifies the signature on the certificate using the previous public key. + * + * @param cert the X509Certificate + * @throws CertPathValidatorException if certificate does not verify + */ + private void verifySignature(X509Certificate cert) + throws CertPathValidatorException + { + String msg = "signature"; + if (debug != null) + debug.println("---checking " + msg + "..."); + + try { + cert.verify(prevPubKey, sigProvider); + } catch (SignatureException e) { + throw new CertPathValidatorException + (msg + " check failed", e, null, -1, + BasicReason.INVALID_SIGNATURE); + } catch (GeneralSecurityException e) { + throw new CertPathValidatorException(msg + " check failed", e); + } + + if (debug != null) + debug.println(msg + " verified."); + } + + /** + * Internal method to verify the validity on a certificate + */ + private void verifyValidity(X509Certificate cert) + throws CertPathValidatorException + { + String msg = "validity"; + if (debug != null) + debug.println("---checking " + msg + ":" + date.toString() + "..."); + + try { + cert.checkValidity(date); + } catch (CertificateExpiredException e) { + throw new CertPathValidatorException + (msg + " check failed", e, null, -1, BasicReason.EXPIRED); + } catch (CertificateNotYetValidException e) { + throw new CertPathValidatorException + (msg + " check failed", e, null, -1, BasicReason.NOT_YET_VALID); + } + + if (debug != null) + debug.println(msg + " verified."); + } + + /** + * Internal method to check that cert has a valid DN to be next in a chain + */ + private void verifyNameChaining(X509Certificate cert) + throws CertPathValidatorException + { + if (prevSubject != null) { + + String msg = "subject/issuer name chaining"; + if (debug != null) + debug.println("---checking " + msg + "..."); + + X500Principal currIssuer = cert.getIssuerX500Principal(); + + // reject null or empty issuer DNs + if (X500Name.asX500Name(currIssuer).isEmpty()) { + throw new CertPathValidatorException + (msg + " check failed: " + + "empty/null issuer DN in certificate is invalid", null, + null, -1, PKIXReason.NAME_CHAINING); + } + + if (!(currIssuer.equals(prevSubject))) { + throw new CertPathValidatorException + (msg + " check failed", null, null, -1, + PKIXReason.NAME_CHAINING); + } + + if (debug != null) + debug.println(msg + " verified."); + } + } + + /** + * Internal method to manage state information at each iteration + */ + private void updateState(X509Certificate currCert) + throws CertPathValidatorException + { + PublicKey cKey = currCert.getPublicKey(); + if (debug != null) { + debug.println("BasicChecker.updateState issuer: " + + currCert.getIssuerX500Principal().toString() + "; subject: " + + currCert.getSubjectX500Principal() + "; serial#: " + + currCert.getSerialNumber().toString()); + } + if (PKIX.isDSAPublicKeyWithoutParams(cKey)) { + // cKey needs to inherit DSA parameters from prev key + cKey = makeInheritedParamsKey(cKey, prevPubKey); + if (debug != null) debug.println("BasicChecker.updateState Made " + + "key with inherited params"); + } + prevPubKey = cKey; + prevSubject = currCert.getSubjectX500Principal(); + } + + /** + * Internal method to create a new key with inherited key parameters. + * + * @param keyValueKey key from which to obtain key value + * @param keyParamsKey key from which to obtain key parameters + * @return new public key having value and parameters + * @throws CertPathValidatorException if keys are not appropriate types + * for this operation + */ + static PublicKey makeInheritedParamsKey(PublicKey keyValueKey, + PublicKey keyParamsKey) throws CertPathValidatorException + { + if (!(keyValueKey instanceof DSAPublicKey) || + !(keyParamsKey instanceof DSAPublicKey)) + throw new CertPathValidatorException("Input key is not " + + "appropriate type for " + + "inheriting parameters"); + DSAParams params = ((DSAPublicKey)keyParamsKey).getParams(); + if (params == null) + throw new CertPathValidatorException("Key parameters missing"); + try { + BigInteger y = ((DSAPublicKey)keyValueKey).getY(); + KeyFactory kf = CryptoInsts.getKeyFactory("DSA"); + DSAPublicKeySpec ks = new DSAPublicKeySpec(y, + params.getP(), + params.getQ(), + params.getG()); + return kf.generatePublic(ks); + } catch (GeneralSecurityException e) { + throw new CertPathValidatorException("Unable to generate key with" + + " inherited parameters: " + + e.getMessage(), e); + } + } + + /** + * return the public key associated with the last certificate processed + * + * @return PublicKey the last public key processed + */ + PublicKey getPublicKey() { + return prevPubKey; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BuildStep.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BuildStep.java new file mode 100644 index 000000000..78f24b1a1 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/BuildStep.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.security.cert.X509Certificate; + +/** + * Describes one step of a certification path build, consisting of a + * Vertex state description, a certificate, a possible throwable, + * and a result code. + * + * @author Anne Anderson + * @since 1.4 + * @see sun.security.provider.certpath.Vertex + */ +public class BuildStep { + + private final Vertex vertex; + private X509Certificate cert; + private Throwable throwable; + private final int result; + + /** + * result code associated with a certificate that may continue a path from + * the current certificate. + */ + public static final int POSSIBLE = 1; + + /** + * result code associated with a certificate that was tried, but that + * represents an unsuccessful path, so the certificate has been backed out + * to allow backtracking to the next possible path. + */ + public static final int BACK = 2; + + /** + * result code associated with a certificate that successfully continues the + * current path, but does not yet reach the target. + */ + public static final int FOLLOW = 3; + + /** + * result code associated with a certificate that represents the end of the + * last possible path, where no path successfully reached the target. + */ + public static final int FAIL = 4; + + /** + * result code associated with a certificate that represents the end of a + * path that successfully reaches the target. + */ + public static final int SUCCEED = 5; + + /** + * construct a BuildStep + * + * @param vtx description of the vertex at this step + * @param res result, where result is one of POSSIBLE, BACK, + * FOLLOW, FAIL, SUCCEED + */ + public BuildStep(Vertex vtx, int res) { + vertex = vtx; + if (vertex != null) { + cert = vertex.getCertificate(); + throwable = vertex.getThrowable(); + } + result = res; + } + + /** + * return vertex description for this build step + * + * @return Vertex + */ + public Vertex getVertex() { + return vertex; + } + + /** + * return the certificate associated with this build step + * + * @return X509Certificate + */ + public X509Certificate getCertificate() { + return cert; + } + + /** + * return string form of issuer name from certificate associated with this + * build step + * + * @return String form of issuer name or null, if no certificate. + */ + public String getIssuerName() { + return getIssuerName(null); + } + + /** + * return string form of issuer name from certificate associated with this + * build step, or a default name if no certificate associated with this + * build step, or if issuer name could not be obtained from the certificate. + * + * @param defaultName name to use as default if unable to return an issuer + * name from the certificate, or if no certificate. + * @return String form of issuer name or defaultName, if no certificate or + * exception received while trying to extract issuer name from certificate. + */ + public String getIssuerName(String defaultName) { + return (cert == null ? defaultName + : cert.getIssuerX500Principal().toString()); + } + + /** + * return string form of subject name from certificate associated with this + * build step. + * + * @return String form of subject name or null, if no certificate. + */ + public String getSubjectName() { + return getSubjectName(null); + } + + /** + * return string form of subject name from certificate associated with this + * build step, or a default name if no certificate associated with this + * build step, or if subject name could not be obtained from the + * certificate. + * + * @param defaultName name to use as default if unable to return a subject + * name from the certificate, or if no certificate. + * @return String form of subject name or defaultName, if no certificate or + * if an exception was received while attempting to extract the subject name + * from the certificate. + */ + public String getSubjectName(String defaultName) { + return (cert == null ? defaultName + : cert.getSubjectX500Principal().toString()); + } + + /** + * return the exception associated with this build step. + * + * @return Throwable + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * return the result code associated with this build step. The result codes + * are POSSIBLE, FOLLOW, BACK, FAIL, SUCCEED. + * + * @return int result code + */ + public int getResult() { + return result; + } + + /** + * return a string representing the meaning of the result code associated + * with this build step. + * + * @param res result code + * @return String string representing meaning of the result code + */ + public String resultToString(int res) { + String resultString = ""; + switch (res) { + case POSSIBLE: + resultString = "Certificate to be tried.\n"; + break; + case BACK: + resultString = "Certificate backed out since path does not " + + "satisfy build requirements.\n"; + break; + case FOLLOW: + resultString = "Certificate satisfies conditions.\n"; + break; + case FAIL: + resultString = "Certificate backed out since path does not " + + "satisfy conditions.\n"; + break; + case SUCCEED: + resultString = "Certificate satisfies conditions.\n"; + break; + default: + resultString = "Internal error: Invalid step result value.\n"; + } + return resultString; + } + + /** + * return a string representation of this build step, showing minimal + * detail. + * + * @return String + */ + @Override + public String toString() { + String out; + switch (result) { + case BACK: + case FAIL: + out = resultToString(result); + out = out + vertex.throwableToString(); + break; + case FOLLOW: + case SUCCEED: + case POSSIBLE: + out = resultToString(result); + break; + default: + out = "Internal Error: Invalid step result\n"; + } + return out; + } + + /** + * return a string representation of this build step, showing all detail of + * the vertex state appropriate to the result of this build step, and the + * certificate contents. + * + * @return String + */ + public String verboseToString() { + String out = resultToString(getResult()); + switch (result) { + case BACK: + case FAIL: + out = out + vertex.throwableToString(); + break; + case FOLLOW: + case SUCCEED: + out = out + vertex.moreToString(); + break; + default: + break; + } + out = out + "Certificate contains:\n" + vertex.certToString(); + return out; + } + + /** + * return a string representation of this build step, including all possible + * detail of the vertex state, but not including the certificate contents. + * + * @return String + */ + public String fullToString() { + return resultToString(getResult()) + vertex.toString(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/Builder.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/Builder.java new file mode 100644 index 000000000..051f02bc0 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/Builder.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.AccessController; +import java.security.GeneralSecurityException; +import java.security.cert.*; +import java.util.*; + +import net.tongsuo.sun.security.action.GetBooleanAction; +import net.tongsuo.sun.security.x509.GeneralNameInterface; +import net.tongsuo.sun.security.x509.GeneralNames; +import net.tongsuo.sun.security.x509.GeneralSubtrees; +import net.tongsuo.sun.security.x509.NameConstraintsExtension; +import net.tongsuo.sun.security.x509.SubjectAlternativeNameExtension; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * Abstract class representing a builder, which is able to retrieve + * matching certificates and is able to verify a particular certificate. + * + * @since 1.4 + * @author Sean Mullan + * @author Yassir Elley + */ + +public abstract class Builder { + + private static final Debug debug = Debug.getInstance("certpath"); + private Set matchingPolicies; + final PKIX.BuilderParams buildParams; + final X509CertSelector targetCertConstraints; + + /** + * Flag indicating whether support for the caIssuers field of the + * Authority Information Access extension shall be enabled. Currently + * disabled by default for compatibility reasons. + */ + final static boolean USE_AIA = AccessController.doPrivileged + (new GetBooleanAction("net.tongsuo.pkix.enableAIAcaIssuers")); + + /** + * Initialize the builder with the input parameters. + * + * @param buildParams the parameter set used to build a certification path + */ + Builder(PKIX.BuilderParams buildParams) { + this.buildParams = buildParams; + this.targetCertConstraints = + (X509CertSelector)buildParams.targetCertConstraints(); + } + + /** + * Retrieves certificates from the list of certStores using the buildParams + * and the currentState as a filter + * + * @param currentState the current State + * @param certStores list of CertStores + */ + abstract Collection getMatchingCerts + (State currentState, List certStores) + throws CertStoreException, CertificateException, IOException; + + /** + * Verifies the cert against the currentState, using the certPathList + * generated thus far to help with loop detection + * + * @param cert the certificate to be verified + * @param currentState the current state against which the cert is verified + * @param certPathList the certPathList generated thus far + */ + abstract void verifyCert(X509Certificate cert, State currentState, + List certPathList) + throws GeneralSecurityException; + + /** + * Verifies whether the input certificate completes the path. + * When building in the forward direction, a trust anchor will + * complete the path. + * + * @param cert the certificate to test + * @return a boolean value indicating whether the cert completes the path. + */ + abstract boolean isPathCompleted(X509Certificate cert); + + /** + * Adds the certificate to the certPathList + * + * @param cert the certificate to be added + * @param certPathList the certification path list + */ + abstract void addCertToPath(X509Certificate cert, + LinkedList certPathList); + + /** + * Removes final certificate from the certPathList + * + * @param certPathList the certification path list + */ + abstract void removeFinalCertFromPath + (LinkedList certPathList); + + /** + * get distance of one GeneralName from another + * + * @param base GeneralName at base of subtree + * @param test GeneralName to be tested against base + * @param incomparable the value to return if the names are + * incomparable + * @return distance of test name from base, where 0 + * means exact match, 1 means test is an immediate + * child of base, 2 means test is a grandchild, etc. + * -1 means test is a parent of base, -2 means test + * is a grandparent, etc. + */ + static int distance(GeneralNameInterface base, + GeneralNameInterface test, int incomparable) + { + switch (base.constrains(test)) { + case GeneralNameInterface.NAME_DIFF_TYPE: + if (debug != null) { + debug.println("Builder.distance(): Names are different types"); + } + return incomparable; + case GeneralNameInterface.NAME_SAME_TYPE: + if (debug != null) { + debug.println("Builder.distance(): Names are same type but " + + "in different subtrees"); + } + return incomparable; + case GeneralNameInterface.NAME_MATCH: + return 0; + case GeneralNameInterface.NAME_WIDENS: + case GeneralNameInterface.NAME_NARROWS: + break; + default: // should never occur + return incomparable; + } + + /* names are in same subtree */ + return test.subtreeDepth() - base.subtreeDepth(); + } + + /** + * get hop distance of one GeneralName from another in links where + * the names need not have an ancestor/descendant relationship. + * For example, the hop distance from ou=D,ou=C,o=B,c=US to + * ou=F,ou=E,ou=C,o=B,c=US is 3: D->C, C->E, E->F. The hop distance + * from ou=C,o=B,c=US to ou=D,ou=C,o=B,c=US is -1: C->D + * + * @param base GeneralName + * @param test GeneralName to be tested against base + * @param incomparable the value to return if the names are + * incomparable + * @return distance of test name from base measured in hops in the + * namespace hierarchy, where 0 means exact match. Result + * is positive if path is some number of up hops followed by + * some number of down hops; result is negative if path is + * some number of down hops. + */ + static int hops(GeneralNameInterface base, GeneralNameInterface test, + int incomparable) + { + int baseRtest = base.constrains(test); + switch (baseRtest) { + case GeneralNameInterface.NAME_DIFF_TYPE: + if (debug != null) { + debug.println("Builder.hops(): Names are different types"); + } + return incomparable; + case GeneralNameInterface.NAME_SAME_TYPE: + /* base and test are in different subtrees */ + break; + case GeneralNameInterface.NAME_MATCH: + /* base matches test */ + return 0; + case GeneralNameInterface.NAME_WIDENS: + /* base is ancestor of test */ + case GeneralNameInterface.NAME_NARROWS: + /* base is descendant of test */ + return test.subtreeDepth() - base.subtreeDepth(); + default: // should never occur + return incomparable; + } + + /* names are in different subtrees */ + if (base.getType() != GeneralNameInterface.NAME_DIRECTORY) { + if (debug != null) { + debug.println("Builder.hops(): hopDistance not implemented " + + "for this name type"); + } + return incomparable; + } + X500Name baseName = (X500Name)base; + X500Name testName = (X500Name)test; + X500Name commonName = baseName.commonAncestor(testName); + if (commonName == null) { + if (debug != null) { + debug.println("Builder.hops(): Names are in different " + + "namespaces"); + } + return incomparable; + } else { + int commonDistance = commonName.subtreeDepth(); + int baseDistance = baseName.subtreeDepth(); + int testDistance = testName.subtreeDepth(); + return baseDistance + testDistance - (2 * commonDistance); + } + } + + /** + * Determine how close a given certificate gets you toward + * a given target. + * + * @param constraints Current NameConstraints; if null, + * then caller must verify NameConstraints + * independently, realizing that this certificate + * may not actually lead to the target at all. + * @param cert Candidate certificate for chain + * @param target GeneralNameInterface name of target + * @return distance from this certificate to target: + *

    + *
  • -1 means certificate could be CA for target, but + * there are no NameConstraints limiting how close + *
  • 0 means certificate subject or subjectAltName + * matches target + *
  • 1 means certificate is permitted to be CA for + * target. + *
  • 2 means certificate is permitted to be CA for + * parent of target. + *
  • >0 in general, means certificate is permitted + * to be a CA for this distance higher in the naming + * hierarchy than the target, plus 1. + *
+ *

Note that the subject and/or subjectAltName of the + * candidate cert does not have to be an ancestor of the + * target in order to be a CA that can issue a certificate to + * the target. In these cases, the target distance is calculated + * by inspecting the NameConstraints extension in the candidate + * certificate. For example, suppose the target is an X.500 DN with + * a value of "CN=mullan,OU=ireland,O=sun,C=us" and the + * NameConstraints extension in the candidate certificate + * includes a permitted component of "O=sun,C=us", which implies + * that the candidate certificate is allowed to issue certs in + * the "O=sun,C=us" namespace. The target distance is 3 + * ((distance of permitted NC from target) + 1). + * The (+1) is added to distinguish the result from the case + * which returns (0). + * @throws IOException if certificate does not get closer + */ + static int targetDistance(NameConstraintsExtension constraints, + X509Certificate cert, GeneralNameInterface target) + throws IOException + { + /* ensure that certificate satisfies existing name constraints */ + if (constraints != null && !constraints.verify(cert)) { + throw new IOException("certificate does not satisfy existing name " + + "constraints"); + } + + X509CertImpl certImpl; + try { + certImpl = X509CertImpl.toImpl(cert); + } catch (CertificateException e) { + throw new IOException("Invalid certificate", e); + } + /* see if certificate subject matches target */ + X500Name subject = X500Name.asX500Name(certImpl.getSubjectX500Principal()); + if (subject.equals(target)) { + /* match! */ + return 0; + } + + SubjectAlternativeNameExtension altNameExt = + certImpl.getSubjectAlternativeNameExtension(); + if (altNameExt != null) { + GeneralNames altNames = altNameExt.getNames(); + /* see if any alternative name matches target */ + if (altNames != null) { + for (int j = 0, n = altNames.size(); j < n; j++) { + GeneralNameInterface altName = altNames.get(j).getName(); + if (altName.equals(target)) { + return 0; + } + } + } + } + + + /* no exact match; see if certificate can get us to target */ + + /* first, get NameConstraints out of certificate */ + NameConstraintsExtension ncExt = certImpl.getNameConstraintsExtension(); + if (ncExt == null) { + return -1; + } + + /* merge certificate's NameConstraints with current NameConstraints */ + if (constraints != null) { + constraints.merge(ncExt); + } else { + // Make sure we do a clone here, because we're probably + // going to modify this object later, and we don't want to + // be sharing it with a Certificate object! + constraints = (NameConstraintsExtension) ncExt.clone(); + } + + if (debug != null) { + debug.println("Builder.targetDistance() merged constraints: " + + constraints); + } + /* reduce permitted by excluded */ + GeneralSubtrees permitted = constraints.getPermittedSubtrees(); + GeneralSubtrees excluded = constraints.getExcludedSubtrees(); + if (permitted != null) { + permitted.reduce(excluded); + } + if (debug != null) { + debug.println("Builder.targetDistance() reduced constraints: " + + permitted); + } + /* see if new merged constraints allow target */ + if (!constraints.verify(target)) { + throw new IOException("New certificate not allowed to sign " + + "certificate for target"); + } + /* find distance to target, if any, in permitted */ + if (permitted == null) { + /* certificate is unconstrained; could sign for anything */ + return -1; + } + for (int i = 0, n = permitted.size(); i < n; i++) { + GeneralNameInterface perName = permitted.get(i).getName().getName(); + int distance = distance(perName, target, -1); + if (distance >= 0) { + return distance + 1; + } + } + /* no matching type in permitted; cert holder could certify target */ + return -1; + } + + /** + * This method can be used as an optimization to filter out + * certificates that do not have policies which are valid. + * It returns the set of policies (String OIDs) that should exist in + * the certificate policies extension of the certificate that is + * needed by the builder. The logic applied is as follows: + *

+ * 1) If some initial policies have been set *and* policy mappings are + * inhibited, then acceptable certificates are those that include + * the ANY_POLICY OID or with policies that intersect with the + * initial policies. + * 2) If no initial policies have been set *or* policy mappings are + * not inhibited then we don't have much to work with. All we know is + * that a certificate must have *some* policy because if it didn't + * have any policy then the policy tree would become null (and validation + * would fail). + * + * @return the Set of policies any of which must exist in a + * cert's certificate policies extension in order for a cert to be selected. + */ + Set getMatchingPolicies() { + if (matchingPolicies != null) { + Set initialPolicies = buildParams.initialPolicies(); + if ((!initialPolicies.isEmpty()) && + (!initialPolicies.contains(PolicyChecker.ANY_POLICY)) && + (buildParams.policyMappingInhibited())) + { + matchingPolicies = new HashSet<>(initialPolicies); + matchingPolicies.add(PolicyChecker.ANY_POLICY); + } else { + // we just return an empty set to make sure that there is + // at least a certificate policies extension in the cert + matchingPolicies = Collections.emptySet(); + } + } + return matchingPolicies; + } + + /** + * Search the specified CertStores and add all certificates matching + * selector to resultCerts. Self-signed certs are not useful here + * and therefore ignored. + * + * If the targetCert criterion of the selector is set, only that cert + * is examined and the CertStores are not searched. + * + * If checkAll is true, all CertStores are searched for matching certs. + * If false, the method returns as soon as the first CertStore returns + * a matching cert(s). + * + * Returns true iff resultCerts changed (a cert was added to the collection) + */ + boolean addMatchingCerts(X509CertSelector selector, + Collection certStores, + Collection resultCerts, + boolean checkAll) + { + X509Certificate targetCert = selector.getCertificate(); + if (targetCert != null) { + // no need to search CertStores + if (selector.match(targetCert) && !X509CertImpl.isSelfSigned + (targetCert, buildParams.sigProvider())) { + if (debug != null) { + debug.println("Builder.addMatchingCerts: " + + "adding target cert" + + "\n SN: " + Debug.toHexString( + targetCert.getSerialNumber()) + + "\n Subject: " + targetCert.getSubjectX500Principal() + + "\n Issuer: " + targetCert.getIssuerX500Principal()); + } + return resultCerts.add(targetCert); + } + return false; + } + boolean add = false; + for (CertStore store : certStores) { + try { + Collection certs = + store.getCertificates(selector); + for (Certificate cert : certs) { + if (!X509CertImpl.isSelfSigned + ((X509Certificate)cert, buildParams.sigProvider())) { + if (resultCerts.add((X509Certificate)cert)) { + add = true; + } + } + } + if (!checkAll && add) { + return true; + } + } catch (CertStoreException cse) { + // if getCertificates throws a CertStoreException, we ignore + // it and move on to the next CertStore + if (debug != null) { + debug.println("Builder.addMatchingCerts, non-fatal " + + "exception retrieving certs: " + cse); + cse.printStackTrace(); + } + } + } + return add; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertId.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertId.java new file mode 100644 index 000000000..ec43f4817 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertId.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.sun.security.util.DerEncoder; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.SerialNumber; + +/** + * This class corresponds to the CertId field in OCSP Request + * and the OCSP Response. The ASN.1 definition for CertID is defined + * in RFC 2560 as: + *

+ *
+ * CertID          ::=     SEQUENCE {
+ *      hashAlgorithm       AlgorithmIdentifier,
+ *      issuerNameHash      OCTET STRING, -- Hash of Issuer's DN
+ *      issuerKeyHash       OCTET STRING, -- Hash of Issuers public key
+ *      serialNumber        CertificateSerialNumber
+ *      }
+ *
+ * 
+ * + * @author Ram Marti + */ + +public class CertId implements DerEncoder { + + private static final boolean debug = false; + private static final AlgorithmId SHA1_ALGID + = new AlgorithmId(AlgorithmId.SHA_oid); + private final AlgorithmId hashAlgId; + private final byte[] issuerNameHash; + private final byte[] issuerKeyHash; + private final SerialNumber certSerialNumber; + private int myhash = -1; // hashcode for this CertId + + /** + * Creates a CertId. The hash algorithm used is SHA-1. + */ + public CertId(X509Certificate issuerCert, SerialNumber serialNumber) + throws IOException { + + this(issuerCert.getSubjectX500Principal(), + issuerCert.getPublicKey(), serialNumber); + } + + public CertId(X500Principal issuerName, PublicKey issuerKey, + SerialNumber serialNumber) throws IOException { + + // compute issuerNameHash + MessageDigest md; + try { + md = CryptoInsts.getMessageDigest("SHA1"); + } catch (NoSuchAlgorithmException nsae) { + throw new IOException("Unable to create CertId", nsae); + } + hashAlgId = SHA1_ALGID; + md.update(issuerName.getEncoded()); + issuerNameHash = md.digest(); + + // compute issuerKeyHash (remove the tag and length) + byte[] pubKey = issuerKey.getEncoded(); + DerValue val = new DerValue(pubKey); + DerValue[] seq = new DerValue[2]; + seq[0] = val.data.getDerValue(); // AlgorithmID + seq[1] = val.data.getDerValue(); // Key + byte[] keyBytes = seq[1].getBitString(); + md.update(keyBytes); + issuerKeyHash = md.digest(); + certSerialNumber = serialNumber; + + if (debug) { + HexDumpEncoder encoder = new HexDumpEncoder(); + System.out.println("Issuer Name is " + issuerName); + System.out.println("issuerNameHash is " + + encoder.encodeBuffer(issuerNameHash)); + System.out.println("issuerKeyHash is " + + encoder.encodeBuffer(issuerKeyHash)); + System.out.println("SerialNumber is " + serialNumber.getNumber()); + } + } + + /** + * Creates a CertId from its ASN.1 DER encoding. + */ + public CertId(DerInputStream derIn) throws IOException { + hashAlgId = AlgorithmId.parse(derIn.getDerValue()); + issuerNameHash = derIn.getOctetString(); + issuerKeyHash = derIn.getOctetString(); + certSerialNumber = new SerialNumber(derIn); + } + + /** + * Return the hash algorithm identifier. + */ + public AlgorithmId getHashAlgorithm() { + return hashAlgId; + } + + /** + * Return the hash value for the issuer name. + */ + public byte[] getIssuerNameHash() { + return issuerNameHash; + } + + /** + * Return the hash value for the issuer key. + */ + public byte[] getIssuerKeyHash() { + return issuerKeyHash; + } + + /** + * Return the serial number. + */ + public BigInteger getSerialNumber() { + return certSerialNumber.getNumber(); + } + + /** + * Encode the CertId using ASN.1 DER. + * The hash algorithm used is SHA-1. + */ + @Override + public void encode(DerOutputStream out) { + + DerOutputStream tmp = new DerOutputStream(); + hashAlgId.encode(tmp); + tmp.putOctetString(issuerNameHash); + tmp.putOctetString(issuerKeyHash); + certSerialNumber.encode(tmp); + out.write(DerValue.tag_Sequence, tmp); + + if (debug) { + HexDumpEncoder encoder = new HexDumpEncoder(); + System.out.println("Encoded certId is " + + encoder.encode(out.toByteArray())); + } + } + + /** + * Returns a hashcode value for this CertId. + * + * @return the hashcode value. + */ + @Override public int hashCode() { + if (myhash == -1) { + myhash = hashAlgId.hashCode(); + for (int i = 0; i < issuerNameHash.length; i++) { + myhash += issuerNameHash[i] * i; + } + for (int i = 0; i < issuerKeyHash.length; i++) { + myhash += issuerKeyHash[i] * i; + } + myhash += certSerialNumber.getNumber().hashCode(); + } + return myhash; + } + + /** + * Compares this CertId for equality with the specified + * object. Two CertId objects are considered equal if their hash algorithms, + * their issuer name and issuer key hash values and their serial numbers + * are equal. + * + * @param other the object to test for equality with this object. + * @return true if the objects are considered equal, false otherwise. + */ + @Override public boolean equals(Object other) { + if (this == other) { + return true; + } + if ((!(other instanceof CertId))) { + return false; + } + + CertId that = (CertId) other; + return hashAlgId.equals(that.getHashAlgorithm()) && + Arrays.equals(issuerNameHash, that.getIssuerNameHash()) && + Arrays.equals(issuerKeyHash, that.getIssuerKeyHash()) && + certSerialNumber.getNumber().equals(that.getSerialNumber()); + } + + /** + * Create a string representation of the CertId. + */ + @Override public String toString() { + HexDumpEncoder encoder = new HexDumpEncoder(); + return "CertId \n" + + "Algorithm: " + hashAlgId.toString() + "\n" + + "issuerNameHash \n" + + encoder.encode(issuerNameHash) + + "\nissuerKeyHash: \n" + + encoder.encode(issuerKeyHash) + + "\n" + certSerialNumber.toString(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathConstraintsParameters.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathConstraintsParameters.java new file mode 100644 index 000000000..98f40f360 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathConstraintsParameters.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.security.Key; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +import net.tongsuo.sun.security.util.CollectionUtil; +import net.tongsuo.sun.security.util.ConstraintsParameters; +import net.tongsuo.sun.security.validator.Validator; + +/** + * This class contains parameters for checking certificates against + * constraints specified in the jdk.certpath.disabledAlgorithms security + * property. + */ +public class CertPathConstraintsParameters implements ConstraintsParameters { + // The public key of the certificate + private final Key key; + // The certificate's trust anchor which will be checked against the + // jdkCA constraint, if specified. + private final TrustAnchor anchor; + // The PKIXParameter validity date or the timestamp of the signed JAR + // file, if this chain is associated with a timestamped signed JAR. + private final Date date; + // The variant or usage of this certificate + private final String variant; + // The certificate being checked (may be null if a raw public key, a CRL + // or an OCSPResponse is being checked) + private final X509Certificate cert; + + public CertPathConstraintsParameters(X509Certificate cert, + String variant, TrustAnchor anchor, Date date) { + this(cert.getPublicKey(), variant, anchor, date, cert); + } + + public CertPathConstraintsParameters(Key key, String variant, + TrustAnchor anchor, Date date) { + this(key, variant, anchor, date, null); + } + + private CertPathConstraintsParameters(Key key, String variant, + TrustAnchor anchor, Date date, X509Certificate cert) { + this.key = key; + this.variant = (variant == null ? Validator.VAR_GENERIC : variant); + this.anchor = anchor; + this.date = date; + this.cert = cert; + } + + @Override + public boolean anchorIsJdkCA() { + return CertPathHelper.isJdkCA(anchor); + } + + @Override + public Set getKeys() { + return (key == null) ? CollectionUtil.set() : CollectionUtil.set(key); + } + + @Override + public Date getDate() { + return date; + } + + @Override + public String getVariant() { + return variant; + } + + @Override + public String extendedExceptionMsg() { + return (cert == null ? "." + : " used with certificate: " + + cert.getSubjectX500Principal()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[\n"); + sb.append(" Variant: ").append(variant); + if (anchor != null) { + sb.append("\n Anchor: ").append(anchor); + } + if (cert != null) { + sb.append("\n Cert Issuer: ") + .append(cert.getIssuerX500Principal()); + sb.append("\n Cert Subject: ") + .append(cert.getSubjectX500Principal()); + } + if (key != null) { + sb.append("\n Key: ").append(key.getAlgorithm()); + } + if (date != null) { + sb.append("\n Date: ").append(date); + } + sb.append("\n]"); + return sb.toString(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathHelper.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathHelper.java new file mode 100644 index 000000000..c373f7c9d --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CertPathHelper.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2002, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.util.Date; +import java.util.Set; + +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; +import java.security.cert.X509CRLSelector; + +import net.tongsuo.sun.security.x509.GeneralNameInterface; + +/** + * Helper class that allows access to JDK specific known-public methods in the + * java.security.cert package. It relies on a subclass in the + * java.security.cert packages that is initialized before any of these methods + * are called (achieved via static initializers). + * + * The methods are made available in this fashion for performance reasons. + * + * @author Andreas Sterbenz + */ +public abstract class CertPathHelper { + + /** + * Object used to tunnel the calls. Initialized by CertPathHelperImpl. + */ + protected static CertPathHelper instance; + + protected CertPathHelper() { + // empty + } + + protected abstract void implSetPathToNames(X509CertSelector sel, + Set names); + + protected abstract void implSetDateAndTime(X509CRLSelector sel, Date date, long skew); + + protected abstract boolean implIsJdkCA(TrustAnchor anchor); + + static void setPathToNames(X509CertSelector sel, + Set names) { + instance.implSetPathToNames(sel, names); + } + + public static void setDateAndTime(X509CRLSelector sel, Date date, long skew) { + instance.implSetDateAndTime(sel, date, skew); + } + + public static boolean isJdkCA(TrustAnchor anchor) { + return anchor != null && instance.implIsJdkCA(anchor); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CollectionCertStore.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CollectionCertStore.java new file mode 100644 index 000000000..ddbf35c24 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/CollectionCertStore.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.security.InvalidAlgorithmParameterException; +import java.security.cert.Certificate; +import java.security.cert.CRL; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.HashSet; +import java.security.cert.CertSelector; +import java.security.cert.CertStoreException; +import java.security.cert.CertStoreParameters; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.CRLSelector; +import java.security.cert.CertStoreSpi; + +/** + * A CertStore that retrieves Certificates and + * CRLs from a Collection. + *

+ * Before calling the {@link #engineGetCertificates engineGetCertificates} or + * {@link #engineGetCRLs engineGetCRLs} methods, the + * {@link #CollectionCertStore(CertStoreParameters) + * CollectionCertStore(CertStoreParameters)} constructor is called to + * create the CertStore and establish the + * Collection from which Certificates and + * CRLs will be retrieved. If the specified + * Collection contains an object that is not a + * Certificate or CRL, that object will be + * ignored. + *

+ * Concurrent Access + *

+ * As described in the javadoc for CertStoreSpi, the + * engineGetCertificates and engineGetCRLs methods + * must be thread-safe. That is, multiple threads may concurrently + * invoke these methods on a single CollectionCertStore + * object (or more than one) with no ill effects. + *

+ * This is achieved by requiring that the Collection passed to + * the {@link #CollectionCertStore(CertStoreParameters) + * CollectionCertStore(CertStoreParameters)} constructor (via the + * CollectionCertStoreParameters object) must have fail-fast + * iterators. Simultaneous modifications to the Collection can thus be + * detected and certificate or CRL retrieval can be retried. The fact that + * Certificates and CRLs must be thread-safe is also + * essential. + * + * @see java.security.cert.CertStore + * + * @since 1.4 + * @author Steve Hanna + */ +public class CollectionCertStore extends CertStoreSpi { + + private final Collection coll; + + /** + * Creates a CertStore with the specified parameters. + * For this class, the parameters object must be an instance of + * CollectionCertStoreParameters. The Collection + * included in the CollectionCertStoreParameters object + * must be thread-safe. + * + * @param params the algorithm parameters + * @exception InvalidAlgorithmParameterException if params is not an + * instance of CollectionCertStoreParameters + */ + public CollectionCertStore(CertStoreParameters params) + throws InvalidAlgorithmParameterException + { + super(params); + if (!(params instanceof CollectionCertStoreParameters)) + throw new InvalidAlgorithmParameterException( + "parameters must be CollectionCertStoreParameters"); + coll = ((CollectionCertStoreParameters) params).getCollection(); + } + + /** + * Returns a Collection of Certificates that + * match the specified selector. If no Certificates + * match the selector, an empty Collection will be returned. + * + * @param selector a CertSelector used to select which + * Certificates should be returned. Specify null + * to return all Certificates. + * @return a Collection of Certificates that + * match the specified selector + * @throws CertStoreException if an exception occurs + */ + @Override + public Collection engineGetCertificates + (CertSelector selector) throws CertStoreException { + if (coll == null) { + throw new CertStoreException("Collection is null"); + } + // Tolerate a few ConcurrentModificationExceptions + for (int c = 0; c < 10; c++) { + try { + HashSet result = new HashSet<>(); + if (selector != null) { + for (Object o : coll) { + if ((o instanceof Certificate) && + selector.match((Certificate) o)) + result.add((Certificate)o); + } + } else { + for (Object o : coll) { + if (o instanceof Certificate) + result.add((Certificate)o); + } + } + return(result); + } catch (ConcurrentModificationException e) { } + } + throw new ConcurrentModificationException("Too many " + + "ConcurrentModificationExceptions"); + } + + /** + * Returns a Collection of CRLs that + * match the specified selector. If no CRLs + * match the selector, an empty Collection will be returned. + * + * @param selector a CRLSelector used to select which + * CRLs should be returned. Specify null + * to return all CRLs. + * @return a Collection of CRLs that + * match the specified selector + * @throws CertStoreException if an exception occurs + */ + @Override + public Collection engineGetCRLs(CRLSelector selector) + throws CertStoreException + { + if (coll == null) + throw new CertStoreException("Collection is null"); + + // Tolerate a few ConcurrentModificationExceptions + for (int c = 0; c < 10; c++) { + try { + HashSet result = new HashSet<>(); + if (selector != null) { + for (Object o : coll) { + if ((o instanceof CRL) && selector.match((CRL) o)) + result.add((CRL)o); + } + } else { + for (Object o : coll) { + if (o instanceof CRL) + result.add((CRL)o); + } + } + return result; + } catch (ConcurrentModificationException e) { } + } + throw new ConcurrentModificationException("Too many " + + "ConcurrentModificationExceptions"); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ConstraintsChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ConstraintsChecker.java new file mode 100644 index 000000000..42ccc7789 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ConstraintsChecker.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.PKIXReason; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import net.tongsuo.sun.security.x509.NameConstraintsExtension; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * ConstraintsChecker is a PKIXCertPathChecker that checks + * constraints information on a PKIX certificate, namely basic constraints + * and name constraints. + * + * @since 1.4 + * @author Yassir Elley + */ +class ConstraintsChecker extends PKIXCertPathChecker { + + private static final Debug debug = Debug.getInstance("certpath"); + /* length of cert path */ + private final int certPathLength; + /* current maximum path length (as defined in PKIX) */ + private int maxPathLength; + /* current index of cert */ + private int i; + private NameConstraintsExtension prevNC; + + private Set supportedExts; + + /** + * Creates a ConstraintsChecker. + * + * @param certPathLength the length of the certification path + */ + ConstraintsChecker(int certPathLength) { + this.certPathLength = certPathLength; + } + + @Override + public void init(boolean forward) throws CertPathValidatorException { + if (!forward) { + i = 0; + maxPathLength = certPathLength; + prevNC = null; + } else { + throw new CertPathValidatorException + ("forward checking not supported"); + } + } + + @Override + public boolean isForwardCheckingSupported() { + return false; + } + + @Override + public Set getSupportedExtensions() { + if (supportedExts == null) { + supportedExts = new HashSet(2); + supportedExts.add(PKIXExtensions.BasicConstraints_Id.toString()); + supportedExts.add(PKIXExtensions.NameConstraints_Id.toString()); + supportedExts = Collections.unmodifiableSet(supportedExts); + } + return supportedExts; + } + + /** + * Performs the basic constraints and name constraints + * checks on the certificate using its internal state. + * + * @param cert the Certificate to be checked + * @param unresCritExts a Collection of OID strings + * representing the current set of unresolved critical extensions + * @throws CertPathValidatorException if the specified certificate + * does not pass the check + */ + @Override + public void check(Certificate cert, Collection unresCritExts) + throws CertPathValidatorException + { + X509Certificate currCert = (X509Certificate)cert; + + i++; + // MUST run NC check second, since it depends on BC check to + // update remainingCerts + checkBasicConstraints(currCert); + verifyNameConstraints(currCert); + + if (unresCritExts != null && !unresCritExts.isEmpty()) { + unresCritExts.remove(PKIXExtensions.BasicConstraints_Id.toString()); + unresCritExts.remove(PKIXExtensions.NameConstraints_Id.toString()); + } + } + + /** + * Internal method to check the name constraints against a cert + */ + private void verifyNameConstraints(X509Certificate currCert) + throws CertPathValidatorException + { + String msg = "name constraints"; + if (debug != null) { + debug.println("---checking " + msg + "..."); + } + + // check name constraints only if there is a previous name constraint + // and either the currCert is the final cert or the currCert is not + // self-issued + if (prevNC != null && ((i == certPathLength) || + !X509CertImpl.isSelfIssued(currCert))) { + if (debug != null) { + debug.println("prevNC = " + prevNC + + ", currDN = " + currCert.getSubjectX500Principal()); + } + + try { + if (!prevNC.verify(currCert)) { + throw new CertPathValidatorException(msg + " check failed", + null, null, -1, PKIXReason.INVALID_NAME); + } + } catch (IOException ioe) { + throw new CertPathValidatorException(ioe); + } + } + + // merge name constraints regardless of whether cert is self-issued + prevNC = mergeNameConstraints(currCert, prevNC); + + if (debug != null) + debug.println(msg + " verified."); + } + + /** + * Helper to fold sets of name constraints together + */ + static NameConstraintsExtension mergeNameConstraints( + X509Certificate currCert, NameConstraintsExtension prevNC) + throws CertPathValidatorException + { + X509CertImpl currCertImpl; + try { + currCertImpl = X509CertImpl.toImpl(currCert); + } catch (CertificateException ce) { + throw new CertPathValidatorException(ce); + } + + NameConstraintsExtension newConstraints = + currCertImpl.getNameConstraintsExtension(); + + if (debug != null) { + debug.println("prevNC = " + prevNC + + ", newNC = " + newConstraints); + } + + // if there are no previous name constraints, we just return the + // new name constraints. + if (prevNC == null) { + if (debug != null) { + debug.println("mergedNC = " + newConstraints); + } + if (newConstraints == null) { + return null; + } else { + // Make sure we do a clone here, because we're probably + // going to modify this object later and we don't want to + // be sharing it with a Certificate object! + return (NameConstraintsExtension)newConstraints.clone(); + } + } else { + try { + // after merge, prevNC should contain the merged constraints + prevNC.merge(newConstraints); + } catch (IOException ioe) { + throw new CertPathValidatorException(ioe); + } + if (debug != null) { + debug.println("mergedNC = " + prevNC); + } + return prevNC; + } + } + + /** + * Internal method to check that a given cert meets basic constraints. + */ + private void checkBasicConstraints(X509Certificate currCert) + throws CertPathValidatorException + { + String msg = "basic constraints"; + if (debug != null) { + debug.println("---checking " + msg + "..."); + debug.println("i = " + i + + ", maxPathLength = " + maxPathLength); + } + + /* check if intermediate cert */ + if (i < certPathLength) { + // RFC5280: If certificate i is a version 3 certificate, verify + // that the basicConstraints extension is present and that cA is + // set to TRUE. (If certificate i is a version 1 or version 2 + // certificate, then the application MUST either verify that + // certificate i is a CA certificate through out-of-band means + // or reject the certificate. Conforming implementations may + // choose to reject all version 1 and version 2 intermediate + // certificates.) + // + // We choose to reject all version 1 and version 2 intermediate + // certificates except that it is self issued by the trust + // anchor in order to support key rollover or changes in + // certificate policies. + int pathLenConstraint = -1; + if (currCert.getVersion() < 3) { // version 1 or version 2 + if (i == 1) { // issued by a trust anchor + if (X509CertImpl.isSelfIssued(currCert)) { + pathLenConstraint = Integer.MAX_VALUE; + } + } + } else { + pathLenConstraint = currCert.getBasicConstraints(); + } + + if (pathLenConstraint == -1) { + throw new CertPathValidatorException + (msg + " check failed: this is not a CA certificate", + null, null, -1, PKIXReason.NOT_CA_CERT); + } + + if (!X509CertImpl.isSelfIssued(currCert)) { + if (maxPathLength <= 0) { + throw new CertPathValidatorException + (msg + " check failed: pathLenConstraint violated - " + + "this cert must be the last cert in the " + + "certification path", null, null, -1, + PKIXReason.PATH_TOO_LONG); + } + maxPathLength--; + } + if (pathLenConstraint < maxPathLength) + maxPathLength = pathLenConstraint; + } + + if (debug != null) { + debug.println("after processing, maxPathLength = " + maxPathLength); + debug.println(msg + " verified."); + } + } + + /** + * Merges the specified maxPathLength with the pathLenConstraint + * obtained from the certificate. + * + * @param cert the X509Certificate + * @param maxPathLength the previous maximum path length + * @return the new maximum path length constraint (-1 means no more + * certificates can follow, Integer.MAX_VALUE means path length is + * unconstrained) + */ + static int mergeBasicConstraints(X509Certificate cert, int maxPathLength) { + + int pathLenConstraint = cert.getBasicConstraints(); + + if (!X509CertImpl.isSelfIssued(cert)) { + maxPathLength--; + } + + if (pathLenConstraint < maxPathLength) { + maxPathLength = pathLenConstraint; + } + + return maxPathLength; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/DistributionPointFetcher.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/DistributionPointFetcher.java new file mode 100644 index 000000000..47cd956af --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/DistributionPointFetcher.java @@ -0,0 +1,778 @@ +/* + * Copyright (c) 2002, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.*; +import java.net.URI; +import java.security.*; +import java.security.cert.*; +import javax.security.auth.x500.X500Principal; +import java.util.*; + +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.cert.URICertStoreParameters; +import net.tongsuo.sun.security.x509.AuthorityKeyIdentifierExtension; +import net.tongsuo.sun.security.x509.CRLDistributionPointsExtension; +import net.tongsuo.sun.security.x509.DistributionPoint; +import net.tongsuo.sun.security.x509.DistributionPointName; +import net.tongsuo.sun.security.x509.GeneralName; +import net.tongsuo.sun.security.x509.GeneralNameInterface; +import net.tongsuo.sun.security.x509.GeneralNames; +import net.tongsuo.sun.security.x509.IssuingDistributionPointExtension; +import net.tongsuo.sun.security.x509.KeyIdentifier; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.RDN; +import net.tongsuo.sun.security.x509.ReasonFlags; +import net.tongsuo.sun.security.x509.SerialNumber; +import net.tongsuo.sun.security.x509.URIName; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CRLImpl; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * Class to obtain CRLs via the CRLDistributionPoints extension. + * Note that the functionality of this class must be explicitly enabled + * via a system property, see the USE_CRLDP variable below. + * + * This class uses the URICertStore class to fetch CRLs. The URICertStore + * class also implements CRL caching: see the class description for more + * information. + * + * @author Andreas Sterbenz + * @author Sean Mullan + * @since 1.4.2 + */ +public class DistributionPointFetcher { + + private static final Debug debug = Debug.getInstance("certpath"); + + private static final boolean[] ALL_REASONS = + {true, true, true, true, true, true, true, true, true}; + + /** + * Private instantiation only. + */ + private DistributionPointFetcher() {} + + /** + * Return the X509CRLs matching this selector. The selector must be + * an X509CRLSelector with certificateChecking set. + */ + public static Collection getCRLs(X509CRLSelector selector, + boolean signFlag, + PublicKey prevKey, + X509Certificate prevCert, + String provider, + List certStores, + boolean[] reasonsMask, + Set trustAnchors, + Date validity, + String variant, + TrustAnchor anchor) + throws CertStoreException + { + X509Certificate cert = selector.getCertificateChecking(); + if (cert == null) { + return Collections.emptySet(); + } + try { + X509CertImpl certImpl = X509CertImpl.toImpl(cert); + if (debug != null) { + debug.println("DistributionPointFetcher.getCRLs: Checking " + + "CRLDPs for " + certImpl.getSubjectX500Principal()); + } + CRLDistributionPointsExtension ext = + certImpl.getCRLDistributionPointsExtension(); + if (ext == null) { + if (debug != null) { + debug.println("No CRLDP ext"); + } + return Collections.emptySet(); + } + List points = + ext.getDistributionPoints(); + Set results = new HashSet<>(); + for (Iterator t = points.iterator(); + t.hasNext() && !Arrays.equals(reasonsMask, ALL_REASONS); ) { + DistributionPoint point = t.next(); + Collection crls = getCRLs(selector, certImpl, + point, reasonsMask, signFlag, prevKey, prevCert, provider, + certStores, trustAnchors, validity, variant, anchor); + results.addAll(crls); + } + if (debug != null) { + debug.println("Returning " + results.size() + " CRLs"); + } + return results; + } catch (CertificateException e) { + return Collections.emptySet(); + } + } + + /** + * Download CRLs from the given distribution point, verify and return them. + * See the top of the class for current limitations. + * + * @throws CertStoreException if there is an error retrieving the CRLs + * from one of the GeneralNames and no other CRLs are retrieved from + * the other GeneralNames. If more than one GeneralName throws an + * exception then the one from the last GeneralName is thrown. + */ + private static Collection getCRLs(X509CRLSelector selector, + X509CertImpl certImpl, DistributionPoint point, boolean[] reasonsMask, + boolean signFlag, PublicKey prevKey, X509Certificate prevCert, + String provider, List certStores, + Set trustAnchors, Date validity, String variant, + TrustAnchor anchor) + throws CertStoreException { + + // check for full name + GeneralNames fullName = point.getFullName(); + if (fullName == null) { + // check for relative name + RDN relativeName = point.getRelativeName(); + if (relativeName == null) { + return Collections.emptySet(); + } + try { + GeneralNames crlIssuers = point.getCRLIssuer(); + if (crlIssuers == null) { + fullName = getFullNames + ((X500Name) certImpl.getIssuerDN(), relativeName); + } else { + // should only be one CRL Issuer + if (crlIssuers.size() != 1) { + return Collections.emptySet(); + } else { + fullName = getFullNames + ((X500Name) crlIssuers.get(0).getName(), relativeName); + } + } + } catch (IOException ioe) { + return Collections.emptySet(); + } + } + Collection possibleCRLs = new ArrayList<>(); + CertStoreException savedCSE = null; + for (Iterator t = fullName.iterator(); t.hasNext(); ) { + try { + GeneralName name = t.next(); + if (name.getType() == GeneralNameInterface.NAME_DIRECTORY) { + X500Name x500Name = (X500Name) name.getName(); + possibleCRLs.addAll( + getCRLs(x500Name, certImpl.getIssuerX500Principal(), + certStores)); + } else if (name.getType() == GeneralNameInterface.NAME_URI) { + URIName uriName = (URIName)name.getName(); + X509CRL crl = getCRL(uriName); + if (crl != null) { + possibleCRLs.add(crl); + } + } + } catch (CertStoreException cse) { + savedCSE = cse; + } + } + // only throw CertStoreException if no CRLs are retrieved + if (possibleCRLs.isEmpty() && savedCSE != null) { + throw savedCSE; + } + + Collection crls = new ArrayList<>(2); + for (X509CRL crl : possibleCRLs) { + try { + // make sure issuer is not set + // we check the issuer in verifyCRLs method + selector.setIssuerNames(null); + if (selector.match(crl) && verifyCRL(certImpl, point, crl, + reasonsMask, signFlag, prevKey, prevCert, provider, + trustAnchors, certStores, validity, variant, anchor)) { + crls.add(crl); + } + } catch (IOException | CRLException e) { + // don't add the CRL + if (debug != null) { + debug.println("Exception verifying CRL: " + e.getMessage()); + e.printStackTrace(); + } + } + } + return crls; + } + + /** + * Download CRL from given URI. + */ + private static X509CRL getCRL(URIName name) throws CertStoreException { + URI uri = name.getURI(); + if (debug != null) { + debug.println("Trying to fetch CRL from DP " + uri); + } + +// Event.report(Event.ReporterCategory.CRLCHECK, "event.crl.check", uri.toString()); + CertStore ucs; + try { + ucs = URICertStore.getInstance(new URICertStoreParameters(uri)); + } catch (InvalidAlgorithmParameterException | + NoSuchAlgorithmException e) { + if (debug != null) { + debug.println("Can't create URICertStore: " + e.getMessage()); + } + return null; + } + + Collection crls = ucs.getCRLs(null); + if (crls.isEmpty()) { + return null; + } else { + return (X509CRL) crls.iterator().next(); + } + } + + /** + * Fetch CRLs from certStores. + * + * @throws CertStoreException if there is an error retrieving the CRLs from + * one of the CertStores and no other CRLs are retrieved from + * the other CertStores. If more than one CertStore throws an + * exception then the one from the last CertStore is thrown. + */ + private static Collection getCRLs(X500Name name, + X500Principal certIssuer, + List certStores) + throws CertStoreException + { + if (debug != null) { + debug.println("Trying to fetch CRL from DP " + name); + } + X509CRLSelector xcs = new X509CRLSelector(); + xcs.addIssuer(name.asX500Principal()); + xcs.addIssuer(certIssuer); + Collection crls = new ArrayList<>(); + CertStoreException savedCSE = null; + for (CertStore store : certStores) { + try { + for (CRL crl : store.getCRLs(xcs)) { + crls.add((X509CRL)crl); + } + } catch (CertStoreException cse) { + if (debug != null) { + debug.println("Exception while retrieving " + + "CRLs: " + cse); + cse.printStackTrace(); + } + savedCSE = new PKIX.CertStoreTypeException(store.getType(),cse); + } + } + // only throw CertStoreException if no CRLs are retrieved + if (crls.isEmpty() && savedCSE != null) { + throw savedCSE; + } else { + return crls; + } + } + + /** + * Verifies a CRL for the given certificate's Distribution Point to + * ensure it is appropriate for checking the revocation status. + * + * @param certImpl the certificate whose revocation status is being checked + * @param point one of the distribution points of the certificate + * @param crl the CRL + * @param reasonsMask the interim reasons mask + * @param signFlag true if prevKey can be used to verify the CRL + * @param prevKey the public key that verifies the certificate's signature + * @param prevCert the certificate whose public key verifies + * {@code certImpl}'s signature + * @param provider the Signature provider to use + * @param trustAnchors a {@code Set} of {@code TrustAnchor}s + * @param certStores a {@code List} of {@code CertStore}s to be used in + * finding certificates and CRLs + * @param validity the time for which the validity of the CRL issuer's + * certification path should be determined + * @return true if ok, false if not + */ + static boolean verifyCRL(X509CertImpl certImpl, DistributionPoint point, + X509CRL crl, boolean[] reasonsMask, boolean signFlag, + PublicKey prevKey, X509Certificate prevCert, String provider, + Set trustAnchors, List certStores, + Date validity, String variant, TrustAnchor anchor) + throws CRLException, IOException { + + if (debug != null) { + debug.println("DistributionPointFetcher.verifyCRL: " + + "checking revocation status for" + + "\n SN: " + Debug.toHexString(certImpl.getSerialNumber()) + + "\n Subject: " + certImpl.getSubjectX500Principal() + + "\n Issuer: " + certImpl.getIssuerX500Principal()); + } + + boolean indirectCRL = false; + X509CRLImpl crlImpl = X509CRLImpl.toImpl(crl); + IssuingDistributionPointExtension idpExt = + crlImpl.getIssuingDistributionPointExtension(); + X500Name certIssuer = (X500Name) certImpl.getIssuerDN(); + X500Name crlIssuer = (X500Name) crlImpl.getIssuerDN(); + + // if crlIssuer is set, verify that it matches the issuer of the + // CRL and the CRL contains an IDP extension with the indirectCRL + // boolean asserted. Otherwise, verify that the CRL issuer matches the + // certificate issuer. + GeneralNames pointCrlIssuers = point.getCRLIssuer(); + X500Name pointCrlIssuer = null; + if (pointCrlIssuers != null) { + if (idpExt == null || !idpExt.isIndirectCRL()) { + return false; + } + boolean match = false; + for (Iterator t = pointCrlIssuers.iterator(); + !match && t.hasNext(); ) { + GeneralNameInterface name = t.next().getName(); + if (crlIssuer.equals(name)) { + pointCrlIssuer = (X500Name) name; + match = true; + } + } + if (!match) { + return false; + } + + // we accept the case that a CRL issuer provide status + // information for itself. + if (issues(certImpl, crlImpl, provider)) { + // reset the public key used to verify the CRL's signature + prevKey = certImpl.getPublicKey(); + } else { + indirectCRL = true; + } + } else if (!crlIssuer.equals(certIssuer)) { + if (debug != null) { + debug.println("crl issuer does not equal cert issuer.\n" + + "crl issuer: " + crlIssuer + "\n" + + "cert issuer: " + certIssuer); + } + return false; + } else { + // in case of self-issued indirect CRL issuer. + KeyIdentifier certAKID = certImpl.getAuthKeyId(); + KeyIdentifier crlAKID = crlImpl.getAuthKeyId(); + + if (certAKID == null || crlAKID == null) { + // cannot recognize indirect CRL without AKID + + // we accept the case that a CRL issuer provide status + // information for itself. + if (issues(certImpl, crlImpl, provider)) { + // reset the public key used to verify the CRL's signature + prevKey = certImpl.getPublicKey(); + } + } else if (!certAKID.equals(crlAKID)) { + // we accept the case that a CRL issuer provide status + // information for itself. + if (issues(certImpl, crlImpl, provider)) { + // reset the public key used to verify the CRL's signature + prevKey = certImpl.getPublicKey(); + } else { + indirectCRL = true; + } + } + } + + if (!indirectCRL && !signFlag) { + // cert's key cannot be used to verify the CRL + return false; + } + + if (idpExt != null) { + DistributionPointName idpPoint = idpExt.getDistributionPoint(); + if (idpPoint != null) { + GeneralNames idpNames = idpPoint.getFullName(); + if (idpNames == null) { + RDN relativeName = idpPoint.getRelativeName(); + if (relativeName == null) { + if (debug != null) { + debug.println("IDP must be relative or full DN"); + } + return false; + } + if (debug != null) { + debug.println("IDP relativeName:" + relativeName); + } + idpNames = getFullNames(crlIssuer, relativeName); + } + // if the DP name is present in the IDP CRL extension and the + // DP field is present in the DP, then verify that one of the + // names in the IDP matches one of the names in the DP + if (point.getFullName() != null || + point.getRelativeName() != null) { + GeneralNames pointNames = point.getFullName(); + if (pointNames == null) { + RDN relativeName = point.getRelativeName(); + if (relativeName == null) { + if (debug != null) { + debug.println("DP must be relative or full DN"); + } + return false; + } + if (debug != null) { + debug.println("DP relativeName:" + relativeName); + } + if (indirectCRL) { + if (pointCrlIssuers == null || pointCrlIssuers.size() != 1) { + // RFC 5280: there must be only 1 CRL issuer + // name when relativeName is present + if (debug != null) { + debug.println("must only be one CRL " + + "issuer when relative name present"); + } + return false; + } + // if pointCrlIssuers is not null, pointCrlIssuer + // will also be non-null or the code would have + // returned before now + pointNames = getFullNames + (pointCrlIssuer, relativeName); + } else { + pointNames = getFullNames(certIssuer, relativeName); + } + } + boolean match = false; + for (Iterator i = idpNames.iterator(); + !match && i.hasNext(); ) { + GeneralNameInterface idpName = i.next().getName(); + if (debug != null) { + debug.println("idpName: " + idpName); + } + for (Iterator p = pointNames.iterator(); + !match && p.hasNext(); ) { + GeneralNameInterface pointName = p.next().getName(); + if (debug != null) { + debug.println("pointName: " + pointName); + } + match = idpName.equals(pointName); + } + } + if (!match) { + if (debug != null) { + debug.println("IDP name does not match DP name"); + } + return false; + } + // if the DP name is present in the IDP CRL extension and the + // DP field is absent from the DP, then verify that one of the + // names in the IDP matches one of the names in the crlIssuer + // field of the DP + } else { + // verify that one of the names in the IDP matches one of + // the names in the cRLIssuer of the cert's DP + boolean match = false; + // the DP's fullName and relativeName fields are null + // which means pointCrlIssuers is non-null; the three + // cannot all be missing from a certificate. + for (Iterator t = pointCrlIssuers.iterator(); + !match && t.hasNext(); ) { + GeneralNameInterface crlIssuerName = t.next().getName(); + for (Iterator i = idpNames.iterator(); + !match && i.hasNext(); ) { + GeneralNameInterface idpName = i.next().getName(); + match = crlIssuerName.equals(idpName); + } + } + if (!match) { + return false; + } + } + } + + // if the onlyContainsUserCerts boolean is asserted, verify that the + // cert is not a CA cert + boolean b = idpExt.hasOnlyUserCerts(); + if (b && certImpl.getBasicConstraints() != -1) { + if (debug != null) { + debug.println("cert must be a EE cert"); + } + return false; + } + + // if the onlyContainsCACerts boolean is asserted, verify that the + // cert is a CA cert + b = idpExt.hasOnlyCACerts(); + if (b && certImpl.getBasicConstraints() == -1) { + if (debug != null) { + debug.println("cert must be a CA cert"); + } + return false; + } + + // verify that the onlyContainsAttributeCerts boolean is not + // asserted + b = idpExt.hasOnlyAttributeCerts(); + if (b) { + if (debug != null) { + debug.println("cert must not be an AA cert"); + } + return false; + } + } + + // compute interim reasons mask + boolean[] interimReasonsMask = new boolean[9]; + ReasonFlags reasons = null; + if (idpExt != null) { + reasons = idpExt.getRevocationReasons(); + } + + boolean[] pointReasonFlags = point.getReasonFlags(); + if (reasons != null) { + if (pointReasonFlags != null) { + // set interim reasons mask to the intersection of + // reasons in the DP and onlySomeReasons in the IDP + boolean[] idpReasonFlags = reasons.getFlags(); + for (int i = 0; i < interimReasonsMask.length; i++) { + interimReasonsMask[i] = + (i < idpReasonFlags.length && idpReasonFlags[i]) && + (i < pointReasonFlags.length && pointReasonFlags[i]); + } + } else { + // set interim reasons mask to the value of + // onlySomeReasons in the IDP (and clone it since we may + // modify it) + interimReasonsMask = reasons.getFlags().clone(); + } + } else { + if (pointReasonFlags != null) { + // set interim reasons mask to the value of DP reasons + interimReasonsMask = pointReasonFlags.clone(); + } else { + // set interim reasons mask to the special value all-reasons + Arrays.fill(interimReasonsMask, true); + } + } + + // verify that interim reasons mask includes one or more reasons + // not included in the reasons mask + boolean oneOrMore = false; + for (int i = 0; i < interimReasonsMask.length; i++) { + if (interimReasonsMask[i] && + !(i < reasonsMask.length && reasonsMask[i])) { + oneOrMore = true; + } + } + if (!oneOrMore) { + return false; + } + + // Obtain and validate the certification path for the complete + // CRL issuer (if indirect CRL). If a key usage extension is present + // in the CRL issuer's certificate, verify that the cRLSign bit is set. + if (indirectCRL) { + X509CertSelector certSel = new X509CertSelector(); + certSel.setSubject(crlIssuer.asX500Principal()); + boolean[] crlSign = {false,false,false,false,false,false,true}; + certSel.setKeyUsage(crlSign); + + // Currently, by default, forward builder does not enable + // subject/authority key identifier identifying for target + // certificate, instead, it only compares the CRL issuer and + // the target certificate subject. If the certificate of the + // delegated CRL issuer is a self-issued certificate, the + // builder is unable to find the proper CRL issuer by issuer + // name only, there is a potential dead loop on finding the + // proper issuer. It is of great help to narrow the target + // scope down to aware of authority key identifiers in the + // selector, for the purposes of breaking the dead loop. + AuthorityKeyIdentifierExtension akidext = + crlImpl.getAuthKeyIdExtension(); + if (akidext != null) { + byte[] kid = akidext.getEncodedKeyIdentifier(); + if (kid != null) { + certSel.setSubjectKeyIdentifier(kid); + } + + SerialNumber asn = akidext.getSerialNumber(); + if (asn != null) { + certSel.setSerialNumber(asn.getNumber()); + } + // the subject criterion will be set by builder automatically. + } + + // By now, we have validated the previous certificate, so we can + // trust it during the validation of the CRL issuer. + // In addition to the performance improvement, another benefit is to + // break the dead loop while looking for the issuer back and forth + // between the delegated self-issued certificate and its issuer. + Set newTrustAnchors = new HashSet<>(trustAnchors); + + if (prevKey != null) { + // Add the previous certificate as a trust anchor. + // If prevCert is not null, we want to construct a TrustAnchor + // using the cert object because when the certpath for the CRL + // is built later, the CertSelector will make comparisons with + // the TrustAnchor's trustedCert member rather than its pubKey. + TrustAnchor temporary; + if (prevCert != null) { + temporary = new TrustAnchor(prevCert, null); + } else { + X500Principal principal = certImpl.getIssuerX500Principal(); + temporary = new TrustAnchor(principal, prevKey, null); + } + newTrustAnchors.add(temporary); + } + + PKIXBuilderParameters params; + try { + params = new PKIXBuilderParameters(newTrustAnchors, certSel); + } catch (InvalidAlgorithmParameterException iape) { + throw new CRLException(iape); + } + params.setCertStores(certStores); + params.setSigProvider(provider); + params.setDate(validity); + try { + CertPathBuilder builder = PKIXInsts.getCertPathBuilder("PKIX"); + PKIXCertPathBuilderResult result = + (PKIXCertPathBuilderResult) builder.build(params); + prevKey = result.getPublicKey(); + } catch (GeneralSecurityException e) { + throw new CRLException(e); + } + } + + // check the crl signature algorithm + try { + AlgorithmChecker.check(prevKey, crlImpl.getSigAlgId(), + variant, anchor); + } catch (CertPathValidatorException cpve) { + if (debug != null) { + debug.println("CRL signature algorithm check failed: " + cpve); + } + return false; + } + + // validate the signature on the CRL + try { + crl.verify(prevKey, provider); + } catch (GeneralSecurityException e) { + if (debug != null) { + debug.println("CRL signature failed to verify"); + } + return false; + } + + // reject CRL if any unresolved critical extensions remain in the CRL. + Set unresCritExts = crl.getCriticalExtensionOIDs(); + // remove any that we have processed + if (unresCritExts != null) { + unresCritExts.remove(PKIXExtensions.IssuingDistributionPoint_Id.toString()); + if (!unresCritExts.isEmpty()) { + if (debug != null) { + debug.println("Unrecognized critical extension(s) in CRL: " + + unresCritExts); + for (String ext : unresCritExts) { + debug.println(ext); + } + } + return false; + } + } + + // update reasonsMask + for (int i = 0; i < reasonsMask.length; i++) { + reasonsMask[i] = reasonsMask[i] || + (i < interimReasonsMask.length && interimReasonsMask[i]); + } + + return true; + } + + /** + * Append relative name to the issuer name and return a new + * GeneralNames object. + */ + private static GeneralNames getFullNames(X500Name issuer, RDN rdn) + throws IOException + { + List rdns = new ArrayList<>(issuer.rdns()); + rdns.add(rdn); + X500Name fullName = new X500Name(rdns.toArray(new RDN[0])); + GeneralNames fullNames = new GeneralNames(); + fullNames.add(new GeneralName(fullName)); + return fullNames; + } + + /** + * Verifies whether a CRL is issued by a certain certificate + * + * @param cert the certificate + * @param crl the CRL to be verified + * @param provider the name of the signature provider + */ + private static boolean issues(X509CertImpl cert, X509CRLImpl crl, + String provider) throws IOException + { + boolean matched; + + AdaptableX509CertSelector issuerSelector = + new AdaptableX509CertSelector(); + + // check certificate's key usage + boolean[] usages = cert.getKeyUsage(); + if (usages != null) { + usages[6] = true; // cRLSign + issuerSelector.setKeyUsage(usages); + } + + // check certificate's subject + X500Principal crlIssuer = crl.getIssuerX500Principal(); + issuerSelector.setSubject(crlIssuer); + + /* + * Facilitate certification path construction with authority + * key identifier and subject key identifier. + * + * In practice, conforming CAs MUST use the key identifier method, + * and MUST include authority key identifier extension in all CRLs + * issued. [section 5.2.1, RFC 5280] + */ + AuthorityKeyIdentifierExtension crlAKID = crl.getAuthKeyIdExtension(); + issuerSelector.setSkiAndSerialNumber(crlAKID); + + matched = issuerSelector.match(cert); + + // if AKID is unreliable, verify the CRL signature with the cert + if (matched && (crlAKID == null || + cert.getAuthorityKeyIdentifierExtension() == null)) { + try { + crl.verify(cert.getPublicKey(), provider); + } catch (GeneralSecurityException e) { + matched = false; + } + } + + return matched; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardBuilder.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardBuilder.java new file mode 100644 index 000000000..653df453a --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardBuilder.java @@ -0,0 +1,914 @@ +/* + * Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.PKIXReason; +import java.security.cert.CertStore; +import java.security.cert.CertStoreException; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.security.cert.X509CertSelector; +import java.util.*; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.sun.security.provider.certpath.PKIX.BuilderParams; +import net.tongsuo.sun.security.x509.AccessDescription; +import net.tongsuo.sun.security.x509.AuthorityInfoAccessExtension; +import net.tongsuo.sun.security.x509.AuthorityKeyIdentifierExtension; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * This class represents a forward builder, which is able to retrieve + * matching certificates from CertStores and verify a particular certificate + * against a ForwardState. + * + * @since 1.4 + * @author Yassir Elley + * @author Sean Mullan + */ +class ForwardBuilder extends Builder { + + private static final Debug debug = Debug.getInstance("certpath"); + private final Set trustedCerts; + private final Set trustedSubjectDNs; + private final Set trustAnchors; + private X509CertSelector eeSelector; + private AdaptableX509CertSelector caSelector; + private X509CertSelector caTargetSelector; + TrustAnchor trustAnchor; + private final boolean searchAllCertStores; + + /** + * Initialize the builder with the input parameters. + * + * @param buildParams the parameter set used to build a certification path + */ + ForwardBuilder(BuilderParams buildParams, boolean searchAllCertStores) { + super(buildParams); + + // populate sets of trusted certificates and subject DNs + trustAnchors = buildParams.trustAnchors(); + trustedCerts = new HashSet(trustAnchors.size()); + trustedSubjectDNs = new HashSet(trustAnchors.size()); + for (TrustAnchor anchor : trustAnchors) { + X509Certificate trustedCert = anchor.getTrustedCert(); + if (trustedCert != null) { + trustedCerts.add(trustedCert); + trustedSubjectDNs.add(trustedCert.getSubjectX500Principal()); + } else { + trustedSubjectDNs.add(anchor.getCA()); + } + } + this.searchAllCertStores = searchAllCertStores; + } + + /** + * Retrieves all certs from the specified CertStores that satisfy the + * requirements specified in the parameters and the current + * PKIX state (name constraints, policy constraints, etc.). + * + * @param currentState the current state. + * Must be an instance of ForwardState + * @param certStores list of CertStores + */ + @Override + Collection getMatchingCerts(State currentState, + List certStores) + throws IOException + { + if (debug != null) { + debug.println("ForwardBuilder.getMatchingCerts()..."); + } + + ForwardState currState = (ForwardState) currentState; + + /* + * We store certs in a Set because we don't want duplicates. + * As each cert is added, it is sorted based on the PKIXCertComparator + * algorithm. + */ + Comparator comparator = + new PKIXCertComparator(trustedSubjectDNs, currState.cert); + Set certs = new TreeSet<>(comparator); + + /* + * Only look for EE certs if search has just started. + */ + if (currState.isInitial()) { + getMatchingEECerts(currState, certStores, certs); + } + getMatchingCACerts(currState, certStores, certs); + + return certs; + } + + /* + * Retrieves all end-entity certificates which satisfy constraints + * and requirements specified in the parameters and PKIX state. + */ + private void getMatchingEECerts(ForwardState currentState, + List certStores, + Collection eeCerts) + throws IOException + { + if (debug != null) { + debug.println("ForwardBuilder.getMatchingEECerts()..."); + } + /* + * Compose a certificate matching rule to filter out + * certs which don't satisfy constraints + * + * First, retrieve clone of current target cert constraints, + * and then add more selection criteria based on current validation + * state. Since selector never changes, cache local copy & reuse. + */ + if (eeSelector == null) { + eeSelector = (X509CertSelector) targetCertConstraints.clone(); + + /* + * Match on certificate validity date + */ + eeSelector.setCertificateValid(buildParams.date()); + + /* + * Policy processing optimizations + */ + if (buildParams.explicitPolicyRequired()) { + eeSelector.setPolicy(getMatchingPolicies()); + } + /* + * Require EE certs + */ + eeSelector.setBasicConstraints(-2); + } + + /* Retrieve matching EE certs from CertStores */ + addMatchingCerts(eeSelector, certStores, eeCerts, searchAllCertStores); + } + + /** + * Retrieves all CA certificates which satisfy constraints + * and requirements specified in the parameters and PKIX state. + */ + private void getMatchingCACerts(ForwardState currentState, + List certStores, + Collection caCerts) + throws IOException + { + if (debug != null) { + debug.println("ForwardBuilder.getMatchingCACerts()..."); + } + int initialSize = caCerts.size(); + + /* + * Compose a CertSelector to filter out + * certs which do not satisfy requirements. + */ + X509CertSelector sel; + + if (currentState.isInitial()) { + if (targetCertConstraints.getBasicConstraints() == -2) { + // no need to continue: this means we never can match a CA cert + return; + } + + /* This means a CA is the target, so match on same stuff as + * getMatchingEECerts + */ + if (debug != null) { + debug.println("ForwardBuilder.getMatchingCACerts(): " + + "the target is a CA"); + } + + if (caTargetSelector == null) { + caTargetSelector = + (X509CertSelector) targetCertConstraints.clone(); + + /* + * Since we don't check the validity period of trusted + * certificates, please don't set the certificate valid + * criterion unless the trusted certificate matching is + * completed. + */ + + /* + * Policy processing optimizations + */ + if (buildParams.explicitPolicyRequired()) + caTargetSelector.setPolicy(getMatchingPolicies()); + } + + sel = caTargetSelector; + } else { + + if (caSelector == null) { + caSelector = new AdaptableX509CertSelector(); + + /* + * Since we don't check the validity period of trusted + * certificates, please don't set the certificate valid + * criterion unless the trusted certificate matching is + * completed. + */ + + /* + * Policy processing optimizations + */ + if (buildParams.explicitPolicyRequired()) + caSelector.setPolicy(getMatchingPolicies()); + } + + /* + * Match on subject (issuer of previous cert) + */ + caSelector.setSubject(currentState.issuerDN); + + /* + * Match on subjectNamesTraversed (both DNs and AltNames) + * (checks that current cert's name constraints permit it + * to certify all the DNs and AltNames that have been traversed) + */ + // TODO just ignore this method. +// CertPathHelper.setPathToNames +// (caSelector, currentState.subjectNamesTraversed); + + /* + * check the validity period + */ + caSelector.setValidityPeriod(currentState.cert.getNotBefore(), + currentState.cert.getNotAfter()); + + sel = caSelector; + } + + /* + * For compatibility, conservatively, we don't check the path + * length constraint of trusted anchors. Please don't set the + * basic constraints criterion unless the trusted certificate + * matching is completed. + */ + sel.setBasicConstraints(-1); + + for (X509Certificate trustedCert : trustedCerts) { + if (sel.match(trustedCert)) { + if (debug != null) { + debug.println("ForwardBuilder.getMatchingCACerts: " + + "found matching trust anchor." + + "\n SN: " + + Debug.toHexString(trustedCert.getSerialNumber()) + + "\n Subject: " + + trustedCert.getSubjectX500Principal() + + "\n Issuer: " + + trustedCert.getIssuerX500Principal()); + } + if (caCerts.add(trustedCert) && !searchAllCertStores) { + return; + } + } + } + + /* + * The trusted certificate matching is completed. We need to match + * on certificate validity date. + */ + sel.setCertificateValid(buildParams.date()); + + /* + * Require CA certs with a pathLenConstraint that allows + * at least as many CA certs that have already been traversed + */ + sel.setBasicConstraints(currentState.traversedCACerts); + + /* + * If we have already traversed as many CA certs as the maxPathLength + * will allow us to, then we don't bother looking through these + * certificate pairs. If maxPathLength has a value of -1, this + * means it is unconstrained, so we always look through the + * certificate pairs. + */ + if (currentState.isInitial() || + (buildParams.maxPathLength() == -1) || + (buildParams.maxPathLength() > currentState.traversedCACerts)) + { + if (addMatchingCerts(sel, certStores, + caCerts, searchAllCertStores) + && !searchAllCertStores) { + return; + } + } + + if (!currentState.isInitial() && USE_AIA) { + // check for AuthorityInformationAccess extension + AuthorityInfoAccessExtension aiaExt = + currentState.cert.getAuthorityInfoAccessExtension(); + if (aiaExt != null) { + getCerts(aiaExt, caCerts); + } + } + + if (debug != null) { + int numCerts = caCerts.size() - initialSize; + debug.println("ForwardBuilder.getMatchingCACerts: found " + + numCerts + " CA certs"); + } + } + + /** + * Download Certificates from the given AIA and add them to the + * specified Collection. + */ + // cs.getCertificates(caSelector) returns a collection of X509Certificate's + // because of the selector, so the cast is safe + @SuppressWarnings("unchecked") + private boolean getCerts(AuthorityInfoAccessExtension aiaExt, + Collection certs) + { + if (!USE_AIA) { + return false; + } + List adList = aiaExt.getAccessDescriptions(); + if (adList == null || adList.isEmpty()) { + return false; + } + + boolean add = false; + for (AccessDescription ad : adList) { + CertStore cs = URICertStore.getInstance(ad); + if (cs != null) { + try { + if (certs.addAll((Collection) + cs.getCertificates(caSelector))) { + add = true; + if (!searchAllCertStores) { + return true; + } + } + } catch (CertStoreException cse) { + if (debug != null) { + debug.println("exception getting certs from CertStore:"); + cse.printStackTrace(); + } + } + } + } + return add; + } + + /** + * This inner class compares 2 PKIX certificates according to which + * should be tried first when building a path from the target. + * The preference order is as follows: + * + * Given trusted certificate(s): + * Subject:ou=D,ou=C,o=B,c=A + * + * Preference order for current cert: + * + * 1) The key identifier of an AKID extension (if present) in the + * previous certificate matches the key identifier in the SKID extension + * + * 2) Issuer matches a trusted subject + * Issuer: ou=D,ou=C,o=B,c=A + * + * 3) Issuer is a descendant of a trusted subject (in order of + * number of links to the trusted subject) + * a) Issuer: ou=E,ou=D,ou=C,o=B,c=A [links=1] + * b) Issuer: ou=F,ou=E,ou=D,ou=C,ou=B,c=A [links=2] + * + * 4) Issuer is an ancestor of a trusted subject (in order of number of + * links to the trusted subject) + * a) Issuer: ou=C,o=B,c=A [links=1] + * b) Issuer: o=B,c=A [links=2] + * + * 5) Issuer is in the same namespace as a trusted subject (in order of + * number of links to the trusted subject) + * a) Issuer: ou=G,ou=C,o=B,c=A [links=2] + * b) Issuer: ou=H,o=B,c=A [links=3] + * + * 6) Issuer is an ancestor of certificate subject (in order of number + * of links to the certificate subject) + * a) Issuer: ou=K,o=J,c=A + * Subject: ou=L,ou=K,o=J,c=A + * b) Issuer: o=J,c=A + * Subject: ou=L,ou=K,0=J,c=A + * + * 7) Any other certificates + */ + static class PKIXCertComparator implements Comparator { + + static final String METHOD_NME = "PKIXCertComparator.compare()"; + + private final Set trustedSubjectDNs; + private final X509CertSelector certSkidSelector; + + PKIXCertComparator(Set trustedSubjectDNs, + X509CertImpl previousCert) throws IOException { + this.trustedSubjectDNs = trustedSubjectDNs; + this.certSkidSelector = getSelector(previousCert); + } + + /** + * Returns an X509CertSelector for matching on the authority key + * identifier, or null if not applicable. + */ + private X509CertSelector getSelector(X509CertImpl previousCert) + throws IOException { + if (previousCert != null) { + AuthorityKeyIdentifierExtension akidExt = + previousCert.getAuthorityKeyIdentifierExtension(); + if (akidExt != null) { + byte[] skid = akidExt.getEncodedKeyIdentifier(); + if (skid != null) { + X509CertSelector selector = new X509CertSelector(); + selector.setSubjectKeyIdentifier(skid); + return selector; + } + } + } + return null; + } + + /** + * @param oCert1 First X509Certificate to be compared + * @param oCert2 Second X509Certificate to be compared + * @return -1 if oCert1 is preferable to oCert2, or + * if oCert1 and oCert2 are equally preferable (in this + * case it doesn't matter which is preferable, but we don't + * return 0 because the comparator would behave strangely + * when used in a SortedSet). + * 1 if oCert2 is preferable to oCert1 + * 0 if oCert1.equals(oCert2). We only return 0 if the + * certs are equal so that this comparator behaves + * correctly when used in a SortedSet. + * @throws ClassCastException if either argument is not of type + * X509Certificate + */ + @Override + public int compare(X509Certificate oCert1, X509Certificate oCert2) { + + // if certs are the same, return 0 + if (oCert1.equals(oCert2)) return 0; + + // If akid/skid match then it is preferable + if (certSkidSelector != null) { + if (certSkidSelector.match(oCert1)) { + return -1; + } + if (certSkidSelector.match(oCert2)) { + return 1; + } + } + + X500Principal cIssuer1 = oCert1.getIssuerX500Principal(); + X500Principal cIssuer2 = oCert2.getIssuerX500Principal(); + X500Name cIssuer1Name = X500Name.asX500Name(cIssuer1); + X500Name cIssuer2Name = X500Name.asX500Name(cIssuer2); + + if (debug != null) { + debug.println(METHOD_NME + " o1 Issuer: " + cIssuer1); + debug.println(METHOD_NME + " o2 Issuer: " + cIssuer2); + } + + /* If one cert's issuer matches a trusted subject, then it is + * preferable. + */ + if (debug != null) { + debug.println(METHOD_NME + " MATCH TRUSTED SUBJECT TEST..."); + } + + boolean m1 = trustedSubjectDNs.contains(cIssuer1); + boolean m2 = trustedSubjectDNs.contains(cIssuer2); + if (debug != null) { + debug.println(METHOD_NME + " m1: " + m1); + debug.println(METHOD_NME + " m2: " + m2); + } + if (m1 && m2) { + return -1; + } else if (m1) { + return -1; + } else if (m2) { + return 1; + } + + /* If one cert's issuer is a naming descendant of a trusted subject, + * then it is preferable, in order of increasing naming distance. + */ + if (debug != null) { + debug.println(METHOD_NME + " NAMING DESCENDANT TEST..."); + } + for (X500Principal tSubject : trustedSubjectDNs) { + X500Name tSubjectName = X500Name.asX500Name(tSubject); + int distanceTto1 = + distance(tSubjectName, cIssuer1Name, -1); + int distanceTto2 = + distance(tSubjectName, cIssuer2Name, -1); + if (debug != null) { + debug.println(METHOD_NME +" distanceTto1: " + distanceTto1); + debug.println(METHOD_NME +" distanceTto2: " + distanceTto2); + } + if (distanceTto1 > 0 || distanceTto2 > 0) { + // at least one is positive + if (distanceTto2 <= 0) { // only d1 is positive + return -1; + } else if (distanceTto1 <= 0) { // only d2 is positive + return 1; + } else { // all positive + return distanceTto1 > distanceTto2 ? 1 : -1; + } + } + } + + /* If one cert's issuer is a naming ancestor of a trusted subject, + * then it is preferable, in order of increasing naming distance. + */ + if (debug != null) { + debug.println(METHOD_NME + " NAMING ANCESTOR TEST..."); + } + for (X500Principal tSubject : trustedSubjectDNs) { + X500Name tSubjectName = X500Name.asX500Name(tSubject); + + int distanceTto1 = distance + (tSubjectName, cIssuer1Name, Integer.MAX_VALUE); + int distanceTto2 = distance + (tSubjectName, cIssuer2Name, Integer.MAX_VALUE); + if (debug != null) { + debug.println(METHOD_NME +" distanceTto1: " + distanceTto1); + debug.println(METHOD_NME +" distanceTto2: " + distanceTto2); + } + if (distanceTto1 < 0 || distanceTto2 < 0) { + // at least one is negative + if (distanceTto2 >= 0) { // only d1 is negative + return -1; + } else if (distanceTto1 >= 0) { // only d2 is negative + return 1; + } else { // all negative + return distanceTto1 < distanceTto2 ? 1 : -1; + } + } + } + + /* If one cert's issuer is in the same namespace as a trusted + * subject, then it is preferable, in order of increasing naming + * distance. + */ + if (debug != null) { + debug.println(METHOD_NME +" SAME NAMESPACE AS TRUSTED TEST..."); + } + for (X500Principal tSubject : trustedSubjectDNs) { + X500Name tSubjectName = X500Name.asX500Name(tSubject); + X500Name tAo1 = tSubjectName.commonAncestor(cIssuer1Name); + X500Name tAo2 = tSubjectName.commonAncestor(cIssuer2Name); + if (debug != null) { + debug.println(METHOD_NME +" tAo1: " + tAo1); + debug.println(METHOD_NME +" tAo2: " + tAo2); + } + if (tAo1 != null || tAo2 != null) { + if (tAo1 != null && tAo2 != null) { + int hopsTto1 = hops + (tSubjectName, cIssuer1Name, Integer.MAX_VALUE); + int hopsTto2 = hops + (tSubjectName, cIssuer2Name, Integer.MAX_VALUE); + if (debug != null) { + debug.println(METHOD_NME +" hopsTto1: " + hopsTto1); + debug.println(METHOD_NME +" hopsTto2: " + hopsTto2); + } + if (hopsTto1 == hopsTto2) { + } else if (hopsTto1 > hopsTto2) { + return 1; + } else { // hopsTto1 < hopsTto2 + return -1; + } + } else if (tAo1 == null) { + return 1; + } else { + return -1; + } + } + } + + + /* If one cert's issuer is an ancestor of that cert's subject, + * then it is preferable, in order of increasing naming distance. + */ + if (debug != null) { + debug.println(METHOD_NME+" CERT ISSUER/SUBJECT COMPARISON TEST..."); + } + X500Principal cSubject1 = oCert1.getSubjectX500Principal(); + X500Principal cSubject2 = oCert2.getSubjectX500Principal(); + X500Name cSubject1Name = X500Name.asX500Name(cSubject1); + X500Name cSubject2Name = X500Name.asX500Name(cSubject2); + + if (debug != null) { + debug.println(METHOD_NME + " o1 Subject: " + cSubject1); + debug.println(METHOD_NME + " o2 Subject: " + cSubject2); + } + int distanceStoI1 = distance + (cSubject1Name, cIssuer1Name, Integer.MAX_VALUE); + int distanceStoI2 = distance + (cSubject2Name, cIssuer2Name, Integer.MAX_VALUE); + if (debug != null) { + debug.println(METHOD_NME + " distanceStoI1: " + distanceStoI1); + debug.println(METHOD_NME + " distanceStoI2: " + distanceStoI2); + } + if (distanceStoI2 > distanceStoI1) { + return -1; + } else if (distanceStoI2 < distanceStoI1) { + return 1; + } + + /* Otherwise, certs are equally preferable. + */ + if (debug != null) { + debug.println(METHOD_NME + " no tests matched; RETURN 0"); + } + return -1; + } + } + + /** + * Verifies a matching certificate. + * + * This method executes the validation steps in the PKIX path + * validation algorithm, RFC 5280, which were + * not satisfied by the selection criteria used by getCertificates() + * to find the certs and only the steps that can be executed in a + * forward direction (target to trust anchor). Those steps that can + * only be executed in a reverse direction are deferred until the + * complete path has been built. + * + * Trust anchor certs are not validated, but are used to verify the + * signature and revocation status of the previous cert. + * + * If the last certificate is being verified (the one whose subject + * matches the target subject, then steps in 6.1.4 of the PKIX + * Certification Path Validation algorithm are NOT executed, + * regardless of whether the last cert is an end-entity + * cert or not. This allows callers to certify CA certs as + * well as EE certs. + * + * @param cert the certificate to be verified + * @param currentState the current state against which the cert is verified + * @param certPathList the certPathList generated thus far + */ + @Override + void verifyCert(X509Certificate cert, State currentState, + List certPathList) + throws GeneralSecurityException + { + if (debug != null) { + debug.println("ForwardBuilder.verifyCert(SN: " + + Debug.toHexString(cert.getSerialNumber()) + + "\n Issuer: " + cert.getIssuerX500Principal() + ")" + + "\n Subject: " + cert.getSubjectX500Principal() + ")"); + } + + ForwardState currState = (ForwardState)currentState; + + // Don't bother to verify untrusted certificate more. + currState.untrustedChecker.check(cert, Collections.emptySet()); + + /* + * check for looping - abort a loop if we encounter the same + * certificate twice + */ + if (certPathList != null) { + for (X509Certificate cpListCert : certPathList) { + if (cert.equals(cpListCert)) { + if (debug != null) { + debug.println("loop detected!!"); + } + throw new CertPathValidatorException("loop detected"); + } + } + } + + /* check if trusted cert */ + boolean isTrustedCert = trustedCerts.contains(cert); + + /* we don't perform any validation of the trusted cert */ + if (!isTrustedCert) { + /* + * Check CRITICAL private extensions for user checkers that + * support forward checking (forwardCheckers) and remove + * ones we know how to check. + */ + Set unresCritExts = cert.getCriticalExtensionOIDs(); + if (unresCritExts == null) { + unresCritExts = Collections.emptySet(); + } + for (PKIXCertPathChecker checker : currState.forwardCheckers) { + checker.check(cert, unresCritExts); + } + + /* + * Remove extensions from user checkers that don't support + * forward checking. After this step, we will have removed + * all extensions that all user checkers are capable of + * processing. + */ + for (PKIXCertPathChecker checker : buildParams.certPathCheckers()) { + if (!checker.isForwardCheckingSupported()) { + Set supportedExts = checker.getSupportedExtensions(); + if (supportedExts != null) { + unresCritExts.removeAll(supportedExts); + } + } + } + + /* + * Look at the remaining extensions and remove any ones we know how + * to check. If there are any left, throw an exception! + */ + if (!unresCritExts.isEmpty()) { + unresCritExts.remove(PKIXExtensions.BasicConstraints_Id.toString()); + unresCritExts.remove(PKIXExtensions.NameConstraints_Id.toString()); + unresCritExts.remove(PKIXExtensions.CertificatePolicies_Id.toString()); + unresCritExts.remove(PKIXExtensions.PolicyMappings_Id.toString()); + unresCritExts.remove(PKIXExtensions.PolicyConstraints_Id.toString()); + unresCritExts.remove(PKIXExtensions.InhibitAnyPolicy_Id.toString()); + unresCritExts.remove(PKIXExtensions.SubjectAlternativeName_Id.toString()); + unresCritExts.remove(PKIXExtensions.KeyUsage_Id.toString()); + unresCritExts.remove(PKIXExtensions.ExtendedKeyUsage_Id.toString()); + + if (!unresCritExts.isEmpty()) + throw new CertPathValidatorException + ("Unrecognized critical extension(s)", null, null, -1, + PKIXReason.UNRECOGNIZED_CRIT_EXT); + } + } + + /* + * if this is the target certificate (init=true), then we are + * not able to do any more verification, so just return + */ + if (currState.isInitial()) { + return; + } + + /* we don't perform any validation of the trusted cert */ + if (!isTrustedCert) { + /* Make sure this is a CA cert */ + if (cert.getBasicConstraints() == -1) { + throw new CertificateException("cert is NOT a CA cert"); + } + + /* + * Check keyUsage extension + */ + KeyChecker.verifyCAKeyUsage(cert); + } + + /* + * the following checks are performed even when the cert + * is a trusted cert, since we are only extracting the + * subjectDN, and publicKey from the cert + * in order to verify a previous cert + */ + + /* + * Check signature only if no key requiring key parameters has been + * encountered. + */ + if (!currState.keyParamsNeeded()) { + (currState.cert).verify(cert.getPublicKey(), + buildParams.sigProvider()); + } + } + + /** + * Verifies whether the input certificate completes the path. + * First checks the cert against each trust anchor that was specified, + * in order, and returns true if the cert matches the trust anchor + * specified as a certificate or has the same key and subject of an anchor + * specified as a trusted {pubkey, caname} pair. + * If no match has been found, does a second check of the cert against + * anchors specified as a trusted {pubkey, caname} pair to see if the cert + * was issued by that anchor. + * Returns false if none of the trust anchors are valid for this cert. + * + * @param cert the certificate to test + * @return a boolean value indicating whether the cert completes the path. + */ + @Override + boolean isPathCompleted(X509Certificate cert) { + List otherAnchors = new ArrayList<>(); + // first, check if cert is already trusted + for (TrustAnchor anchor : trustAnchors) { + if (anchor.getTrustedCert() != null) { + if (cert.equals(anchor.getTrustedCert())) { + this.trustAnchor = anchor; + return true; + } else { + continue; + } + } + X500Principal principal = anchor.getCA(); + PublicKey publicKey = anchor.getCAPublicKey(); + + if (principal != null && publicKey != null && + principal.equals(cert.getSubjectX500Principal())) { + if (publicKey.equals(cert.getPublicKey())) { + // the cert itself is a trust anchor + this.trustAnchor = anchor; + return true; + } + // else, it is a self-issued certificate of the anchor + } + otherAnchors.add(anchor); + } + // next, check if cert is issued by anchor specified by key/name + for (TrustAnchor anchor : otherAnchors) { + X500Principal principal = anchor.getCA(); + PublicKey publicKey = anchor.getCAPublicKey(); + // Check subject/issuer name chaining + if (principal == null || + !principal.equals(cert.getIssuerX500Principal())) { + continue; + } + + // skip anchor if it contains a DSA key with no DSA params + if (PKIX.isDSAPublicKeyWithoutParams(publicKey)) { + continue; + } + + /* + * Check signature + */ + try { + cert.verify(publicKey, buildParams.sigProvider()); + } catch (InvalidKeyException ike) { + if (debug != null) { + debug.println("ForwardBuilder.isPathCompleted() invalid " + + "DSA key found"); + } + continue; + } catch (GeneralSecurityException e){ + if (debug != null) { + debug.println("ForwardBuilder.isPathCompleted() " + + "unexpected exception"); + e.printStackTrace(); + } + continue; + } + + this.trustAnchor = anchor; + return true; + } + + return false; + } + + /** Adds the certificate to the certPathList + * + * @param cert the certificate to be added + * @param certPathList the certification path list + */ + @Override + void addCertToPath(X509Certificate cert, + LinkedList certPathList) + { + certPathList.addFirst(cert); + } + + /** Removes final certificate from the certPathList + * + * @param certPathList the certification path list + */ + @Override + void removeFinalCertFromPath(LinkedList certPathList) { + certPathList.removeFirst(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardState.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardState.java new file mode 100644 index 000000000..793abe719 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ForwardState.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.sun.security.x509.GeneralName; +import net.tongsuo.sun.security.x509.GeneralNameInterface; +import net.tongsuo.sun.security.x509.GeneralNames; +import net.tongsuo.sun.security.x509.SubjectAlternativeNameExtension; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * A specification of a forward PKIX validation state + * which is initialized by each build and updated each time a + * certificate is added to the current path. + * @since 1.4 + * @author Yassir Elley + */ +class ForwardState implements State { + + private static final Debug debug = Debug.getInstance("certpath"); + + /* The issuer DN of the last cert in the path */ + X500Principal issuerDN; + + /* The last cert in the path */ + X509CertImpl cert; + + /* The set of subjectDNs and subjectAltNames of all certs in the path */ + HashSet subjectNamesTraversed; + + /* + * The number of intermediate CA certs which have been traversed so + * far in the path + */ + int traversedCACerts; + + /* Flag indicating if state is initial (path is just starting) */ + private boolean init = true; + + + /* the untrusted certificates checker */ + UntrustedChecker untrustedChecker; + + /* The list of user-defined checkers that support forward checking */ + ArrayList forwardCheckers; + + /* Flag indicating if key needing to inherit key parameters has been + * encountered. + */ + boolean keyParamsNeededFlag = false; + + /** + * Returns a boolean flag indicating if the state is initial + * (just starting) + * + * @return boolean flag indicating if the state is initial (just starting) + */ + @Override + public boolean isInitial() { + return init; + } + + /** + * Return boolean flag indicating whether a public key that needs to inherit + * key parameters has been encountered. + * + * @return boolean true if key needing to inherit parameters has been + * encountered; false otherwise. + */ + @Override + public boolean keyParamsNeeded() { + return keyParamsNeededFlag; + } + + /** + * Display state for debugging purposes + */ + @Override + public String toString() { + return "State [" + + "\n issuerDN of last cert: " + issuerDN + + "\n traversedCACerts: " + traversedCACerts + + "\n init: " + init + + "\n keyParamsNeeded: " + keyParamsNeededFlag + + "\n subjectNamesTraversed: \n" + + subjectNamesTraversed + + "]\n"; + } + + /** + * Initialize the state. + * + * @param certPathCheckers the list of user-defined PKIXCertPathCheckers + */ + public void initState(List certPathCheckers) + throws CertPathValidatorException + { + subjectNamesTraversed = new HashSet<>(); + traversedCACerts = 0; + + /* + * Populate forwardCheckers with every user-defined checker + * that supports forward checking and initialize the forwardCheckers + */ + forwardCheckers = new ArrayList<>(); + for (PKIXCertPathChecker checker : certPathCheckers) { + if (checker.isForwardCheckingSupported()) { + checker.init(true); + forwardCheckers.add(checker); + } + } + + init = true; + } + + /** + * Update the state with the next certificate added to the path. + * + * @param cert the certificate which is used to update the state + */ + @Override + public void updateState(X509Certificate cert) + throws CertificateException, IOException, CertPathValidatorException { + + if (cert == null) + return; + + X509CertImpl icert = X509CertImpl.toImpl(cert); + + /* see if certificate key has null parameters */ + if (PKIX.isDSAPublicKeyWithoutParams(icert.getPublicKey())) { + keyParamsNeededFlag = true; + } + + /* update certificate */ + this.cert = icert; + + /* update issuer DN */ + issuerDN = cert.getIssuerX500Principal(); + + if (!X509CertImpl.isSelfIssued(cert)) { + + /* + * update traversedCACerts only if this is a non-self-issued + * intermediate CA cert + */ + if (!init && cert.getBasicConstraints() != -1) { + traversedCACerts++; + } + } + + /* update subjectNamesTraversed only if this is the EE cert or if + this cert is not self-issued */ + if (init || !X509CertImpl.isSelfIssued(cert)) { + X500Principal subjName = cert.getSubjectX500Principal(); + subjectNamesTraversed.add(X500Name.asX500Name(subjName)); + + SubjectAlternativeNameExtension subjAltNameExt + = icert.getSubjectAlternativeNameExtension(); + if (subjAltNameExt != null) { + GeneralNames gNames = subjAltNameExt.getNames(); + for (GeneralName gName : gNames.names()) { + subjectNamesTraversed.add(gName.getName()); + } + } + } + + init = false; + } + + /* + * Clone current state. The state is cloned as each cert is + * added to the path. This is necessary if backtracking occurs, + * and a prior state needs to be restored. + * + * Note that this is a SMART clone. Not all fields are fully copied, + * because some of them will + * not have their contents modified by subsequent calls to updateState. + */ + @Override + @SuppressWarnings("unchecked") // Safe casts assuming clone() works correctly + public Object clone() { + try { + ForwardState clonedState = (ForwardState) super.clone(); + + /* clone checkers, if cloneable */ + clonedState.forwardCheckers = (ArrayList) + forwardCheckers.clone(); + ListIterator li = + clonedState.forwardCheckers.listIterator(); + while (li.hasNext()) { + PKIXCertPathChecker checker = li.next(); + if (checker != null) { + li.set((PKIXCertPathChecker)checker.clone()); + } + } + + /* + * Shallow copy traversed names. There is no need to + * deep copy contents, since the elements of the Set + * are never modified by subsequent calls to updateState(). + */ + clonedState.subjectNamesTraversed + = (HashSet)subjectNamesTraversed.clone(); + return clonedState; + } catch (CloneNotSupportedException e) { + throw new InternalError(e.toString(), e); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/IndexedCollectionCertStore.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/IndexedCollectionCertStore.java new file mode 100644 index 000000000..8a89456af --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/IndexedCollectionCertStore.java @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2002, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.util.*; + +import java.security.InvalidAlgorithmParameterException; +import java.security.cert.*; + +import javax.security.auth.x500.X500Principal; + +/** + * A CertStore that retrieves Certificates and + * CRLs from a Collection. + *

+ * This implementation is functionally equivalent to CollectionCertStore + * with two differences: + *

    + *
  1. Upon construction, the elements in the specified Collection are + * partially indexed. X509Certificates are indexed by subject, X509CRLs + * by issuer, non-X509 Certificates and CRLs are copied without indexing, + * other objects are ignored. This increases CertStore construction time + * but allows significant speedups for searches which specify the indexed + * attributes, in particular for large Collections (reduction from linear + * time to effectively constant time). Searches for non-indexed queries + * are as fast (or marginally faster) than for the standard + * CollectionCertStore. Certificate subjects and CRL issuers + * were found to be specified in most searches used internally by the + * CertPath provider. Additional attributes could be indexed if there are + * queries that justify the effort. + * + *
  2. Changes to the specified Collection after construction time are + * not detected and ignored. This is because there is no way to efficiently + * detect if a Collection has been modified, a full traversal would be + * required. That would degrade lookup performance to linear time and + * eliminated the benefit of indexing. We may fix this via the introduction + * of new public APIs in the future. + *
+ *

+ * Before calling the {@link #engineGetCertificates engineGetCertificates} or + * {@link #engineGetCRLs engineGetCRLs} methods, the + * {@link #CollectionCertStore(CertStoreParameters) + * CollectionCertStore(CertStoreParameters)} constructor is called to + * create the CertStore and establish the + * Collection from which Certificates and + * CRLs will be retrieved. If the specified + * Collection contains an object that is not a + * Certificate or CRL, that object will be + * ignored. + *

+ * Concurrent Access + *

+ * As described in the javadoc for CertStoreSpi, the + * engineGetCertificates and engineGetCRLs methods + * must be thread-safe. That is, multiple threads may concurrently + * invoke these methods on a single CollectionCertStore + * object (or more than one) with no ill effects. + *

+ * This is achieved by requiring that the Collection passed to + * the {@link #CollectionCertStore(CertStoreParameters) + * CollectionCertStore(CertStoreParameters)} constructor (via the + * CollectionCertStoreParameters object) must have fail-fast + * iterators. Simultaneous modifications to the Collection can thus be + * detected and certificate or CRL retrieval can be retried. The fact that + * Certificates and CRLs must be thread-safe is also + * essential. + * + * @see java.security.cert.CertStore + * @see CollectionCertStore + * + * @author Andreas Sterbenz + */ +public class IndexedCollectionCertStore extends CertStoreSpi { + + /** + * Map X500Principal(subject) -> X509Certificate | List of X509Certificate + */ + private Map certSubjects; + /** + * Map X500Principal(issuer) -> X509CRL | List of X509CRL + */ + private Map crlIssuers; + /** + * Sets of non-X509 certificates and CRLs + */ + private Set otherCertificates; + private Set otherCRLs; + + /** + * Creates a CertStore with the specified parameters. + * For this class, the parameters object must be an instance of + * CollectionCertStoreParameters. + * + * @param params the algorithm parameters + * @exception InvalidAlgorithmParameterException if params is not an + * instance of CollectionCertStoreParameters + */ + public IndexedCollectionCertStore(CertStoreParameters params) + throws InvalidAlgorithmParameterException { + super(params); + if (!(params instanceof CollectionCertStoreParameters)) { + throw new InvalidAlgorithmParameterException( + "parameters must be CollectionCertStoreParameters"); + } + Collection coll = ((CollectionCertStoreParameters)params).getCollection(); + if (coll == null) { + throw new InvalidAlgorithmParameterException + ("Collection must not be null"); + } + buildIndex(coll); + } + + /** + * Index the specified Collection copying all references to Certificates + * and CRLs. + */ + private void buildIndex(Collection coll) { + certSubjects = new HashMap<>(); + crlIssuers = new HashMap<>(); + otherCertificates = null; + otherCRLs = null; + for (Object obj : coll) { + if (obj instanceof X509Certificate) { + indexCertificate((X509Certificate)obj); + } else if (obj instanceof X509CRL) { + indexCRL((X509CRL)obj); + } else if (obj instanceof Certificate) { + if (otherCertificates == null) { + otherCertificates = new HashSet<>(); + } + otherCertificates.add((Certificate)obj); + } else if (obj instanceof CRL) { + if (otherCRLs == null) { + otherCRLs = new HashSet<>(); + } + otherCRLs.add((CRL)obj); + } else { + // ignore + } + } + if (otherCertificates == null) { + otherCertificates = Collections.emptySet(); + } + if (otherCRLs == null) { + otherCRLs = Collections.emptySet(); + } + } + + /** + * Add an X509Certificate to the index. + */ + private void indexCertificate(X509Certificate cert) { + X500Principal subject = cert.getSubjectX500Principal(); + Object oldEntry = certSubjects.put(subject, cert); + if (oldEntry != null) { // assume this is unlikely + if (oldEntry instanceof X509Certificate) { + if (cert.equals(oldEntry)) { + return; + } + List list = new ArrayList<>(2); + list.add(cert); + list.add((X509Certificate)oldEntry); + certSubjects.put(subject, list); + } else { + @SuppressWarnings("unchecked") // See certSubjects javadoc. + List list = (List)oldEntry; + if (!list.contains(cert)) { + list.add(cert); + } + certSubjects.put(subject, list); + } + } + } + + /** + * Add an X509CRL to the index. + */ + private void indexCRL(X509CRL crl) { + X500Principal issuer = crl.getIssuerX500Principal(); + Object oldEntry = crlIssuers.put(issuer, crl); + if (oldEntry != null) { // assume this is unlikely + if (oldEntry instanceof X509CRL) { + if (crl.equals(oldEntry)) { + return; + } + List list = new ArrayList<>(2); + list.add(crl); + list.add((X509CRL)oldEntry); + crlIssuers.put(issuer, list); + } else { + // See crlIssuers javadoc. + @SuppressWarnings("unchecked") + List list = (List)oldEntry; + if (!list.contains(crl)) { + list.add(crl); + } + crlIssuers.put(issuer, list); + } + } + } + + /** + * Returns a Collection of Certificates that + * match the specified selector. If no Certificates + * match the selector, an empty Collection will be returned. + * + * @param selector a CertSelector used to select which + * Certificates should be returned. Specify null + * to return all Certificates. + * @return a Collection of Certificates that + * match the specified selector + * @throws CertStoreException if an exception occurs + */ + @Override + public Collection engineGetCertificates(CertSelector selector) + throws CertStoreException { + + // no selector means match all + if (selector == null) { + Set matches = new HashSet<>(); + matchX509Certs(new X509CertSelector(), matches); + matches.addAll(otherCertificates); + return matches; + } + + if (!(selector instanceof X509CertSelector)) { + Set matches = new HashSet<>(); + matchX509Certs(selector, matches); + for (Certificate cert : otherCertificates) { + if (selector.match(cert)) { + matches.add(cert); + } + } + return matches; + } + + if (certSubjects.isEmpty()) { + return Collections.emptySet(); + } + X509CertSelector x509Selector = (X509CertSelector)selector; + // see if the subject is specified + X500Principal subject; + X509Certificate matchCert = x509Selector.getCertificate(); + if (matchCert != null) { + subject = matchCert.getSubjectX500Principal(); + } else { + subject = x509Selector.getSubject(); + } + if (subject != null) { + // yes, narrow down candidates to indexed possibilities + Object entry = certSubjects.get(subject); + if (entry == null) { + return Collections.emptySet(); + } + if (entry instanceof X509Certificate) { + X509Certificate x509Entry = (X509Certificate)entry; + if (x509Selector.match(x509Entry)) { + return Collections.singleton(x509Entry); + } else { + return Collections.emptySet(); + } + } else { + // See certSubjects javadoc. + @SuppressWarnings("unchecked") + List list = (List)entry; + Set matches = new HashSet<>(16); + for (X509Certificate cert : list) { + if (x509Selector.match(cert)) { + matches.add(cert); + } + } + return matches; + } + } + // cannot use index, iterate all + Set matches = new HashSet<>(16); + matchX509Certs(x509Selector, matches); + return matches; + } + + /** + * Iterate through all the X509Certificates and add matches to the + * collection. + */ + private void matchX509Certs(CertSelector selector, + Collection matches) { + + for (Object obj : certSubjects.values()) { + if (obj instanceof X509Certificate) { + X509Certificate cert = (X509Certificate)obj; + if (selector.match(cert)) { + matches.add(cert); + } + } else { + // See certSubjects javadoc. + @SuppressWarnings("unchecked") + List list = (List)obj; + for (X509Certificate cert : list) { + if (selector.match(cert)) { + matches.add(cert); + } + } + } + } + } + + /** + * Returns a Collection of CRLs that + * match the specified selector. If no CRLs + * match the selector, an empty Collection will be returned. + * + * @param selector a CRLSelector used to select which + * CRLs should be returned. Specify null + * to return all CRLs. + * @return a Collection of CRLs that + * match the specified selector + * @throws CertStoreException if an exception occurs + */ + @Override + public Collection engineGetCRLs(CRLSelector selector) + throws CertStoreException { + + if (selector == null) { + Set matches = new HashSet<>(); + matchX509CRLs(new X509CRLSelector(), matches); + matches.addAll(otherCRLs); + return matches; + } + + if (!(selector instanceof X509CRLSelector)) { + Set matches = new HashSet<>(); + matchX509CRLs(selector, matches); + for (CRL crl : otherCRLs) { + if (selector.match(crl)) { + matches.add(crl); + } + } + return matches; + } + + if (crlIssuers.isEmpty()) { + return Collections.emptySet(); + } + X509CRLSelector x509Selector = (X509CRLSelector)selector; + // see if the issuer is specified + Collection issuers = x509Selector.getIssuers(); + if (issuers != null) { + HashSet matches = new HashSet<>(16); + for (X500Principal issuer : issuers) { + Object entry = crlIssuers.get(issuer); + if (entry == null) { + // empty + } else if (entry instanceof X509CRL) { + X509CRL crl = (X509CRL)entry; + if (x509Selector.match(crl)) { + matches.add(crl); + } + } else { // List + // See crlIssuers javadoc. + @SuppressWarnings("unchecked") + List list = (List)entry; + for (X509CRL crl : list) { + if (x509Selector.match(crl)) { + matches.add(crl); + } + } + } + } + return matches; + } + // cannot use index, iterate all + Set matches = new HashSet<>(16); + matchX509CRLs(x509Selector, matches); + return matches; + } + + /** + * Iterate through all the X509CRLs and add matches to the + * collection. + */ + private void matchX509CRLs(CRLSelector selector, Collection matches) { + for (Object obj : crlIssuers.values()) { + if (obj instanceof X509CRL) { + X509CRL crl = (X509CRL)obj; + if (selector.match(crl)) { + matches.add(crl); + } + } else { + // See crlIssuers javadoc. + @SuppressWarnings("unchecked") + List list = (List)obj; + for (X509CRL crl : list) { + if (selector.match(crl)) { + matches.add(crl); + } + } + } + } + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/KeyChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/KeyChecker.java new file mode 100644 index 000000000..53ebc84e1 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/KeyChecker.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.util.*; +import java.security.cert.*; +import java.security.cert.PKIXReason; + +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.util.Debug; + +/** + * KeyChecker is a PKIXCertPathChecker that checks that the + * keyCertSign bit is set in the keyUsage extension in an intermediate CA + * certificate. It also checks whether the final certificate in a + * certification path meets the specified target constraints specified as + * a CertSelector in the PKIXParameters passed to the CertPathValidator. + * + * @since 1.4 + * @author Yassir Elley + */ +class KeyChecker extends PKIXCertPathChecker { + + private static final Debug debug = Debug.getInstance("certpath"); + private final int certPathLen; + private final CertSelector targetConstraints; + private int remainingCerts; + + private Set supportedExts; + + /** + * Creates a KeyChecker. + * + * @param certPathLen allowable cert path length + * @param targetCertSel a CertSelector object specifying the constraints + * on the target certificate + */ + KeyChecker(int certPathLen, CertSelector targetCertSel) { + this.certPathLen = certPathLen; + this.targetConstraints = targetCertSel; + } + + /** + * Initializes the internal state of the checker from parameters + * specified in the constructor + */ + @Override + public void init(boolean forward) throws CertPathValidatorException { + if (!forward) { + remainingCerts = certPathLen; + } else { + throw new CertPathValidatorException + ("forward checking not supported"); + } + } + + @Override + public boolean isForwardCheckingSupported() { + return false; + } + + @Override + public Set getSupportedExtensions() { + if (supportedExts == null) { + supportedExts = new HashSet(3); + supportedExts.add(PKIXExtensions.KeyUsage_Id.toString()); + supportedExts.add(PKIXExtensions.ExtendedKeyUsage_Id.toString()); + supportedExts.add(PKIXExtensions.SubjectAlternativeName_Id.toString()); + supportedExts = Collections.unmodifiableSet(supportedExts); + } + return supportedExts; + } + + /** + * Checks that keyUsage and target constraints are satisfied by + * the specified certificate. + * + * @param cert the Certificate + * @param unresCritExts the unresolved critical extensions + * @throws CertPathValidatorException if certificate does not verify + */ + @Override + public void check(Certificate cert, Collection unresCritExts) + throws CertPathValidatorException + { + X509Certificate currCert = (X509Certificate)cert; + + remainingCerts--; + + // if final certificate, check that target constraints are satisfied + if (remainingCerts == 0) { + if (targetConstraints != null && + !targetConstraints.match(currCert)) { + throw new CertPathValidatorException("target certificate " + + "constraints check failed"); + } + } else { + // otherwise, verify that keyCertSign bit is set in CA certificate + verifyCAKeyUsage(currCert); + } + + // remove the extensions that we have checked + if (unresCritExts != null && !unresCritExts.isEmpty()) { + unresCritExts.remove(PKIXExtensions.KeyUsage_Id.toString()); + unresCritExts.remove(PKIXExtensions.ExtendedKeyUsage_Id.toString()); + unresCritExts.remove(PKIXExtensions.SubjectAlternativeName_Id.toString()); + } + } + + // the index of keyCertSign in the boolean KeyUsage array + private static final int KEY_CERT_SIGN = 5; + /** + * Verifies the key usage extension in a CA cert. + * The key usage extension, if present, must assert the keyCertSign bit. + * The extended key usage extension is not checked (see CR 4776794 for + * more information). + */ + static void verifyCAKeyUsage(X509Certificate cert) + throws CertPathValidatorException { + String msg = "CA key usage"; + if (debug != null) { + debug.println("KeyChecker.verifyCAKeyUsage() ---checking " + msg + + "..."); + } + + boolean[] keyUsageBits = cert.getKeyUsage(); + + // getKeyUsage returns null if the KeyUsage extension is not present + // in the certificate - in which case there is nothing to check + if (keyUsageBits == null) { + return; + } + + // throw an exception if the keyCertSign bit is not set + if (!keyUsageBits[KEY_CERT_SIGN]) { + throw new CertPathValidatorException + (msg + " check failed: keyCertSign bit is not set", null, + null, -1, PKIXReason.INVALID_KEY_USAGE); + } + + if (debug != null) { + debug.println("KeyChecker.verifyCAKeyUsage() " + msg + + " verified."); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSP.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSP.java new file mode 100644 index 000000000..74e22adad --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSP.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2009, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertPathValidatorException.BasicReason; +import java.security.cert.CRLReason; +import java.security.cert.Extension; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.IOUtils; +import net.tongsuo.sun.security.x509.AccessDescription; +import net.tongsuo.sun.security.x509.AuthorityInfoAccessExtension; +import net.tongsuo.sun.security.x509.GeneralName; +import net.tongsuo.sun.security.x509.GeneralNameInterface; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.URIName; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.action.GetIntegerAction; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * This is a class that checks the revocation status of a certificate(s) using + * OCSP. It is not a PKIXCertPathChecker and therefore can be used outside + * the CertPathValidator framework. It is useful when you want to + * just check the revocation status of a certificate, and you don't want to + * incur the overhead of validating all the certificates in the + * associated certificate chain. + * + * @author Sean Mullan + */ +public final class OCSP { + + private static final Debug debug = Debug.getInstance("certpath"); + + private static final int DEFAULT_CONNECT_TIMEOUT = 15000; + + /** + * Integer value indicating the timeout length, in seconds, to be + * used for the OCSP check. A timeout of zero is interpreted as + * an infinite timeout. + */ + private static final int CONNECT_TIMEOUT = initializeTimeout(); + + /** + * Initialize the timeout length by getting the OCSP timeout + * system property. If the property has not been set, or if its + * value is negative, set the timeout length to the default. + */ + private static int initializeTimeout() { + @SuppressWarnings("removal") + Integer tmp = java.security.AccessController.doPrivileged( + new GetIntegerAction("com.sun.security.ocsp.timeout")); + if (tmp == null || tmp < 0) { + return DEFAULT_CONNECT_TIMEOUT; + } + // Convert to milliseconds, as the system property will be + // specified in seconds + return tmp * 1000; + } + + private OCSP() {} + + /** + * Checks the revocation status of a list of certificates using OCSP. + * + * @param certIds the CertIds to be checked + * @param responderURI the URI of the OCSP responder + * @param issuerInfo the issuer's certificate and/or subject and public key + * @param responderCert the OCSP responder's certificate + * @param date the time the validity of the OCSP responder's certificate + * should be checked against. If null, the current time is used. + * @param extensions zero or more OCSP extensions to be included in the + * request. If no extensions are requested, an empty {@code List} must + * be used. A {@code null} value is not allowed. + * @return the OCSPResponse + * @throws IOException if there is an exception connecting to or + * communicating with the OCSP responder + * @throws CertPathValidatorException if an exception occurs while + * encoding the OCSP Request or validating the OCSP Response + */ + static OCSPResponse check(List certIds, URI responderURI, + OCSPResponse.IssuerInfo issuerInfo, + X509Certificate responderCert, Date date, + List extensions, String variant) + throws IOException, CertPathValidatorException + { + byte[] nonce = null; + for (Extension ext : extensions) { + if (ext.getId().equals(PKIXExtensions.OCSPNonce_Id.toString())) { + nonce = ext.getValue(); + } + } + + OCSPResponse ocspResponse; + try { + byte[] response = getOCSPBytes(certIds, responderURI, extensions); + ocspResponse = new OCSPResponse(response); + + // verify the response + ocspResponse.verify(certIds, issuerInfo, responderCert, date, + nonce, variant); + } catch (IOException ioe) { + throw new CertPathValidatorException( + "Unable to determine revocation status due to network error", + ioe, null, -1, BasicReason.UNDETERMINED_REVOCATION_STATUS); + } + + return ocspResponse; + } + + + /** + * Send an OCSP request, then read and return the OCSP response bytes. + * + * @param certIds the CertIds to be checked + * @param responderURI the URI of the OCSP responder + * @param extensions zero or more OCSP extensions to be included in the + * request. If no extensions are requested, an empty {@code List} must + * be used. A {@code null} value is not allowed. + * + * @return the OCSP response bytes + * + * @throws IOException if there is an exception connecting to or + * communicating with the OCSP responder + */ + public static byte[] getOCSPBytes(List certIds, URI responderURI, + List extensions) throws IOException { + OCSPRequest request = new OCSPRequest(certIds, extensions); + byte[] bytes = request.encodeBytes(); + String responder = responderURI.toString(); + + if (debug != null) { + debug.println("connecting to OCSP service at: " + responder); + } +// Event.report(Event.ReporterCategory.CRLCHECK, "event.ocsp.check", +// responder); + + URL url; + HttpURLConnection con = null; + try { + StringBuilder encodedGetReq = new StringBuilder(responder); + if (!responder.endsWith("/")) { + encodedGetReq.append("/"); + } + encodedGetReq.append(URLEncoder.encode( + Base64.getEncoder().encodeToString(bytes), String.valueOf(UTF_8))); + + if (encodedGetReq.length() <= 255) { + url = new URL(encodedGetReq.toString()); + con = (HttpURLConnection)url.openConnection(); + con.setDoOutput(true); + con.setDoInput(true); + con.setRequestMethod("GET"); + } else { + url = responderURI.toURL(); + con = (HttpURLConnection)url.openConnection(); + con.setConnectTimeout(CONNECT_TIMEOUT); + con.setReadTimeout(CONNECT_TIMEOUT); + con.setDoOutput(true); + con.setDoInput(true); + con.setRequestMethod("POST"); + con.setRequestProperty + ("Content-type", "application/ocsp-request"); + con.setRequestProperty + ("Content-length", String.valueOf(bytes.length)); + OutputStream out = con.getOutputStream(); + out.write(bytes); + out.flush(); + } + + // Check the response. Non-200 codes will generate an exception + // but path validation may complete successfully if revocation info + // can be obtained elsewhere (e.g. CRL). + int respCode = con.getResponseCode(); + if (respCode != HttpURLConnection.HTTP_OK) { + String msg = "Received HTTP error: " + respCode + " - " + + con.getResponseMessage(); + if (debug != null) { + debug.println(msg); + } + throw new IOException(msg); + } + + int contentLength = con.getContentLength(); + return (contentLength == -1) ? IOUtils.readAllBytes(con.getInputStream()) : + IOUtils.readExactlyNBytes(con.getInputStream(), + contentLength); + } finally { + if (con != null) { + con.disconnect(); + } + } + } + + /** + * Returns the URI of the OCSP Responder as specified in the + * certificate's Authority Information Access extension, or null if + * not specified. + * + * @param cert the certificate + * @return the URI of the OCSP Responder, or null if not specified + */ + // Called by com.sun.deploy.security.TrustDecider + public static URI getResponderURI(X509Certificate cert) { + try { + return getResponderURI(X509CertImpl.toImpl(cert)); + } catch (CertificateException ce) { + // treat this case as if the cert had no extension + return null; + } + } + + static URI getResponderURI(X509CertImpl certImpl) { + + // Examine the certificate's AuthorityInfoAccess extension + AuthorityInfoAccessExtension aia = + certImpl.getAuthorityInfoAccessExtension(); + if (aia == null) { + return null; + } + + List descriptions = aia.getAccessDescriptions(); + for (AccessDescription description : descriptions) { + if (description.getAccessMethod().equals( + (Object) AccessDescription.Ad_OCSP_Id)) { + + GeneralName generalName = description.getAccessLocation(); + if (generalName.getType() == GeneralNameInterface.NAME_URI) { + URIName uri = (URIName) generalName.getName(); + return uri.getURI(); + } + } + } + return null; + } + + /** + * The Revocation Status of a certificate. + */ + public interface RevocationStatus { + enum CertStatus { GOOD, REVOKED, UNKNOWN } + + /** + * Returns the revocation status. + */ + CertStatus getCertStatus(); + /** + * Returns the time when the certificate was revoked, or null + * if it has not been revoked. + */ + Date getRevocationTime(); + /** + * Returns the reason the certificate was revoked, or null if it + * has not been revoked. + */ + CRLReason getRevocationReason(); + + /** + * Returns a Map of additional extensions. + */ + Map getSingleExtensions(); + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPNonceExtension.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPNonceExtension.java new file mode 100644 index 000000000..0fe6e146d --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPNonceExtension.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.util.Objects; +import java.security.SecureRandom; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.x509.Extension; +import net.tongsuo.sun.security.x509.PKIXExtensions; + +/** + * Represent the OCSP Nonce Extension. + * This extension, if present, provides a nonce value in OCSP requests + * and responses. This will cryptographically bind requests and responses + * and help to prevent replay attacks (see RFC 6960, section 4.4.1). + * + * @see Extension + */ +public final class OCSPNonceExtension extends Extension { + + /** + * Attribute name. + */ + private static final String EXTENSION_NAME = "OCSPNonce"; + private final byte[] nonceData; + + /** + * Create an {@code OCSPNonceExtension} by providing the nonce length. + * The criticality is set to false, and the OID for the extension will + * be the value defined by "id-pkix-ocsp-nonce" from RFC 6960. + * + * @param length the number of random bytes composing the nonce + * + * @throws IOException if any errors happen during encoding of the + * extension. + * @throws IllegalArgumentException if length is not a positive integer. + */ + public OCSPNonceExtension(int length) throws IOException { + this(false, length); + } + + /** + * Create an {@code OCSPNonceExtension} by providing the nonce length and + * criticality setting. The OID for the extension will + * be the value defined by "id-pkix-ocsp-nonce" from RFC 6960. + * + * @param isCritical a boolean flag indicating whether the criticality bit + * is set for this extension + * @param length the number of random bytes composing the nonce + * + * @throws IOException if any errors happen during encoding of the + * extension. + * @throws IllegalArgumentException if length is not in the range of 1 to 32. + */ + public OCSPNonceExtension(boolean isCritical, int length) + throws IOException { + this.extensionId = PKIXExtensions.OCSPNonce_Id; + this.critical = isCritical; + + // RFC 8954, section 2.1: the length of the nonce MUST be at least 1 octet + // and can be up to 32 octets. + if (length > 0 && length <= 32) { + SecureRandom rng = new SecureRandom(); + this.nonceData = new byte[length]; + rng.nextBytes(nonceData); + this.extensionValue = new DerValue(DerValue.tag_OctetString, + nonceData).toByteArray(); + } else { + throw new IllegalArgumentException( + "Length of nonce must be at least 1 byte and can be up to 32 bytes"); + } + } + + /** + * Create an {@code OCSPNonceExtension} by providing a nonce value. + * The criticality is set to false, and the OID for the extension will + * be the value defined by "id-pkix-ocsp-nonce" from RFC 6960. + * + * @param incomingNonce The nonce data to be set for the extension. This + * must be a non-null array of at least one byte long. + * + * @throws IOException if any errors happen during encoding of the + * extension. + * @throws IllegalArgumentException if the incomingNonce length is not a + * positive integer. + * @throws NullPointerException if the incomingNonce is null. + */ + public OCSPNonceExtension(byte[] incomingNonce) throws IOException { + this(false, incomingNonce); + } + + /** + * Create an {@code OCSPNonceExtension} by providing a nonce value and + * criticality setting. The OID for the extension will + * be the value defined by "id-pkix-ocsp-nonce" from RFC 6960. + * + * @param isCritical a boolean flag indicating whether the criticality bit + * is set for this extension + * @param incomingNonce The nonce data to be set for the extension. This + * must be a non-null array of at least one byte long and can be up to + * 32 bytes. + * + * @throws IOException if any errors happen during encoding of the + * extension. + * @throws IllegalArgumentException if the incomingNonce length is not + * in the range of 1 to 32. + * @throws NullPointerException if the incomingNonce is null. + */ + public OCSPNonceExtension(boolean isCritical, byte[] incomingNonce) + throws IOException { + this.extensionId = PKIXExtensions.OCSPNonce_Id; + this.critical = isCritical; + + Objects.requireNonNull(incomingNonce, "Nonce data must be non-null"); + // RFC 8954, section 2.1: the length of the nonce MUST be at least 1 octet + // and can be up to 32 octets. + if (incomingNonce.length > 0 && incomingNonce.length <= 32) { + this.nonceData = incomingNonce.clone(); + this.extensionValue = new DerValue(DerValue.tag_OctetString, + nonceData).toByteArray(); + } else { + throw new IllegalArgumentException( + "Nonce data must be at least 1 byte and can be up to 32 bytes in length"); + } + } + + /** + * Return the nonce bytes themselves, without any DER encoding. + * + * @return A copy of the underlying nonce bytes + */ + public byte[] getNonceValue() { + return nonceData.clone(); + } + + /** + * Returns a printable representation of the {@code OCSPNonceExtension}. + * + * @return a string representation of the extension. + */ + @Override + public String toString() { + return super.toString() + EXTENSION_NAME + ": " + + ((nonceData == null) ? "" : Debug.toString(nonceData)) + + "\n"; + } + + /** + * Return the name of the extension as a {@code String} + * + * @return the name of the extension + */ + public String getName() { + return EXTENSION_NAME; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPRequest.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPRequest.java new file mode 100644 index 000000000..c629d16d4 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPRequest.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.cert.Extension; +import java.util.Collections; +import java.util.List; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerOutputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.x509.PKIXExtensions; + +/** + * This class can be used to generate an OCSP request and send it over + * an output stream. Currently we do not support signing requests. + * The OCSP Request is specified in RFC 2560 and + * the ASN.1 definition is as follows: + *

+ *
+ * OCSPRequest     ::=     SEQUENCE {
+ *      tbsRequest                  TBSRequest,
+ *      optionalSignature   [0]     EXPLICIT Signature OPTIONAL }
+ *
+ *   TBSRequest      ::=     SEQUENCE {
+ *      version             [0]     EXPLICIT Version DEFAULT v1,
+ *      requestorName       [1]     EXPLICIT GeneralName OPTIONAL,
+ *      requestList                 SEQUENCE OF Request,
+ *      requestExtensions   [2]     EXPLICIT Extensions OPTIONAL }
+ *
+ *  Signature       ::=     SEQUENCE {
+ *      signatureAlgorithm      AlgorithmIdentifier,
+ *      signature               BIT STRING,
+ *      certs               [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL
+ *   }
+ *
+ *  Version         ::=             INTEGER  {  v1(0) }
+ *
+ *  Request         ::=     SEQUENCE {
+ *      reqCert                     CertID,
+ *      singleRequestExtensions     [0] EXPLICIT Extensions OPTIONAL }
+ *
+ *  CertID          ::= SEQUENCE {
+ *       hashAlgorithm  AlgorithmIdentifier,
+ *       issuerNameHash OCTET STRING, -- Hash of Issuer's DN
+ *       issuerKeyHash  OCTET STRING, -- Hash of Issuers public key
+ *       serialNumber   CertificateSerialNumber
+ * }
+ *
+ * 
+ * + * @author Ram Marti + */ + +class OCSPRequest { + + private static final Debug debug = Debug.getInstance("certpath"); + private static final boolean dump = debug != null && Debug.isOn("ocsp"); + + // List of request CertIds + private final List certIds; + private final List extensions; + private byte[] nonce; + + /* + * Constructs an OCSPRequest. This constructor is used + * to construct an unsigned OCSP Request for a single user cert. + */ + OCSPRequest(CertId certId) { + this(Collections.singletonList(certId)); + } + + OCSPRequest(List certIds) { + this.certIds = certIds; + this.extensions = Collections.emptyList(); + } + + OCSPRequest(List certIds, List extensions) { + this.certIds = certIds; + this.extensions = extensions; + } + + byte[] encodeBytes() throws IOException { + + // encode tbsRequest + DerOutputStream tmp = new DerOutputStream(); + DerOutputStream requestsOut = new DerOutputStream(); + for (CertId certId : certIds) { + DerOutputStream certIdOut = new DerOutputStream(); + certId.encode(certIdOut); + requestsOut.write(DerValue.tag_Sequence, certIdOut); + } + + tmp.write(DerValue.tag_Sequence, requestsOut); + if (!extensions.isEmpty()) { + DerOutputStream extOut = new DerOutputStream(); + for (Extension ext : extensions) { + ext.encode(extOut); + if (ext.getId().equals( + PKIXExtensions.OCSPNonce_Id.toString())) { + nonce = ext.getValue(); + } + } + DerOutputStream extsOut = new DerOutputStream(); + extsOut.write(DerValue.tag_Sequence, extOut); + tmp.write(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte)2), extsOut); + } + + DerOutputStream tbsRequest = new DerOutputStream(); + tbsRequest.write(DerValue.tag_Sequence, tmp); + + // OCSPRequest without the signature + DerOutputStream ocspRequest = new DerOutputStream(); + ocspRequest.write(DerValue.tag_Sequence, tbsRequest); + + byte[] bytes = ocspRequest.toByteArray(); + + if (dump) { + HexDumpEncoder hexEnc = new HexDumpEncoder(); + debug.println("OCSPRequest bytes...\n\n" + + hexEnc.encode(bytes) + "\n"); + } + + return bytes; + } + + List getCertIds() { + return certIds; + } + + byte[] getNonce() { + return nonce; + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPResponse.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPResponse.java new file mode 100644 index 000000000..3a1f4f35e --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/OCSPResponse.java @@ -0,0 +1,1091 @@ +/* + * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.*; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertPathValidatorException.BasicReason; +import java.security.cert.CRLReason; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.crypto.CryptoInsts; +import net.tongsuo.sun.security.action.GetIntegerAction; +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.DerInputStream; +import net.tongsuo.sun.security.util.DerValue; +import net.tongsuo.sun.security.util.HexDumpEncoder; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.util.ObjectIdentifier; +import net.tongsuo.sun.security.util.Oid; +import net.tongsuo.sun.security.util.SignatureUtil; +import net.tongsuo.sun.security.x509.AlgorithmId; +import net.tongsuo.sun.security.x509.Extension; +import net.tongsuo.sun.security.x509.KeyIdentifier; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.X509CertImpl; + +/** + * This class is used to process an OCSP response. + * The OCSP Response is defined + * in RFC 2560 and the ASN.1 encoding is as follows: + *
+ *
+ *  OCSPResponse ::= SEQUENCE {
+ *      responseStatus         OCSPResponseStatus,
+ *      responseBytes          [0] EXPLICIT ResponseBytes OPTIONAL }
+ *
+ *   OCSPResponseStatus ::= ENUMERATED {
+ *       successful            (0),  --Response has valid confirmations
+ *       malformedRequest      (1),  --Illegal confirmation request
+ *       internalError         (2),  --Internal error in issuer
+ *       tryLater              (3),  --Try again later
+ *                                   --(4) is not used
+ *       sigRequired           (5),  --Must sign the request
+ *       unauthorized          (6)   --Request unauthorized
+ *   }
+ *
+ *   ResponseBytes ::=       SEQUENCE {
+ *       responseType   OBJECT IDENTIFIER,
+ *       response       OCTET STRING }
+ *
+ *   BasicOCSPResponse       ::= SEQUENCE {
+ *      tbsResponseData      ResponseData,
+ *      signatureAlgorithm   AlgorithmIdentifier,
+ *      signature            BIT STRING,
+ *      certs                [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
+ *
+ *   The value for signature SHALL be computed on the hash of the DER
+ *   encoding ResponseData.
+ *
+ *   ResponseData ::= SEQUENCE {
+ *      version              [0] EXPLICIT Version DEFAULT v1,
+ *      responderID              ResponderID,
+ *      producedAt               GeneralizedTime,
+ *      responses                SEQUENCE OF SingleResponse,
+ *      responseExtensions   [1] EXPLICIT Extensions OPTIONAL }
+ *
+ *   ResponderID ::= CHOICE {
+ *      byName               [1] Name,
+ *      byKey                [2] KeyHash }
+ *
+ *   KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
+ *   (excluding the tag and length fields)
+ *
+ *   SingleResponse ::= SEQUENCE {
+ *      certID                       CertID,
+ *      certStatus                   CertStatus,
+ *      thisUpdate                   GeneralizedTime,
+ *      nextUpdate         [0]       EXPLICIT GeneralizedTime OPTIONAL,
+ *      singleExtensions   [1]       EXPLICIT Extensions OPTIONAL }
+ *
+ *   CertStatus ::= CHOICE {
+ *       good        [0]     IMPLICIT NULL,
+ *       revoked     [1]     IMPLICIT RevokedInfo,
+ *       unknown     [2]     IMPLICIT UnknownInfo }
+ *
+ *   RevokedInfo ::= SEQUENCE {
+ *       revocationTime              GeneralizedTime,
+ *       revocationReason    [0]     EXPLICIT CRLReason OPTIONAL }
+ *
+ *   UnknownInfo ::= NULL -- this can be replaced with an enumeration
+ *
+ * 
+ * + * @author Ram Marti + */ + +public final class OCSPResponse { + + public enum ResponseStatus { + SUCCESSFUL, // Response has valid confirmations + MALFORMED_REQUEST, // Illegal request + INTERNAL_ERROR, // Internal error in responder + TRY_LATER, // Try again later + UNUSED, // is not used + SIG_REQUIRED, // Must sign the request + UNAUTHORIZED // Request unauthorized + } + + private static final ResponseStatus[] rsvalues = ResponseStatus.values(); + + private static final Debug debug = Debug.getInstance("certpath"); + private static final boolean dump = debug != null && Debug.isOn("ocsp"); + private static final ObjectIdentifier OCSP_BASIC_RESPONSE_OID = + Oid.of(KnownOIDs.OCSPBasicResponse); + private static final int CERT_STATUS_GOOD = 0; + private static final int CERT_STATUS_REVOKED = 1; + private static final int CERT_STATUS_UNKNOWN = 2; + + // ResponderID CHOICE tags + private static final int NAME_TAG = 1; + private static final int KEY_TAG = 2; + + // Default maximum clock skew in milliseconds (15 minutes) + // allowed when checking validity of OCSP responses + private static final int DEFAULT_MAX_CLOCK_SKEW = 900000; + + /** + * Integer value indicating the maximum allowable clock skew, + * in milliseconds, to be used for the OCSP check. + */ + private static final int MAX_CLOCK_SKEW = initializeClockSkew(); + + /** + * Initialize the maximum allowable clock skew by getting the OCSP + * clock skew system property. If the property has not been set, or if its + * value is negative, set the skew to the default. + */ + private static int initializeClockSkew() { + @SuppressWarnings("removal") + Integer tmp = java.security.AccessController.doPrivileged( + new GetIntegerAction("net.tongsuo.pkix.ocsp.clockSkew")); + if (tmp == null || tmp < 0) { + return DEFAULT_MAX_CLOCK_SKEW; + } + // Convert to milliseconds, as the system property will be + // specified in seconds + return tmp * 1000; + } + + // an array of all the CRLReasons (used in SingleResponse) + private static final CRLReason[] values = CRLReason.values(); + + private final ResponseStatus responseStatus; + private final Map singleResponseMap; + private final AlgorithmId sigAlgId; + private final byte[] signature; + private final byte[] tbsResponseData; + private final byte[] responseNonce; + private final List certs; + private X509CertImpl signerCert = null; + private final ResponderId respId; + private Date producedAtDate = null; + private final Map responseExtensions; + + /* + * Create an OCSP response from its ASN.1 DER encoding. + * + * @param bytes The DER-encoded bytes for an OCSP response + */ + public OCSPResponse(byte[] bytes) throws IOException { + if (dump) { + HexDumpEncoder hexEnc = new HexDumpEncoder(); + debug.println("OCSPResponse bytes...\n\n" + + hexEnc.encode(bytes) + "\n"); + } + DerValue der = new DerValue(bytes); + if (der.tag != DerValue.tag_Sequence) { + throw new IOException("Bad encoding in OCSP response: " + + "expected ASN.1 SEQUENCE tag."); + } + DerInputStream derIn = der.getData(); + + // responseStatus + int status = derIn.getEnumerated(); + if (status >= 0 && status < rsvalues.length) { + responseStatus = rsvalues[status]; + } else { + // unspecified responseStatus + throw new IOException("Unknown OCSPResponse status: " + status); + } + if (debug != null) { + debug.println("OCSP response status: " + responseStatus); + } + if (responseStatus != ResponseStatus.SUCCESSFUL) { + // no need to continue, responseBytes are not set. + singleResponseMap = Collections.emptyMap(); + certs = new ArrayList<>(); + sigAlgId = null; + signature = null; + tbsResponseData = null; + responseNonce = null; + responseExtensions = Collections.emptyMap(); + respId = null; + return; + } + + // responseBytes + der = derIn.getDerValue(); + if (!der.isContextSpecific((byte)0)) { + throw new IOException("Bad encoding in responseBytes element " + + "of OCSP response: expected ASN.1 context specific tag 0."); + } + DerValue tmp = der.data.getDerValue(); + if (tmp.tag != DerValue.tag_Sequence) { + throw new IOException("Bad encoding in responseBytes element " + + "of OCSP response: expected ASN.1 SEQUENCE tag."); + } + + // responseType + derIn = tmp.data; + ObjectIdentifier responseType = derIn.getOID(); + if (responseType.equals(OCSP_BASIC_RESPONSE_OID)) { + if (debug != null) { + debug.println("OCSP response type: basic"); + } + } else { + if (debug != null) { + debug.println("OCSP response type: " + responseType); + } + throw new IOException("Unsupported OCSP response type: " + + responseType); + } + + // BasicOCSPResponse + DerInputStream basicOCSPResponse = + new DerInputStream(derIn.getOctetString()); + + DerValue[] seqTmp = basicOCSPResponse.getSequence(3); + if (seqTmp.length < 3) { + throw new IOException("Unexpected BasicOCSPResponse value"); + } + + DerValue responseData = seqTmp[0]; + + // Need the DER encoded ResponseData to verify the signature later + tbsResponseData = seqTmp[0].toByteArray(); + + // tbsResponseData + if (responseData.tag != DerValue.tag_Sequence) { + throw new IOException("Bad encoding in tbsResponseData " + + "element of OCSP response: expected ASN.1 SEQUENCE tag."); + } + DerInputStream seqDerIn = responseData.data; + DerValue seq = seqDerIn.getDerValue(); + + // version + if (seq.isContextSpecific((byte)0)) { + // seq[0] is version + if (seq.isConstructed() && seq.isContextSpecific()) { + //System.out.println ("version is available"); + seq = seq.data.getDerValue(); + int version = seq.getInteger(); + if (seq.data.available() != 0) { + throw new IOException("Bad encoding in version " + + " element of OCSP response: bad format"); + } + seq = seqDerIn.getDerValue(); + } + } + + // responderID + respId = new ResponderId(seq.toByteArray()); + if (debug != null) { + debug.println("Responder ID: " + respId); + } + + // producedAt + seq = seqDerIn.getDerValue(); + producedAtDate = seq.getGeneralizedTime(); + if (debug != null) { + debug.println("OCSP response produced at: " + producedAtDate); + } + + // responses + DerValue[] singleResponseDer = seqDerIn.getSequence(1); + singleResponseMap = new HashMap<>(singleResponseDer.length); + if (debug != null) { + debug.println("OCSP number of SingleResponses: " + + singleResponseDer.length); + } + for (DerValue srDer : singleResponseDer) { + SingleResponse singleResponse = new SingleResponse(srDer); + singleResponseMap.put(singleResponse.getCertId(), singleResponse); + } + + // responseExtensions + Map tmpExtMap = new HashMap<>(); + if (seqDerIn.available() > 0) { + seq = seqDerIn.getDerValue(); + if (seq.isContextSpecific((byte)1)) { + tmpExtMap = parseExtensions(seq); + } + } + responseExtensions = tmpExtMap; + + // Attach the nonce value if found in the extension map + Extension nonceExt = (Extension)tmpExtMap.get( + PKIXExtensions.OCSPNonce_Id.toString()); + responseNonce = (nonceExt != null) ? + nonceExt.getExtensionValue() : null; + if (debug != null && responseNonce != null) { + debug.println("Response nonce: " + Arrays.toString(responseNonce)); + } + + // signatureAlgorithmId + sigAlgId = AlgorithmId.parse(seqTmp[1]); + + // signature + signature = seqTmp[2].getBitString(); + + // if seq[3] is available , then it is a sequence of certificates + if (seqTmp.length > 3) { + // certs are available + DerValue seqCert = seqTmp[3]; + if (!seqCert.isContextSpecific((byte)0)) { + throw new IOException("Bad encoding in certs element of " + + "OCSP response: expected ASN.1 context specific tag 0."); + } + DerValue[] derCerts = seqCert.getData().getSequence(3); + certs = new ArrayList<>(derCerts.length); + try { + for (int i = 0; i < derCerts.length; i++) { + X509CertImpl cert = + new X509CertImpl(derCerts[i].toByteArray()); + certs.add(cert); + + if (debug != null) { + debug.println("OCSP response cert #" + (i + 1) + ": " + + cert.getSubjectX500Principal()); + } + } + } catch (CertificateException ce) { + throw new IOException("Bad encoding in X509 Certificate", ce); + } + } else { + certs = new ArrayList<>(); + } + } + + void verify(List certIds, IssuerInfo issuerInfo, + X509Certificate responderCert, Date date, byte[] nonce, + String variant) + throws CertPathValidatorException + { + switch (responseStatus) { + case SUCCESSFUL: + break; + case TRY_LATER: + case INTERNAL_ERROR: + throw new CertPathValidatorException( + "OCSP response error: " + responseStatus, null, null, -1, + BasicReason.UNDETERMINED_REVOCATION_STATUS); + case UNAUTHORIZED: + default: + throw new CertPathValidatorException("OCSP response error: " + + responseStatus); + } + + // Check that the response includes a response for all of the + // certs that were supplied in the request + for (CertId certId : certIds) { + SingleResponse sr = getSingleResponse(certId); + if (sr == null) { + if (debug != null) { + debug.println("No response found for CertId: " + certId); + } + throw new CertPathValidatorException( + "OCSP response does not include a response for a " + + "certificate supplied in the OCSP request"); + } + if (debug != null) { + debug.println("Status of certificate (with serial number " + + certId.getSerialNumber() + ") is: " + sr.getCertStatus()); + } + } + + // Locate the signer cert + if (signerCert == null) { + // Add the Issuing CA cert and/or Trusted Responder cert to the list + // of certs from the OCSP response + try { + if (issuerInfo.getCertificate() != null) { + certs.add(X509CertImpl.toImpl(issuerInfo.getCertificate())); + } + if (responderCert != null) { + certs.add(X509CertImpl.toImpl(responderCert)); + } + } catch (CertificateException ce) { + throw new CertPathValidatorException( + "Invalid issuer or trusted responder certificate", ce); + } + + if (respId.getType() == ResponderId.Type.BY_NAME) { + X500Principal rName = respId.getResponderName(); + for (X509CertImpl cert : certs) { + if (cert.getSubjectX500Principal().equals(rName)) { + signerCert = cert; + break; + } + } + } else if (respId.getType() == ResponderId.Type.BY_KEY) { + KeyIdentifier ridKeyId = respId.getKeyIdentifier(); + for (X509CertImpl cert : certs) { + // Match responder's key identifier against the cert's SKID + // This will match if the SKID is encoded using the 160-bit + // SHA-1 hash method as defined in RFC 5280. + KeyIdentifier certKeyId = cert.getSubjectKeyId(); + if (ridKeyId.equals(certKeyId)) { + signerCert = cert; + break; + } else { + // The certificate does not have a SKID or may have + // been using a different algorithm (ex: see RFC 7093). + // Check if the responder's key identifier matches + // against a newly generated key identifier of the + // cert's public key using the 160-bit SHA-1 method. + try { + certKeyId = new KeyIdentifier(cert.getPublicKey()); + } catch (IOException e) { + // ignore + } + if (ridKeyId.equals(certKeyId)) { + signerCert = cert; + break; + } + } + } + } + } + + // Check whether the signer cert returned by the responder is trusted + boolean signedByTrustedResponder = false; + if (signerCert != null) { + // Check if the response is signed by the issuing CA + if (signerCert.getSubjectX500Principal().equals( + issuerInfo.getName()) && + signerCert.getPublicKey().equals( + issuerInfo.getPublicKey())) { + if (debug != null) { + debug.println("OCSP response is signed by the target's " + + "Issuing CA"); + } + // cert is trusted, now verify the signed response + + // Check if the response is signed by a trusted responder + } else if (signerCert.equals(responderCert)) { + signedByTrustedResponder = true; + if (debug != null) { + debug.println("OCSP response is signed by a Trusted " + + "Responder"); + } + // cert is trusted, now verify the signed response + + // Check if the response is signed by an authorized responder + } else if (signerCert.getIssuerX500Principal().equals( + issuerInfo.getName())) { + + // Check for the OCSPSigning key purpose + try { + List keyPurposes = signerCert.getExtendedKeyUsage(); + if (keyPurposes == null || + !keyPurposes.contains(KnownOIDs.OCSPSigning.value())) { + throw new CertPathValidatorException( + "Responder's certificate not valid for signing " + + "OCSP responses"); + } + } catch (CertificateParsingException cpe) { + // assume cert is not valid for signing + throw new CertPathValidatorException( + "Responder's certificate not valid for signing " + + "OCSP responses", cpe); + } + + // Check algorithm constraints specified in security property + // "jdk.certpath.disabledAlgorithms". + AlgorithmChecker algChecker = + new AlgorithmChecker(issuerInfo.getAnchor(), date, + variant); + algChecker.init(false); + algChecker.check(signerCert, Collections.emptySet()); + + // check the validity + try { + if (date == null) { + signerCert.checkValidity(); + } else { + signerCert.checkValidity(date); + } + } catch (CertificateException e) { + throw new CertPathValidatorException( + "Responder's certificate not within the " + + "validity period", e); + } + + // check for revocation + // + // A CA may specify that an OCSP client can trust a + // responder for the lifetime of the responder's + // certificate. The CA does so by including the + // extension id-pkix-ocsp-nocheck. + // + Extension noCheck = + signerCert.getExtension(PKIXExtensions.OCSPNoCheck_Id); + if (noCheck != null) { + if (debug != null) { + debug.println("Responder's certificate includes " + + "the extension id-pkix-ocsp-nocheck."); + } + } else { + // we should do the revocation checking of the + // authorized responder in a future update. + } + + // verify the signature + try { + signerCert.verify(issuerInfo.getPublicKey()); + if (debug != null) { + debug.println("OCSP response is signed by an " + + "Authorized Responder"); + } + // cert is trusted, now verify the signed response + + } catch (GeneralSecurityException e) { + signerCert = null; + } + } else { + throw new CertPathValidatorException( + "Responder's certificate is not authorized to sign " + + "OCSP responses"); + } + } + + // Confirm that the signed response was generated using the public + // key from the trusted responder cert + if (signerCert != null) { + // Check algorithm constraints specified in security property + // "jdk.certpath.disabledAlgorithms". + AlgorithmChecker.check(signerCert.getPublicKey(), sigAlgId, variant, + signedByTrustedResponder + ? new TrustAnchor(responderCert, null) + : issuerInfo.getAnchor()); + + if (!verifySignature(signerCert)) { + throw new CertPathValidatorException( + "Error verifying OCSP Response's signature"); + } + } else { + // Need responder's cert in order to verify the signature + throw new CertPathValidatorException( + "Unable to verify OCSP Response's signature"); + } + + if (nonce != null) { + if (responseNonce != null && !Arrays.equals(nonce, responseNonce)) { + throw new CertPathValidatorException("Nonces don't match"); + } + } + + // Check freshness of OCSPResponse + long now = (date == null) ? System.currentTimeMillis() : date.getTime(); + Date nowPlusSkew = new Date(now + MAX_CLOCK_SKEW); + Date nowMinusSkew = new Date(now - MAX_CLOCK_SKEW); + for (SingleResponse sr : singleResponseMap.values()) { + if (debug != null) { + String until = ""; + if (sr.nextUpdate != null) { + until = " until " + sr.nextUpdate; + } + debug.println("OCSP response validity interval is from " + + sr.thisUpdate + until); + debug.println("Checking validity of OCSP response on " + + new Date(now) + " with allowed interval between " + + nowMinusSkew + " and " + nowPlusSkew); + } + + // Check that the test date is within the validity interval: + // [ thisUpdate - MAX_CLOCK_SKEW, + // MAX(thisUpdate, nextUpdate) + MAX_CLOCK_SKEW ] + if (nowPlusSkew.before(sr.thisUpdate) || + nowMinusSkew.after( + sr.nextUpdate != null ? sr.nextUpdate : sr.thisUpdate)) + { + throw new CertPathValidatorException( + "Response is unreliable: its validity " + + "interval is out-of-date"); + } + } + } + + /** + * Returns the OCSP ResponseStatus. + * + * @return the {@code ResponseStatus} for this OCSP response + */ + public ResponseStatus getResponseStatus() { + return responseStatus; + } + + /* + * Verify the signature of the OCSP response. + */ + private boolean verifySignature(X509Certificate cert) + throws CertPathValidatorException { + + try { + Signature respSignature = CryptoInsts.getSignature(sigAlgId.getName()); + SignatureUtil.initVerifyWithParam(respSignature, + cert.getPublicKey(), + SignatureUtil.getParamSpec(sigAlgId.getName(), + sigAlgId.getEncodedParams())); + respSignature.update(tbsResponseData); + + if (respSignature.verify(signature)) { + if (debug != null) { + debug.println("Verified signature of OCSP Response"); + } + return true; + + } else { + if (debug != null) { + debug.println( + "Error verifying signature of OCSP Response"); + } + return false; + } + } catch (InvalidAlgorithmParameterException | InvalidKeyException + | NoSuchAlgorithmException | SignatureException e) + { + throw new CertPathValidatorException(e); + } + } + + /** + * Returns the SingleResponse of the specified CertId, or null if + * there is no response for that CertId. + * + * @param certId the {@code CertId} for a {@code SingleResponse} to be + * searched for in the OCSP response. + * + * @return the {@code SingleResponse} for the provided {@code CertId}, + * or {@code null} if it is not found. + */ + public SingleResponse getSingleResponse(CertId certId) { + return singleResponseMap.get(certId); + } + + /** + * Return a set of all CertIds in this {@code OCSPResponse} + * + * @return an unmodifiable set containing every {@code CertId} in this + * response. + */ + public Set getCertIds() { + return Collections.unmodifiableSet(singleResponseMap.keySet()); + } + + /* + * Returns the certificate for the authority that signed the OCSP response. + */ + X509Certificate getSignerCertificate() { + return signerCert; // set in verify() + } + + /** + * Get the {@code ResponderId} from this {@code OCSPResponse} + * + * @return the {@code ResponderId} from this response or {@code null} + * if no responder ID is in the body of the response e.g. a + * response with a status other than SUCCESS. + */ + public ResponderId getResponderId() { + return respId; + } + + /** + * Provide a String representation of an OCSPResponse + * + * @return a human-readable representation of the OCSPResponse + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("OCSP Response:\n"); + sb.append("Response Status: ").append(responseStatus).append("\n"); + sb.append("Responder ID: ").append(respId).append("\n"); + sb.append("Produced at: ").append(producedAtDate).append("\n"); + int count = singleResponseMap.size(); + sb.append(count).append(count == 1 ? + " response:\n" : " responses:\n"); + for (SingleResponse sr : singleResponseMap.values()) { + sb.append(sr).append("\n"); + } + if (responseExtensions != null && responseExtensions.size() > 0) { + count = responseExtensions.size(); + sb.append(count).append(count == 1 ? + " extension:\n" : " extensions:\n"); + for (String extId : responseExtensions.keySet()) { + sb.append(responseExtensions.get(extId)).append("\n"); + } + } + + return sb.toString(); + } + + /** + * Build a String-Extension map from DER encoded data. + * @param derVal A {@code DerValue} object built from a SEQUENCE of + * extensions + * + * @return a {@code Map} using the OID in string form as the keys. If no + * extensions are found or an empty SEQUENCE is passed in, then + * an empty {@code Map} will be returned. + * + * @throws IOException if any decoding errors occur. + */ + private static Map + parseExtensions(DerValue derVal) throws IOException { + DerValue[] extDer = derVal.data.getSequence(3); + Map extMap = + new HashMap<>(extDer.length); + + for (DerValue extDerVal : extDer) { + Extension ext = new Extension(extDerVal); + if (debug != null) { + debug.println("Extension: " + ext); + } + // We don't support any extensions yet. Therefore, if it + // is critical we must throw an exception because we + // don't know how to process it. + if (ext.isCritical()) { + throw new IOException("Unsupported OCSP critical extension: " + + ext.getExtensionId()); + } + extMap.put(ext.getId(), ext); + } + + return extMap; + } + + /* + * A class representing a single OCSP response. + */ + public static final class SingleResponse implements OCSP.RevocationStatus { + private final CertId certId; + private final CertStatus certStatus; + private final Date thisUpdate; + private final Date nextUpdate; + private final Date revocationTime; + private final CRLReason revocationReason; + private final Map singleExtensions; + + private SingleResponse(DerValue der) throws IOException { + if (der.tag != DerValue.tag_Sequence) { + throw new IOException("Bad ASN.1 encoding in SingleResponse"); + } + DerInputStream tmp = der.data; + + certId = new CertId(tmp.getDerValue().data); + DerValue derVal = tmp.getDerValue(); + short tag = (byte)(derVal.tag & 0x1f); + if (tag == CERT_STATUS_REVOKED) { + certStatus = CertStatus.REVOKED; + revocationTime = derVal.data.getGeneralizedTime(); + if (derVal.data.available() != 0) { + DerValue dv = derVal.data.getDerValue(); + tag = (byte)(dv.tag & 0x1f); + if (tag == 0) { + int reason = dv.data.getEnumerated(); + // if reason out-of-range just leave as UNSPECIFIED + if (reason >= 0 && reason < values.length) { + revocationReason = values[reason]; + } else { + revocationReason = CRLReason.UNSPECIFIED; + } + } else { + revocationReason = CRLReason.UNSPECIFIED; + } + } else { + revocationReason = CRLReason.UNSPECIFIED; + } + // RevokedInfo + if (debug != null) { + debug.println("Revocation time: " + revocationTime); + debug.println("Revocation reason: " + revocationReason); + } + } else { + revocationTime = null; + revocationReason = null; + if (tag == CERT_STATUS_GOOD) { + certStatus = CertStatus.GOOD; + } else if (tag == CERT_STATUS_UNKNOWN) { + certStatus = CertStatus.UNKNOWN; + } else { + throw new IOException("Invalid certificate status"); + } + } + + thisUpdate = tmp.getGeneralizedTime(); + if (debug != null) { + debug.println("thisUpdate: " + thisUpdate); + } + + // Parse optional fields like nextUpdate and singleExtensions + Date tmpNextUpdate = null; + Map tmpMap = null; + + // Check for the first optional item, it could be nextUpdate + // [CONTEXT 0] or singleExtensions [CONTEXT 1] + if (tmp.available() > 0) { + derVal = tmp.getDerValue(); + + // nextUpdate processing + if (derVal.isContextSpecific((byte)0)) { + tmpNextUpdate = derVal.data.getGeneralizedTime(); + if (debug != null) { + debug.println("nextUpdate: " + tmpNextUpdate); + } + + // If more data exists in the singleResponse, it + // can only be singleExtensions. Get this DER value + // for processing in the next block + derVal = tmp.available() > 0 ? tmp.getDerValue() : null; + } + + // singleExtensions processing + if (derVal != null) { + if (derVal.isContextSpecific((byte)1)) { + tmpMap = parseExtensions(derVal); + + // There should not be any other items in the + // singleResponse at this point. + if (tmp.available() > 0) { + throw new IOException(tmp.available() + + " bytes of additional data in singleResponse"); + } + } else { + // Unknown item in the singleResponse + throw new IOException("Unsupported singleResponse " + + "item, tag = " + String.format("%02X", derVal.tag)); + } + } + } + + nextUpdate = tmpNextUpdate; + singleExtensions = (tmpMap != null) ? tmpMap : + Collections.emptyMap(); + if (debug != null) { + for (java.security.cert.Extension ext : + singleExtensions.values()) { + debug.println("singleExtension: " + ext); + } + } + } + + /* + * Return the certificate's revocation status code + */ + @Override + public CertStatus getCertStatus() { + return certStatus; + } + + /** + * Get the Cert ID that this SingleResponse is for. + * + * @return the {@code CertId} for this {@code SingleResponse} + */ + public CertId getCertId() { + return certId; + } + + /** + * Get the {@code thisUpdate} field from this {@code SingleResponse}. + * + * @return a {@link Date} object containing the thisUpdate date + */ + public Date getThisUpdate() { + return (thisUpdate != null ? (Date) thisUpdate.clone() : null); + } + + /** + * Get the {@code nextUpdate} field from this {@code SingleResponse}. + * + * @return a {@link Date} object containing the nexUpdate date or + * {@code null} if a nextUpdate field is not present in the response. + */ + public Date getNextUpdate() { + return (nextUpdate != null ? (Date) nextUpdate.clone() : null); + } + + /** + * Get the {@code revocationTime} field from this + * {@code SingleResponse}. + * + * @return a {@link Date} object containing the revocationTime date or + * {@code null} if the {@code SingleResponse} does not have a status + * of {@code REVOKED}. + */ + @Override + public Date getRevocationTime() { + return (revocationTime != null ? (Date) revocationTime.clone() : + null); + } + + /** + * Get the {@code revocationReason} field for the + * {@code SingleResponse}. + * + * @return a {@link CRLReason} containing the revocation reason, or + * {@code null} if a revocation reason was not provided or the + * response status is not {@code REVOKED}. + */ + @Override + public CRLReason getRevocationReason() { + return revocationReason; + } + + /** + * Get the {@code singleExtensions} for this {@code SingleResponse}. + * + * @return a {@link Map} of {@link Extension} objects, keyed by + * their OID value in string form. + */ + @Override + public Map getSingleExtensions() { + return Collections.unmodifiableMap(singleExtensions); + } + + /** + * Construct a string representation of a single OCSP response. + */ + @Override public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SingleResponse:\n"); + sb.append(certId); + sb.append("\nCertStatus: ").append(certStatus).append("\n"); + if (certStatus == CertStatus.REVOKED) { + sb.append("revocationTime is "); + sb.append(revocationTime).append("\n"); + sb.append("revocationReason is "); + sb.append(revocationReason).append("\n"); + } + sb.append("thisUpdate is ").append(thisUpdate).append("\n"); + if (nextUpdate != null) { + sb.append("nextUpdate is ").append(nextUpdate).append("\n"); + } + for (java.security.cert.Extension ext : singleExtensions.values()) { + sb.append("singleExtension: "); + sb.append(ext.toString()).append("\n"); + } + return sb.toString(); + } + } + + /** + * Helper class that allows consumers to pass in issuer information. This + * will always consist of the issuer's name and public key, but may also + * contain a certificate if the originating data is in that form. The + * trust anchor for the certificate chain will be included for certpath + * disabled algorithm checking. + */ + static final class IssuerInfo { + private final TrustAnchor anchor; + private final X509Certificate certificate; + private final X500Principal name; + private final PublicKey pubKey; + + IssuerInfo(TrustAnchor anchor) { + this(anchor, (anchor != null) ? anchor.getTrustedCert() : null); + } + + IssuerInfo(X509Certificate issuerCert) { + this(null, issuerCert); + } + + IssuerInfo(TrustAnchor anchor, X509Certificate issuerCert) { + if (anchor == null && issuerCert == null) { + throw new NullPointerException("TrustAnchor and issuerCert " + + "cannot be null"); + } + this.anchor = anchor; + if (issuerCert != null) { + name = issuerCert.getSubjectX500Principal(); + pubKey = issuerCert.getPublicKey(); + certificate = issuerCert; + } else { + name = anchor.getCA(); + pubKey = anchor.getCAPublicKey(); + certificate = anchor.getTrustedCert(); + } + } + + /** + * Get the certificate in this IssuerInfo if present. + * + * @return the {@code X509Certificate} used to create this IssuerInfo + * object, or {@code null} if a certificate was not used in its + * creation. + */ + X509Certificate getCertificate() { + return certificate; + } + + /** + * Get the name of this issuer. + * + * @return an {@code X500Principal} corresponding to this issuer's + * name. If derived from an issuer's {@code X509Certificate} this + * would be equivalent to the certificate subject name. + */ + X500Principal getName() { + return name; + } + + /** + * Get the public key for this issuer. + * + * @return a {@code PublicKey} for this issuer. + */ + PublicKey getPublicKey() { + return pubKey; + } + + /** + * Get the TrustAnchor for the certificate chain. + * + * @return a {@code TrustAnchor}. + */ + TrustAnchor getAnchor() { + return anchor; + } + + /** + * Create a string representation of this IssuerInfo. + * + * @return a {@code String} form of this IssuerInfo object. + */ + @Override + public String toString() { + return "Issuer Info:\n" + + "Name: " + name.toString() + "\n" + + "Public Key:\n" + pubKey.toString() + "\n"; + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIX.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIX.java new file mode 100644 index 000000000..1502541c1 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIX.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package net.tongsuo.sun.security.provider.certpath; + +import java.security.InvalidAlgorithmParameterException; +import java.security.PublicKey; +import java.security.Timestamp; +import java.security.cert.*; +import java.security.interfaces.DSAPublicKey; +import java.util.*; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.sun.security.validator.Validator; +import net.tongsuo.sun.security.util.Debug; + +/** + * Common utility methods and classes used by the PKIX CertPathValidator and + * CertPathBuilder implementation. + */ +class PKIX { + + private static final Debug debug = Debug.getInstance("certpath"); + + private PKIX() { } + + static boolean isDSAPublicKeyWithoutParams(PublicKey publicKey) { + return (publicKey instanceof DSAPublicKey && + ((DSAPublicKey)publicKey).getParams() == null); + } + + static ValidatorParams checkParams(CertPath cp, CertPathParameters params) + throws InvalidAlgorithmParameterException + { + if (!(params instanceof PKIXParameters)) { + throw new InvalidAlgorithmParameterException("inappropriate " + + "params, must be an instance of PKIXParameters"); + } + return new ValidatorParams(cp, (PKIXParameters)params); + } + + static BuilderParams checkBuilderParams(CertPathParameters params) + throws InvalidAlgorithmParameterException + { + if (!(params instanceof PKIXBuilderParameters)) { + throw new InvalidAlgorithmParameterException("inappropriate " + + "params, must be an instance of PKIXBuilderParameters"); + } + return new BuilderParams((PKIXBuilderParameters)params); + } + + /** + * PKIXParameters that are shared by the PKIX CertPathValidator + * implementation. Provides additional functionality and avoids + * unnecessary cloning. + */ + static class ValidatorParams { + private final PKIXParameters params; + private CertPath certPath; + private List checkers; + private List stores; + private boolean gotDate; + private Date date; + private Set policies; + private boolean gotConstraints; + private CertSelector constraints; + private final Set anchors; + private List certs; + private Timestamp timestamp; + private Date timestampDate; + private String variant = Validator.VAR_GENERIC; + + ValidatorParams(CertPath cp, PKIXParameters params) + throws InvalidAlgorithmParameterException + { + this(params); + if (!cp.getType().equals("X.509") && !cp.getType().equals("X509")) { + throw new InvalidAlgorithmParameterException("inappropriate " + + "CertPath type specified, must be X.509 or X509"); + } + this.certPath = cp; + } + + ValidatorParams(PKIXParameters params) + throws InvalidAlgorithmParameterException + { + if (params instanceof PKIXExtendedParameters) { + timestamp = ((PKIXExtendedParameters) params).getTimestamp(); + variant = ((PKIXExtendedParameters) params).getVariant(); + } + + this.anchors = params.getTrustAnchors(); + // Make sure that none of the trust anchors include name constraints + // (not supported). + for (TrustAnchor anchor : this.anchors) { + if (anchor.getNameConstraints() != null) { + throw new InvalidAlgorithmParameterException + ("name constraints in trust anchor not supported"); + } + } + this.params = params; + } + + CertPath certPath() { + return certPath; + } + // called by CertPathBuilder after path has been built + void setCertPath(CertPath cp) { + this.certPath = cp; + } + List certificates() { + if (certs == null) { + if (certPath == null) { + certs = Collections.emptyList(); + } else { + // Reverse the ordering for validation so that the target + // cert is the last certificate + @SuppressWarnings("unchecked") + List xc = new ArrayList<> + ((List)certPath.getCertificates()); + Collections.reverse(xc); + certs = xc; + } + } + return certs; + } + List certPathCheckers() { + if (checkers == null) + checkers = params.getCertPathCheckers(); + return checkers; + } + List certStores() { + if (stores == null) + stores = params.getCertStores(); + return stores; + } + // The date() param is used when enforcing the validity period + // of certificates and when checking the time period of revocation data. + // The main difference between the date() and timestamp() method is + // that the date() method only uses the timestamp (if specified) + // for certificates in a code signer's chain. + Date date() { + if (!gotDate) { + // Use timestamp if checking signed code that is + // timestamped, otherwise use date parameter. + // Note that TSA server certificates do not use the + // timestamp, which means that an expired TSA certificate + // is considered a validation failure. This policy means + // that signed and timestamped code is valid until the TSA + // certificate expires (assuming all other checks are valid). + if (timestamp != null && + variant.equals(Validator.VAR_CODE_SIGNING)) { + date = timestamp.getTimestamp(); + } else { + date = params.getDate(); + if (date == null) + date = new Date(); + } + gotDate = true; + } + return date; + } + Set initialPolicies() { + if (policies == null) + policies = params.getInitialPolicies(); + return policies; + } + CertSelector targetCertConstraints() { + if (!gotConstraints) { + constraints = params.getTargetCertConstraints(); + gotConstraints = true; + } + return constraints; + } + Set trustAnchors() { + return anchors; + } + boolean revocationEnabled() { + return params.isRevocationEnabled(); + } + boolean policyMappingInhibited() { + return params.isPolicyMappingInhibited(); + } + boolean explicitPolicyRequired() { + return params.isExplicitPolicyRequired(); + } + boolean policyQualifiersRejected() { + return params.getPolicyQualifiersRejected(); + } + String sigProvider() { return params.getSigProvider(); } + boolean anyPolicyInhibited() { return params.isAnyPolicyInhibited(); } + + // in rare cases we need access to the original params, for example + // in order to clone CertPathCheckers before building a new chain + PKIXParameters getPKIXParameters() { + return params; + } + + String variant() { + return variant; + } + // The timestamp() param is passed as the date param when creating an + // AlgorithmChecker. An AlgorithmChecker always uses the timestamp + // if specified in order to enforce the denyAfter constraint. + Date timestamp() { + // return timestamp date if set, otherwise use date parameter + if (timestampDate == null) { + timestampDate = (timestamp != null) + ? timestamp.getTimestamp() : date(); + } + return timestampDate; + } + } + + static class BuilderParams extends ValidatorParams { + private PKIXBuilderParameters params; + private List stores; + private X500Principal targetSubject; + + BuilderParams(PKIXBuilderParameters params) + throws InvalidAlgorithmParameterException + { + super(params); + checkParams(params); + } + private void checkParams(PKIXBuilderParameters params) + throws InvalidAlgorithmParameterException + { + CertSelector sel = targetCertConstraints(); + if (!(sel instanceof X509CertSelector)) { + throw new InvalidAlgorithmParameterException("the " + + "targetCertConstraints parameter must be an " + + "X509CertSelector"); + } + this.params = params; + this.targetSubject = getTargetSubject( + certStores(), (X509CertSelector)targetCertConstraints()); + } + @Override List certStores() { + if (stores == null) { + // reorder CertStores so that local CertStores are tried first + stores = new ArrayList<>(params.getCertStores()); + stores.sort(new CertStoreComparator()); + } + return stores; + } + int maxPathLength() { return params.getMaxPathLength(); } + PKIXBuilderParameters params() { return params; } + X500Principal targetSubject() { return targetSubject; } + + /** + * Returns the target subject DN from the first X509Certificate that + * is fetched that matches the specified X509CertSelector. + */ + private static X500Principal getTargetSubject(List stores, + X509CertSelector sel) + throws InvalidAlgorithmParameterException + { + X500Principal subject = sel.getSubject(); + if (subject != null) { + return subject; + } + X509Certificate cert = sel.getCertificate(); + if (cert != null) { + subject = cert.getSubjectX500Principal(); + } + if (subject != null) { + return subject; + } + for (CertStore store : stores) { + try { + Collection certs = + store.getCertificates(sel); + if (!certs.isEmpty()) { + X509Certificate xc = + (X509Certificate)certs.iterator().next(); + return xc.getSubjectX500Principal(); + } + } catch (CertStoreException e) { + // ignore but log it + if (debug != null) { + debug.println("BuilderParams.getTargetSubjectDN: " + + "non-fatal exception retrieving certs: " + e); + e.printStackTrace(); + } + } + } + throw new InvalidAlgorithmParameterException + ("Could not determine unique target subject"); + } + } + + /** + * A CertStoreException with additional information about the type of + * CertStore that generated the exception. + */ + static class CertStoreTypeException extends CertStoreException { + private static final long serialVersionUID = 7463352639238322556L; + + private final String type; + + CertStoreTypeException(String type, CertStoreException cse) { + super(cse.getMessage(), cse.getCause()); + this.type = type; + } + String getType() { + return type; + } + } + + /** + * Comparator that orders CertStores so that local CertStores come before + * remote CertStores. + */ + private static class CertStoreComparator implements Comparator { + @Override + public int compare(CertStore store1, CertStore store2) { + if (store1.getType().equals("Collection") || + store1.getCertStoreParameters() instanceof + CollectionCertStoreParameters) { + return -1; + } else { + return 1; + } + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXCertPathValidator.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXCertPathValidator.java new file mode 100644 index 000000000..db0590b60 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXCertPathValidator.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.cert.*; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; + +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.sun.security.util.Debug; + +/** + * This class implements the PKIX validation algorithm for certification + * paths consisting exclusively of X509Certificates. It uses + * the specified input parameter set (which must be a + * PKIXParameters object). + * + * @since 1.4 + * @author Yassir Elley + */ +public final class PKIXCertPathValidator extends CertPathValidatorSpi { + + private static final Debug debug = Debug.getInstance("certpath"); + private static final AtomicLong validationCounter = new AtomicLong(); + + /** + * Default constructor. + */ + public PKIXCertPathValidator() {} + + @Override + public CertPathChecker engineGetRevocationChecker() { + return new RevocationChecker(); + } + + /** + * Validates a certification path consisting exclusively of + * X509Certificates using the PKIX validation algorithm, + * which uses the specified input parameter set. + * The input parameter set must be a PKIXParameters object. + * + * @param cp the X509 certification path + * @param params the input PKIX parameter set + * @return the result + * @throws CertPathValidatorException if cert path does not validate. + * @throws InvalidAlgorithmParameterException if the specified + * parameters are inappropriate for this CertPathValidator + */ + @Override + public CertPathValidatorResult engineValidate(CertPath cp, + CertPathParameters params) + throws CertPathValidatorException, InvalidAlgorithmParameterException + { + PKIX.ValidatorParams valParams = PKIX.checkParams(cp, params); + return validate(valParams); + } + + private static PKIXCertPathValidatorResult validate(PKIX.ValidatorParams params) + throws CertPathValidatorException + { + if (debug != null) + debug.println("PKIXCertPathValidator.engineValidate()..."); + + // Retrieve the first certificate in the certpath + // (to be used later in pre-screening) + AdaptableX509CertSelector selector = null; + List certList = params.certificates(); + if (!certList.isEmpty()) { + selector = new AdaptableX509CertSelector(); + X509Certificate firstCert = certList.get(0); + // check trusted certificate's subject + selector.setSubject(firstCert.getIssuerX500Principal()); + /* + * Facilitate certification path construction with authority + * key identifier and subject key identifier. + */ + try { + X509CertImpl firstCertImpl = X509CertImpl.toImpl(firstCert); + selector.setSkiAndSerialNumber( + firstCertImpl.getAuthorityKeyIdentifierExtension()); + } catch (CertificateException | IOException e) { + // ignore + } + } + + CertPathValidatorException lastException = null; + + // We iterate through the set of trust anchors until we find + // one that works at which time we stop iterating + for (TrustAnchor anchor : params.trustAnchors()) { + X509Certificate trustedCert = anchor.getTrustedCert(); + if (trustedCert != null) { + // if this trust anchor is not worth trying, + // we move on to the next one + if (selector != null && !selector.match(trustedCert)) { + if (debug != null && Debug.isOn("verbose")) { + debug.println("NO - don't try this trustedCert"); + } + continue; + } + + if (debug != null) { + debug.println("YES - try this trustedCert"); + debug.println("anchor.getTrustedCert()." + + "getSubjectX500Principal() = " + + trustedCert.getSubjectX500Principal()); + } + } else { + if (debug != null) { + debug.println("PKIXCertPathValidator.engineValidate(): " + + "anchor.getTrustedCert() == null"); + } + } + + try { + return validate(anchor, params); + } catch (CertPathValidatorException cpe) { + // remember this exception + lastException = cpe; + } + } + + // could not find a trust anchor that verified + // (a) if we did a validation and it failed, use that exception + if (lastException != null) { + throw lastException; + } + // (b) otherwise, generate new exception + throw new CertPathValidatorException + ("Path does not chain with any of the trust anchors", + null, null, -1, PKIXReason.NO_TRUST_ANCHOR); + } + + private static PKIXCertPathValidatorResult validate(TrustAnchor anchor, + PKIX.ValidatorParams params) + throws CertPathValidatorException + { + // check if anchor is untrusted + UntrustedChecker untrustedChecker = new UntrustedChecker(); + X509Certificate anchorCert = anchor.getTrustedCert(); + if (anchorCert != null) { + untrustedChecker.check(anchorCert); + } + + int certPathLen = params.certificates().size(); + + // create PKIXCertPathCheckers + List certPathCheckers = new ArrayList<>(); + // add standard checkers that we will be using + certPathCheckers.add(untrustedChecker); + certPathCheckers.add(new AlgorithmChecker(anchor, params.timestamp(), + params.variant())); + certPathCheckers.add(new KeyChecker(certPathLen, + params.targetCertConstraints())); + certPathCheckers.add(new ConstraintsChecker(certPathLen)); + PolicyNodeImpl rootNode = + new PolicyNodeImpl(null, PolicyChecker.ANY_POLICY, null, false, + Collections.singleton(PolicyChecker.ANY_POLICY), + false); + PolicyChecker pc = new PolicyChecker(params.initialPolicies(), + certPathLen, + params.explicitPolicyRequired(), + params.policyMappingInhibited(), + params.anyPolicyInhibited(), + params.policyQualifiersRejected(), + rootNode); + certPathCheckers.add(pc); + + BasicChecker bc = new BasicChecker(anchor, params.date(), + params.sigProvider(), false); + certPathCheckers.add(bc); + + boolean revCheckerAdded = false; + List checkers = params.certPathCheckers(); + for (PKIXCertPathChecker checker : checkers) { + if (checker instanceof PKIXRevocationChecker) { + if (revCheckerAdded) { + throw new CertPathValidatorException( + "Only one PKIXRevocationChecker can be specified"); + } + revCheckerAdded = true; + // if it's our own, initialize it + if (checker instanceof RevocationChecker) { + ((RevocationChecker)checker).init(anchor, params); + } + } + } + // only add a RevocationChecker if revocation is enabled and + // a PKIXRevocationChecker has not already been added + if (params.revocationEnabled() && !revCheckerAdded) { + certPathCheckers.add(new RevocationChecker(anchor, params)); + } + // add user-specified checkers + certPathCheckers.addAll(checkers); + + PKIXMasterCertPathValidator.validate(params.certPath(), + params.certificates(), + certPathCheckers); + +// X509ValidationEvent xve = new X509ValidationEvent(); +// if (xve.shouldCommit() || EventHelper.isLoggingSecurity()) { +// int[] certIds = params.certificates().stream() +// .mapToInt(Certificate::hashCode) +// .toArray(); +// int anchorCertId = (anchorCert != null) ? +// anchorCert.hashCode() : anchor.getCAPublicKey().hashCode(); +// if (xve.shouldCommit()) { +// xve.certificateId = anchorCertId; +// int certificatePos = 1; // most trusted CA +// xve.certificatePosition = certificatePos; +// xve.validationCounter = validationCounter.incrementAndGet(); +// xve.commit(); +// // now, iterate through remaining +// for (int id : certIds) { +// xve.certificateId = id; +// xve.certificatePosition = ++certificatePos; +// xve.commit(); +// +// } +// } +// if (EventHelper.isLoggingSecurity()) { +// EventHelper.logX509ValidationEvent(anchorCertId, certIds); +// } +// } + return new PKIXCertPathValidatorResult(anchor, pc.getPolicyTree(), + bc.getPublicKey()); + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXExtendedParameters.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXExtendedParameters.java new file mode 100644 index 000000000..a7d2c1b6e --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXExtendedParameters.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +package net.tongsuo.sun.security.provider.certpath; + +import java.security.InvalidAlgorithmParameterException; +import java.security.Timestamp; +import java.security.cert.CertSelector; +import java.security.cert.CertStore; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.TrustAnchor; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * This class is a wrapper for PKIXBuilderParameters so that a Timestamp object + * and a string for the variant type, can be passed when doing certpath + * checking. + */ + +public class PKIXExtendedParameters extends PKIXBuilderParameters { + + private final PKIXBuilderParameters p; + private Timestamp jarTimestamp; + private final String variant; + + public PKIXExtendedParameters(PKIXBuilderParameters params, + Timestamp timestamp, String variant) + throws InvalidAlgorithmParameterException { + super(params.getTrustAnchors(), null); + p = params; + jarTimestamp = timestamp; + this.variant = variant; + } + + public Timestamp getTimestamp() { + return jarTimestamp; + } + public void setTimestamp(Timestamp t) { + jarTimestamp = t; + } + + public String getVariant() { + return variant; + } + + @Override + public void setDate(Date d) { + p.setDate(d); + } + + @Override + public void addCertPathChecker(PKIXCertPathChecker c) { + p.addCertPathChecker(c); + } + + @Override + public void setMaxPathLength(int maxPathLength) { + p.setMaxPathLength(maxPathLength); + } + + @Override + public int getMaxPathLength() { + return p.getMaxPathLength(); + } + + @Override + public String toString() { + return p.toString(); + } + + @Override + public Set getTrustAnchors() { + return p.getTrustAnchors(); + } + + @Override + public void setTrustAnchors(Set trustAnchors) + throws InvalidAlgorithmParameterException { + // To avoid problems with PKIXBuilderParameter's constructors + if (p == null) { + return; + } + p.setTrustAnchors(trustAnchors); + } + + @Override + public Set getInitialPolicies() { + return p.getInitialPolicies(); + } + + @Override + public void setInitialPolicies(Set initialPolicies) { + p.setInitialPolicies(initialPolicies); + } + + @Override + public void setCertStores(List stores) { + p.setCertStores(stores); + } + + @Override + public void addCertStore(CertStore store) { + p.addCertStore(store); + } + + @Override + public List getCertStores() { + return p.getCertStores(); + } + + @Override + public void setRevocationEnabled(boolean val) { + p.setRevocationEnabled(val); + } + + @Override + public boolean isRevocationEnabled() { + return p.isRevocationEnabled(); + } + + @Override + public void setExplicitPolicyRequired(boolean val) { + p.setExplicitPolicyRequired(val); + } + + @Override + public boolean isExplicitPolicyRequired() { + return p.isExplicitPolicyRequired(); + } + + @Override + public void setPolicyMappingInhibited(boolean val) { + p.setPolicyMappingInhibited(val); + } + + @Override + public boolean isPolicyMappingInhibited() { + return p.isPolicyMappingInhibited(); + } + + @Override + public void setAnyPolicyInhibited(boolean val) { + p.setAnyPolicyInhibited(val); + } + + @Override + public boolean isAnyPolicyInhibited() { + return p.isAnyPolicyInhibited(); + } + + @Override + public void setPolicyQualifiersRejected(boolean qualifiersRejected) { + p.setPolicyQualifiersRejected(qualifiersRejected); + } + + @Override + public boolean getPolicyQualifiersRejected() { + return p.getPolicyQualifiersRejected(); + } + + @Override + public Date getDate() { + return p.getDate(); + } + + @Override + public void setCertPathCheckers(List checkers) { + p.setCertPathCheckers(checkers); + } + + @Override + public List getCertPathCheckers() { + return p.getCertPathCheckers(); + } + + @Override + public String getSigProvider() { + return p.getSigProvider(); + } + + @Override + public void setSigProvider(String sigProvider) { + p.setSigProvider(sigProvider); + } + + @Override + public CertSelector getTargetCertConstraints() { + return p.getTargetCertConstraints(); + } + + @Override + public void setTargetCertConstraints(CertSelector selector) { + // To avoid problems with PKIXBuilderParameter's constructors + if (p == null) { + return; + } + p.setTargetCertConstraints(selector); + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXMasterCertPathValidator.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXMasterCertPathValidator.java new file mode 100644 index 000000000..cdc2fee82 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PKIXMasterCertPathValidator.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import net.tongsuo.sun.security.util.Debug; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidatorException; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.PKIXReason; +import java.security.cert.X509Certificate; + +/** + * This class is initialized with a list of PKIXCertPathCheckers + * and is used to verify the certificates in a CertPath by + * feeding each certificate to each PKIXCertPathChecker. + * + * @since 1.4 + * @author Yassir Elley + */ +class PKIXMasterCertPathValidator { + + private static final Debug debug = Debug.getInstance("certpath"); + + /** + * Validates a certification path consisting exclusively of + * X509Certificates using the specified + * PKIXCertPathCheckers. It is assumed that the + * PKIXCertPathCheckers + * have been initialized with any input parameters they may need. + * + * @param cpOriginal the original X509 CertPath passed in by the user + * @param reversedCertList the reversed X509 CertPath (as a List) + * @param certPathCheckers the PKIXCertPathCheckers + * @throws CertPathValidatorException if cert path does not validate + */ + static void validate(CertPath cpOriginal, + List reversedCertList, + List certPathCheckers) + throws CertPathValidatorException + { + // we actually process reversedCertList, but we keep cpOriginal because + // we need to return the original certPath when we throw an exception. + // we will also need to modify the index appropriately when we + // throw an exception. + + int cpSize = reversedCertList.size(); + + if (debug != null) { + debug.println("--------------------------------------------------" + + "------------"); + debug.println("Executing PKIX certification path validation " + + "algorithm."); + } + + for (int i = 0; i < cpSize; i++) { + + /* The basic loop algorithm is that we get the + * current certificate, we verify the current certificate using + * information from the previous certificate and from the state, + * and we modify the state for the next loop by setting the + * current certificate of this loop to be the previous certificate + * of the next loop. The state is initialized during first loop. + */ + X509Certificate currCert = reversedCertList.get(i); + + if (debug != null) { + debug.println("Checking cert" + (i+1) + " - Subject: " + + currCert.getSubjectX500Principal()); + } + + Set unresCritExts = currCert.getCriticalExtensionOIDs(); + if (unresCritExts == null) { + unresCritExts = Collections.emptySet(); + } + + if (debug != null && !unresCritExts.isEmpty()) { + StringJoiner joiner = new StringJoiner(", ", "{", "}"); + for (String oid : unresCritExts) { + joiner.add(oid); + } + debug.println("Set of critical extensions: " + + joiner.toString()); + } + + for (int j = 0; j < certPathCheckers.size(); j++) { + + PKIXCertPathChecker currChecker = certPathCheckers.get(j); + if (debug != null) { + debug.println("-Using checker" + (j + 1) + " ... [" + + currChecker.getClass().getName() + "]"); + } + + if (i == 0) + currChecker.init(false); + + try { + currChecker.check(currCert, unresCritExts); + + if (debug != null) { + debug.println("-checker" + (j + 1) + + " validation succeeded"); + } + + } catch (CertPathValidatorException cpve) { + throw new CertPathValidatorException(cpve.getMessage(), + (cpve.getCause() != null) ? cpve.getCause() : cpve, + cpOriginal, cpSize - (i + 1), cpve.getReason()); + } + } + + if (!unresCritExts.isEmpty()) { + throw new CertPathValidatorException("unrecognized " + + "critical extension(s)", null, cpOriginal, cpSize-(i+1), + PKIXReason.UNRECOGNIZED_CRIT_EXT); + } + + if (debug != null) + debug.println("\ncert" + (i+1) + " validation succeeded.\n"); + } + + if (debug != null) { + debug.println("Cert path validation succeeded. (PKIX validation " + + "algorithm)"); + debug.println("-------------------------------------------------" + + "-------------"); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyChecker.java new file mode 100644 index 000000000..f64a5c86f --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyChecker.java @@ -0,0 +1,864 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.PKIXCertPathChecker; +import java.security.cert.PKIXReason; +import java.security.cert.PolicyNode; +import java.security.cert.PolicyQualifierInfo; +import java.security.cert.X509Certificate; +import java.util.*; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.x509.CertificatePoliciesExtension; +import net.tongsuo.sun.security.x509.CertificatePolicyMap; +import net.tongsuo.sun.security.x509.InhibitAnyPolicyExtension; +import static net.tongsuo.sun.security.x509.PKIXExtensions.*; +import net.tongsuo.sun.security.x509.PolicyConstraintsExtension; +import net.tongsuo.sun.security.x509.PolicyInformation; +import net.tongsuo.sun.security.x509.PolicyMappingsExtension; +import net.tongsuo.sun.security.x509.X509CertImpl; + +/** + * PolicyChecker is a PKIXCertPathChecker that checks policy + * information on a PKIX certificate, namely certificate policies, policy + * mappings, policy constraints and policy qualifiers. + * + * @since 1.4 + * @author Yassir Elley + */ +class PolicyChecker extends PKIXCertPathChecker { + + private final Set initPolicies; + private final int certPathLen; + private final boolean expPolicyRequired; + private final boolean polMappingInhibited; + private final boolean anyPolicyInhibited; + private final boolean rejectPolicyQualifiers; + private PolicyNodeImpl rootNode; + private int explicitPolicy; + private int policyMapping; + private int inhibitAnyPolicy; + private int certIndex; + + private Set supportedExts; + + private static final Debug debug = Debug.getInstance("certpath"); + static final String ANY_POLICY = KnownOIDs.CE_CERT_POLICIES_ANY.value(); + + /** + * Constructs a Policy Checker. + * + * @param initialPolicies Set of initial policies + * @param certPathLen length of the certification path to be checked + * @param expPolicyRequired true if explicit policy is required + * @param polMappingInhibited true if policy mapping is inhibited + * @param anyPolicyInhibited true if the ANY_POLICY OID should be inhibited + * @param rejectPolicyQualifiers true if pol qualifiers are to be rejected + * @param rootNode the initial root node of the valid policy tree + */ + PolicyChecker(Set initialPolicies, int certPathLen, + boolean expPolicyRequired, boolean polMappingInhibited, + boolean anyPolicyInhibited, boolean rejectPolicyQualifiers, + PolicyNodeImpl rootNode) + { + if (initialPolicies.isEmpty()) { + // if no initialPolicies are specified by user, set + // initPolicies to be anyPolicy by default + this.initPolicies = new HashSet<>(1); + this.initPolicies.add(ANY_POLICY); + } else { + this.initPolicies = new HashSet<>(initialPolicies); + } + this.certPathLen = certPathLen; + this.expPolicyRequired = expPolicyRequired; + this.polMappingInhibited = polMappingInhibited; + this.anyPolicyInhibited = anyPolicyInhibited; + this.rejectPolicyQualifiers = rejectPolicyQualifiers; + this.rootNode = rootNode; + } + + /** + * Initializes the internal state of the checker from parameters + * specified in the constructor + * + * @param forward a boolean indicating whether this checker should be + * initialized capable of building in the forward direction + * @throws CertPathValidatorException if user wants to enable forward + * checking and forward checking is not supported. + */ + @Override + public void init(boolean forward) throws CertPathValidatorException { + if (forward) { + throw new CertPathValidatorException + ("forward checking not supported"); + } + + certIndex = 1; + explicitPolicy = (expPolicyRequired ? 0 : certPathLen + 1); + policyMapping = (polMappingInhibited ? 0 : certPathLen + 1); + inhibitAnyPolicy = (anyPolicyInhibited ? 0 : certPathLen + 1); + } + + /** + * Checks if forward checking is supported. Forward checking refers + * to the ability of the PKIXCertPathChecker to perform its checks + * when presented with certificates in the forward direction (from + * target to anchor). + * + * @return true if forward checking is supported, false otherwise + */ + @Override + public boolean isForwardCheckingSupported() { + return false; + } + + /** + * Gets an immutable Set of the OID strings for the extensions that + * the PKIXCertPathChecker supports (i.e. recognizes, is able to + * process), or null if no extensions are + * supported. All OID strings that a PKIXCertPathChecker might + * possibly be able to process should be included. + * + * @return the Set of extensions supported by this PKIXCertPathChecker, + * or null if no extensions are supported + */ + @Override + public Set getSupportedExtensions() { + if (supportedExts == null) { + supportedExts = new HashSet(4); + supportedExts.add(CertificatePolicies_Id.toString()); + supportedExts.add(PolicyMappings_Id.toString()); + supportedExts.add(PolicyConstraints_Id.toString()); + supportedExts.add(InhibitAnyPolicy_Id.toString()); + supportedExts = Collections.unmodifiableSet(supportedExts); + } + return supportedExts; + } + + /** + * Performs the policy processing checks on the certificate using its + * internal state. + * + * @param cert the Certificate to be processed + * @param unresCritExts the unresolved critical extensions + * @throws CertPathValidatorException if the certificate does not verify + */ + @Override + public void check(Certificate cert, Collection unresCritExts) + throws CertPathValidatorException + { + // now do the policy checks + checkPolicy((X509Certificate) cert); + + if (unresCritExts != null && !unresCritExts.isEmpty()) { + unresCritExts.remove(CertificatePolicies_Id.toString()); + unresCritExts.remove(PolicyMappings_Id.toString()); + unresCritExts.remove(PolicyConstraints_Id.toString()); + unresCritExts.remove(InhibitAnyPolicy_Id.toString()); + } + } + + /** + * Internal method to run through all the checks. + * + * @param currCert the certificate to be processed + * @exception CertPathValidatorException Exception thrown if + * the certificate does not verify + */ + private void checkPolicy(X509Certificate currCert) + throws CertPathValidatorException + { + String msg = "certificate policies"; + if (debug != null) { + debug.println("PolicyChecker.checkPolicy() ---checking " + msg + + "..."); + debug.println("PolicyChecker.checkPolicy() certIndex = " + + certIndex); + debug.println("PolicyChecker.checkPolicy() BEFORE PROCESSING: " + + "explicitPolicy = " + explicitPolicy); + debug.println("PolicyChecker.checkPolicy() BEFORE PROCESSING: " + + "policyMapping = " + policyMapping); + debug.println("PolicyChecker.checkPolicy() BEFORE PROCESSING: " + + "inhibitAnyPolicy = " + inhibitAnyPolicy); + debug.println("PolicyChecker.checkPolicy() BEFORE PROCESSING: " + + "policyTree = " + rootNode); + } + + X509CertImpl currCertImpl; + try { + currCertImpl = X509CertImpl.toImpl(currCert); + } catch (CertificateException ce) { + throw new CertPathValidatorException(ce); + } + + boolean finalCert = (certIndex == certPathLen); + + rootNode = processPolicies(certIndex, initPolicies, explicitPolicy, + policyMapping, inhibitAnyPolicy, rejectPolicyQualifiers, rootNode, + currCertImpl, finalCert); + + if (!finalCert) { + explicitPolicy = mergeExplicitPolicy(explicitPolicy, currCertImpl, + false); + policyMapping = mergePolicyMapping(policyMapping, currCertImpl); + inhibitAnyPolicy = mergeInhibitAnyPolicy(inhibitAnyPolicy, + currCertImpl); + } + + certIndex++; + + if (debug != null) { + debug.println("PolicyChecker.checkPolicy() AFTER PROCESSING: " + + "explicitPolicy = " + explicitPolicy); + debug.println("PolicyChecker.checkPolicy() AFTER PROCESSING: " + + "policyMapping = " + policyMapping); + debug.println("PolicyChecker.checkPolicy() AFTER PROCESSING: " + + "inhibitAnyPolicy = " + inhibitAnyPolicy); + debug.println("PolicyChecker.checkPolicy() AFTER PROCESSING: " + + "policyTree = " + rootNode); + debug.println("PolicyChecker.checkPolicy() " + msg + " verified"); + } + } + + /** + * Merges the specified explicitPolicy value with the + * requireExplicitPolicy field of the PolicyConstraints + * extension obtained from the certificate. An explicitPolicy + * value of -1 implies no constraint. + * + * @param explicitPolicy an integer which indicates if a non-null + * valid policy tree is required + * @param currCert the Certificate to be processed + * @param finalCert a boolean indicating whether currCert is + * the final cert in the cert path + * @return returns the new explicitPolicy value + * @exception CertPathValidatorException Exception thrown if an error + * occurs + */ + static int mergeExplicitPolicy(int explicitPolicy, X509CertImpl currCert, + boolean finalCert) throws CertPathValidatorException { + if ((explicitPolicy > 0) && !X509CertImpl.isSelfIssued(currCert)) { + explicitPolicy--; + } + + PolicyConstraintsExtension polConstExt + = currCert.getPolicyConstraintsExtension(); + if (polConstExt == null) + return explicitPolicy; + int require = polConstExt.getRequire(); + if (debug != null) { + debug.println("PolicyChecker.mergeExplicitPolicy() " + + "require Index from cert = " + require); + } + if (!finalCert) { + if (require != -1) { + if ((explicitPolicy == -1) || (require < explicitPolicy)) { + explicitPolicy = require; + } + } + } else { + if (require == 0) + explicitPolicy = require; + } + + return explicitPolicy; + } + + /** + * Merges the specified policyMapping value with the + * inhibitPolicyMapping field of the PolicyConstraints + * extension obtained from the certificate. A policyMapping + * value of -1 implies no constraint. + * + * @param policyMapping an integer which indicates if policy mapping + * is inhibited + * @param currCert the Certificate to be processed + * @return returns the new policyMapping value + * @exception CertPathValidatorException Exception thrown if an error + * occurs + */ + static int mergePolicyMapping(int policyMapping, X509CertImpl currCert) + throws CertPathValidatorException { + if ((policyMapping > 0) && !X509CertImpl.isSelfIssued(currCert)) { + policyMapping--; + } + + PolicyConstraintsExtension polConstExt + = currCert.getPolicyConstraintsExtension(); + if (polConstExt == null) + return policyMapping; + + int inhibit = polConstExt.getInhibit(); + if (debug != null) + debug.println("PolicyChecker.mergePolicyMapping() " + + "inhibit Index from cert = " + inhibit); + + if (inhibit != -1) { + if ((policyMapping == -1) || (inhibit < policyMapping)) { + policyMapping = inhibit; + } + } + + return policyMapping; + } + + /** + * Merges the specified inhibitAnyPolicy value with the + * SkipCerts value of the InhibitAnyPolicy + * extension obtained from the certificate. + * + * @param inhibitAnyPolicy an integer which indicates whether + * "any-policy" is considered a match + * @param currCert the Certificate to be processed + * @return returns the new inhibitAnyPolicy value + * @exception CertPathValidatorException Exception thrown if an error + * occurs + */ + static int mergeInhibitAnyPolicy(int inhibitAnyPolicy, + X509CertImpl currCert) throws CertPathValidatorException { + if ((inhibitAnyPolicy > 0) && !X509CertImpl.isSelfIssued(currCert)) { + inhibitAnyPolicy--; + } + + InhibitAnyPolicyExtension inhAnyPolExt = (InhibitAnyPolicyExtension) + currCert.getExtension(InhibitAnyPolicy_Id); + if (inhAnyPolExt == null) + return inhibitAnyPolicy; + + int skipCerts = inhAnyPolExt.getSkipCerts(); + if (debug != null) + debug.println("PolicyChecker.mergeInhibitAnyPolicy() " + + "skipCerts Index from cert = " + skipCerts); + + if (skipCerts != -1) { + if (skipCerts < inhibitAnyPolicy) { + inhibitAnyPolicy = skipCerts; + } + } + return inhibitAnyPolicy; + } + + /** + * Processes certificate policies in the certificate. + * + * @param certIndex the index of the certificate + * @param initPolicies the initial policies required by the user + * @param explicitPolicy an integer which indicates if a non-null + * valid policy tree is required + * @param policyMapping an integer which indicates if policy + * mapping is inhibited + * @param inhibitAnyPolicy an integer which indicates whether + * "any-policy" is considered a match + * @param rejectPolicyQualifiers a boolean indicating whether the + * user wants to reject policies that have qualifiers + * @param origRootNode the root node of the valid policy tree + * @param currCert the Certificate to be processed + * @param finalCert a boolean indicating whether currCert is the final + * cert in the cert path + * @return the root node of the valid policy tree after modification + * @exception CertPathValidatorException Exception thrown if an + * error occurs while processing policies. + */ + static PolicyNodeImpl processPolicies(int certIndex, Set initPolicies, + int explicitPolicy, int policyMapping, int inhibitAnyPolicy, + boolean rejectPolicyQualifiers, PolicyNodeImpl origRootNode, + X509CertImpl currCert, boolean finalCert) + throws CertPathValidatorException + { + boolean policiesCritical = false; + List policyInfo; + PolicyNodeImpl rootNode; + Set anyQuals = new HashSet<>(); + + if (origRootNode == null) + rootNode = null; + else + rootNode = origRootNode.copyTree(); + + // retrieve policyOIDs from currCert + CertificatePoliciesExtension currCertPolicies + = currCert.getCertificatePoliciesExtension(); + + // PKIX: Section 6.1.3: Step (d) + if ((currCertPolicies != null) && (rootNode != null)) { + policiesCritical = currCertPolicies.isCritical(); + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "policiesCritical = " + policiesCritical); + + policyInfo = currCertPolicies.getCertPolicies(); + + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "rejectPolicyQualifiers = " + rejectPolicyQualifiers); + + boolean foundAnyPolicy = false; + + // process each policy in cert + for (PolicyInformation curPolInfo : policyInfo) { + String curPolicy = + curPolInfo.getPolicyIdentifier().getIdentifier().toString(); + + if (curPolicy.equals(ANY_POLICY)) { + foundAnyPolicy = true; + anyQuals = curPolInfo.getPolicyQualifiers(); + } else { + // PKIX: Section 6.1.3: Step (d)(1) + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "processing policy: " + curPolicy); + + // retrieve policy qualifiers from cert + Set pQuals = + curPolInfo.getPolicyQualifiers(); + + // reject cert if we find critical policy qualifiers and + // the policyQualifiersRejected flag is set in the params + if (!pQuals.isEmpty() && rejectPolicyQualifiers && + policiesCritical) { + throw new CertPathValidatorException( + "critical policy qualifiers present in certificate", + null, null, -1, PKIXReason.INVALID_POLICY); + } + + // PKIX: Section 6.1.3: Step (d)(1)(i) + boolean foundMatch = processParents(certIndex, + policiesCritical, rejectPolicyQualifiers, rootNode, + curPolicy, pQuals, false); + + if (!foundMatch) { + // PKIX: Section 6.1.3: Step (d)(1)(ii) + processParents(certIndex, policiesCritical, + rejectPolicyQualifiers, rootNode, curPolicy, + pQuals, true); + } + } + } + + // PKIX: Section 6.1.3: Step (d)(2) + if (foundAnyPolicy) { + if ((inhibitAnyPolicy > 0) || + (!finalCert && X509CertImpl.isSelfIssued(currCert))) { + if (debug != null) { + debug.println("PolicyChecker.processPolicies() " + + "processing policy: " + ANY_POLICY); + } + processParents(certIndex, policiesCritical, + rejectPolicyQualifiers, rootNode, ANY_POLICY, anyQuals, + true); + } + } + + // PKIX: Section 6.1.3: Step (d)(3) + rootNode.prune(certIndex); + if (!rootNode.getChildren().hasNext()) { + rootNode = null; + } + } else if (currCertPolicies == null) { + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "no policies present in cert"); + // PKIX: Section 6.1.3: Step (e) + rootNode = null; + } + + // We delay PKIX: Section 6.1.3: Step (f) to the end + // because the code that follows may delete some nodes + // resulting in a null tree + if (rootNode != null) { + if (!finalCert) { + // PKIX: Section 6.1.4: Steps (a)-(b) + rootNode = processPolicyMappings(currCert, certIndex, + policyMapping, rootNode, policiesCritical, anyQuals); + } + } + + // At this point, we optimize the PKIX algorithm by + // removing those nodes which would later have + // been removed by PKIX: Section 6.1.5: Step (g)(iii) + + if (rootNode != null && !initPolicies.contains(ANY_POLICY)) { + rootNode = removeInvalidNodes(rootNode, certIndex, + initPolicies, currCertPolicies); + + // PKIX: Section 6.1.5: Step (g)(iii) + if ((rootNode != null) && finalCert) { + // rewrite anyPolicy leaf nodes (see method comments) + rootNode = rewriteLeafNodes(certIndex, initPolicies, rootNode); + } + } + + + if (finalCert) { + // PKIX: Section 6.1.5: Steps (a) and (b) + explicitPolicy = mergeExplicitPolicy(explicitPolicy, currCert, + true); + } + + // PKIX: Section 6.1.3: Step (f) + // verify that either explicit policy is greater than 0 or + // the valid_policy_tree is not equal to NULL + + if ((explicitPolicy == 0) && (rootNode == null)) { + throw new CertPathValidatorException + ("non-null policy tree required and policy tree is null", + null, null, -1, PKIXReason.INVALID_POLICY); + } + + return rootNode; + } + + /** + * Rewrite leaf nodes at the end of validation as described in RFC 5280 + * section 6.1.5: Step (g)(iii). Leaf nodes with anyPolicy are replaced + * by nodes explicitly representing initial policies not already + * represented by leaf nodes. + * + * This method should only be called when processing the final cert + * and if the policy tree is not null and initial policies is not + * anyPolicy. + * + * @param certIndex the depth of the tree + * @param initPolicies Set of user specified initial policies + * @param rootNode the root of the policy tree + */ + private static PolicyNodeImpl rewriteLeafNodes(int certIndex, + Set initPolicies, PolicyNodeImpl rootNode) { + Set anyNodes = + rootNode.getPolicyNodesValid(certIndex, ANY_POLICY); + if (anyNodes.isEmpty()) { + return rootNode; + } + PolicyNodeImpl anyNode = anyNodes.iterator().next(); + PolicyNodeImpl parentNode = (PolicyNodeImpl)anyNode.getParent(); + parentNode.deleteChild(anyNode); + // see if there are any initialPolicies not represented by leaf nodes + Set initial = new HashSet<>(initPolicies); + for (PolicyNodeImpl node : rootNode.getPolicyNodes(certIndex)) { + initial.remove(node.getValidPolicy()); + } + if (initial.isEmpty()) { + // we deleted the anyPolicy node and have nothing to re-add, + // so we need to prune the tree + rootNode.prune(certIndex); + if (!rootNode.getChildren().hasNext()) { + rootNode = null; + } + } else { + boolean anyCritical = anyNode.isCritical(); + Set anyQualifiers = + anyNode.getPolicyQualifiers(); + for (String policy : initial) { + Set expectedPolicies = Collections.singleton(policy); + new PolicyNodeImpl(parentNode, policy, + anyQualifiers, anyCritical, expectedPolicies, false); + } + } + return rootNode; + } + + /** + * Finds the policy nodes of depth (certIndex-1) where curPolicy + * is in the expected policy set and creates a new child node + * appropriately. If matchAny is true, then a value of ANY_POLICY + * in the expected policy set will match any curPolicy. If matchAny + * is false, then the expected policy set must exactly contain the + * curPolicy to be considered a match. This method returns a boolean + * value indicating whether a match was found. + * + * @param certIndex the index of the certificate whose policy is + * being processed + * @param policiesCritical a boolean indicating whether the certificate + * policies extension is critical + * @param rejectPolicyQualifiers a boolean indicating whether the + * user wants to reject policies that have qualifiers + * @param rootNode the root node of the valid policy tree + * @param curPolicy a String representing the policy being processed + * @param pQuals the policy qualifiers of the policy being processed or an + * empty Set if there are no qualifiers + * @param matchAny a boolean indicating whether a value of ANY_POLICY + * in the expected policy set will be considered a match + * @return a boolean indicating whether a match was found + */ + private static boolean processParents(int certIndex, + boolean policiesCritical, boolean rejectPolicyQualifiers, + PolicyNodeImpl rootNode, String curPolicy, + Set pQuals, + boolean matchAny) { + boolean foundMatch = false; + + if (debug != null) + debug.println("PolicyChecker.processParents(): matchAny = " + + matchAny); + + // find matching parents + Set parentNodes = + rootNode.getPolicyNodesExpected(certIndex - 1, + curPolicy, matchAny); + + // for each matching parent, extend policy tree + for (PolicyNodeImpl curParent : parentNodes) { + if (debug != null) + debug.println("PolicyChecker.processParents() " + + "found parent:\n" + curParent.asString()); + + foundMatch = true; + + Set curExpPols; + + if (curPolicy.equals(ANY_POLICY)) { + // do step 2 + Set parExpPols = curParent.getExpectedPolicies(); + parentExplicitPolicies: + for (String curParExpPol : parExpPols) { + + Iterator childIter = + curParent.getChildren(); + while (childIter.hasNext()) { + PolicyNodeImpl childNode = childIter.next(); + String childPolicy = childNode.getValidPolicy(); + if (curParExpPol.equals(childPolicy)) { + if (debug != null) + debug.println(childPolicy + " in parent's " + + "expected policy set already appears in " + + "child node"); + continue parentExplicitPolicies; + } + } + + Set expPols = new HashSet<>(); + expPols.add(curParExpPol); + + new PolicyNodeImpl + (curParent, curParExpPol, pQuals, + policiesCritical, expPols, false); + } + } else { + curExpPols = new HashSet<>(); + curExpPols.add(curPolicy); + + new PolicyNodeImpl + (curParent, curPolicy, pQuals, + policiesCritical, curExpPols, false); + } + } + + return foundMatch; + } + + /** + * Processes policy mappings in the certificate. + * + * @param currCert the Certificate to be processed + * @param certIndex the index of the current certificate + * @param policyMapping an integer which indicates if policy + * mapping is inhibited + * @param rootNode the root node of the valid policy tree + * @param policiesCritical a boolean indicating if the certificate policies + * extension is critical + * @param anyQuals the qualifiers associated with ANY-POLICY, or an empty + * Set if there are no qualifiers associated with ANY-POLICY + * @return the root node of the valid policy tree after modification + * @exception CertPathValidatorException exception thrown if an error + * occurs while processing policy mappings + */ + private static PolicyNodeImpl processPolicyMappings(X509CertImpl currCert, + int certIndex, int policyMapping, PolicyNodeImpl rootNode, + boolean policiesCritical, Set anyQuals) + throws CertPathValidatorException + { + PolicyMappingsExtension polMappingsExt + = currCert.getPolicyMappingsExtension(); + + if (polMappingsExt == null) + return rootNode; + + if (debug != null) + debug.println("PolicyChecker.processPolicyMappings() " + + "inside policyMapping check"); + + List maps; + maps = polMappingsExt.getMaps(); + + boolean childDeleted = false; + for (CertificatePolicyMap polMap : maps) { + String issuerDomain + = polMap.getIssuerIdentifier().getIdentifier().toString(); + String subjectDomain + = polMap.getSubjectIdentifier().getIdentifier().toString(); + if (debug != null) { + debug.println("PolicyChecker.processPolicyMappings() " + + "issuerDomain = " + issuerDomain); + debug.println("PolicyChecker.processPolicyMappings() " + + "subjectDomain = " + subjectDomain); + } + + if (issuerDomain.equals(ANY_POLICY)) { + throw new CertPathValidatorException + ("encountered an issuerDomainPolicy of ANY_POLICY", + null, null, -1, PKIXReason.INVALID_POLICY); + } + + if (subjectDomain.equals(ANY_POLICY)) { + throw new CertPathValidatorException + ("encountered a subjectDomainPolicy of ANY_POLICY", + null, null, -1, PKIXReason.INVALID_POLICY); + } + + Set validNodes = + rootNode.getPolicyNodesValid(certIndex, issuerDomain); + if (!validNodes.isEmpty()) { + for (PolicyNodeImpl curNode : validNodes) { + if ((policyMapping > 0) || (policyMapping == -1)) { + curNode.addExpectedPolicy(subjectDomain); + } else if (policyMapping == 0) { + PolicyNodeImpl parentNode = + (PolicyNodeImpl) curNode.getParent(); + if (debug != null) + debug.println("PolicyChecker.processPolicyMappings" + + "() before deleting: policy tree = " + + rootNode); + parentNode.deleteChild(curNode); + childDeleted = true; + if (debug != null) + debug.println("PolicyChecker.processPolicyMappings" + + "() after deleting: policy tree = " + + rootNode); + } + } + } else { // no node of depth i has a valid policy + if ((policyMapping > 0) || (policyMapping == -1)) { + Set validAnyNodes = + rootNode.getPolicyNodesValid(certIndex, ANY_POLICY); + for (PolicyNodeImpl curAnyNode : validAnyNodes) { + PolicyNodeImpl curAnyNodeParent = + (PolicyNodeImpl) curAnyNode.getParent(); + + Set expPols = new HashSet<>(); + expPols.add(subjectDomain); + + new PolicyNodeImpl + (curAnyNodeParent, issuerDomain, anyQuals, + policiesCritical, expPols, true); + } + } + } + } + + if (childDeleted) { + rootNode.prune(certIndex); + if (!rootNode.getChildren().hasNext()) { + if (debug != null) + debug.println("setting rootNode to null"); + rootNode = null; + } + } + + return rootNode; + } + + /** + * Removes those nodes which do not intersect with the initial policies + * specified by the user. + * + * @param rootNode the root node of the valid policy tree + * @param certIndex the index of the certificate being processed + * @param initPolicies the Set of policies required by the user + * @param currCertPolicies the CertificatePoliciesExtension of the + * certificate being processed + * @return the root node of the valid policy tree after modification + * @exception CertPathValidatorException Exception thrown if error occurs. + */ + private static PolicyNodeImpl removeInvalidNodes(PolicyNodeImpl rootNode, + int certIndex, Set initPolicies, + CertificatePoliciesExtension currCertPolicies) + throws CertPathValidatorException + { + List policyInfo = currCertPolicies.getCertPolicies(); + + boolean childDeleted = false; + for (PolicyInformation curPolInfo : policyInfo) { + String curPolicy = + curPolInfo.getPolicyIdentifier().getIdentifier().toString(); + + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "processing policy second time: " + curPolicy); + + Set validNodes = + rootNode.getPolicyNodesValid(certIndex, curPolicy); + for (PolicyNodeImpl curNode : validNodes) { + PolicyNodeImpl parentNode = (PolicyNodeImpl)curNode.getParent(); + if (parentNode.getValidPolicy().equals(ANY_POLICY)) { + if ((!initPolicies.contains(curPolicy)) && + (!curPolicy.equals(ANY_POLICY))) { + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "before deleting: policy tree = " + rootNode); + parentNode.deleteChild(curNode); + childDeleted = true; + if (debug != null) + debug.println("PolicyChecker.processPolicies() " + + "after deleting: policy tree = " + rootNode); + } + } + } + } + + if (childDeleted) { + rootNode.prune(certIndex); + if (!rootNode.getChildren().hasNext()) { + rootNode = null; + } + } + + return rootNode; + } + + /** + * Gets the root node of the valid policy tree, or null if the + * valid policy tree is null. Marks each node of the returned tree + * immutable and thread-safe. + * + * @return the root node of the valid policy tree, or null if + * the valid policy tree is null + */ + PolicyNode getPolicyTree() { + if (rootNode == null) + return null; + else { + PolicyNodeImpl policyTree = rootNode.copyTree(); + policyTree.setImmutable(); + return policyTree; + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyNodeImpl.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyNodeImpl.java new file mode 100644 index 000000000..8f5704f15 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/PolicyNodeImpl.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import net.tongsuo.sun.security.util.KnownOIDs; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import java.security.cert.*; + +/** + * Implements the PolicyNode interface. + *

+ * This class provides an implementation of the PolicyNode + * interface, and is used internally to build and search Policy Trees. + * While the implementation is mutable during construction, it is immutable + * before returning to a client and no mutable public or protected methods + * are exposed by this implementation, as per the contract of PolicyNode. + * + * @since 1.4 + * @author Seth Proctor + * @author Sean Mullan + */ +final class PolicyNodeImpl implements PolicyNode { + + /** + * Use to specify the special policy "Any Policy" + */ + private static final String ANY_POLICY + = KnownOIDs.CE_CERT_POLICIES_ANY.value(); + + // every node has one parent, and zero or more children + private final PolicyNodeImpl mParent; + private final HashSet mChildren; + + // the 4 fields specified by RFC 5280 + private final String mValidPolicy; + private final HashSet mQualifierSet; + private final boolean mCriticalityIndicator; + private final HashSet mExpectedPolicySet; + private boolean mOriginalExpectedPolicySet; + + // the tree depth + private final int mDepth; + // immutability flag + private boolean isImmutable = false; + + /** + * Constructor which takes a PolicyNodeImpl representing the + * parent in the Policy Tree to this node. If null, this is the + * root of the tree. The constructor also takes the associated data + * for this node, as found in the certificate. It also takes a boolean + * argument specifying whether this node is being created as a result + * of policy mapping. + * + * @param parent the PolicyNode above this in the tree, or null if this + * node is the tree's root node + * @param validPolicy a String representing this node's valid policy OID + * @param qualifierSet the Set of qualifiers for this policy + * @param criticalityIndicator a boolean representing whether the + * extension is critical + * @param expectedPolicySet a Set of expected policies + * @param generatedByPolicyMapping a boolean indicating whether this + * node was generated by a policy mapping + */ + PolicyNodeImpl(PolicyNodeImpl parent, String validPolicy, + Set qualifierSet, + boolean criticalityIndicator, Set expectedPolicySet, + boolean generatedByPolicyMapping) { + mParent = parent; + mChildren = new HashSet<>(); + + if (validPolicy != null) + mValidPolicy = validPolicy; + else + mValidPolicy = ""; + + if (qualifierSet != null) + mQualifierSet = new HashSet<>(qualifierSet); + else + mQualifierSet = new HashSet<>(); + + mCriticalityIndicator = criticalityIndicator; + + if (expectedPolicySet != null) + mExpectedPolicySet = new HashSet<>(expectedPolicySet); + else + mExpectedPolicySet = new HashSet<>(); + + mOriginalExpectedPolicySet = !generatedByPolicyMapping; + + // see if we're the root, and act appropriately + if (mParent != null) { + mDepth = mParent.getDepth() + 1; + mParent.addChild(this); + } else { + mDepth = 0; + } + } + + /** + * Alternate constructor which makes a new node with the policy data + * in an existing PolicyNodeImpl. + * + * @param parent a PolicyNode that's the new parent of the node, or + * null if this is the root node + * @param node a PolicyNode containing the policy data to copy + */ + PolicyNodeImpl(PolicyNodeImpl parent, PolicyNodeImpl node) { + this(parent, node.mValidPolicy, node.mQualifierSet, + node.mCriticalityIndicator, node.mExpectedPolicySet, false); + } + + @Override + public PolicyNode getParent() { + return mParent; + } + + @Override + public Iterator getChildren() { + return Collections.unmodifiableSet(mChildren).iterator(); + } + + @Override + public int getDepth() { + return mDepth; + } + + @Override + public String getValidPolicy() { + return mValidPolicy; + } + + @Override + public Set getPolicyQualifiers() { + return Collections.unmodifiableSet(mQualifierSet); + } + + @Override + public Set getExpectedPolicies() { + return Collections.unmodifiableSet(mExpectedPolicySet); + } + + @Override + public boolean isCritical() { + return mCriticalityIndicator; + } + + /** + * Return a printable representation of the PolicyNode. + * Starting at the node on which this method is called, + * it recurses through the tree and prints out each node. + * + * @return a String describing the contents of the Policy Node + */ + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(this.asString()); + + for (PolicyNodeImpl node : mChildren) { + buffer.append(node); + } + return buffer.toString(); + } + + // private methods and package private operations + + boolean isImmutable() { + return isImmutable; + } + + /** + * Sets the immutability flag of this node and all of its children + * to true. + */ + void setImmutable() { + if (isImmutable) + return; + for (PolicyNodeImpl node : mChildren) { + node.setImmutable(); + } + isImmutable = true; + } + + /** + * Private method sets a child node. This is called from the child's + * constructor. + * + * @param child new PolicyNodeImpl child node + */ + private void addChild(PolicyNodeImpl child) { + if (isImmutable) { + throw new IllegalStateException("PolicyNode is immutable"); + } + mChildren.add(child); + } + + /** + * Adds an expectedPolicy to the expected policy set. + * If this is the original expected policy set initialized + * by the constructor, then the expected policy set is cleared + * before the expected policy is added. + * + * @param expectedPolicy a String representing an expected policy. + */ + void addExpectedPolicy(String expectedPolicy) { + if (isImmutable) { + throw new IllegalStateException("PolicyNode is immutable"); + } + if (mOriginalExpectedPolicySet) { + mExpectedPolicySet.clear(); + mOriginalExpectedPolicySet = false; + } + mExpectedPolicySet.add(expectedPolicy); + } + + /** + * Removes all paths which don't reach the specified depth. + * + * @param depth an int representing the desired minimum depth of all paths + */ + void prune(int depth) { + if (isImmutable) + throw new IllegalStateException("PolicyNode is immutable"); + + // if we have no children, we can't prune below us... + if (mChildren.size() == 0) + return; + + Iterator it = mChildren.iterator(); + while (it.hasNext()) { + PolicyNodeImpl node = it.next(); + node.prune(depth); + // now that we've called prune on the child, see if we should + // remove it from the tree + if ((node.mChildren.size() == 0) && (depth > mDepth + 1)) + it.remove(); + } + } + + /** + * Deletes the specified child node of this node, if it exists. + * + * @param childNode the child node to be deleted + */ + void deleteChild(PolicyNode childNode) { + if (isImmutable) { + throw new IllegalStateException("PolicyNode is immutable"); + } + mChildren.remove(childNode); + } + + /** + * Returns a copy of the tree, without copying the policy-related data, + * rooted at the node on which this was called. + * + * @return a copy of the tree + */ + PolicyNodeImpl copyTree() { + return copyTree(null); + } + + private PolicyNodeImpl copyTree(PolicyNodeImpl parent) { + PolicyNodeImpl newNode = new PolicyNodeImpl(parent, this); + + for (PolicyNodeImpl node : mChildren) { + node.copyTree(newNode); + } + + return newNode; + } + + /** + * Returns all nodes at the specified depth in the tree. + * + * @param depth an int representing the depth of the desired nodes + * @return a Set of all nodes at the specified depth + */ + Set getPolicyNodes(int depth) { + Set set = new HashSet<>(); + getPolicyNodes(depth, set); + return set; + } + + /** + * Add all nodes at depth to set and return the Set. + * Internal recursion helper. + */ + private void getPolicyNodes(int depth, Set set) { + // if we've reached the desired depth, then return + if (mDepth == depth) { + set.add(this); + } else { + for (PolicyNodeImpl node : mChildren) { + node.getPolicyNodes(depth, set); + } + } + } + + /** + * Finds all nodes at the specified depth whose expected_policy_set + * contains the specified expected OID (if matchAny is false) + * or the special OID "any value" (if matchAny is true). + * + * @param depth an int representing the desired depth + * @param expectedOID a String encoding the valid OID to match + * @param matchAny a boolean indicating whether an expected_policy_set + * containing ANY_POLICY should be considered a match + * @return a Set of matched PolicyNodes + */ + Set getPolicyNodesExpected(int depth, + String expectedOID, boolean matchAny) { + + if (expectedOID.equals(ANY_POLICY)) { + return getPolicyNodes(depth); + } else { + return getPolicyNodesExpectedHelper(depth, expectedOID, matchAny); + } + } + + private Set getPolicyNodesExpectedHelper(int depth, + String expectedOID, boolean matchAny) { + + HashSet set = new HashSet<>(); + + if (mDepth < depth) { + for (PolicyNodeImpl node : mChildren) { + set.addAll(node.getPolicyNodesExpectedHelper(depth, + expectedOID, + matchAny)); + } + } else { + if (matchAny) { + if (mExpectedPolicySet.contains(ANY_POLICY)) + set.add(this); + } else { + if (mExpectedPolicySet.contains(expectedOID)) + set.add(this); + } + } + + return set; + } + + /** + * Finds all nodes at the specified depth that contains the + * specified valid OID + * + * @param depth an int representing the desired depth + * @param validOID a String encoding the valid OID to match + * @return a Set of matched PolicyNodes + */ + Set getPolicyNodesValid(int depth, String validOID) { + HashSet set = new HashSet<>(); + + if (mDepth < depth) { + for (PolicyNodeImpl node : mChildren) { + set.addAll(node.getPolicyNodesValid(depth, validOID)); + } + } else { + if (mValidPolicy.equals(validOID)) + set.add(this); + } + + return set; + } + + private static String policyToString(String oid) { + if (oid.equals(ANY_POLICY)) { + return "anyPolicy"; + } else { + return oid; + } + } + + /** + * Prints out some data on this node. + */ + String asString() { + if (mParent == null) { + return "anyPolicy ROOT\n"; + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = getDepth(); i < n; i++) { + sb.append(" "); + } + sb.append(policyToString(getValidPolicy())); + sb.append(" CRIT: "); + sb.append(isCritical()); + sb.append(" EP: "); + for (String policy : getExpectedPolicies()) { + sb.append(policyToString(policy)); + sb.append(" "); + } + sb.append(" ("); + sb.append(getDepth()); + sb.append(")\n"); + return sb.toString(); + } + } +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ResponderId.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ResponderId.java new file mode 100644 index 000000000..2bc325d82 --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/ResponderId.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.util.Arrays; +import java.io.IOException; +import java.security.PublicKey; +import javax.security.auth.x500.X500Principal; +import net.tongsuo.sun.security.x509.KeyIdentifier; +import net.tongsuo.sun.security.util.DerValue; + +/** + * Class for ResponderId entities as described in RFC6960. ResponderId objects + * are used to uniquely identify OCSP responders. + *

+ * The RFC 6960 defines a ResponderID structure as: + *

+ * ResponderID ::= CHOICE {
+ *      byName              [1] Name,
+ *      byKey               [2] KeyHash }
+ *
+ * KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
+ * (excluding the tag and length fields)
+ *
+ * Name is defined in RFC 5280.
+ * 
+ * + * @see ResponderId.Type + * @since 9 + */ +public final class ResponderId { + + /** + * A {@code ResponderId} enumeration describing the accepted forms for a + * {@code ResponderId}. + * + * @see ResponderId + * @since 9 + */ + public enum Type { + /** + * A BY_NAME {@code ResponderId} will be built from a subject name, + * either as an {@code X500Principal} or a DER-encoded byte array. + */ + BY_NAME(1, "byName"), + + /** + * A BY_KEY {@code ResponderId} will be built from a public key + * identifier, either derived from a {@code PublicKey} or directly + * from a DER-encoded byte array containing the key identifier. + */ + BY_KEY(2, "byKey"); + + private final int tagNumber; + private final String ridTypeName; + + Type(int value, String name) { + this.tagNumber = value; + this.ridTypeName = name; + } + + public int value() { + return tagNumber; + } + + @Override + public String toString() { + return ridTypeName; + } + } + + private final Type type; + private X500Principal responderName; + private KeyIdentifier responderKeyId; + private final byte[] encodedRid; + + /** + * Constructs a {@code ResponderId} object using an {@code X500Principal}. + * When encoded in DER this object will use the BY_NAME option. + * + * @param subjectName the subject name of the certificate used + * to sign OCSP responses. + * + * @throws IOException if the internal DER-encoding of the + * {@code X500Principal} fails. + */ + public ResponderId(X500Principal subjectName) throws IOException { + responderName = subjectName; + responderKeyId = null; + encodedRid = principalToBytes(); + type = Type.BY_NAME; + } + + /** + * Constructs a {@code ResponderId} object using a {@code PublicKey}. + * When encoded in DER this object will use the byKey option, a + * SHA-1 hash of the responder's public key. + * + * @param pubKey the OCSP responder's public key + * + * @throws IOException if the internal DER-encoding of the + * {@code KeyIdentifier} fails. + */ + public ResponderId(PublicKey pubKey) throws IOException { + responderKeyId = new KeyIdentifier(pubKey); + responderName = null; + encodedRid = keyIdToBytes(); + type = Type.BY_KEY; + } + + /** + * Constructs a {@code ResponderId} object from its DER-encoding. + * + * @param encodedData the DER-encoded bytes + * + * @throws IOException if the encodedData is not properly DER encoded + */ + public ResponderId(byte[] encodedData) throws IOException { + DerValue outer = new DerValue(encodedData); + + if (outer.isContextSpecific((byte)Type.BY_NAME.value()) + && outer.isConstructed()) { + // Use the X500Principal constructor as a way to sanity + // check the incoming data. + responderName = new X500Principal(outer.getDataBytes()); + encodedRid = principalToBytes(); + type = Type.BY_NAME; + } else if (outer.isContextSpecific((byte)Type.BY_KEY.value()) + && outer.isConstructed()) { + // Use the KeyIdentifier constructor as a way to sanity + // check the incoming data. + responderKeyId = + new KeyIdentifier(new DerValue(outer.getDataBytes())); + encodedRid = keyIdToBytes(); + type = Type.BY_KEY; + } else { + throw new IOException("Invalid ResponderId content"); + } + } + + /** + * Encode a {@code ResponderId} in DER form + * + * @return a byte array containing the DER-encoded representation for this + * {@code ResponderId} + */ + public byte[] getEncoded() { + return encodedRid.clone(); + } + + /** + * Return the type of {@code ResponderId} + * + * @return a number corresponding to the context-specific tag number + * used in the DER-encoding for a {@code ResponderId} + */ + public ResponderId.Type getType() { + return type; + } + + /** + * Get the length of the encoded {@code ResponderId} (including the tag and + * length of the explicit tagging from the outer ASN.1 CHOICE). + * + * @return the length of the encoded {@code ResponderId} + */ + public int length() { + return encodedRid.length; + } + + /** + * Obtain the underlying {@code X500Principal} from a {@code ResponderId} + * + * @return the {@code X500Principal} for this {@code ResponderId} if it + * is a BY_NAME variant. If the {@code ResponderId} is a BY_KEY + * variant, this routine will return {@code null}. + */ + public X500Principal getResponderName() { + return responderName; + } + + /** + * Obtain the underlying key identifier from a {@code ResponderId} + * + * @return the {@code KeyIdentifier} for this {@code ResponderId} if it + * is a BY_KEY variant. If the {@code ResponderId} is a BY_NAME + * variant, this routine will return {@code null}. + */ + public KeyIdentifier getKeyIdentifier() { + return responderKeyId; + } + + /** + * Compares the specified object with this {@code ResponderId} for equality. + * A ResponderId will only be considered equivalent if both the type and + * data value are equal. Two ResponderIds initialized by name and + * key ID, respectively, will not be equal even if the + * ResponderId objects are created from the same source certificate. + * + * @param obj the object to be compared against + * + * @return true if the specified object is equal to this {@code Responderid} + */ + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (this == obj) { + return true; + } + + if (obj instanceof ResponderId) { + ResponderId respObj = (ResponderId)obj; + return Arrays.equals(encodedRid, respObj.getEncoded()); + } + + return false; + } + + /** + * Returns the hash code value for this {@code ResponderId} + * + * @return the hash code value for this {@code ResponderId} + */ + @Override + public int hashCode() { + return Arrays.hashCode(encodedRid); + } + + /** + * Create a String representation of this {@code ResponderId} + * + * @return a String representation of this {@code ResponderId} + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + switch (type) { + case BY_NAME: + sb.append(type).append(": ").append(responderName); + break; + case BY_KEY: + sb.append(type).append(": "); + for (byte keyIdByte : responderKeyId.getIdentifier()) { + sb.append(String.format("%02X", keyIdByte)); + } + break; + default: + sb.append("Unknown ResponderId Type: ").append(type); + } + return sb.toString(); + } + + /** + * Convert the responderName data member into its DER-encoded form + * + * @return the DER encoding for a responder ID byName option, including + * explicit context-specific tagging. + * + * @throws IOException if any encoding error occurs + */ + private byte[] principalToBytes() throws IOException { + DerValue dv = new DerValue(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte)Type.BY_NAME.value()), + responderName.getEncoded()); + return dv.toByteArray(); + } + + /** + * Convert the responderKeyId data member into its DER-encoded form + * + * @return the DER encoding for a responder ID byKey option, including + * explicit context-specific tagging. + * + * @throws IOException if any encoding error occurs + */ + private byte[] keyIdToBytes() throws IOException { + // Place the KeyIdentifier bytes into an OCTET STRING + DerValue inner = new DerValue(DerValue.tag_OctetString, + responderKeyId.getIdentifier()); + + // Mark the OCTET STRING-wrapped KeyIdentifier bytes + // as EXPLICIT CONTEXT 2 + DerValue outer = new DerValue(DerValue.createTag(DerValue.TAG_CONTEXT, + true, (byte)Type.BY_KEY.value()), inner.toByteArray()); + + return outer.toByteArray(); + } + +} diff --git a/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/RevocationChecker.java b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/RevocationChecker.java new file mode 100644 index 000000000..bde77f20f --- /dev/null +++ b/openjdk/src/main/java/net/tongsuo/sun/security/provider/certpath/RevocationChecker.java @@ -0,0 +1,1224 @@ +/* + * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package net.tongsuo.sun.security.provider.certpath; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.AccessController; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedAction; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.CertPathValidatorException.BasicReason; +import java.security.cert.Extension; +import java.security.cert.*; +import java.util.*; +import javax.security.auth.x500.X500Principal; + +import net.tongsuo.pkix.PKIXInsts; +import net.tongsuo.sun.security.util.CollectionUtil; + +import net.tongsuo.sun.security.util.Debug; +import net.tongsuo.sun.security.util.KnownOIDs; +import net.tongsuo.sun.security.x509.AccessDescription; +import net.tongsuo.sun.security.x509.AuthorityInfoAccessExtension; +import net.tongsuo.sun.security.x509.CRLDistributionPointsExtension; +import net.tongsuo.sun.security.x509.DistributionPoint; +import net.tongsuo.sun.security.x509.GeneralName; +import net.tongsuo.sun.security.x509.GeneralNames; +import net.tongsuo.sun.security.x509.PKIXExtensions; +import net.tongsuo.sun.security.x509.X500Name; +import net.tongsuo.sun.security.x509.X509CRLEntryImpl; +import net.tongsuo.sun.security.x509.X509CertImpl; +import net.tongsuo.java.util.HexFormat; + +class RevocationChecker extends PKIXRevocationChecker { + + private static final Debug debug = Debug.getInstance("certpath"); + + private TrustAnchor anchor; + private PKIX.ValidatorParams params; + private boolean onlyEE; + private boolean softFail; + private boolean crlDP; + private URI responderURI; + private X509Certificate responderCert; + private List certStores; + private Map ocspResponses; + private List ocspExtensions; + private final boolean legacy; + private final LinkedList softFailExceptions = + new LinkedList<>(); + + // state variables + private OCSPResponse.IssuerInfo issuerInfo; + private PublicKey prevPubKey; + private boolean crlSignFlag; + private int certIndex; + + private enum Mode { PREFER_OCSP, PREFER_CRLS, ONLY_CRLS, ONLY_OCSP } + private Mode mode = Mode.PREFER_OCSP; + + private static class RevocationProperties { + boolean onlyEE; + boolean ocspEnabled; + boolean crlDPEnabled; + String ocspUrl; + String ocspSubject; + String ocspIssuer; + String ocspSerial; + boolean ocspNonce; + } + private RevocationProperties rp; + private static final int DEFAULT_NONCE_BYTES = 16; + + RevocationChecker() { + legacy = false; + } + + RevocationChecker(TrustAnchor anchor, PKIX.ValidatorParams params) + throws CertPathValidatorException + { + legacy = true; + init(anchor, params); + } + + void init(TrustAnchor anchor, PKIX.ValidatorParams params) + throws CertPathValidatorException + { + rp = getRevocationProperties(); + URI uri = getOcspResponder(); + responderURI = (uri == null) ? toURI(rp.ocspUrl) : uri; + X509Certificate cert = getOcspResponderCert(); + responderCert = (cert == null) + ? getResponderCert(rp, params.trustAnchors(), + params.certStores()) + : cert; + Set