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

Valery Kharseko
3 days ago 06dad488671eb71d730cfc481a96edf424e95df5
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/*
 * 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.quicksetup.installer;
 
import static org.opends.messages.QuickSetupMessages.*;
import static org.opends.quicksetup.util.Utils.createProtectedFile;
import static org.opends.server.config.ConfigConstants.ADS_CERTIFICATE_ALIAS;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
 
import org.opends.quicksetup.ApplicationException;
import org.opends.quicksetup.ReturnCode;
import org.opends.server.util.CertificateManager;
import org.opends.server.util.SetupUtils;
 
/**
 * Provisions the trust store used for server to server communication, {@code
 * ads-truststore}, from a key store the operator already holds.
 * <p>
 * Replication reads both the key pair it presents on the replication port and the
 * certificates it trusts there from that file, and from nowhere else: neither the key
 * store configured for LDAPS nor the one of the administration connector is consulted.
 * Without this, a server installs with the self-signed {@code ads-certificate} the trust
 * store backend generates on the first start, and securing replication with an
 * organisation's own certificates means stopping every server afterwards and repeating a
 * {@code keytool} procedure by hand on each of them.
 * <p>
 * Two properties of the trust store shape what this class does:
 * <ul>
 * <li>Only a trusted certificate entry is a trust anchor. The certificate chain of a key
 * entry is not: the trust managers take the certificate the key belongs to and none of
 * its issuers. The issuing certificates are therefore imported as trusted certificate
 * entries of their own, and a key pair which comes without any, and without certificates
 * named separately, is reported rather than left to fail as a handshake later on.</li>
 * <li>The {@code ads-certificate} key pair is not provisioned. It is the crypto manager
 * instance key, published to the topology under {@code cn=instance keys,cn=admin data}
 * and read by its alias, and the trust store backend generates it on the first start when
 * the alias is free.</li>
 * </ul>
 */
final class AdsTrustStoreProvisioner
{
  /** The prefix of the aliases the trusted certificates are imported under. */
  private static final String CA_ALIAS_PREFIX = "ads-ca-";
 
  private final String trustStorePath;
  private final String pinFilePath;
 
  /**
   * Creates a provisioner for the provided trust store.
   *
   * @param trustStorePath
   *          The path of the trust store file to create.
   * @param pinFilePath
   *          The path of the file to write the generated PIN of the trust store to.
   */
  AdsTrustStoreProvisioner(String trustStorePath, String pinFilePath)
  {
    this.trustStorePath = trustStorePath;
    this.pinFilePath = pinFilePath;
  }
 
