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