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
106    @Override
107    public X509Certificate getRootCertificate() throws CertException {
108        if (rootCertificate == null) {
109            rootCertificate = getCertificate(getRootService().getRootKeyStore(),
110                    getRootService().getRootCertificateAlias());
111        }
112        return rootCertificate;
113    }
114
115    protected Date getCertStartDate() {
116        Calendar cal = Calendar.getInstance();
117        return cal.getTime();
118    }
119
120    protected Date getCertEndDate() {
121        Calendar cal = Calendar.getInstance();
122        cal.add(Calendar.MONTH, CERTIFICATE_DURATION_IN_MONTHS);
123        return cal.getTime();
124    }
125
126    @Override
127    public KeyStore initializeUser(UserInfo userInfo, String suppliedPassword) throws CertException {
128        char[] password = suppliedPassword.toCharArray();
129        KeyStore ks = null;
130        String userName = userInfo.getUserFields().get(CNField.UserID);
131        AliasWrapper keystoreAlias = new AliasWrapper(userName);
132        try {
133            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
134            ks.load(null, password);
135            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
136            keyGen.initialize(KEY_SIZE);
137            KeyPair keyPair = keyGen.genKeyPair();
138            java.security.cert.Certificate[] chain = { getRootCertificate() };
139            ks.setKeyEntry(keystoreAlias.getId(AliasType.KEY), keyPair.getPrivate(), password, chain);
140            X509Certificate cert = getCertificate(keyPair, userInfo);
141            ks.setCertificateEntry(keystoreAlias.getId(AliasType.CERT), cert);
142        } catch (CertificateException e) {
143            throw new CertException(e);
144        } catch (IOException e) {
145            throw new CertException(e);
146        } catch (KeyStoreException e) {
147            throw new CertException(e);
148        } catch (NoSuchAlgorithmException e) {
149            throw new CertException(e);
150        }
151        return ks;
152    }
153
154    @Override
155    public KeyPair getKeyPair(KeyStore ks, String keyAlias, String certAlias, String keyPassword) throws CertException {
156        KeyPair keyPair = null;
157        try {
158            if (!ks.containsAlias(keyAlias)) {
159                throw new CertException("Missing keystore key entry for key alias:" + keyAlias);
160            }
161            if (!ks.containsAlias(certAlias)) {
162                throw new CertException("Missing keystore certificate entry for :" + certAlias);
163            }
164            PrivateKey privateKey = (PrivateKey) ks.getKey(keyAlias, keyPassword.toCharArray());
165            X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias);
166            PublicKey publicKey = cert.getPublicKey();
167            keyPair = new KeyPair(publicKey, privateKey);
168        } catch (UnrecoverableKeyException e) {
169            throw new CertException(e);
170        } catch (KeyStoreException e) {
171            throw new CertException(e);
172        } catch (NoSuchAlgorithmException e) {
173            throw new CertException(e);
174        }
175        return keyPair;
176    }
177
178    @Override
179    public X509Certificate getCertificate(KeyStore ks, String certificateAlias) throws CertException {
180        X509Certificate certificate = null;
181        try {
182
183            if (ks == null) {
184                throw new CertException("Keystore missing for " + certificateAlias);
185            }
186            if (ks.containsAlias(certificateAlias)) {
187                certificate = (X509Certificate) ks.getCertificate(certificateAlias);
188            } else {
189                throw new CertException("Certificate not found");
190            }
191        } catch (KeyStoreException e) {
192            throw new CertException(e);
193        }
194        return certificate;
195    }
196
197    protected X509Certificate getCertificate(KeyPair keyPair, UserInfo userInfo) throws CertException {
198        X509Certificate rootCertificate = getRootCertificate();
199        X500Principal issuer = rootCertificate.getIssuerX500Principal();
200        BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
201        X500Principal principal = userInfo.getX500Principal();
202        String email = userInfo.getUserFields().get(CNField.Email);
203        try {
204            JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
205            X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, serial, getCertStartDate(),
206                    getCertEndDate(), principal, keyPair.getPublic());
207            builder.addExtension(Extension.authorityKeyIdentifier, false,
208                           extUtils.createAuthorityKeyIdentifier(rootCertificate))
209                   .addExtension(Extension.subjectKeyIdentifier, false,
210                           extUtils.createSubjectKeyIdentifier(keyPair.getPublic()))
211                   .addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
212                   .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature))
213                   .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
214                   .addExtension(Extension.subjectAlternativeName, false,
215                           new GeneralNames(new GeneralName(GeneralName.rfc822Name, email)));
216            ContentSigner signer = new JcaContentSignerBuilder(CERT_SIGNATURE_ALGORITHM).setProvider("BC")
217                                                                                        .build(keyPair.getPrivate());
218            return new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer));
219        } catch (GeneralSecurityException | OperatorException | IOException e) {
220            throw new CertException(e);
221        }
222    }
223
224    @Override
225    public KeyStore getKeyStore(InputStream keystoreIS, String password) throws CertException {
226        KeyStore ks;
227        try {
228            ks = java.security.KeyStore.getInstance(KEYSTORE_TYPE);
229            ks.load(keystoreIS, password.toCharArray());
230        } catch (KeyStoreException e) {
231            throw new CertException(e);
232        } catch (NoSuchAlgorithmException e) {
233            throw new CertException(e);
234        } catch (CertificateException e) {
235            throw new CertException(e);
236        } catch (IOException e) {
237            if (String.valueOf(e.getMessage()).contains("password was incorrect")) {
238                // "Keystore was tampered with, or password was incorrect"
239                // is not very useful to end-users
240                throw new CertException("Incorrect password");
241            }
242            throw new CertException(e);
243        }
244        return ks;
245    }
246
247    @Override
248    public String getCertificateEmail(X509Certificate certificate) throws CertException {
249        try {
250            @SuppressWarnings("unchecked")
251            Collection<List<?>> altNames = X509ExtensionUtil.getSubjectAlternativeNames(certificate);
252            for (List<?> names : altNames) {
253                if (Integer.valueOf(GeneralName.rfc822Name).equals(names.get(0))) {
254                    return (String) names.get(1);
255                }
256            }
257            return null;
258        } catch (GeneralSecurityException e) {
259            throw new CertException(e);
260        }
261    }
262
263    @Override
264    public void storeCertificate(KeyStore keystore, OutputStream os, String keystorePassword) throws CertException {
265        try {
266            keystore.store(os, keystorePassword.toCharArray());
267        } catch (KeyStoreException e) {
268            throw new CertException(e);
269        } catch (NoSuchAlgorithmException e) {
270            throw new CertException(e);
271        } catch (CertificateException e) {
272            throw new CertException(e);
273        } catch (IOException e) {
274            throw new CertException(e);
275        }
276    }
277
278    protected RootService getRootService() throws CertException {
279        if (rootService == null) {
280            rootService = Framework.getService(RootService.class);
281        }
282        return rootService;
283    }
284}