001/*
002 * (C) Copyright 2011-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Wojciech Sulejman
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.platform.signature.core.pki;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.math.BigInteger;
026import java.security.GeneralSecurityException;
027import java.security.KeyPair;
028import java.security.KeyPairGenerator;
029import java.security.KeyStore;
030import java.security.KeyStoreException;
031import java.security.NoSuchAlgorithmException;
032import java.security.PrivateKey;
033import java.security.PublicKey;
034import java.security.Security;
035import java.security.UnrecoverableKeyException;
036import java.security.cert.CertificateException;
037import java.security.cert.X509Certificate;
038import java.util.Calendar;
039import java.util.Collection;
040import java.util.Date;
041import java.util.List;
042
043import javax.security.auth.x500.X500Principal;
044
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047import org.bouncycastle.asn1.x509.BasicConstraints;
048import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
049import org.bouncycastle.asn1.x509.Extension;
050import org.bouncycastle.asn1.x509.GeneralName;
051import org.bouncycastle.asn1.x509.GeneralNames;
052import org.bouncycastle.asn1.x509.KeyPurposeId;
053import org.bouncycastle.asn1.x509.KeyUsage;
054import org.bouncycastle.cert.X509v3CertificateBuilder;
055import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
056import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
057import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
058import org.bouncycastle.jce.provider.BouncyCastleProvider;
059import org.bouncycastle.operator.ContentSigner;
060import org.bouncycastle.operator.OperatorException;
061import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
062import org.bouncycastle.x509.extension.X509ExtensionUtil;
063import org.nuxeo.ecm.platform.signature.api.exception.CertException;
064import org.nuxeo.ecm.platform.signature.api.pki.CertService;
065import org.nuxeo.ecm.platform.signature.api.pki.RootService;
066import org.nuxeo.ecm.platform.signature.api.user.AliasType;
067import org.nuxeo.ecm.platform.signature.api.user.AliasWrapper;
068import org.nuxeo.ecm.platform.signature.api.user.CNField;
069import org.nuxeo.ecm.platform.signature.api.user.UserInfo;
070import org.nuxeo.runtime.api.Framework;
071import org.nuxeo.runtime.model.DefaultComponent;
072
073/**
074 * Base implementation of the certification service.
075 */
076public class CertServiceImpl extends DefaultComponent implements CertService {
077
078    protected RootService rootService;
079
080    private static final Log LOG = LogFactory.getLog(CertServiceImpl.class);
081
082    @Override
083    public void setRootService(RootService rootService) {
084        this.rootService = rootService;
085    }
086
087    protected X509Certificate rootCertificate;
088
089    private static final int CERTIFICATE_DURATION_IN_MONTHS = 12;
090
091    private static final String CERT_SIGNATURE_ALGORITHM = "SHA256WithRSAEncryption";
092
093    private static final String KEY_ALGORITHM = "RSA";
094
095    private static final int KEY_SIZE = 1024;
096
097    private static final String KEYSTORE_TYPE = "JKS";
098
099    static {
100        if (Security.getProvider("BC") == null) {
101            Security.addProvider(new BouncyCastleProvider());
102        }
103    }
104
105    @Override
106    public X509Certificate getRootCertificate() throws CertException {
107        if (rootCertificate == null) {
108            rootCertificate = getCertificate(getRootService().getRootKeyStore(),
109                    getRootService().getRootCertificateAlias());
110        }
111        return rootCertificate;
112    }
113
114    protected Date getCertStartDate() {
115        Calendar cal = Calendar.getInstance();
116        return cal.getTime();
117    }
118
119    protected Date getCertEndDate() {
120        Calendar cal = Calendar.getInstance();
121        cal.add(Calendar.MONTH, CERTIFICATE_DURATION_IN_MONTHS);
122        return cal.getTime();
123    }
124
125    @Override
126    public KeyStore initializeUser(UserInfo userInfo, String suppliedPassword) throws CertException {
127        char[] password = suppliedPassword.toCharArray();
128        KeyStore ks = null;
129        String userName = userInfo.getUserFields().get(CNField.UserID);
130        AliasWrapper keystoreAlias = new AliasWrapper(userName);
131        try {
132            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
133            ks.load(null, password);
134            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
135            keyGen.initialize(KEY_SIZE);
136            KeyPair keyPair = keyGen.genKeyPair();
137            java.security.cert.Certificate[] chain = { getRootCertificate() };
138            ks.setKeyEntry(keystoreAlias.getId(AliasType.KEY), keyPair.getPrivate(), password, chain);
139            X509Certificate cert = getCertificate(keyPair, userInfo);
140            ks.setCertificateEntry(keystoreAlias.getId(AliasType.CERT), cert);
141        } catch (CertificateException e) {
142            throw new CertException(e);
143        } catch (IOException e) {
144            throw new CertException(e);
145        } catch (KeyStoreException e) {
146            throw new CertException(e);
147        } catch (NoSuchAlgorithmException e) {
148            throw new CertException(e);
149        }
150        return ks;
151    }
152
153    @Override
154    public KeyPair getKeyPair(KeyStore ks, String keyAlias, String certAlias, String keyPassword) throws CertException {
155        KeyPair keyPair = null;
156        try {
157            if (!ks.containsAlias(keyAlias)) {
158                throw new CertException("Missing keystore key entry for key alias:" + keyAlias);
159            }
160            if (!ks.containsAlias(certAlias)) {
161                throw new CertException("Missing keystore certificate entry for :" + certAlias);
162            }
163            PrivateKey privateKey = (PrivateKey) ks.getKey(keyAlias, keyPassword.toCharArray());
164            X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias);
165            PublicKey publicKey = cert.getPublicKey();
166            keyPair = new KeyPair(publicKey, privateKey);
167        } catch (UnrecoverableKeyException e) {
168            throw new CertException(e);
169        } catch (KeyStoreException e) {
170            throw new CertException(e);
171        } catch (NoSuchAlgorithmException e) {
172            throw new CertException(e);
173        }
174        return keyPair;
175    }
176
177    @Override
178    public X509Certificate getCertificate(KeyStore ks, String certificateAlias) throws CertException {
179        X509Certificate certificate = null;
180        try {
181
182            if (ks == null) {
183                throw new CertException("Keystore missing for " + certificateAlias);
184            }
185            if (ks.containsAlias(certificateAlias)) {
186                certificate = (X509Certificate) ks.getCertificate(certificateAlias);
187            } else {
188                throw new CertException("Certificate not found");
189            }
190        } catch (KeyStoreException e) {
191            throw new CertException(e);
192        }
193        return certificate;
194    }
195
196    protected X509Certificate getCertificate(KeyPair keyPair, UserInfo userInfo) throws CertException {
197        X509Certificate rootCertificate = getRootCertificate();
198        X500Principal issuer = rootCertificate.getIssuerX500Principal();
199        BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
200        X500Principal principal = userInfo.getX500Principal();
201        String email = userInfo.getUserFields().get(CNField.Email);
202        try {
203            JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
204            X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, serial, getCertStartDate(),
205                    getCertEndDate(), principal, keyPair.getPublic());
206            builder.addExtension(Extension.authorityKeyIdentifier, false,
207                    extUtils.createAuthorityKeyIdentifier(rootCertificate))
208                   .addExtension(Extension.subjectKeyIdentifier, false,
209                           extUtils.createSubjectKeyIdentifier(keyPair.getPublic()))
210                   .addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
211                   .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature))
212                   .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
213                   .addExtension(Extension.subjectAlternativeName, false,
214                           new GeneralNames(new GeneralName(GeneralName.rfc822Name, email)));
215            ContentSigner signer = new JcaContentSignerBuilder(CERT_SIGNATURE_ALGORITHM).setProvider("BC")
216                                                                                        .build(keyPair.getPrivate());
217            return new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer));
218        } catch (GeneralSecurityException | OperatorException | IOException e) {
219            throw new CertException(e);
220        }
221    }
222
223    @Override
224    public KeyStore getKeyStore(InputStream keystoreIS, String password) throws CertException {
225        KeyStore ks;
226        try {
227            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
228            ks.load(keystoreIS, password.toCharArray());
229        } catch (KeyStoreException e) {
230            throw new CertException(e);
231        } catch (NoSuchAlgorithmException e) {
232            throw new CertException(e);
233        } catch (CertificateException e) {
234            throw new CertException(e);
235        } catch (IOException e) {
236            if (String.valueOf(e.getMessage()).contains("password was incorrect")) {
237                // "Keystore was tampered with, or password was incorrect"
238                // is not very useful to end-users
239                throw new CertException("Incorrect password");
240            }
241            throw new CertException(e);
242        }
243        return ks;
244    }
245
246    @Override
247    public String getCertificateEmail(X509Certificate certificate) throws CertException {
248        try {
249            @SuppressWarnings("unchecked")
250            Collection<List<?>> altNames = X509ExtensionUtil.getSubjectAlternativeNames(certificate);
251            for (List<?> names : altNames) {
252                if (Integer.valueOf(GeneralName.rfc822Name).equals(names.get(0))) {
253                    return (String) names.get(1);
254                }
255            }
256            return null;
257        } catch (GeneralSecurityException e) {
258            throw new CertException(e);
259        }
260    }
261
262    @Override
263    public void storeCertificate(KeyStore keystore, OutputStream os, String keystorePassword) throws CertException {
264        try {
265            keystore.store(os, keystorePassword.toCharArray());
266        } catch (KeyStoreException e) {
267            throw new CertException(e);
268        } catch (NoSuchAlgorithmException e) {
269            throw new CertException(e);
270        } catch (CertificateException e) {
271            throw new CertException(e);
272        } catch (IOException e) {
273            throw new CertException(e);
274        }
275    }
276
277    protected RootService getRootService() throws CertException {
278        if (rootService == null) {
279            rootService = Framework.getService(RootService.class);
280        }
281        return rootService;
282    }
283}