001/*
002 * (C) Copyright 2011-2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Wojciech Sulejman
016 *     Florent Guillaume
017 */
018package org.nuxeo.ecm.platform.signature.core.pki;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.math.BigInteger;
024import java.security.InvalidKeyException;
025import java.security.KeyPair;
026import java.security.KeyPairGenerator;
027import java.security.KeyStore;
028import java.security.KeyStoreException;
029import java.security.NoSuchAlgorithmException;
030import java.security.NoSuchProviderException;
031import java.security.PrivateKey;
032import java.security.PublicKey;
033import java.security.Security;
034import java.security.UnrecoverableKeyException;
035import java.security.cert.CertificateEncodingException;
036import java.security.cert.CertificateException;
037import java.security.cert.CertificateParsingException;
038import java.security.cert.X509Certificate;
039import java.util.Calendar;
040import java.util.Date;
041import java.util.Enumeration;
042import java.util.Vector;
043
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.bouncycastle.asn1.ASN1Set;
047import org.bouncycastle.asn1.DERObjectIdentifier;
048import org.bouncycastle.asn1.DEROctetString;
049import org.bouncycastle.asn1.DERSet;
050import org.bouncycastle.asn1.pkcs.Attribute;
051import org.bouncycastle.asn1.pkcs.CertificationRequest;
052import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
053import org.bouncycastle.asn1.x509.BasicConstraints;
054import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
055import org.bouncycastle.asn1.x509.GeneralName;
056import org.bouncycastle.asn1.x509.GeneralNames;
057import org.bouncycastle.asn1.x509.KeyPurposeId;
058import org.bouncycastle.asn1.x509.KeyUsage;
059import org.bouncycastle.asn1.x509.X509Extension;
060import org.bouncycastle.asn1.x509.X509Extensions;
061import org.bouncycastle.jce.PKCS10CertificationRequest;
062import org.bouncycastle.jce.provider.BouncyCastleProvider;
063import org.bouncycastle.x509.X509V3CertificateGenerator;
064import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;
065import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure;
066import org.nuxeo.ecm.platform.signature.api.exception.CertException;
067import org.nuxeo.ecm.platform.signature.api.pki.CertService;
068import org.nuxeo.ecm.platform.signature.api.pki.RootService;
069import org.nuxeo.ecm.platform.signature.api.user.AliasType;
070import org.nuxeo.ecm.platform.signature.api.user.AliasWrapper;
071import org.nuxeo.ecm.platform.signature.api.user.CNField;
072import org.nuxeo.ecm.platform.signature.api.user.UserInfo;
073import org.nuxeo.runtime.api.Framework;
074import org.nuxeo.runtime.model.DefaultComponent;
075
076/**
077 * Base implementation of the certification service.
078 */
079public class CertServiceImpl extends DefaultComponent implements CertService {
080
081    protected RootService rootService;
082
083    private static final Log LOG = LogFactory.getLog(CertServiceImpl.class);
084
085    @Override
086    public void setRootService(RootService rootService) {
087        this.rootService = rootService;
088    }
089
090    protected X509Certificate rootCertificate;
091
092    private static final int CERTIFICATE_DURATION_IN_MONTHS = 12;
093
094    private static final String CERT_SIGNATURE_ALGORITHM = "SHA256WithRSAEncryption";
095
096    private static final String KEY_ALGORITHM = "RSA";
097
098    private static final int KEY_SIZE = 1024;
099
100    private static final String KEYSTORE_TYPE = "JKS";
101
102    static {
103        if (Security.getProvider("BC") == null) {
104            Security.addProvider(new BouncyCastleProvider());
105        }
106    }
107
108    protected X509Certificate createCertificateFromCSR(PKCS10CertificationRequest csr) throws CertException {
109        X509Certificate cert;
110        try {
111            X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
112            certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
113            certGen.setIssuerDN(getRootCertificate().getIssuerX500Principal());
114            certGen.setSubjectDN(csr.getCertificationRequestInfo().getSubject());
115            certGen.setNotBefore(getCertStartDate());
116            certGen.setNotAfter(getCertEndDate());
117            certGen.setPublicKey(csr.getPublicKey("BC"));
118            certGen.setSignatureAlgorithm(CERT_SIGNATURE_ALGORITHM);
119            certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,
120                    new SubjectKeyIdentifierStructure(csr.getPublicKey("BC")));
121            certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(
122                    getRootCertificate()));
123            certGen.addExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false));
124            certGen.addExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.digitalSignature));
125            certGen.addExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(
126                    KeyPurposeId.id_kp_serverAuth));
127
128            ASN1Set attributes = csr.getCertificationRequestInfo().getAttributes();
129            for (int i = 0; i != attributes.size(); i++) {
130                Attribute attr = Attribute.getInstance(attributes.getObjectAt(i));
131                if (attr.getAttrType().equals(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) {
132                    X509Extensions extensions = X509Extensions.getInstance(attr.getAttrValues().getObjectAt(0));
133                    @SuppressWarnings("rawtypes")
134                    Enumeration e = extensions.oids();
135                    while (e.hasMoreElements()) {
136                        DERObjectIdentifier oid = (DERObjectIdentifier) e.nextElement();
137                        X509Extension ext = extensions.getExtension(oid);
138                        certGen.addExtension(oid, ext.isCritical(), ext.getValue().getOctets());
139                    }
140                }
141            }
142
143            KeyPair rootKeyPair = getKeyPair(rootService.getRootKeyStore(), rootService.getRootKeyAlias(),
144                    rootService.getRootCertificateAlias(), rootService.getRootKeyPassword());
145            cert = certGen.generate(rootKeyPair.getPrivate(), "BC");
146        } catch (CertificateParsingException e) {
147            throw new CertException(e);
148        } catch (CertificateEncodingException e) {
149            throw new CertException(e);
150        } catch (InvalidKeyException e) {
151            throw new CertException(e);
152        } catch (IllegalStateException e) {
153            throw new CertException(e);
154        } catch (NoSuchProviderException e) {
155            throw new CertException(e);
156        } catch (NoSuchAlgorithmException e) {
157            throw new CertException(e);
158        } catch (java.security.SignatureException e) {
159            throw new CertException(e);
160        }
161        LOG.debug("Certificate generated for subject: " + cert.getSubjectDN());
162        return cert;
163    }
164
165    @Override
166    public X509Certificate getRootCertificate() throws CertException {
167        if (rootCertificate == null) {
168            rootCertificate = getCertificate(getRootService().getRootKeyStore(),
169                    getRootService().getRootCertificateAlias());
170        }
171        return rootCertificate;
172    }
173
174    protected Date getCertStartDate() {
175        Calendar cal = Calendar.getInstance();
176        return cal.getTime();
177    }
178
179    protected Date getCertEndDate() {
180        Calendar cal = Calendar.getInstance();
181        cal.add(Calendar.MONTH, CERTIFICATE_DURATION_IN_MONTHS);
182        return cal.getTime();
183    }
184
185    @Override
186    public KeyStore initializeUser(UserInfo userInfo, String suppliedPassword) throws CertException {
187        char[] password = suppliedPassword.toCharArray();
188        KeyStore ks = null;
189        String userName = userInfo.getUserFields().get(CNField.UserID);
190        AliasWrapper keystoreAlias = new AliasWrapper(userName);
191        try {
192            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
193            ks.load(null, password);
194            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
195            keyGen.initialize(KEY_SIZE);
196            KeyPair keyPair = keyGen.genKeyPair();
197            java.security.cert.Certificate[] chain = { getRootCertificate() };
198            ks.setKeyEntry(keystoreAlias.getId(AliasType.KEY), keyPair.getPrivate(), password, chain);
199            X509Certificate cert = getCertificate(keyPair, userInfo);
200            ks.setCertificateEntry(keystoreAlias.getId(AliasType.CERT), cert);
201        } catch (CertificateException e) {
202            throw new CertException(e);
203        } catch (IOException e) {
204            throw new CertException(e);
205        } catch (KeyStoreException e) {
206            throw new CertException(e);
207        } catch (NoSuchAlgorithmException e) {
208            throw new CertException(e);
209        }
210        return ks;
211    }
212
213    @Override
214    public KeyPair getKeyPair(KeyStore ks, String keyAlias, String certAlias, String keyPassword) throws CertException {
215        KeyPair keyPair = null;
216        try {
217            if (!ks.containsAlias(keyAlias)) {
218                throw new CertException("Missing keystore key entry for key alias:" + keyAlias);
219            }
220            if (!ks.containsAlias(certAlias)) {
221                throw new CertException("Missing keystore certificate entry for :" + certAlias);
222            }
223            PrivateKey privateKey = (PrivateKey) ks.getKey(keyAlias, keyPassword.toCharArray());
224            X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias);
225            PublicKey publicKey = cert.getPublicKey();
226            keyPair = new KeyPair(publicKey, privateKey);
227        } catch (UnrecoverableKeyException e) {
228            throw new CertException(e);
229        } catch (KeyStoreException e) {
230            throw new CertException(e);
231        } catch (NoSuchAlgorithmException e) {
232            throw new CertException(e);
233        }
234        return keyPair;
235    }
236
237    @Override
238    public X509Certificate getCertificate(KeyStore ks, String certificateAlias) throws CertException {
239        X509Certificate certificate = null;
240        try {
241
242            if (ks == null) {
243                throw new CertException("Keystore missing for " + certificateAlias);
244            }
245            if (ks.containsAlias(certificateAlias)) {
246                certificate = (X509Certificate) ks.getCertificate(certificateAlias);
247            } else {
248                throw new CertException("Certificate not found");
249            }
250        } catch (KeyStoreException e) {
251            throw new CertException(e);
252        }
253        return certificate;
254    }
255
256    protected X509Certificate getCertificate(KeyPair keyPair, UserInfo userInfo) throws CertException {
257        PKCS10CertificationRequest csr = (PKCS10CertificationRequest) generateCSR(keyPair, userInfo);
258        X509Certificate certificate = createCertificateFromCSR(csr);
259        return certificate;
260    }
261
262    protected CertificationRequest generateCSR(KeyPair keyPair, UserInfo userInfo) throws CertException {
263
264        CertificationRequest csr;
265
266        GeneralNames subjectAltName = new GeneralNames(new GeneralName(GeneralName.rfc822Name,
267                userInfo.getUserFields().get(CNField.Email)));
268
269        Vector<DERObjectIdentifier> objectIdentifiers = new Vector<DERObjectIdentifier>();
270        Vector<X509Extension> extensionValues = new Vector<X509Extension>();
271
272        objectIdentifiers.add(X509Extensions.SubjectAlternativeName);
273        extensionValues.add(new X509Extension(false, new DEROctetString(subjectAltName)));
274
275        X509Extensions extensions = new X509Extensions(objectIdentifiers, extensionValues);
276
277        Attribute attribute = new Attribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, new DERSet(extensions));
278        try {
279            csr = new PKCS10CertificationRequest(CERT_SIGNATURE_ALGORITHM, userInfo.getX500Principal(),
280                    keyPair.getPublic(), new DERSet(attribute), keyPair.getPrivate());
281        } catch (InvalidKeyException e) {
282            throw new CertException(e);
283        } catch (NoSuchAlgorithmException e) {
284            throw new CertException(e);
285        } catch (NoSuchProviderException e) {
286            throw new CertException(e);
287        } catch (java.security.SignatureException e) {
288            throw new CertException(e);
289        }
290        return csr;
291    }
292
293    @Override
294    public KeyStore getKeyStore(InputStream keystoreIS, String password) throws CertException {
295        KeyStore ks;
296        try {
297            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
298            ks.load(keystoreIS, password.toCharArray());
299        } catch (KeyStoreException e) {
300            throw new CertException(e);
301        } catch (NoSuchAlgorithmException e) {
302            throw new CertException(e);
303        } catch (CertificateException e) {
304            throw new CertException(e);
305        } catch (IOException e) {
306            if (String.valueOf(e.getMessage()).contains("password was incorrect")) {
307                // "Keystore was tampered with, or password was incorrect"
308                // is not very useful to end-users
309                throw new CertException("Incorrect password");
310            }
311            throw new CertException(e);
312        }
313        return ks;
314    }
315
316    @Override
317    public String getCertificateEmail(X509Certificate certificate) throws CertException {
318        String emailOID = "2.5.29.17";
319        byte[] emailBytes = certificate.getExtensionValue(emailOID);
320        String certificateEmail = null;
321        try {
322            byte[] octets = ((DEROctetString) org.bouncycastle.asn1.ASN1Object.fromByteArray(emailBytes)).getOctets();
323            GeneralNames generalNameCont = GeneralNames.getInstance(org.bouncycastle.asn1.ASN1Object.fromByteArray(octets));
324            GeneralName[] generalNames = generalNameCont.getNames();
325            if (generalNames.length > 0) {
326                GeneralName generalName = generalNames[0];
327                certificateEmail = generalName.getName().toString();
328            }
329        } catch (IOException e) {
330            throw new CertException("Email could not be extracted from certificate", e);
331        }
332        return certificateEmail;
333    }
334
335    @Override
336    public void storeCertificate(KeyStore keystore, OutputStream os, String keystorePassword) throws CertException {
337        try {
338            keystore.store(os, keystorePassword.toCharArray());
339        } catch (KeyStoreException e) {
340            throw new CertException(e);
341        } catch (NoSuchAlgorithmException e) {
342            throw new CertException(e);
343        } catch (CertificateException e) {
344            throw new CertException(e);
345        } catch (IOException e) {
346            throw new CertException(e);
347        }
348    }
349
350    protected RootService getRootService() throws CertException {
351        if (rootService == null) {
352            rootService = Framework.getService(RootService.class);
353        }
354        return rootService;
355    }
356}