8349759: Add unit test for CertificateBuilder and SimpleOCSPServer test utilities

Reviewed-by: mullan
This commit is contained in:
Jamil Nimeh 2025-02-21 20:30:02 +00:00
parent b45c32cd4f
commit 9d9d7a17d3
3 changed files with 309 additions and 60 deletions

View File

@ -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<String> 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 <IssKeyAlg>");
}
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<BigInteger, SimpleOCSPServer.CertStatusInfo> 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[:<KeyBitLen>]
bitLen = (algComps.length >= 2) ?
Integer.parseInt(algComps[1]) : 2048;
kpg.initialize(bitLen);
break;
case "RSASSA-PSS":
// Form is RSASSA-PSS[:<KeyBitLen>[: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);
}
}

View File

@ -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;
}

View File

@ -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<CertId, CertStatusInfo> statusDb =
private final ResponderId respId;
private String sigAlgName;
private final Map<CertId, CertStatusInfo> 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<X509Certificate> getCertificates() {
return Collections.unmodifiableList(certificates);
@ -1295,7 +1299,8 @@ public class SimpleOCSPServer {
private final Map<String, Extension> responseExtensions;
private byte[] signature;
private final List<X509Certificate> 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<CertId, CertStatusInfo> itemMap,
Map<String, Extension> reqExtensions) throws IOException {
Map<String, Extension> 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) {