001/*
002 *   Licensed to the Apache Software Foundation (ASF) under one
003 *   or more contributor license agreements.  See the NOTICE file
004 *   distributed with this work for additional information
005 *   regarding copyright ownership.  The ASF licenses this file
006 *   to you under the Apache License, Version 2.0 (the
007 *   "License"); you may not use this file except in compliance
008 *   with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *   Unless required by applicable law or agreed to in writing,
013 *   software distributed under the License is distributed on an
014 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *   KIND, either express or implied.  See the License for the
016 *   specific language governing permissions and limitations
017 *   under the License.
018 *
019 */
020package org.apache.directory.server.core.security;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.math.BigInteger;
028import java.net.InetAddress;
029import java.nio.file.Files;
030import java.nio.file.Paths;
031import java.security.InvalidKeyException;
032import java.security.KeyPair;
033import java.security.KeyPairGenerator;
034import java.security.KeyStore;
035import java.security.KeyStoreException;
036import java.security.NoSuchAlgorithmException;
037import java.security.NoSuchProviderException;
038import java.security.SecureRandom;
039import java.security.Security;
040import java.security.SignatureException;
041import java.security.cert.CertificateException;
042import java.security.cert.X509Certificate;
043import java.util.Date;
044import java.util.Enumeration;
045
046import javax.net.ssl.KeyManagerFactory;
047
048import org.apache.directory.api.util.Strings;
049
050import sun.security.x509.AlgorithmId;
051import sun.security.x509.BasicConstraintsExtension;
052import sun.security.x509.CertificateAlgorithmId;
053import sun.security.x509.CertificateExtensions;
054import sun.security.x509.CertificateSerialNumber;
055import sun.security.x509.CertificateValidity;
056import sun.security.x509.CertificateVersion;
057import sun.security.x509.CertificateX509Key;
058import sun.security.x509.DNSName;
059import sun.security.x509.GeneralName;
060import sun.security.x509.GeneralNames;
061import sun.security.x509.IPAddressName;
062import sun.security.x509.SubjectAlternativeNameExtension;
063import sun.security.x509.X500Name;
064import sun.security.x509.X509CertImpl;
065import sun.security.x509.X509CertInfo;
066
067/**
068 * Helper class used to generate self-signed certificates, and load a KeyStore
069 * 
070 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
071 */
072@SuppressWarnings("restriction")
073public final class CertificateUtil
074{
075    private static final boolean SELF_SIGNED = true;
076    private static final boolean CA_SIGNED = false;
077    private static final boolean CRITICAL = true;
078        
079    private CertificateUtil()
080    {
081        // Nothing to do
082    }
083    
084    
085    private static void setInfo( X509CertInfo info, X500Name subject, X500Name issuer, KeyPair keyPair, int days, 
086        String algoStr, boolean isCA ) 
087        throws CertificateException, IOException, NoSuchAlgorithmException
088    {
089        Date from = new Date();
090        Date to = new Date( from.getTime() + days * 86_400_000L );
091        CertificateValidity interval = new CertificateValidity( from, to );
092
093        // Feed the certificate info structure
094        // version         [0]  EXPLICIT Version DEFAULT v1
095        // Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }
096        info.set( X509CertInfo.VERSION, new CertificateVersion( CertificateVersion.V3 ) );
097        
098        // serialNumber         CertificateSerialNumber
099        // CertificateSerialNumber  ::=  INTEGER
100        BigInteger serialNumber = new BigInteger( 64, new SecureRandom() );
101        info.set( X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber( serialNumber ) );
102
103        // signature            AlgorithmIdentifier
104        AlgorithmId algo = AlgorithmId.get( algoStr );
105        info.set( X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId( algo ) );
106
107        // issuer               Name
108        // Name ::= CHOICE {
109        //          RDNSequence }
110        // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
111        // RelativeDistinguishedName ::=
112        //          SET OF AttributeTypeAndValue
113        // AttributeTypeAndValue ::= SEQUENCE {
114        //          type     AttributeType,
115        //          value    AttributeValue }
116        // AttributeType ::= OBJECT IDENTIFIER
117        // AttributeValue ::= ANY DEFINED BY AttributeType
118        info.set( X509CertInfo.ISSUER, issuer );
119        
120        // validity             Validity,
121        // Validity ::= SEQUENCE {
122        //          notBefore      Time,
123        //          notAfter       Time }
124        info.set( X509CertInfo.VALIDITY, interval );
125        
126        // subject              Name
127        // Name ::= CHOICE {
128        //          RDNSequence }
129        // RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
130        // RelativeDistinguishedName ::=
131        //          SET OF AttributeTypeAndValue
132        // AttributeTypeAndValue ::= SEQUENCE {
133        //          type     AttributeType,
134        //          value    AttributeValue }
135        // AttributeType ::= OBJECT IDENTIFIER
136        // AttributeValue ::= ANY DEFINED BY AttributeType
137        info.set( X509CertInfo.SUBJECT, subject );
138        
139        // subjectPublicKeyInfo SubjectPublicKeyInfo,
140        // SubjectPublicKeyInfo  ::=  SEQUENCE  {
141        //          algorithm            AlgorithmIdentifier,
142        //          subjectPublicKey     BIT STRING  }
143        info.set( X509CertInfo.KEY, new CertificateX509Key( keyPair.getPublic() ) );
144
145        // Extensions. Basically, a subjectAltName and a Basic-Constraint 
146        CertificateExtensions extensions = new CertificateExtensions();
147
148        // SubjectAltName
149        GeneralNames names = new GeneralNames();
150        names.add( new GeneralName( new DNSName( InetAddress.getLocalHost().getHostName() ) ) );
151        String ipAddress = InetAddress.getLocalHost().getHostAddress();
152        names.add( new GeneralName( new IPAddressName( ipAddress ) ) );
153        
154        // A wildcard
155        //names.add( new GeneralName( 
156        //    new DNSName( 
157        //        new DerValue( 
158        //            DerValue.tag_IA5String, "*.apache.org" ) ) ) );
159        SubjectAlternativeNameExtension subjectAltName = new SubjectAlternativeNameExtension( names );
160        
161        extensions.set( subjectAltName.getExtensionId().toString(), subjectAltName );
162
163        // The Basic-Constraint,
164        BasicConstraintsExtension basicConstraint = new BasicConstraintsExtension( CRITICAL, isCA, -1 );
165        extensions.set( basicConstraint.getExtensionId().toString(), basicConstraint );
166
167        // Inject the extensions into the cert
168        info.set( X509CertInfo.EXTENSIONS, extensions );
169    }
170    
171    
172    /**
173     * Create a self signed certificate
174     * 
175     * @param issuer The Issuer (which is the same as the subject
176     * @param keyPair The asymmetric keyPair
177     * @param days Validity number of days
178     * @param algoStr Algorithm
179     * @return A self signed CA certificate
180     * @throws CertificateException If the info store din the certificate is invalid
181     * @throws IOException If we can't store some info in the certificate
182     * @throws NoSuchAlgorithmException If the algorithm does not exist
183     * @throws SignatureException If the certificate cannot be signed
184     * @throws NoSuchProviderException  If we don't have a security provider
185     * @throws InvalidKeyException  If the KeyPair is invalid
186     */
187    public static X509Certificate generateSelfSignedCertificate( X500Name issuer, KeyPair keyPair,  int days, String algoStr ) 
188        throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
189    {
190        // Create the certificate info
191        X509CertInfo info = new X509CertInfo();
192        
193        // Set the common certificate info
194        setInfo( info, issuer, issuer, keyPair, days, algoStr, SELF_SIGNED );
195        
196        // Sign the cert to identify the algorithm that's used.
197        X509CertImpl certificate = new X509CertImpl( info );
198        certificate.sign( keyPair.getPrivate(), algoStr );
199
200        return certificate;
201    }
202    
203    
204    /**
205     * Generate a Certificate signed by a CA certificate
206     * 
207     * @param issuer The Issuer (which is the same as the subject
208     * @param keyPair The asymmetric keyPair
209     * @param days Validity number of days
210     * @param algoStr Algorithm
211     * @return A self signed CA certificate
212     * @throws CertificateException If the info store din the certificate is invalid
213     * @throws IOException If we can't store some info in the certificate
214     * @throws NoSuchAlgorithmException If the algorithm does not exist
215     * @throws SignatureException If the certificate cannot be signed
216     * @throws NoSuchProviderException  If we don't have a security provider
217     * @throws InvalidKeyException  If the KeyPair is invalid
218     */
219    public static X509Certificate generateCertificate( X500Name subject, X500Name issuer, KeyPair keyPair,  int days, String algoStr ) 
220        throws CertificateException, IOException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
221    {
222        // Create the certificate info
223        X509CertInfo info = new X509CertInfo();
224        
225        // Set the common certificate info
226        setInfo( info, subject, issuer, keyPair, days, algoStr, CA_SIGNED );
227         
228        // Sign the cert to identify the algorithm that's used.
229        X509CertImpl certificate = new X509CertImpl( info );
230        certificate.sign( keyPair.getPrivate(), algoStr );
231
232        return certificate;
233    }
234    
235    
236    /**
237     * Loads the digital certificate from a keystore file
238     *
239     * @param keyStoreFile The KeyStore file to load
240     * @param keyStorePasswordStr The KeyStore password
241     * @return The KeyManager factory it created 
242     * @throws Exception If the KeyStore can't be loaded
243     */
244    public static KeyManagerFactory loadKeyStore( String keyStoreFile, String keyStorePasswordStr ) throws Exception
245    {
246        char[] keyStorePassword = Strings.isEmpty( keyStorePasswordStr ) ? null : keyStorePasswordStr.toCharArray();
247
248        if ( !Strings.isEmpty( keyStoreFile ) )
249        {
250            // We have a provided KeyStore file: read it
251            KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
252
253            try ( InputStream is = Files.newInputStream( Paths.get( keyStoreFile ) ) )
254            {
255                keyStore.load( is, keyStorePassword );
256            }
257    
258            /*
259             * Verify key store:
260             * * Must only contain one entry which must be a key entry
261             * * Must contain a certificate chain
262             * * The private key must be recoverable by the key store password
263             */
264            Enumeration<String> aliases = keyStore.aliases();
265            
266            if ( !aliases.hasMoreElements() )
267            {
268                throw new KeyStoreException( "Key store is empty" );
269            }
270            
271            String alias = aliases.nextElement();
272            
273            if ( aliases.hasMoreElements() )
274            {
275                throw new KeyStoreException( "Key store contains more than one entry" );
276            }
277            
278            if ( !keyStore.isKeyEntry( alias ) )
279            {
280                throw new KeyStoreException( "Key store must contain a key entry" );
281            }
282            
283            if ( keyStore.getCertificateChain( alias ) == null )
284            {
285                throw new KeyStoreException( "Key store must contain a certificate chain" );
286            }
287            
288            if ( keyStore.getKey( alias, keyStorePassword ) == null )
289            {
290                throw new KeyStoreException( "Private key must be recoverable by the key store password" );
291            }
292    
293            // Set up key manager factory to use our key store
294            String algorithm = Security.getProperty( "ssl.KeyManagerFactory.algorithm" );
295    
296            if ( algorithm == null )
297            {
298                algorithm = KeyManagerFactory.getDefaultAlgorithm();
299            }
300    
301            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( algorithm );
302    
303            keyManagerFactory.init( keyStore, keyStorePassword );
304            
305            return keyManagerFactory;
306        }
307        else
308        {
309            return null;
310        }
311    }
312    
313    
314    public static File createTempKeyStore( String keyStoreName, char[] keyStorePassword ) throws IOException, KeyStoreException,
315        NoSuchAlgorithmException, CertificateException, InvalidKeyException, NoSuchProviderException, SignatureException
316    {
317        // Create a temporary keystore, be sure to remove it when exiting the test
318        File keyStoreFile = Files.createTempFile( keyStoreName, "ks" ).toFile();
319        keyStoreFile.deleteOnExit();
320        
321        KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
322        
323        try ( InputStream keyStoreData = new FileInputStream( keyStoreFile ) )
324        {
325            keyStore.load( null, keyStorePassword );
326        }
327
328        // Generate the asymmetric keys, using EC algorithm
329        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "EC" );
330        KeyPair keyPair = keyPairGenerator.generateKeyPair();
331        
332        // Generate the subject's name
333        @SuppressWarnings("restriction")
334        X500Name owner = new X500Name( "apacheds", "directory", "apache", "US" );
335
336        // Create the self-signed certificate
337        X509Certificate certificate = CertificateUtil.generateSelfSignedCertificate( owner, keyPair, 365, "SHA256WithECDSA" );
338        
339        keyStore.setKeyEntry( "apachedsKey", keyPair.getPrivate(), keyStorePassword, new X509Certificate[] { certificate } );
340        
341        try ( FileOutputStream out = new FileOutputStream( keyStoreFile ) )
342        {
343            keyStore.store( out, keyStorePassword );
344        }
345        
346        return keyStoreFile;
347    }
348}