  /**
   * Creates the trust store, holding the provided key pairs and the certificates to trust
   * on the replication port, and writes its PIN file.
   * <p>
   * Everything is read and checked before anything is written: a refusal leaves no file
   * behind, and a failure while writing removes the partial trust store.
   *
   * @param source
   *          The key store holding the key pairs to import.
   * @param aliases
   *          The aliases of the key pairs to import, as the key store spells them, which
   *          become the certificate nicknames the crypto manager presents.
   * @param caCertificateFiles
   *          The files holding certificates to trust, on top of the issuers found in the
   *          certificate chains of the imported key pairs. Every certificate of a file is
   *          trusted. May be empty.
   * @throws ApplicationException
   *           If the key pairs cannot be read, if an alias is one the trust store reserves,
   *           if the trust store would end up trusting no certificate at all, if it
   *           already exists, or if it cannot be written.
   */
  void provision(CertificateManager source, Collection<String> aliases, Collection<File> caCertificateFiles)
      throws ApplicationException
  {
    final List<Certificate> trusted;
    try
    {
      checkReservedAliases(aliases);
      trusted = issuersOf(source, aliases);
      for (File caCertificateFile : caCertificateFiles)
      {
        for (Certificate certificate : certificatesOf(caCertificateFile))
        {
          if (!trusted.contains(certificate))
          {
            trusted.add(certificate);
          }
        }
      }
    }
    catch (ApplicationException e)
    {
      throw e;
    }
    catch (Exception e)
    {
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE.get(trustStorePath, String.valueOf(e)), e);
    }
    if (trusted.isEmpty())
    {
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE_NO_TRUST_ANCHOR.get(
              source.getKeyStorePath(), joinAliases(aliases)), null);
    }
    if (new File(trustStorePath).exists())
    {
      // Only what this run writes is removed on failure, so a store which is already
      // there is left alone rather than overwritten or deleted.
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE_EXISTS.get(trustStorePath), null);
    }
 
    try
    {
      // The trust store is a JKS whatever the type of the key store the key pairs come
      // from: ds-cfg-trust-store-type of the ads-truststore backend says JKS.
      final String pin = new String(SetupUtils.createSelfSignedCertificatePwd());
      final CertificateManager trustStore =
          new CertificateManager(trustStorePath, CertificateManager.KEY_STORE_TYPE_JKS, pin);
      for (String alias : aliases)
      {
        trustStore.importKeyEntry(alias, source, alias);
      }
      int trustedCertificates = 0;
      for (Certificate certificate : trusted)
      {
        trustStore.addTrustedCertificate(CA_ALIAS_PREFIX + ++trustedCertificates, certificate);
      }
      createProtectedFile(pinFilePath, pin);
    }
    catch (Throwable t)
    {
      deletePartialTrustStore();
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE.get(trustStorePath, String.valueOf(t)), t);
    }
  }
 
  /**
   * Refuses the aliases the trust store keeps for itself: {@code ads-certificate} is the
   * instance key the server generates on its first start, which it would skip if the alias
   * were taken, and {@code ads-ca-N} are the certificates trusted here.
   */
  private void checkReservedAliases(Collection<String> aliases) throws ApplicationException
  {
    for (String alias : aliases)
    {
      if (alias.equalsIgnoreCase(ADS_CERTIFICATE_ALIAS)
          || alias.toLowerCase(Locale.ENGLISH).startsWith(CA_ALIAS_PREFIX))
      {
        throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
            ERR_INSTALL_ADS_TRUSTSTORE_RESERVED_ALIAS.get(alias, ADS_CERTIFICATE_ALIAS, CA_ALIAS_PREFIX), null);
      }
    }
  }
 
  /**
   * Returns the issuers of the certificate chains of the provided key pairs, one entry
   * per distinct certificate: the key pairs of one server are usually issued by the same
   * authority, which is then to be trusted once rather than under one alias each.
   */
  private List<Certificate> issuersOf(CertificateManager source, Collection<String> aliases) throws Exception
  {
    final List<Certificate> issuers = new ArrayList<>();
    for (String alias : aliases)
    {
      final Certificate[] chain = source.getCertificateChain(alias);
      if (chain == null || chain.length == 0)
      {
        throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
            ERR_INSTALL_ADS_TRUSTSTORE_NO_KEY_PAIR.get(source.getKeyStorePath(), alias), null);
      }
      for (int i = 1; i < chain.length; i++)
      {
        if (!issuers.contains(chain[i]))
        {
          issuers.add(chain[i]);
        }
      }
    }
    return issuers;
  }
 
  /**
   * Returns every certificate held in the provided file, which may be a single DER or PEM
   * certificate, a PEM bundle or a PKCS#7 chain: a file which holds a chain of authorities
   * is trusted whole, rather than up to its first certificate only.
   */
  private Collection<? extends Certificate> certificatesOf(File caCertificateFile) throws Exception
  {
    final Collection<? extends Certificate> certificates;
    try (InputStream in = new FileInputStream(caCertificateFile))
    {
      certificates = CertificateFactory.getInstance("X.509").generateCertificates(in);
    }
    catch (CertificateException e)
    {
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_UNREADABLE.get(caCertificateFile.getPath(), e.getMessage()), e);
    }
    if (certificates.isEmpty())
    {
      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR,
          ERR_INSTALL_ADS_TRUSTSTORE_CA_CERT_FILE_EMPTY.get(caCertificateFile.getPath()), null);
    }
    return certificates;
  }
 
  private void deletePartialTrustStore()
  {
    new File(trustStorePath).delete();
    new File(pinFilePath).delete();
  }
 
  private String joinAliases(Collection<String> aliases)
  {
    return String.join(", ", aliases);
  }
}