diff --git a/test/lib-test/jdk/test/lib/security/CPVAlgTestWithOCSP.java b/test/lib-test/jdk/test/lib/security/CPVAlgTestWithOCSP.java new file mode 100644 index 00000000000..c1874492a3b --- /dev/null +++ b/test/lib-test/jdk/test/lib/security/CPVAlgTestWithOCSP.java @@ -0,0 +1,230 @@ +/* + * 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. + * + * 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. + */ + +/** + * @test + * @bug 8349759 + * @summary Test the CertificateBuilder and SimpleOCSPServer test utility + * classes using a range of signature algorithms and parameters. + * The goal is to test with both no-parameter and parameterized + * signature algorithms and use the CertPathValidator to validate + * the correctness of the certificate and OCSP server-side structures. + * @modules java.base/sun.security.x509 + * java.base/sun.security.provider.certpath + * java.base/sun.security.util + * @library /test/lib + * @run main/othervm CPVAlgTestWithOCSP RSA + * @run main/othervm CPVAlgTestWithOCSP RSA:3072 + * @run main/othervm CPVAlgTestWithOCSP DSA + * @run main/othervm CPVAlgTestWithOCSP DSA:3072 + * @run main/othervm CPVAlgTestWithOCSP RSASSA-PSS + * @run main/othervm CPVAlgTestWithOCSP RSASSA-PSS:3072 + * @run main/othervm CPVAlgTestWithOCSP RSASSA-PSS:4096:SHA-512:SHA3-384:128:1 + * @run main/othervm CPVAlgTestWithOCSP EC + * @run main/othervm CPVAlgTestWithOCSP EC:secp521r1 + * @run main/othervm CPVAlgTestWithOCSP Ed25519 + * @run main/othervm CPVAlgTestWithOCSP ML-DSA-65 + */ + +import java.math.BigInteger; +import java.security.*; +import java.security.cert.*; +import java.security.cert.Certificate; +import java.security.spec.*; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import jdk.test.lib.security.SimpleOCSPServer; +import jdk.test.lib.security.CertificateBuilder; + +import static java.security.cert.PKIXRevocationChecker.Option.NO_FALLBACK; + +public class CPVAlgTestWithOCSP { + + static final String passwd = "passphrase"; + static final String ROOT_ALIAS = "root"; + static final boolean[] CA_KU_FLAGS = {true, false, false, false, false, + true, true, false, false}; + static final boolean[] EE_KU_FLAGS = {true, false, false, false, false, + false, false, false, false}; + static final List EE_EKU_OIDS = List.of("1.3.6.1.5.5.7.3.1", + "1.3.6.1.5.5.7.3.2"); + + public static void main(String[] args) throws Exception { + if (args == null || args.length < 1) { + throw new RuntimeException( + "Usage: CPVAlgTestWithOCSP "); + } + String keyGenAlg = args[0]; + + // Generate Root and EE keys + KeyPairGenerator keyGen = getKpGen(keyGenAlg); + KeyPair rootCaKP = keyGen.genKeyPair(); + KeyPair eeKp = keyGen.genKeyPair(); + + // Set up the Root CA Cert + // Make a 3 year validity starting from 60 days ago + long start = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60); + long end = start + TimeUnit.DAYS.toMillis(1085); + CertificateBuilder cbld = new CertificateBuilder(); + cbld.setSubjectName("CN=Root CA Cert, O=SomeCompany"). + setPublicKey(rootCaKP.getPublic()). + setSerialNumber(new BigInteger("1")). + setValidity(new Date(start), new Date(end)). + addSubjectKeyIdExt(rootCaKP.getPublic()). + addAuthorityKeyIdExt(rootCaKP.getPublic()). + addBasicConstraintsExt(true, true, -1). + addKeyUsageExt(CA_KU_FLAGS); + + // Make our Root CA Cert! + X509Certificate rootCert = cbld.build(null, rootCaKP.getPrivate()); + log("Root CA Created:\n%s", rootCert); + + // Now build a keystore and add the keys and cert + KeyStore.Builder keyStoreBuilder = + KeyStore.Builder.newInstance("PKCS12", null, + new KeyStore.PasswordProtection("adminadmin0".toCharArray())); + KeyStore rootKeystore = keyStoreBuilder.getKeyStore(); + Certificate[] rootChain = {rootCert}; + rootKeystore.setKeyEntry(ROOT_ALIAS, rootCaKP.getPrivate(), + passwd.toCharArray(), rootChain); + + // Now fire up the OCSP responder + SimpleOCSPServer rootOcsp = new SimpleOCSPServer(rootKeystore, + passwd, ROOT_ALIAS, null); + rootOcsp.enableLog(true); + rootOcsp.setNextUpdateInterval(3600); + rootOcsp.start(); + + // Wait 60 seconds for server ready + boolean readyStatus = rootOcsp.awaitServerReady(60, TimeUnit.SECONDS); + if (!readyStatus) { + throw new RuntimeException("Server not ready"); + } + int rootOcspPort = rootOcsp.getPort(); + String rootRespURI = "http://localhost:" + rootOcspPort; + log("Root OCSP Responder URI is %s", rootRespURI); + + // Let's make an EE cert + // Make a 1 year validity starting from 60 days ago + start = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60); + end = start + TimeUnit.DAYS.toMillis(365); + cbld.reset().setSubjectName("CN=Brave Sir Robin, O=SomeCompany"). + setPublicKey(eeKp.getPublic()). + setValidity(new Date(start), new Date(end)). + addSubjectKeyIdExt(eeKp.getPublic()). + addAuthorityKeyIdExt(rootCaKP.getPublic()). + addKeyUsageExt(EE_KU_FLAGS). + addExtendedKeyUsageExt(EE_EKU_OIDS). + addSubjectAltNameDNSExt(Collections.singletonList("localhost")). + addAIAExt(Collections.singletonList(rootRespURI)); + X509Certificate eeCert = cbld.build(rootCert, rootCaKP.getPrivate()); + log("EE CA Created:\n%s", eeCert); + + // Provide end entity cert revocation info to the Root CA + // OCSP responder. + Map revInfo = + new HashMap<>(); + revInfo.put(eeCert.getSerialNumber(), + new SimpleOCSPServer.CertStatusInfo( + SimpleOCSPServer.CertStatus.CERT_STATUS_GOOD)); + rootOcsp.updateStatusDb(revInfo); + + // validate chain + CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + PKIXRevocationChecker prc = + (PKIXRevocationChecker) cpv.getRevocationChecker(); + prc.setOptions(EnumSet.of(NO_FALLBACK)); + PKIXParameters params = + new PKIXParameters(Set.of(new TrustAnchor(rootCert, null))); + params.addCertPathChecker(prc); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath cp = cf.generateCertPath(List.of(eeCert)); + cpv.validate(cp, params); + } + + private static KeyPairGenerator getKpGen(String keyGenAlg) + throws GeneralSecurityException { + String[] algComps = keyGenAlg.split(":"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(algComps[0]); + int bitLen; + + // Handle any parameters in additional tokenized fields + switch (algComps[0].toUpperCase()) { + case "EC": + // The curve name will be the second token, or secp256r1 + // if not provided. + String curveName = (algComps.length >= 2) ? algComps[1] : + "secp256r1"; + kpg.initialize(new ECGenParameterSpec(curveName)); + break; + case "RSA": + case "DSA": + // Form is RSA|DSA[:] + bitLen = (algComps.length >= 2) ? + Integer.parseInt(algComps[1]) : 2048; + kpg.initialize(bitLen); + break; + case "RSASSA-PSS": + // Form is RSASSA-PSS[:[:HASH:MGFHASH:SALTLEN:TR]] + switch (algComps.length) { + case 1: // Default key length and parameters + kpg.initialize(2048); + break; + case 2: // Specified key length, default params + kpg.initialize(Integer.parseInt(algComps[1])); + break; + default: // len > 2, key length and specified parameters + bitLen = Integer.parseInt(algComps[1]); + String hashAlg = algComps[2]; + MGF1ParameterSpec mSpec = (algComps.length >= 4) ? + new MGF1ParameterSpec(algComps[3]) : + MGF1ParameterSpec.SHA256; + int saltLen = (algComps.length >= 5) ? + Integer.parseInt(algComps[4]) : 32; + int trail = (algComps.length >= 6) ? + Integer.parseInt(algComps[5]) : + PSSParameterSpec.TRAILER_FIELD_BC; + PSSParameterSpec pSpec = new PSSParameterSpec(hashAlg, + "MGF1", mSpec, saltLen, trail); + kpg.initialize(new RSAKeyGenParameterSpec(bitLen, + RSAKeyGenParameterSpec.F4, pSpec)); + break; + } + + // Default: just use the KPG as-is, no additional init needed. + } + + return kpg; + } + + /** + * Log a message on stdout + * + * @param format the format string for the log entry + * @param args zero or more arguments corresponding to the format string + */ + private static void log(String format, Object ... args) { + System.out.format(format + "\n", args); + } +} diff --git a/test/lib/jdk/test/lib/security/CertificateBuilder.java b/test/lib/jdk/test/lib/security/CertificateBuilder.java index 7ff066bc7e0..60358c9a4ea 100644 --- a/test/lib/jdk/test/lib/security/CertificateBuilder.java +++ b/test/lib/jdk/test/lib/security/CertificateBuilder.java @@ -51,7 +51,6 @@ import sun.security.x509.DNSName; import sun.security.x509.GeneralName; import sun.security.x509.GeneralNames; import sun.security.x509.KeyUsageExtension; -import sun.security.x509.SerialNumber; import sun.security.x509.SubjectAlternativeNameExtension; import sun.security.x509.URIName; import sun.security.x509.KeyIdentifier; @@ -288,11 +287,8 @@ public class CertificateBuilder { * * @param bitSettings Boolean array for all nine bit settings in the order * documented in RFC 5280 section 4.2.1.3. - * - * @throws IOException if an encoding error occurs. */ - public CertificateBuilder addKeyUsageExt(boolean[] bitSettings) - throws IOException { + public CertificateBuilder addKeyUsageExt(boolean[] bitSettings) { return addExtension(new KeyUsageExtension(bitSettings)); } @@ -305,11 +301,9 @@ public class CertificateBuilder { * @param maxPathLen The maximum path length issued by this CA. Values * less than zero will omit this field from the resulting extension and * no path length constraint will be asserted. - * - * @throws IOException if an encoding error occurs. */ public CertificateBuilder addBasicConstraintsExt(boolean crit, boolean isCA, - int maxPathLen) throws IOException { + int maxPathLen) { return addExtension(new BasicConstraintsExtension(crit, isCA, maxPathLen)); } @@ -389,7 +383,27 @@ public class CertificateBuilder { } /** - * Build the certificate. + * Build the certificate using the default algorithm for the provided + * signing key. + * + * @param issuerCert The certificate of the issuing authority, or + * {@code null} if the resulting certificate is self-signed. + * @param issuerKey The private key of the issuing authority + * + * @return The resulting {@link X509Certificate} + * + * @throws IOException if an encoding error occurs. + * @throws CertificateException If the certificate cannot be generated + * by the underlying {@link CertificateFactory} + */ + public X509Certificate build(X509Certificate issuerCert, + PrivateKey issuerKey) throws IOException, CertificateException { + return build(issuerCert, issuerKey, + SignatureUtil.getDefaultSigAlgForKey(issuerKey)); + } + + /** + * Build the certificate using the key and specified signing algorithm. * * @param issuerCert The certificate of the issuing authority, or * {@code null} if the resulting certificate is self-signed. @@ -401,14 +415,10 @@ public class CertificateBuilder { * @throws IOException if an encoding error occurs. * @throws CertificateException If the certificate cannot be generated * by the underlying {@link CertificateFactory} - * @throws NoSuchAlgorithmException If an invalid signature algorithm - * is provided. */ public X509Certificate build(X509Certificate issuerCert, PrivateKey issuerKey, String algName) - throws IOException, CertificateException, NoSuchAlgorithmException { - // TODO: add some basic checks (key usage, basic constraints maybe) - + throws IOException, CertificateException { byte[] encodedCert = encodeTopLevel(issuerCert, issuerKey, algName); ByteArrayInputStream bais = new ByteArrayInputStream(encodedCert); return (X509Certificate)factory.generateCertificate(bais); @@ -437,15 +447,14 @@ public class CertificateBuilder { */ private byte[] encodeTopLevel(X509Certificate issuerCert, PrivateKey issuerKey, String algName) - throws CertificateException, IOException, NoSuchAlgorithmException { + throws CertificateException, IOException { - AlgorithmId signAlg = AlgorithmId.get(algName); + AlgorithmId signAlg; DerOutputStream outerSeq = new DerOutputStream(); DerOutputStream topLevelItems = new DerOutputStream(); try { - Signature sig = SignatureUtil.fromKey(signAlg.getName(), issuerKey, (Provider)null); - // Rewrite signAlg, RSASSA-PSS needs some parameters. + Signature sig = SignatureUtil.fromKey(algName, issuerKey, ""); signAlg = SignatureUtil.fromSignature(sig, issuerKey); tbsCertBytes = encodeTbsCert(issuerCert, signAlg); sig.update(tbsCertBytes); @@ -566,7 +575,6 @@ public class CertificateBuilder { */ private void encodeExtensions(DerOutputStream tbsStream) throws IOException { - if (extensions.isEmpty()) { return; } diff --git a/test/lib/jdk/test/lib/security/SimpleOCSPServer.java b/test/lib/jdk/test/lib/security/SimpleOCSPServer.java index e8ce021b138..4e25467ca80 100644 --- a/test/lib/jdk/test/lib/security/SimpleOCSPServer.java +++ b/test/lib/jdk/test/lib/security/SimpleOCSPServer.java @@ -25,6 +25,7 @@ package jdk.test.lib.security; import java.io.*; import java.net.*; +import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.CRLReason; import java.security.cert.X509Certificate; @@ -61,7 +62,7 @@ public class SimpleOCSPServer { static final int FREE_PORT = 0; // CertStatus values - public static enum CertStatus { + public enum CertStatus { CERT_STATUS_GOOD, CERT_STATUS_REVOKED, CERT_STATUS_UNKNOWN, @@ -69,14 +70,14 @@ public class SimpleOCSPServer { // Fields used for the networking portion of the responder private ServerSocket servSocket; - private InetAddress listenAddress; + private final InetAddress listenAddress; private int listenPort; // Keystore information (certs, keys, etc.) - private KeyStore keystore; - private X509Certificate issuerCert; - private X509Certificate signerCert; - private PrivateKey signerKey; + private final KeyStore keystore; + private final X509Certificate issuerCert; + private final X509Certificate signerCert; + private final PrivateKey signerKey; // Fields used for the operational portions of the server private boolean logEnabled = false; @@ -91,9 +92,9 @@ public class SimpleOCSPServer { // Fields used in the generation of responses private long nextUpdateInterval = -1; private Date nextUpdate = null; - private ResponderId respId; - private AlgorithmId sigAlgId; - private Map statusDb = + private final ResponderId respId; + private String sigAlgName; + private final Map statusDb = Collections.synchronizedMap(new HashMap<>()); /** @@ -140,25 +141,24 @@ public class SimpleOCSPServer { public SimpleOCSPServer(InetAddress addr, int port, KeyStore ks, String password, String issuerAlias, String signerAlias) throws GeneralSecurityException, IOException { - Objects.requireNonNull(ks, "Null keystore provided"); + keystore = Objects.requireNonNull(ks, "Null keystore provided"); Objects.requireNonNull(issuerAlias, "Null issuerName provided"); utcDateFmt.setTimeZone(TimeZone.getTimeZone("GMT")); - keystore = ks; - issuerCert = (X509Certificate)ks.getCertificate(issuerAlias); + issuerCert = (X509Certificate)keystore.getCertificate(issuerAlias); if (issuerCert == null) { throw new IllegalArgumentException("Certificate for alias " + issuerAlias + " not found"); } if (signerAlias != null) { - signerCert = (X509Certificate)ks.getCertificate(signerAlias); + signerCert = (X509Certificate)keystore.getCertificate(signerAlias); if (signerCert == null) { throw new IllegalArgumentException("Certificate for alias " + signerAlias + " not found"); } - signerKey = (PrivateKey)ks.getKey(signerAlias, + signerKey = (PrivateKey)keystore.getKey(signerAlias, password.toCharArray()); if (signerKey == null) { throw new IllegalArgumentException("PrivateKey for alias " + @@ -166,14 +166,14 @@ public class SimpleOCSPServer { } } else { signerCert = issuerCert; - signerKey = (PrivateKey)ks.getKey(issuerAlias, + signerKey = (PrivateKey)keystore.getKey(issuerAlias, password.toCharArray()); if (signerKey == null) { throw new IllegalArgumentException("PrivateKey for alias " + issuerAlias + " not found"); } } - sigAlgId = AlgorithmId.get(SignatureUtil.getDefaultSigAlgForKey(signerKey)); + sigAlgName = SignatureUtil.getDefaultSigAlgForKey(signerKey); respId = new ResponderId(signerCert.getSubjectX500Principal()); listenAddress = addr; listenPort = port; @@ -495,8 +495,14 @@ public class SimpleOCSPServer { public void setSignatureAlgorithm(String algName) throws NoSuchAlgorithmException { if (!started) { - sigAlgId = AlgorithmId.get(algName); - log("Signature algorithm set to " + sigAlgId.getName()); + // We don't care about the AlgorithmId object, we're just + // using it to validate the algName parameter. + AlgorithmId.get(algName); + sigAlgName = algName; + log("Signature algorithm set to " + algName); + } else { + log("Signature algorithm cannot be set on a running server, " + + "stop the server first"); } } @@ -604,9 +610,9 @@ public class SimpleOCSPServer { * object may be used to construct OCSP responses. */ public static class CertStatusInfo { - private CertStatus certStatusType; + private final CertStatus certStatusType; private CRLReason reason; - private Date revocationTime; + private final Date revocationTime; /** * Create a Certificate status object by providing the status only. @@ -745,7 +751,7 @@ public class SimpleOCSPServer { // This will be tokenized so we know if we are dealing with // a GET or POST. String[] headerTokens = readLine(in).split(" "); - LocalOcspRequest ocspReq = null; + LocalOcspRequest ocspReq; LocalOcspResponse ocspResp = null; ResponseStatus respStat = ResponseStatus.INTERNAL_ERROR; try { @@ -794,7 +800,7 @@ public class SimpleOCSPServer { out.flush(); log("Closing " + ocspSocket); - } catch (IOException | CertificateException exc) { + } catch (IOException | GeneralSecurityException exc) { err(exc); } } @@ -826,10 +832,10 @@ public class SimpleOCSPServer { append("\r\n"); } sb.append("\r\n"); - - out.write(sb.toString().getBytes("UTF-8")); - out.write(respBytes); log(resp.toString()); + + out.write(sb.toString().getBytes(StandardCharsets.UTF_8)); + out.write(respBytes); } /** @@ -940,7 +946,7 @@ public class SimpleOCSPServer { // "/" off before decoding. return new LocalOcspRequest(Base64.getMimeDecoder().decode( URLDecoder.decode(headerTokens[1].replaceAll("/", ""), - "UTF-8"))); + StandardCharsets.UTF_8))); } /** @@ -974,8 +980,7 @@ public class SimpleOCSPServer { bos.write(b); } } - - return new String(bos.toByteArray(), "UTF-8"); + return bos.toString(StandardCharsets.UTF_8); } } @@ -1052,7 +1057,6 @@ public class SimpleOCSPServer { if (sigItems[2].isContextSpecific((byte)0)) { DerValue[] certDerItems = sigItems[2].data.getSequence(4); - int i = 0; for (DerValue dv : certDerItems) { X509Certificate xc = new X509CertImpl(dv); certificates.add(xc); @@ -1131,7 +1135,7 @@ public class SimpleOCSPServer { * Return the list of X.509 Certificates in this OCSP request. * * @return an unmodifiable {@code List} of zero or more - * {@cpde X509Certificate} objects. + * {@code X509Certificate} objects. */ private List getCertificates() { return Collections.unmodifiableList(certificates); @@ -1295,7 +1299,8 @@ public class SimpleOCSPServer { private final Map responseExtensions; private byte[] signature; private final List certificates; - private final byte[] encodedResponse; + private final Signature signEngine; + private final AlgorithmId sigAlgId; /** * Constructor for the generation of non-successful responses @@ -1305,9 +1310,11 @@ public class SimpleOCSPServer { * @throws IOException if an error happens during encoding * @throws NullPointerException if {@code respStat} is {@code null} * or {@code respStat} is successful. + * @throws GeneralSecurityException if errors occur while obtaining + * the signature object or any algorithm identifier parameters. */ public LocalOcspResponse(OCSPResponse.ResponseStatus respStat) - throws IOException { + throws IOException, GeneralSecurityException { this(respStat, null, null); } @@ -1324,10 +1331,13 @@ public class SimpleOCSPServer { * @throws NullPointerException if {@code respStat} is {@code null} * or {@code respStat} is successful, and a {@code null} {@code itemMap} * has been provided. + * @throws GeneralSecurityException if errors occur while obtaining + * the signature object or any algorithm identifier parameters. */ public LocalOcspResponse(OCSPResponse.ResponseStatus respStat, Map itemMap, - Map reqExtensions) throws IOException { + Map reqExtensions) + throws IOException, GeneralSecurityException { responseStatus = Objects.requireNonNull(respStat, "Illegal null response status"); if (responseStatus == ResponseStatus.SUCCESSFUL) { @@ -1348,13 +1358,18 @@ public class SimpleOCSPServer { certificates.add(signerCert); } certificates.add(issuerCert); + // Create the signature object and AlgorithmId that we'll use + // later to create the signature on this response. + signEngine = SignatureUtil.fromKey(sigAlgName, signerKey, ""); + sigAlgId = SignatureUtil.fromSignature(signEngine, signerKey); } else { respItemMap = null; producedAtDate = null; responseExtensions = null; certificates = null; + signEngine = null; + sigAlgId = null; } - encodedResponse = this.getBytes(); } /** @@ -1436,13 +1451,9 @@ public class SimpleOCSPServer { basicORItemStream.write(tbsResponseBytes); try { - // Create the signature - Signature sig = SignatureUtil.fromKey( - sigAlgId.getName(), signerKey, (Provider)null); - sig.update(tbsResponseBytes); - signature = sig.sign(); - // Rewrite signAlg, RSASSA-PSS needs some parameters. - sigAlgId = SignatureUtil.fromSignature(sig, signerKey); + // Create the signature with the initialized Signature object + signEngine.update(tbsResponseBytes); + signature = signEngine.sign(); sigAlgId.encode(basicORItemStream); basicORItemStream.putBitString(signature); } catch (GeneralSecurityException exc) {