549cf6b9863b4afb48624fc578238333c3c9d506
[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.ByteArrayInputStream;
33 import java.io.IOException;
34 import java.io.InputStreamReader;
35 import java.security.KeyPair;
36 import java.security.NoSuchAlgorithmException;
37 import java.security.PrivateKey;
38 import java.security.PublicKey;
39 import java.security.cert.CertificateParsingException;
40 import java.security.cert.X509Certificate;
41 import java.security.spec.InvalidKeySpecException;
42 import java.util.Collections;
43 import java.util.Date;
44 import java.util.Objects;
45 import java.util.Optional;
46 import org.apache.commons.codec.binary.Base64;
47 import org.apache.http.impl.client.CloseableHttpClient;
48 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
49 import org.bouncycastle.asn1.cmp.CMPCertificate;
50 import org.bouncycastle.asn1.cmp.CertRepMessage;
51 import org.bouncycastle.asn1.cmp.CertResponse;
52 import org.bouncycastle.asn1.cmp.PKIBody;
53 import org.bouncycastle.asn1.cmp.PKIHeader;
54 import org.bouncycastle.asn1.cmp.PKIMessage;
55 import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
56 import org.bouncycastle.asn1.x509.Certificate;
57 import org.bouncycastle.util.io.pem.PemReader;
58 import org.onap.oom.certservice.certification.configuration.model.CaMode;
59 import org.onap.oom.certservice.certification.configuration.model.Cmpv2Server;
60 import org.onap.oom.certservice.certification.exception.KeyDecryptionException;
61 import org.onap.oom.certservice.certification.model.CertificateUpdateModel;
62 import org.onap.oom.certservice.certification.model.CsrModel;
63 import org.onap.oom.certservice.cmpv2client.api.CmpClient;
64 import org.onap.oom.certservice.cmpv2client.exceptions.CmpClientException;
65 import org.onap.oom.certservice.cmpv2client.exceptions.CmpServerException;
66 import org.onap.oom.certservice.cmpv2client.model.Cmpv2CertificationModel;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * Implementation of the CmpClient Interface conforming to RFC4210 (Certificate Management Protocol
72  * (CMP)) and RFC4211 (Certificate Request Message Format (CRMF)) standards.
73  */
74 public class CmpClientImpl implements CmpClient {
75
76     private static final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class);
77     private final CloseableHttpClient httpClient;
78
79     private static final String DEFAULT_CA_NAME = "Certification Authority";
80     private static final String DEFAULT_PROFILE = CaMode.RA.getProfile();
81     private static final ASN1ObjectIdentifier PASSWORD_BASED_MAC = new ASN1ObjectIdentifier("1.2.840.113533.7.66.13");
82
83     public CmpClientImpl(CloseableHttpClient httpClient) {
84         this.httpClient = httpClient;
85     }
86
87     @Override
88     public Cmpv2CertificationModel createCertificate(
89             CsrModel csrModel,
90             Cmpv2Server server,
91             Date notBefore,
92             Date notAfter)
93             throws CmpClientException {
94
95         validate(csrModel, server, httpClient, notBefore, notAfter);
96         final CreateCertRequest certRequest = getIakRvRequest(csrModel, server, notBefore, notAfter, PKIBody.TYPE_INIT_REQ);
97         return executeCmpRequest(csrModel, server, certRequest);
98     }
99
100     @Override
101     public Cmpv2CertificationModel createCertificate(CsrModel csrModel, Cmpv2Server server)
102             throws CmpClientException {
103         return createCertificate(csrModel, server, null, null);
104     }
105
106     @Override
107     public Cmpv2CertificationModel updateCertificate(CsrModel csrModel, Cmpv2Server cmpv2Server,
108         CertificateUpdateModel certificateUpdateModel) throws CmpClientException {
109         validate(csrModel, cmpv2Server, httpClient, null, null);
110
111         final PkiMessageProtection pkiMessageProtection = getSignatureProtection(certificateUpdateModel);
112         final CreateCertRequest certRequest =
113             getCmpMessageBuilderWithCommonRequestValues(csrModel, cmpv2Server)
114                 .with(CreateCertRequest::setCmpRequestType, PKIBody.TYPE_KEY_UPDATE_REQ)
115                 .with(CreateCertRequest::setExtraCerts, getCMPCertificateFromPem(certificateUpdateModel.getEncodedOldCert()))
116                 .with(CreateCertRequest::setProtection, pkiMessageProtection)
117                 .build();
118
119         return executeCmpRequest(csrModel, cmpv2Server, certRequest);
120
121     }
122
123     @Override
124     public Cmpv2CertificationModel certificationRequest(CsrModel csrModel, Cmpv2Server cmpv2Server) throws CmpClientException {
125
126         validate(csrModel, cmpv2Server, httpClient, null, null);
127         final CreateCertRequest certRequest = getIakRvRequest(csrModel, cmpv2Server, null, null, PKIBody.TYPE_CERT_REQ);
128         return executeCmpRequest(csrModel, cmpv2Server, certRequest);
129     }
130
131     private CreateCertRequest getIakRvRequest(
132         CsrModel csrModel,
133         Cmpv2Server server,
134         Date notBefore,
135         Date notAfter,
136         int requestType) {
137
138         final String iak = server.getAuthentication().getIak();
139         final PkiMessageProtection pkiMessageProtection = new PasswordBasedProtection(iak);
140         return getCmpMessageBuilderWithCommonRequestValues(csrModel, server)
141             .with(CreateCertRequest::setNotBefore, notBefore)
142             .with(CreateCertRequest::setNotAfter, notAfter)
143             .with(CreateCertRequest::setSenderKid, server.getAuthentication().getRv())
144             .with(CreateCertRequest::setCmpRequestType, requestType)
145             .with(CreateCertRequest::setProtection, pkiMessageProtection)
146             .build();
147     }
148
149     private Cmpv2CertificationModel executeCmpRequest(CsrModel csrModel, Cmpv2Server cmpv2Server,
150         CreateCertRequest certRequest) throws CmpClientException {
151         final PKIMessage pkiMessage = certRequest.generateCertReq();
152         Cmpv2HttpClient cmpv2HttpClient = new Cmpv2HttpClient(httpClient);
153         return retrieveCertificates(csrModel, cmpv2Server, pkiMessage, cmpv2HttpClient);
154     }
155
156     private CmpMessageBuilder<CreateCertRequest> getCmpMessageBuilderWithCommonRequestValues(CsrModel csrModel,
157         Cmpv2Server cmpv2Server) {
158         KeyPair keyPair = new KeyPair(csrModel.getPublicKey(), csrModel.getPrivateKey());
159         return CmpMessageBuilder.of(CreateCertRequest::new)
160             .with(CreateCertRequest::setIssuerDn, cmpv2Server.getIssuerDN())
161             .with(CreateCertRequest::setSubjectDn, csrModel.getSubjectData())
162             .with(CreateCertRequest::setSansArray, csrModel.getSans())
163             .with(CreateCertRequest::setSubjectKeyPair, keyPair);
164     }
165
166     private SignatureProtection getSignatureProtection(CertificateUpdateModel certificateUpdateModel)
167         throws CmpClientException {
168         try {
169             PrivateKey oldPrivateKey = certificateUpdateModel.getOldPrivateKeyObject();
170             return new SignatureProtection(oldPrivateKey);
171         } catch (NoSuchAlgorithmException | KeyDecryptionException | InvalidKeySpecException e) {
172             throw new CmpClientException("Cannot parse old private key ", e);
173         }
174
175     }
176
177     private CMPCertificate[] getCMPCertificateFromPem(String encodedCertPem) throws CmpClientException {
178         try {
179             Certificate certificate = Certificate.getInstance(
180                 new PemReader(
181                     new InputStreamReader(
182                         new ByteArrayInputStream(
183                             Base64.decodeBase64(encodedCertPem))))
184                 .readPemObject().getContent());
185             CMPCertificate cert = new CMPCertificate(certificate);
186             return new CMPCertificate[]{cert};
187         } catch (IOException | NullPointerException e ) {
188             throw new CmpClientException("Cannot parse old certificate", e);
189         }
190     }
191
192     private void checkCmpResponse(
193             final PKIMessage respPkiMessage, final PublicKey publicKey, final String initAuthPassword)
194             throws CmpClientException {
195         final PKIHeader header = respPkiMessage.getHeader();
196         final AlgorithmIdentifier protectionAlgo = header.getProtectionAlg();
197         verifySignatureWithPublicKey(respPkiMessage, publicKey);
198         if (isPasswordBasedMacAlgorithm(protectionAlgo)) {
199             LOG.info("CMP response is protected by Password Base Mac Algorithm. Attempt to verify protection");
200             verifyPasswordBasedMacProtection(respPkiMessage, initAuthPassword, header, protectionAlgo);
201         }
202     }
203
204     private boolean isPasswordBasedMacAlgorithm(AlgorithmIdentifier protectionAlgo) throws CmpClientException {
205         if (Objects.isNull(protectionAlgo)) {
206             LOG.error("CMP response does not contain Protection Algorithm field");
207             throw new CmpClientException("CMP response does not contain Protection Algorithm field");
208         }
209         return PASSWORD_BASED_MAC.equals(protectionAlgo.getAlgorithm());
210     }
211
212     private void verifySignatureWithPublicKey(PKIMessage respPkiMessage, PublicKey publicKey)
213             throws CmpClientException {
214         if (Objects.nonNull(publicKey)) {
215             LOG.debug("Verifying signature of the response.");
216             verifySignature(respPkiMessage, publicKey);
217         } else {
218             LOG.error("Public Key is not available, therefore cannot verify signature");
219             throw new CmpClientException(
220                     "Public Key is not available, therefore cannot verify signature");
221         }
222     }
223
224     private void verifyPasswordBasedMacProtection(PKIMessage respPkiMessage, String initAuthPassword,
225         PKIHeader header, AlgorithmIdentifier protectionAlgo)
226         throws CmpClientException {
227         LOG.debug("Verifying PasswordBased Protection of the Response.");
228         verifyPasswordBasedProtection(respPkiMessage, initAuthPassword, protectionAlgo);
229         checkImplicitConfirm(header);
230     }
231
232     private Cmpv2CertificationModel checkCmpCertRepMessage(final PKIMessage respPkiMessage)
233             throws CmpClientException {
234         final PKIBody pkiBody = respPkiMessage.getBody();
235         if (Objects.nonNull(pkiBody) && pkiBody.getContent() instanceof CertRepMessage) {
236             final CertRepMessage certRepMessage = (CertRepMessage) pkiBody.getContent();
237             if (Objects.nonNull(certRepMessage)) {
238                 try {
239                     CertResponse certResponse = getCertificateResponseContainingNewCertificate(certRepMessage);
240                     checkServerResponse(certResponse);
241                     return verifyReturnCertChainAndTrustStore(respPkiMessage, certRepMessage, certResponse);
242                 } catch (IOException | CertificateParsingException ex) {
243                     CmpClientException cmpClientException =
244                             new CmpClientException(
245                                     "Exception occurred while retrieving Certificates from response", ex);
246                     LOG.error("Exception occurred while retrieving Certificates from response", ex);
247                     throw cmpClientException;
248                 }
249             } else {
250                 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
251             }
252         }
253         return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
254     }
255
256     private void checkServerResponse(CertResponse certResponse) {
257         if (certResponse.getStatus() != null && certResponse.getStatus().getStatus() != null) {
258             logServerResponse(certResponse);
259             if (certResponse.getStatus().getStatus().intValue() == PkiStatus.REJECTED.getCode()) {
260                 String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
261                 throw new CmpServerException(Optional.ofNullable(serverMessage).orElse("N/A"));
262             }
263         }
264     }
265
266     private void logServerResponse(CertResponse certResponse) {
267         if (LOG.isInfoEnabled()) {
268             LOG.info("Response status code: {}", certResponse.getStatus().getStatus());
269         }
270         if (certResponse.getStatus().getStatusString() != null) {
271             String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
272             LOG.warn("Response status text: {}", serverMessage);
273         }
274         if (LOG.isWarnEnabled() && certResponse.getStatus().getFailInfo() != null) {
275             LOG.warn("Response fail info:   {}", certResponse.getStatus().getFailInfo());
276         }
277     }
278
279     private Cmpv2CertificationModel verifyReturnCertChainAndTrustStore(
280             PKIMessage respPkiMessage, CertRepMessage certRepMessage, CertResponse certResponse)
281             throws CertificateParsingException, CmpClientException, IOException {
282         LOG.info("Verifying certificates returned as part of CertResponse.");
283         final CMPCertificate cmpCertificate =
284                 certResponse.getCertifiedKeyPair().getCertOrEncCert().getCertificate();
285         final Optional<X509Certificate> leafCertificate =
286                 getCertFromByteArray(cmpCertificate.getEncoded(), X509Certificate.class);
287         if (leafCertificate.isPresent()) {
288             return verifyAndReturnCertChainAndTrustSTore(
289                     respPkiMessage, certRepMessage, leafCertificate.get());
290         }
291         return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
292     }
293
294     private CertResponse getCertificateResponseContainingNewCertificate(
295             CertRepMessage certRepMessage) {
296         return certRepMessage.getResponse()[0];
297     }
298
299     /**
300      * Validate inputs for Certificate Creation.
301      *
302      * @param csrModel Certificate Signing Request model. Must not be {@code null}.
303      * @param server   CMPv2 Server. Must not be {@code null}.
304      * @throws IllegalArgumentException if Before Date is set after the After Date.
305      */
306     private static void validate(
307             final CsrModel csrModel,
308             final Cmpv2Server server,
309             final CloseableHttpClient httpClient,
310             final Date notBefore,
311             final Date notAfter) {
312
313         String caName = CmpUtil.isNullOrEmpty(server.getCaName()) ? server.getCaName() : DEFAULT_CA_NAME;
314         String profile = server.getCaMode() != null ? server.getCaMode().getProfile() : DEFAULT_PROFILE;
315         LOG.info(
316                 "Validate before creating Certificate Request for CA :{} in Mode {} ", caName, profile);
317
318         CmpUtil.notNull(csrModel, "CsrModel Instance");
319         CmpUtil.notNull(csrModel.getSubjectData(), "Subject DN");
320         CmpUtil.notNull(csrModel.getPrivateKey(), "Subject private key");
321         CmpUtil.notNull(csrModel.getPublicKey(), "Subject public key");
322         CmpUtil.notNull(server.getIssuerDN(), "Issuer DN");
323         CmpUtil.notNull(server.getUrl(), "External CA URL");
324         CmpUtil.notNull(server.getAuthentication().getIak(), "IAK/RV Password");
325         CmpUtil.notNull(httpClient, "Closeable Http Client");
326
327         if (notBefore != null && notAfter != null && notBefore.compareTo(notAfter) > 0) {
328             throw new IllegalArgumentException("Before Date is set after the After Date");
329         }
330     }
331
332     private Cmpv2CertificationModel retrieveCertificates(
333             CsrModel csrModel, Cmpv2Server server, PKIMessage pkiMessage, Cmpv2HttpClient cmpv2HttpClient)
334             throws CmpClientException {
335         final byte[] respBytes = cmpv2HttpClient.postRequest(pkiMessage, server.getUrl(), server.getCaName());
336         try {
337             final PKIMessage respPkiMessage = PKIMessage.getInstance(respBytes);
338             LOG.info("Received response from Server");
339             checkIfCmpResponseContainsError(respPkiMessage);
340             checkCmpResponse(respPkiMessage, csrModel.getPublicKey(), server.getAuthentication().getIak());
341             return checkCmpCertRepMessage(respPkiMessage);
342         } catch (IllegalArgumentException iae) {
343             CmpClientException cmpClientException =
344                     new CmpClientException(
345                             "Error encountered while processing response from CA server ", iae);
346             LOG.error("Error encountered while processing response from CA server ", iae);
347             throw cmpClientException;
348         }
349     }
350 }