[CMPV2] Fix NPE & enhance error messages
[oom/platform/cert-service.git] / certService / src / main / java / org / onap / oom / certservice / cmpv2client / impl / CmpResponseHelper.java
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2020 Nordix Foundation.
4  * ================================================================================
5  * Modification copyright 2021 Nokia
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  *
19  * SPDX-License-Identifier: Apache-2.0
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.oom.certservice.cmpv2client.impl;
24
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.security.InvalidAlgorithmParameterException;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.NoSuchProviderException;
30 import java.security.cert.CertPath;
31 import java.security.cert.CertPathValidator;
32 import java.security.cert.CertPathValidatorException;
33 import java.security.cert.Certificate;
34 import java.security.cert.CertificateException;
35 import java.security.cert.CertificateFactory;
36 import java.security.cert.CertificateParsingException;
37 import java.security.cert.PKIXCertPathChecker;
38 import java.security.cert.PKIXCertPathValidatorResult;
39 import java.security.cert.PKIXParameters;
40 import java.security.cert.TrustAnchor;
41 import java.security.cert.X509Certificate;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.Date;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.Optional;
50 import org.bouncycastle.asn1.cmp.CMPCertificate;
51 import org.bouncycastle.asn1.cmp.CertRepMessage;
52 import org.bouncycastle.asn1.cmp.ErrorMsgContent;
53 import org.bouncycastle.asn1.cmp.PKIBody;
54 import org.bouncycastle.asn1.cmp.PKIMessage;
55 import org.bouncycastle.asn1.x500.X500Name;
56 import org.bouncycastle.jce.provider.BouncyCastleProvider;
57 import org.onap.oom.certservice.cmpv2client.exceptions.CmpClientException;
58 import org.onap.oom.certservice.cmpv2client.exceptions.CmpServerException;
59 import org.onap.oom.certservice.cmpv2client.model.Cmpv2CertificationModel;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 public final class CmpResponseHelper {
64
65     private static final Logger LOG = LoggerFactory.getLogger(CmpResponseHelper.class);
66
67     private CmpResponseHelper() {
68     }
69
70     static void checkIfCmpResponseContainsError(PKIMessage respPkiMessage) {
71         LOG.info("Response type: {} ", respPkiMessage.getBody().getType());
72         if (respPkiMessage.getBody().getType() == PKIBody.TYPE_ERROR) {
73             final ErrorMsgContent errorMsgContent =
74                 (ErrorMsgContent) respPkiMessage.getBody().getContent();
75             String text = errorMsgContent.getPKIStatusInfo().getStatusString().getStringAt(0).getString();
76             LOG.error("Error in the PkiMessage response: {} ", text);
77             throw new CmpServerException(Optional.ofNullable(text).orElse("N/A"));
78         }
79     }
80
81
82     /**
83      * Puts together certChain and Trust store and verifies the certChain
84      *
85      * @param respPkiMessage  PKIMessage that may contain extra certs used for certchain
86      * @param certRepMessage  CertRepMessage that should contain rootCA for certchain
87      * @param leafCertificate certificate returned from our original Cert Request
88      * @return model for certification containing certificate chain and trusted certificates
89      * @throws CertificateParsingException thrown if error occurs while parsing certificate
90      * @throws IOException                 thrown if IOException occurs while parsing certificate
91      * @throws CmpClientException          thrown if error occurs during the verification of the certChain
92      */
93     static Cmpv2CertificationModel verifyAndReturnCertChainAndTrustSTore(
94         PKIMessage respPkiMessage, CertRepMessage certRepMessage, X509Certificate leafCertificate)
95         throws CertificateParsingException, IOException, CmpClientException {
96         Map<X500Name, X509Certificate> certificates = mapAllCertificates(respPkiMessage, certRepMessage);
97         return extractCertificationModel(certificates, leafCertificate);
98     }
99
100     private static Map<X500Name, X509Certificate> mapAllCertificates(
101         PKIMessage respPkiMessage, CertRepMessage certRepMessage
102     )
103         throws IOException, CertificateParsingException, CmpClientException {
104
105         Map<X500Name, X509Certificate> certificates = new HashMap<>();
106
107         CMPCertificate[] extraCerts = respPkiMessage.getExtraCerts();
108         certificates.putAll(mapCertificates(extraCerts));
109
110         CMPCertificate[] caPubsCerts = certRepMessage.getCaPubs();
111         certificates.putAll(mapCertificates(caPubsCerts));
112
113         return certificates;
114     }
115
116     private static Map<X500Name, X509Certificate> mapCertificates(
117         CMPCertificate[] cmpCertificates)
118         throws CertificateParsingException, CmpClientException, IOException {
119
120         Map<X500Name, X509Certificate> certificates = new HashMap<>();
121         if (cmpCertificates != null) {
122             for (CMPCertificate certificate : cmpCertificates) {
123                 getCertFromByteArray(certificate.getEncoded(), X509Certificate.class)
124                     .ifPresent(x509Certificate ->
125                         certificates.put(extractSubjectDn(x509Certificate), x509Certificate)
126                     );
127             }
128         }
129
130         return certificates;
131     }
132
133     private static Cmpv2CertificationModel extractCertificationModel(
134         Map<X500Name, X509Certificate> certificates, X509Certificate leafCertificate
135     )
136         throws CmpClientException {
137         List<X509Certificate> certificateChain = new ArrayList<>();
138         X509Certificate previousCertificateInChain;
139         X509Certificate nextCertificateInChain = leafCertificate;
140         do {
141             certificateChain.add(nextCertificateInChain);
142             certificates.remove(extractSubjectDn(nextCertificateInChain));
143             previousCertificateInChain = nextCertificateInChain;
144             nextCertificateInChain = certificates.get(extractIssuerDn(nextCertificateInChain));
145             verify(previousCertificateInChain, nextCertificateInChain, null);
146         }
147         while (!isSelfSign(nextCertificateInChain));
148         List<X509Certificate> trustedCertificates = new ArrayList<>(certificates.values());
149
150         return new Cmpv2CertificationModel(certificateChain, trustedCertificates);
151     }
152
153     private static boolean isSelfSign(X509Certificate certificate) {
154         return extractIssuerDn(certificate).equals(extractSubjectDn(certificate));
155     }
156
157     private static X500Name extractIssuerDn(X509Certificate x509Certificate) {
158         return X500Name.getInstance(x509Certificate.getIssuerDN());
159     }
160
161     private static X500Name extractSubjectDn(X509Certificate x509Certificate) {
162         return X500Name.getInstance(x509Certificate.getSubjectDN());
163     }
164
165
166     /**
167      * Check the certificate with CA certificate.
168      *
169      * @param certificate          X.509 certificate to verify. May not be null.
170      * @param caCertChain          Collection of X509Certificates. May not be null, an empty list or a Collection with
171      *                             null entries.
172      * @param date                 Date to verify at, or null to use current time.
173      * @param pkixCertPathCheckers optional PKIXCertPathChecker implementations to use during cert path validation
174      * @throws CmpClientException if certificate could not be validated
175      */
176     private static void verify(
177         X509Certificate certificate,
178         X509Certificate caCertChain,
179         Date date,
180         PKIXCertPathChecker... pkixCertPathCheckers)
181         throws CmpClientException {
182         try {
183             verifyCertificates(certificate, caCertChain, date, pkixCertPathCheckers);
184         } catch (CertPathValidatorException cpve) {
185             CmpClientException cmpClientException =
186                 new CmpClientException(
187                     "Invalid certificate or certificate not issued by specified CA: ", cpve);
188             LOG.error("Invalid certificate or certificate not issued by specified CA: ", cpve);
189             throw cmpClientException;
190         } catch (CertificateException ce) {
191             CmpClientException cmpClientException =
192                 new CmpClientException("Something was wrong with the supplied certificate", ce);
193             LOG.error("Something was wrong with the supplied certificate", ce);
194             throw cmpClientException;
195         } catch (NoSuchProviderException nspe) {
196             CmpClientException cmpClientException =
197                 new CmpClientException("BouncyCastle provider not found.", nspe);
198             LOG.error("BouncyCastle provider not found.", nspe);
199             throw cmpClientException;
200         } catch (NoSuchAlgorithmException nsae) {
201             CmpClientException cmpClientException =
202                 new CmpClientException("Algorithm PKIX was not found.", nsae);
203             LOG.error("Algorithm PKIX was not found.", nsae);
204             throw cmpClientException;
205         } catch (InvalidAlgorithmParameterException iape) {
206             CmpClientException cmpClientException =
207                 new CmpClientException(
208                     "Either ca certificate chain was empty,"
209                         + " or the certificate was on an inappropriate type for a PKIX path checker.",
210                     iape);
211             LOG.error(
212                 "Either ca certificate chain was empty, "
213                     + "or the certificate was on an inappropriate type for a PKIX path checker.",
214                 iape);
215             throw cmpClientException;
216         }
217     }
218
219     private static void verifyCertificates(
220         X509Certificate certificate,
221         X509Certificate caCertChain,
222         Date date,
223         PKIXCertPathChecker[] pkixCertPathCheckers)
224         throws CertificateException, NoSuchProviderException, InvalidAlgorithmParameterException,
225         NoSuchAlgorithmException, CertPathValidatorException {
226         if (caCertChain == null) {
227             final String noRootCaCertificateMessage = "Server response does not contain proper root CA certificate";
228             throw new CertificateException(noRootCaCertificateMessage);
229         }
230         LOG.debug(
231             "Verifying certificate {} as part of cert chain with certificate {}",
232             certificate.getSubjectDN().getName(),
233             caCertChain.getSubjectDN().getName());
234         CertPath cp = getCertPath(certificate);
235         PKIXParameters params = getPkixParameters(caCertChain, date, pkixCertPathCheckers);
236         CertPathValidator cpv =
237             CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME);
238         PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) cpv.validate(cp, params);
239         if (LOG.isDebugEnabled()) {
240             LOG.debug("Certificate verify result:{} ", result);
241         }
242     }
243
244     private static PKIXParameters getPkixParameters(
245         X509Certificate caCertChain, Date date, PKIXCertPathChecker[] pkixCertPathCheckers)
246         throws InvalidAlgorithmParameterException {
247         TrustAnchor anchor = new TrustAnchor(caCertChain, null);
248         PKIXParameters params = new PKIXParameters(Collections.singleton(anchor));
249         for (final PKIXCertPathChecker pkixCertPathChecker : pkixCertPathCheckers) {
250             params.addCertPathChecker(pkixCertPathChecker);
251         }
252         params.setRevocationEnabled(false);
253         params.setDate(date);
254         return params;
255     }
256
257     private static CertPath getCertPath(X509Certificate certificate)
258         throws CertificateException, NoSuchProviderException {
259         ArrayList<X509Certificate> certlist = new ArrayList<>();
260         certlist.add(certificate);
261         return CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME)
262             .generateCertPath(certlist);
263     }
264
265     /**
266      * Returns a CertificateFactory that can be used to create certificates from byte arrays and such.
267      *
268      * @param provider Security provider that should be used to create certificates, default BC is null is passed.
269      * @return CertificateFactory for creating certificate
270      */
271     private static CertificateFactory getCertificateFactory(final String provider)
272         throws CmpClientException {
273         LOG.debug("Creating certificate Factory to generate certificate using provider {}", provider);
274         final String prov;
275         prov = Objects.requireNonNullElse(provider, BouncyCastleProvider.PROVIDER_NAME);
276         try {
277             return CertificateFactory.getInstance("X.509", prov);
278         } catch (NoSuchProviderException nspe) {
279             CmpClientException cmpClientException = new CmpClientException("NoSuchProvider: ", nspe);
280             LOG.error("NoSuchProvider: ", nspe);
281             throw cmpClientException;
282         } catch (CertificateException ce) {
283             CmpClientException cmpClientException = new CmpClientException("CertificateException: ", ce);
284             LOG.error("CertificateException: ", ce);
285             throw cmpClientException;
286         }
287     }
288
289     /**
290      * @param cert       byte array that contains certificate
291      * @param returnType the type of Certificate to be returned, for example X509Certificate.class. Certificate.class
292      *                   can be used if certificate type is unknown.
293      * @throws CertificateParsingException if the byte array does not contain a proper certificate.
294      */
295     static <T extends Certificate> Optional<X509Certificate> getCertFromByteArray(
296         byte[] cert, Class<T> returnType) throws CertificateParsingException, CmpClientException {
297         LOG.debug("Retrieving certificate of type {} from byte array.", returnType);
298         String prov = BouncyCastleProvider.PROVIDER_NAME;
299
300         if (returnType.equals(X509Certificate.class)) {
301             return parseX509Certificate(prov, cert);
302         } else {
303             LOG.debug("Certificate of type {} was skipped, because type of certificate is not 'X509Certificate'.",
304                 returnType);
305             return Optional.empty();
306         }
307     }
308
309
310     /**
311      * Parse a X509Certificate from an array of bytes
312      *
313      * @param provider a provider name
314      * @param cert     a byte array containing an encoded certificate
315      * @return a decoded X509Certificate
316      * @throws CertificateParsingException if the byte array wasn't valid, or contained a certificate other than an X509
317      *                                     Certificate.
318      */
319     private static Optional<X509Certificate> parseX509Certificate(String provider, byte[] cert)
320         throws CertificateParsingException, CmpClientException {
321         LOG.debug("Parsing X509Certificate from bytes with provider {}", provider);
322         final CertificateFactory cf = getCertificateFactory(provider);
323         X509Certificate result;
324         try {
325             result = (X509Certificate) Objects.requireNonNull(cf).generateCertificate(new ByteArrayInputStream(cert));
326             return Optional.ofNullable(result);
327         } catch (CertificateException ce) {
328             throw new CertificateParsingException("Could not parse byte array as X509Certificate ", ce);
329         }
330     }
331 }