mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 20:18:48 +00:00
562 lines
21 KiB
Java
562 lines
21 KiB
Java
/*
|
|
* Copyright (c) 2025, 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 sun.security.ssl;
|
|
|
|
import java.net.Socket;
|
|
import java.security.AlgorithmConstraints;
|
|
import java.security.Principal;
|
|
import java.security.cert.CertPathValidatorException;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Set;
|
|
import javax.net.ssl.SNIHostName;
|
|
import javax.net.ssl.SNIServerName;
|
|
import javax.net.ssl.SSLEngine;
|
|
import javax.net.ssl.SSLSocket;
|
|
import javax.net.ssl.StandardConstants;
|
|
import javax.net.ssl.X509ExtendedKeyManager;
|
|
import javax.security.auth.x500.X500Principal;
|
|
import sun.security.provider.certpath.AlgorithmChecker;
|
|
import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE;
|
|
import sun.security.util.KnownOIDs;
|
|
import sun.security.validator.Validator;
|
|
|
|
/*
|
|
* Layer that adds algorithm constraints and certificate checking functionality
|
|
* to a key manager:
|
|
* 1) Check against peer supported certificate signature algorithms (sent with
|
|
* "signature_algorithms_cert" TLS extension).
|
|
* 2) Check against local TLS algorithm constraints ("java.security" config
|
|
* file).
|
|
* 3) Mark alias results based on validity period and certificate extensions,
|
|
* so results can be sorted to find the best match. See "CheckResult" and
|
|
* "EntryStatus" for details.
|
|
*/
|
|
|
|
abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager {
|
|
|
|
// Indicates whether we should skip the certificate checks.
|
|
private final boolean checksDisabled;
|
|
|
|
protected X509KeyManagerCertChecking() {
|
|
checksDisabled = isCheckingDisabled();
|
|
}
|
|
|
|
abstract boolean isCheckingDisabled();
|
|
|
|
// TODO move this method to a public interface / class
|
|
abstract String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers,
|
|
QuicTLSEngineImpl quicTLSEngine);
|
|
|
|
// TODO move this method to a public interface / class
|
|
abstract String chooseQuicServerAlias(String keyType,
|
|
X500Principal[] issuers,
|
|
QuicTLSEngineImpl quicTLSEngine);
|
|
|
|
// Entry point to do all certificate checks.
|
|
protected EntryStatus checkAlias(int keyStoreIndex, String alias,
|
|
Certificate[] chain, Date verificationDate, List<KeyType> keyTypes,
|
|
Set<X500Principal> issuerSet, CheckType checkType,
|
|
AlgorithmConstraints constraints,
|
|
List<SNIServerName> requestedServerNames, String idAlgorithm) {
|
|
|
|
// --- Mandatory checks ---
|
|
|
|
if ((chain == null) || (chain.length == 0)) {
|
|
return null;
|
|
}
|
|
|
|
for (Certificate cert : chain) {
|
|
if (!(cert instanceof X509Certificate)) {
|
|
// Not an X509Certificate, ignore this alias
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Check key type, get key type index.
|
|
int keyIndex = -1;
|
|
int j = 0;
|
|
|
|
for (KeyType keyType : keyTypes) {
|
|
if (keyType.matches(chain)) {
|
|
keyIndex = j;
|
|
break;
|
|
}
|
|
j++;
|
|
}
|
|
|
|
if (keyIndex == -1) {
|
|
if (SSLLogger.isOn() && SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine("Ignore alias " + alias
|
|
+ ": key algorithm does not match");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Check issuers
|
|
if (issuerSet != null && !issuerSet.isEmpty()) {
|
|
boolean found = false;
|
|
for (Certificate cert : chain) {
|
|
X509Certificate xcert = (X509Certificate) cert;
|
|
if (issuerSet.contains(xcert.getIssuerX500Principal())) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (SSLLogger.isOn() && SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine(
|
|
"Ignore alias " + alias
|
|
+ ": issuers do not match");
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- Optional checks, depending on "checksDisabled" toggle ---
|
|
|
|
// Check the algorithm constraints
|
|
if (constraints != null &&
|
|
!conformsToAlgorithmConstraints(constraints, chain,
|
|
checkType.getValidator())) {
|
|
|
|
if (SSLLogger.isOn() && SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine("Ignore alias " + alias +
|
|
": certificate chain does not conform to " +
|
|
"algorithm constraints");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Endpoint certificate check
|
|
CheckResult checkResult = certificateCheck(checkType,
|
|
(X509Certificate) chain[0],
|
|
verificationDate == null ? new Date() : verificationDate,
|
|
requestedServerNames, idAlgorithm);
|
|
|
|
return new EntryStatus(
|
|
keyStoreIndex, keyIndex, alias, chain, checkResult);
|
|
}
|
|
|
|
// Gets algorithm constraints of the socket.
|
|
protected AlgorithmConstraints getAlgorithmConstraints(Socket socket) {
|
|
|
|
if (checksDisabled) {
|
|
return null;
|
|
}
|
|
|
|
if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) {
|
|
return SSLAlgorithmConstraints.forSocket(
|
|
sslSocket, SIGNATURE_CONSTRAINTS_MODE.PEER, true);
|
|
}
|
|
|
|
return SSLAlgorithmConstraints.DEFAULT;
|
|
}
|
|
|
|
// Gets algorithm constraints of the engine.
|
|
protected AlgorithmConstraints getAlgorithmConstraints(SSLEngine engine) {
|
|
|
|
if (checksDisabled) {
|
|
return null;
|
|
}
|
|
|
|
return SSLAlgorithmConstraints.forEngine(
|
|
engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true);
|
|
}
|
|
|
|
// Gets algorithm constraints of QUIC TLS engine.
|
|
protected AlgorithmConstraints getAlgorithmConstraints(QuicTLSEngineImpl engine) {
|
|
|
|
if (checksDisabled) {
|
|
return null;
|
|
}
|
|
|
|
return SSLAlgorithmConstraints.forQUIC(
|
|
engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true);
|
|
}
|
|
|
|
// Algorithm constraints check.
|
|
private boolean conformsToAlgorithmConstraints(
|
|
AlgorithmConstraints constraints, Certificate[] chain,
|
|
String variant) {
|
|
|
|
if (checksDisabled) {
|
|
return true;
|
|
}
|
|
|
|
AlgorithmChecker checker = new AlgorithmChecker(constraints, variant);
|
|
try {
|
|
checker.init(false);
|
|
} catch (CertPathValidatorException cpve) {
|
|
// unlikely to happen
|
|
if (SSLLogger.isOn() && SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine(
|
|
"Cannot initialize algorithm constraints checker",
|
|
cpve);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// It is a forward checker, so we need to check from trust to target.
|
|
for (int i = chain.length - 1; i >= 0; i--) {
|
|
Certificate cert = chain[i];
|
|
try {
|
|
// We don't care about the unresolved critical extensions.
|
|
checker.check(cert, Collections.emptySet());
|
|
} catch (CertPathValidatorException cpve) {
|
|
if (SSLLogger.isOn() && SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine("Certificate does not conform to " +
|
|
"algorithm constraints", cert, cpve);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Certificate check.
|
|
private CheckResult certificateCheck(
|
|
CheckType checkType, X509Certificate cert, Date date,
|
|
List<SNIServerName> serverNames, String idAlgorithm) {
|
|
return checksDisabled ? CheckResult.OK
|
|
: checkType.check(cert, date, serverNames, idAlgorithm);
|
|
}
|
|
|
|
// enum for the result of the extension check
|
|
// NOTE: the order of the constants is important as they are used
|
|
// for sorting, i.e. OK is best, followed by EXPIRED and EXTENSION_MISMATCH
|
|
enum CheckResult {
|
|
OK, // ok or not checked
|
|
INSENSITIVE, // server name indication insensitive
|
|
EXPIRED, // extensions valid but cert expired
|
|
EXTENSION_MISMATCH, // extensions invalid (expiration not checked)
|
|
}
|
|
|
|
// enum for the type of certificate check we want to perform
|
|
// (client or server)
|
|
// also includes the check code itself
|
|
enum CheckType {
|
|
|
|
// enum constant for "no check" (currently not used)
|
|
NONE(Collections.emptySet()),
|
|
|
|
// enum constant for "tls client" check
|
|
// valid EKU for TLS client: any, tls_client
|
|
CLIENT(new HashSet<>(List.of(
|
|
KnownOIDs.anyExtendedKeyUsage.value(),
|
|
KnownOIDs.clientAuth.value()
|
|
))),
|
|
|
|
// enum constant for "tls server" check
|
|
// valid EKU for TLS server: any, tls_server, ns_sgc, ms_sgc
|
|
SERVER(new HashSet<>(List.of(
|
|
KnownOIDs.anyExtendedKeyUsage.value(),
|
|
KnownOIDs.serverAuth.value(),
|
|
KnownOIDs.NETSCAPE_ExportApproved.value(),
|
|
KnownOIDs.MICROSOFT_ExportApproved.value()
|
|
)));
|
|
|
|
// set of valid EKU values for this type
|
|
final Set<String> validEku;
|
|
|
|
CheckType(Set<String> validEku) {
|
|
this.validEku = validEku;
|
|
}
|
|
|
|
private static boolean getBit(boolean[] keyUsage, int bit) {
|
|
return (bit < keyUsage.length) && keyUsage[bit];
|
|
}
|
|
|
|
// Check if this certificate is appropriate for this type of use.
|
|
// First check extensions, if they match then check expiration.
|
|
// NOTE: `conformsToAlgorithmConstraints` call above also does some
|
|
// basic keyUsage checks.
|
|
CheckResult check(X509Certificate cert, Date date,
|
|
List<SNIServerName> serverNames, String idAlgorithm) {
|
|
|
|
if (this == NONE) {
|
|
return CheckResult.OK;
|
|
}
|
|
|
|
// check extensions
|
|
try {
|
|
// check extended key usage
|
|
List<String> certEku = cert.getExtendedKeyUsage();
|
|
if ((certEku != null) &&
|
|
Collections.disjoint(validEku, certEku)) {
|
|
// if extension is present and does not contain any of
|
|
// the valid EKU OIDs, return extension_mismatch
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
|
|
// check key usage
|
|
boolean[] ku = cert.getKeyUsage();
|
|
if (ku != null) {
|
|
String algorithm = cert.getPublicKey().getAlgorithm();
|
|
boolean supportsDigitalSignature = getBit(ku, 0);
|
|
switch (algorithm) {
|
|
case "RSA":
|
|
// require either signature bit
|
|
// or if server also allow key encipherment bit
|
|
if (!supportsDigitalSignature) {
|
|
if (this == CLIENT || !getBit(ku, 2)) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
}
|
|
break;
|
|
case "RSASSA-PSS":
|
|
if (!supportsDigitalSignature && (this == SERVER)) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
break;
|
|
case "DSA":
|
|
// require signature bit
|
|
if (!supportsDigitalSignature) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
break;
|
|
case "DH":
|
|
// require key agreement bit
|
|
if (!getBit(ku, 4)) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
break;
|
|
case "EC":
|
|
// require signature bit
|
|
if (!supportsDigitalSignature) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
// For servers, also require key agreement.
|
|
// This is not totally accurate as the keyAgreement
|
|
// bit is only necessary for static ECDH key
|
|
// exchange and not ephemeral ECDH. We leave it in
|
|
// for now until there are signs that this check
|
|
// causes problems for real world EC certificates.
|
|
if (this == SERVER && !getBit(ku, 4)) {
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (CertificateException e) {
|
|
// extensions unparseable, return failure
|
|
return CheckResult.EXTENSION_MISMATCH;
|
|
}
|
|
|
|
try {
|
|
cert.checkValidity(date);
|
|
} catch (CertificateException e) {
|
|
return CheckResult.EXPIRED;
|
|
}
|
|
|
|
if (serverNames != null && !serverNames.isEmpty()) {
|
|
for (SNIServerName serverName : serverNames) {
|
|
if (serverName.getType() ==
|
|
StandardConstants.SNI_HOST_NAME) {
|
|
if (!(serverName instanceof SNIHostName)) {
|
|
try {
|
|
serverName = new SNIHostName(
|
|
serverName.getEncoded());
|
|
} catch (IllegalArgumentException iae) {
|
|
// unlikely to happen, just in case ...
|
|
if (SSLLogger.isOn() &&
|
|
SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine("Illegal server name: "
|
|
+ serverName);
|
|
}
|
|
|
|
return CheckResult.INSENSITIVE;
|
|
}
|
|
}
|
|
String hostname =
|
|
((SNIHostName) serverName).getAsciiName();
|
|
|
|
try {
|
|
X509TrustManagerImpl.checkIdentity(hostname,
|
|
cert, idAlgorithm);
|
|
} catch (CertificateException e) {
|
|
if (SSLLogger.isOn() &&
|
|
SSLLogger.isOn("keymanager")) {
|
|
SSLLogger.fine(
|
|
"Certificate identity does not match "
|
|
+ "Server Name Indication (SNI): "
|
|
+ hostname);
|
|
}
|
|
return CheckResult.INSENSITIVE;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return CheckResult.OK;
|
|
}
|
|
|
|
String getValidator() {
|
|
if (this == CLIENT) {
|
|
return Validator.VAR_TLS_CLIENT;
|
|
} else if (this == SERVER) {
|
|
return Validator.VAR_TLS_SERVER;
|
|
}
|
|
return Validator.VAR_GENERIC;
|
|
}
|
|
}
|
|
|
|
// A candidate match.
|
|
// Identifies the entry by key store index and alias
|
|
// and includes the result of the certificate check.
|
|
protected static class EntryStatus implements Comparable<EntryStatus> {
|
|
|
|
final int keyStoreIndex;
|
|
final int keyIndex;
|
|
final String alias;
|
|
final CheckResult checkResult;
|
|
|
|
EntryStatus(int keyStoreIndex, int keyIndex, String alias,
|
|
Certificate[] chain, CheckResult checkResult) {
|
|
this.keyStoreIndex = keyStoreIndex;
|
|
this.keyIndex = keyIndex;
|
|
this.alias = alias;
|
|
this.checkResult = checkResult;
|
|
}
|
|
|
|
@Override
|
|
public int compareTo(EntryStatus other) {
|
|
int result = this.checkResult.compareTo(other.checkResult);
|
|
return (result == 0) ? (this.keyIndex - other.keyIndex) : result;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
String s = alias + " (verified: " + checkResult + ")";
|
|
if (keyStoreIndex == 0) {
|
|
return s;
|
|
} else {
|
|
return "KeyStore #" + keyStoreIndex + ", alias: " + s;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Class to help verify that the public key algorithm (and optionally
|
|
// the signature algorithm) of a certificate matches what we need.
|
|
protected static class KeyType {
|
|
|
|
final String keyAlgorithm;
|
|
|
|
// In TLS 1.2, the signature algorithm has been obsoleted by the
|
|
// supported_signature_algorithms, and the certificate type no longer
|
|
// restricts the algorithm used to sign the certificate.
|
|
//
|
|
// However, because we don't support certificate type checking other
|
|
// than rsa_sign, dss_sign and ecdsa_sign, we don't have to check the
|
|
// protocol version here.
|
|
final String sigKeyAlgorithm;
|
|
|
|
KeyType(String algorithm) {
|
|
int k = algorithm.indexOf('_');
|
|
if (k == -1) {
|
|
keyAlgorithm = algorithm;
|
|
sigKeyAlgorithm = null;
|
|
} else {
|
|
keyAlgorithm = algorithm.substring(0, k);
|
|
sigKeyAlgorithm = algorithm.substring(k + 1);
|
|
}
|
|
}
|
|
|
|
boolean matches(Certificate[] chain) {
|
|
if (!chain[0].getPublicKey().getAlgorithm().equals(keyAlgorithm)) {
|
|
return false;
|
|
}
|
|
if (sigKeyAlgorithm == null) {
|
|
return true;
|
|
}
|
|
if (chain.length > 1) {
|
|
// if possible, check the public key in the issuer cert
|
|
return sigKeyAlgorithm.equals(
|
|
chain[1].getPublicKey().getAlgorithm());
|
|
} else {
|
|
// Check the signature algorithm of the certificate itself.
|
|
// Look for the "withRSA" in "SHA1withRSA", etc.
|
|
X509Certificate issuer = (X509Certificate) chain[0];
|
|
String sigAlgName =
|
|
issuer.getSigAlgName().toUpperCase(Locale.ENGLISH);
|
|
String pattern =
|
|
"WITH" + sigKeyAlgorithm.toUpperCase(Locale.ENGLISH);
|
|
return sigAlgName.endsWith(pattern);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make a list of key types.
|
|
protected static List<KeyType> getKeyTypes(String... keyTypes) {
|
|
if ((keyTypes == null) ||
|
|
(keyTypes.length == 0) || (keyTypes[0] == null)) {
|
|
return null;
|
|
}
|
|
List<KeyType> list = new ArrayList<>(keyTypes.length);
|
|
for (String keyType : keyTypes) {
|
|
list.add(new KeyType(keyType));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
// Make a set out of the array.
|
|
protected static Set<X500Principal> getIssuerSet(Principal[] issuers) {
|
|
|
|
if (issuers != null && issuers.length != 0) {
|
|
Set<X500Principal> ret = new HashSet<>(issuers.length);
|
|
|
|
for (Principal p : issuers) {
|
|
if (p instanceof X500Principal) {
|
|
ret.add((X500Principal) p);
|
|
} else {
|
|
// Normally, this will never happen but try to recover if
|
|
// it does.
|
|
try {
|
|
ret.add(new X500Principal(p.getName()));
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
return ret.isEmpty() ? null : ret;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|