Merge "[OOM-CERT-SERVICE] Add handling cmp response when PBM value is missing."
[oom/platform/cert-service.git] / certService / src / main / java / org / onap / oom / certservice / cmpv2client / impl / CmpClientImpl.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 static org.onap.oom.certservice.cmpv2client.impl.CmpResponseHelper.checkIfCmpResponseContainsError;
26 import static org.onap.oom.certservice.cmpv2client.impl.CmpResponseHelper.getCertFromByteArray;
27 import static org.onap.oom.certservice.cmpv2client.impl.CmpResponseHelper.verifyAndReturnCertChainAndTrustSTore;
28 import static org.onap.oom.certservice.cmpv2client.impl.CmpResponseValidationHelper.checkImplicitConfirm;
29 import static org.onap.oom.certservice.cmpv2client.impl.CmpResponseValidationHelper.verifyPasswordBasedProtection;
30 import static org.onap.oom.certservice.cmpv2client.impl.CmpResponseValidationHelper.verifySignature;
31
32 import java.io.IOException;
33 import java.security.KeyPair;
34 import java.security.PublicKey;
35 import java.security.cert.CertificateParsingException;
36 import java.security.cert.X509Certificate;
37 import java.util.Collections;
38 import java.util.Date;
39 import java.util.Objects;
40 import java.util.Optional;
41 import org.apache.http.impl.client.CloseableHttpClient;
42 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
43 import org.bouncycastle.asn1.cmp.CMPCertificate;
44 import org.bouncycastle.asn1.cmp.CertRepMessage;
45 import org.bouncycastle.asn1.cmp.CertResponse;
46 import org.bouncycastle.asn1.cmp.PKIBody;
47 import org.bouncycastle.asn1.cmp.PKIHeader;
48 import org.bouncycastle.asn1.cmp.PKIMessage;
49 import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
50 import org.onap.oom.certservice.certification.configuration.model.CaMode;
51 import org.onap.oom.certservice.certification.configuration.model.Cmpv2Server;
52 import org.onap.oom.certservice.certification.model.CsrModel;
53 import org.onap.oom.certservice.cmpv2client.api.CmpClient;
54 import org.onap.oom.certservice.cmpv2client.exceptions.CmpClientException;
55 import org.onap.oom.certservice.cmpv2client.exceptions.CmpServerException;
56 import org.onap.oom.certservice.cmpv2client.model.Cmpv2CertificationModel;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 /**
61  * Implementation of the CmpClient Interface conforming to RFC4210 (Certificate Management Protocol
62  * (CMP)) and RFC4211 (Certificate Request Message Format (CRMF)) standards.
63  */
64 public class CmpClientImpl implements CmpClient {
65
66     private static final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class);
67     private final CloseableHttpClient httpClient;
68
69     private static final String DEFAULT_CA_NAME = "Certification Authority";
70     private static final String DEFAULT_PROFILE = CaMode.RA.getProfile();
71     private static final ASN1ObjectIdentifier PASSWORD_BASED_MAC = new ASN1ObjectIdentifier("1.2.840.113533.7.66.13");
72
73     public CmpClientImpl(CloseableHttpClient httpClient) {
74         this.httpClient = httpClient;
75     }
76
77     @Override
78     public Cmpv2CertificationModel createCertificate(
79             CsrModel csrModel,
80             Cmpv2Server server,
81             Date notBefore,
82             Date notAfter)
83             throws CmpClientException {
84
85         validate(csrModel, server, httpClient, notBefore, notAfter);
86         KeyPair keyPair = new KeyPair(csrModel.getPublicKey(), csrModel.getPrivateKey());
87
88         final String iak = server.getAuthentication().getIak();
89         final PkiMessageProtection pkiMessageProtection = new PasswordBasedProtection(iak);
90         final CreateCertRequest certRequest =
91                 CmpMessageBuilder.of(CreateCertRequest::new)
92                         .with(CreateCertRequest::setIssuerDn, server.getIssuerDN())
93                         .with(CreateCertRequest::setSubjectDn, csrModel.getSubjectData())
94                         .with(CreateCertRequest::setSansArray, csrModel.getSans())
95                         .with(CreateCertRequest::setSubjectKeyPair, keyPair)
96                         .with(CreateCertRequest::setNotBefore, notBefore)
97                         .with(CreateCertRequest::setNotAfter, notAfter)
98                         .with(CreateCertRequest::setSenderKid, server.getAuthentication().getRv())
99                         .with(CreateCertRequest::setProtection, pkiMessageProtection)
100                         .build();
101
102         final PKIMessage pkiMessage = certRequest.generateCertReq();
103         Cmpv2HttpClient cmpv2HttpClient = new Cmpv2HttpClient(httpClient);
104         return retrieveCertificates(csrModel, server, pkiMessage, cmpv2HttpClient);
105     }
106
107     @Override
108     public Cmpv2CertificationModel createCertificate(CsrModel csrModel, Cmpv2Server server)
109             throws CmpClientException {
110         return createCertificate(csrModel, server, null, null);
111     }
112
113     private void checkCmpResponse(
114             final PKIMessage respPkiMessage, final PublicKey publicKey, final String initAuthPassword)
115             throws CmpClientException {
116         final PKIHeader header = respPkiMessage.getHeader();
117         final AlgorithmIdentifier protectionAlgo = header.getProtectionAlg();
118         verifySignatureWithPublicKey(respPkiMessage, publicKey);
119         if (isPasswordBasedMacAlgorithm(protectionAlgo)) {
120             LOG.info("CMP response is protected by Password Base Mac Algorithm. Attempt to verify protection");
121             verifyPasswordBasedMacProtection(respPkiMessage, initAuthPassword, header, protectionAlgo);
122         }
123     }
124
125     private boolean isPasswordBasedMacAlgorithm(AlgorithmIdentifier protectionAlgo) throws CmpClientException {
126         if (Objects.isNull(protectionAlgo)) {
127             LOG.error("CMP response does not contain Protection Algorithm field");
128             throw new CmpClientException("CMP response does not contain Protection Algorithm field");
129         }
130         return PASSWORD_BASED_MAC.equals(protectionAlgo.getAlgorithm());
131     }
132
133     private void verifySignatureWithPublicKey(PKIMessage respPkiMessage, PublicKey publicKey)
134             throws CmpClientException {
135         if (Objects.nonNull(publicKey)) {
136             LOG.debug("Verifying signature of the response.");
137             verifySignature(respPkiMessage, publicKey);
138         } else {
139             LOG.error("Public Key is not available, therefore cannot verify signature");
140             throw new CmpClientException(
141                     "Public Key is not available, therefore cannot verify signature");
142         }
143     }
144
145     private void verifyPasswordBasedMacProtection(PKIMessage respPkiMessage, String initAuthPassword,
146         PKIHeader header, AlgorithmIdentifier protectionAlgo)
147         throws CmpClientException {
148         LOG.debug("Verifying PasswordBased Protection of the Response.");
149         verifyPasswordBasedProtection(respPkiMessage, initAuthPassword, protectionAlgo);
150         checkImplicitConfirm(header);
151     }
152
153     private Cmpv2CertificationModel checkCmpCertRepMessage(final PKIMessage respPkiMessage)
154             throws CmpClientException {
155         final PKIBody pkiBody = respPkiMessage.getBody();
156         if (Objects.nonNull(pkiBody) && pkiBody.getContent() instanceof CertRepMessage) {
157             final CertRepMessage certRepMessage = (CertRepMessage) pkiBody.getContent();
158             if (Objects.nonNull(certRepMessage)) {
159                 try {
160                     CertResponse certResponse = getCertificateResponseContainingNewCertificate(certRepMessage);
161                     checkServerResponse(certResponse);
162                     return verifyReturnCertChainAndTrustStore(respPkiMessage, certRepMessage, certResponse);
163                 } catch (IOException | CertificateParsingException ex) {
164                     CmpClientException cmpClientException =
165                             new CmpClientException(
166                                     "Exception occurred while retrieving Certificates from response", ex);
167                     LOG.error("Exception occurred while retrieving Certificates from response", ex);
168                     throw cmpClientException;
169                 }
170             } else {
171                 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
172             }
173         }
174         return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
175     }
176
177     private void checkServerResponse(CertResponse certResponse) {
178         if (certResponse.getStatus() != null && certResponse.getStatus().getStatus() != null) {
179             logServerResponse(certResponse);
180             if (certResponse.getStatus().getStatus().intValue() == PkiStatus.REJECTED.getCode()) {
181                 String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
182                 throw new CmpServerException(Optional.ofNullable(serverMessage).orElse("N/A"));
183             }
184         }
185     }
186
187     private void logServerResponse(CertResponse certResponse) {
188         if (LOG.isInfoEnabled()) {
189             LOG.info("Response status code: {}", certResponse.getStatus().getStatus());
190         }
191         if (certResponse.getStatus().getStatusString() != null) {
192             String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
193             LOG.warn("Response status text: {}", serverMessage);
194         }
195         if (LOG.isWarnEnabled() && certResponse.getStatus().getFailInfo() != null) {
196             LOG.warn("Response fail info:   {}", certResponse.getStatus().getFailInfo());
197         }
198     }
199
200     private Cmpv2CertificationModel verifyReturnCertChainAndTrustStore(
201             PKIMessage respPkiMessage, CertRepMessage certRepMessage, CertResponse certResponse)
202             throws CertificateParsingException, CmpClientException, IOException {
203         LOG.info("Verifying certificates returned as part of CertResponse.");
204         final CMPCertificate cmpCertificate =
205                 certResponse.getCertifiedKeyPair().getCertOrEncCert().getCertificate();
206         final Optional<X509Certificate> leafCertificate =
207                 getCertFromByteArray(cmpCertificate.getEncoded(), X509Certificate.class);
208         if (leafCertificate.isPresent()) {
209             return verifyAndReturnCertChainAndTrustSTore(
210                     respPkiMessage, certRepMessage, leafCertificate.get());
211         }
212         return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
213     }
214
215     private CertResponse getCertificateResponseContainingNewCertificate(
216             CertRepMessage certRepMessage) {
217         return certRepMessage.getResponse()[0];
218     }
219
220     /**
221      * Validate inputs for Certificate Creation.
222      *
223      * @param csrModel Certificate Signing Request model. Must not be {@code null}.
224      * @param server   CMPv2 Server. Must not be {@code null}.
225      * @throws IllegalArgumentException if Before Date is set after the After Date.
226      */
227     private static void validate(
228             final CsrModel csrModel,
229             final Cmpv2Server server,
230             final CloseableHttpClient httpClient,
231             final Date notBefore,
232             final Date notAfter) {
233
234         String caName = CmpUtil.isNullOrEmpty(server.getCaName()) ? server.getCaName() : DEFAULT_CA_NAME;
235         String profile = server.getCaMode() != null ? server.getCaMode().getProfile() : DEFAULT_PROFILE;
236         LOG.info(
237                 "Validate before creating Certificate Request for CA :{} in Mode {} ", caName, profile);
238
239         CmpUtil.notNull(csrModel, "CsrModel Instance");
240         CmpUtil.notNull(csrModel.getSubjectData(), "Subject DN");
241         CmpUtil.notNull(csrModel.getPrivateKey(), "Subject private key");
242         CmpUtil.notNull(csrModel.getPublicKey(), "Subject public key");
243         CmpUtil.notNull(server.getIssuerDN(), "Issuer DN");
244         CmpUtil.notNull(server.getUrl(), "External CA URL");
245         CmpUtil.notNull(server.getAuthentication().getIak(), "IAK/RV Password");
246         CmpUtil.notNull(httpClient, "Closeable Http Client");
247
248         if (notBefore != null && notAfter != null && notBefore.compareTo(notAfter) > 0) {
249             throw new IllegalArgumentException("Before Date is set after the After Date");
250         }
251     }
252
253     private Cmpv2CertificationModel retrieveCertificates(
254             CsrModel csrModel, Cmpv2Server server, PKIMessage pkiMessage, Cmpv2HttpClient cmpv2HttpClient)
255             throws CmpClientException {
256         final byte[] respBytes = cmpv2HttpClient.postRequest(pkiMessage, server.getUrl(), server.getCaName());
257         try {
258             final PKIMessage respPkiMessage = PKIMessage.getInstance(respBytes);
259             LOG.info("Received response from Server");
260             checkIfCmpResponseContainsError(respPkiMessage);
261             checkCmpResponse(respPkiMessage, csrModel.getPublicKey(), server.getAuthentication().getIak());
262             return checkCmpCertRepMessage(respPkiMessage);
263         } catch (IllegalArgumentException iae) {
264             CmpClientException cmpClientException =
265                     new CmpClientException(
266                             "Error encountered while processing response from CA server ", iae);
267             LOG.error("Error encountered while processing response from CA server ", iae);
268             throw cmpClientException;
269         }
270     }
271 }