mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
yesterday 70d9a179cdd8d975b44e1815c249e20d9f91097f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.util;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
 
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
 
/**
 * A miniature Certificate Authority for tests which need a CA-signed key pair rather
 * than a self-signed one: the key stores {@code setup} is given by an operator who runs
 * their own PKI.
 * <p>
 * The class name deliberately avoids the {@code Test} prefix and the {@code Test},
 * {@code TestCase} suffixes: those are the patterns the failsafe configuration picks up
 * as test classes, and every test class is required to extend {@code DirectoryServerTestCase}.
 */
public final class CertificateFixture
{
  private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
  private static final int VALIDITY_DAYS = 365;
 
  private final KeyPair caKeyPair;
  private final X509Certificate caCertificate;
  private final String caSubject;
 
  /**
   * Creates a certificate authority whose certificate is self-signed, as a root CA is.
   *
   * @param caSubject
   *          The subject DN of the CA certificate, for instance {@code "CN=Example CA"}.
   * @throws Exception
   *           If the CA key pair or certificate cannot be generated.
   */
  public CertificateFixture(String caSubject) throws Exception
  {
    this.caSubject = caSubject;
    this.caKeyPair = newKeyPair();
    this.caCertificate = sign(caSubject, caKeyPair.getPublic(), caSubject, caKeyPair, true);
  }
 
  /**
   * Returns the certificate of this authority, the one which has to be trusted for the
   * certificates it issues to be accepted.
   *
   * @return The CA certificate.
   */
  public X509Certificate getCaCertificate()
  {
    return caCertificate;
  }
 
  /**
   * Writes the CA certificate to the provided file, DER encoded, as {@code keytool
   * -exportcert} does.
   *
   * @param file
   *          The file to write the certificate to.
   * @throws Exception
   *           If the file cannot be written.
   */
  public void writeCaCertificate(File file) throws Exception
  {
    try (final OutputStream out = new FileOutputStream(file))
    {
      out.write(caCertificate.getEncoded());
    }
  }
 
  /**
   * Writes certificates to the provided file, PEM encoded one after the other, as a CA
   * publishes the chain of its authorities in one {@code ca-chain.crt} file.
   *
   * @param file
   *          The file to write the certificates to.
   * @param certificates
   *          The certificates to write, in order.
   * @throws Exception
   *           If the file cannot be written.
   */
  public static void writeCertificates(File file, Certificate... certificates) throws Exception
  {
    final Base64.Encoder encoder = Base64.getMimeEncoder(64, new byte[] { '\n' });
    try (final Writer out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.US_ASCII))
    {
      for (Certificate certificate : certificates)
      {
        out.write("-----BEGIN CERTIFICATE-----\n");
        out.write(encoder.encodeToString(certificate.getEncoded()));
        out.write("\n-----END CERTIFICATE-----\n");
      }
    }
  }
 
  /**
   * Adds one key pair signed by this authority to a key store, creating the key store if
   * it does not exist yet.
   *
   * @param file
   *          The key store file to create or extend.
   * @param storeType
   *          The key store type, for instance {@code "PKCS12"} or {@code "JKS"}.
   * @param password
   *          The password protecting both the store and the private key, as the key
   *          managers of the server require them to be identical.
   * @param alias
   *          The alias to store the key pair under.
   * @param subject
   *          The subject DN of the issued certificate.
   * @param withChain
   *          {@code true} to store the CA certificate along with the issued certificate,
   *          as a properly built key store does, {@code false} to store the issued
   *          certificate on its own.
   * @throws Exception
   *           If the key store cannot be written.
   */
  public void addKeyEntry(File file, String storeType, String password, String alias, String subject,
      boolean withChain) throws Exception
  {
    addKeyEntry(file, storeType, password, password, alias, subject, withChain);
  }
 
  /**
   * Adds one key pair signed by this authority to a key store, with a private key
   * protected by a password of its own: what {@code keytool -genkeypair -keypass} leaves,
   * and what the key managers of the server cannot unlock.
   *
   * @param file
   *          The key store file to create or extend.
   * @param storeType
   *          The key store type, for instance {@code "PKCS12"} or {@code "JKS"}.
   * @param storePassword
   *          The password protecting the store.
   * @param keyPassword
   *          The password protecting the private key.
   * @param alias
   *          The alias to store the key pair under.
   * @param subject
   *          The subject DN of the issued certificate.
   * @param withChain
   *          {@code true} to store the CA certificate along with the issued certificate,
   *          {@code false} to store the issued certificate on its own.
   * @throws Exception
   *           If the key store cannot be written.
   */
  public void addKeyEntry(File file, String storeType, String storePassword, String keyPassword, String alias,
      String subject, boolean withChain) throws Exception
  {
    final KeyPair keyPair = newKeyPair();
    final X509Certificate certificate = sign(subject, keyPair.getPublic(), caSubject, caKeyPair, false);
    final Certificate[] chain = withChain
        ? new Certificate[] { certificate, caCertificate }
        : new Certificate[] { certificate };
 
    final KeyStore keyStore = KeyStore.getInstance(storeType);
    if (file.exists())
    {
      try (final InputStream in = new FileInputStream(file))
      {
        keyStore.load(in, storePassword.toCharArray());
      }
    }
    else
    {
      keyStore.load(null, storePassword.toCharArray());
    }
    keyStore.setKeyEntry(alias, keyPair.getPrivate(), keyPassword.toCharArray(), chain);
    try (final OutputStream out = new FileOutputStream(file))
    {
      keyStore.store(out, storePassword.toCharArray());
    }
  }
 
  private static KeyPair newKeyPair() throws Exception
  {
    final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(2048);
    return generator.generateKeyPair();
  }
 
  private static X509Certificate sign(String subject, java.security.PublicKey subjectKey, String issuer,
      KeyPair issuerKeyPair, boolean isCa) throws Exception
  {
    final Instant now = Instant.now();
    final JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
        new X500Name(issuer),
        new BigInteger(64, new SecureRandom()),
        Date.from(now.minus(1, ChronoUnit.DAYS)),
        Date.from(now.plus(VALIDITY_DAYS, ChronoUnit.DAYS)),
        new X500Name(subject),
        subjectKey);
    builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCa));
 
    final ContentSigner signer =
        new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).build(issuerKeyPair.getPrivate());
    return new JcaX509CertificateConverter().getCertificate(builder.build(signer));
  }
}