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
11 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.oom.certservice.cmpv2client.impl;
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;
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;
71 * Implementation of the CmpClient Interface conforming to RFC4210 (Certificate Management Protocol
72 * (CMP)) and RFC4211 (Certificate Request Message Format (CRMF)) standards.
74 public class CmpClientImpl implements CmpClient {
76 private static final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class);
77 private final CloseableHttpClient httpClient;
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");
83 public CmpClientImpl(CloseableHttpClient httpClient) {
84 this.httpClient = httpClient;
88 public Cmpv2CertificationModel createCertificate(
93 throws CmpClientException {
95 validate(csrModel, server, httpClient, notBefore, notAfter);
97 final String iak = server.getAuthentication().getIak();
98 final PkiMessageProtection pkiMessageProtection = new PasswordBasedProtection(iak);
99 final CreateCertRequest certRequest =
100 getCmpMessageBuilderWithCommonRequestValues(csrModel, server)
101 .with(CreateCertRequest::setNotBefore, notBefore)
102 .with(CreateCertRequest::setNotAfter, notAfter)
103 .with(CreateCertRequest::setSenderKid, server.getAuthentication().getRv())
104 .with(CreateCertRequest::setCmpRequestType, PKIBody.TYPE_INIT_REQ)
105 .with(CreateCertRequest::setProtection, pkiMessageProtection)
108 return executeCmpRequest(csrModel, server, certRequest);
112 public Cmpv2CertificationModel createCertificate(CsrModel csrModel, Cmpv2Server server)
113 throws CmpClientException {
114 return createCertificate(csrModel, server, null, null);
118 public Cmpv2CertificationModel updateCertificate(CsrModel csrModel, Cmpv2Server cmpv2Server,
119 CertificateUpdateModel certificateUpdateModel) throws CmpClientException {
120 validate(csrModel, cmpv2Server, httpClient, null, null);
122 final PkiMessageProtection pkiMessageProtection = getSignatureProtection(certificateUpdateModel);
123 final CreateCertRequest certRequest =
124 getCmpMessageBuilderWithCommonRequestValues(csrModel, cmpv2Server)
125 .with(CreateCertRequest::setCmpRequestType, PKIBody.TYPE_KEY_UPDATE_REQ)
126 .with(CreateCertRequest::setExtraCerts, getCMPCertificateFromPem(certificateUpdateModel.getEncodedOldCert()))
127 .with(CreateCertRequest::setProtection, pkiMessageProtection)
130 return executeCmpRequest(csrModel, cmpv2Server, certRequest);
134 private Cmpv2CertificationModel executeCmpRequest(CsrModel csrModel, Cmpv2Server cmpv2Server,
135 CreateCertRequest certRequest) throws CmpClientException {
136 final PKIMessage pkiMessage = certRequest.generateCertReq();
137 Cmpv2HttpClient cmpv2HttpClient = new Cmpv2HttpClient(httpClient);
138 return retrieveCertificates(csrModel, cmpv2Server, pkiMessage, cmpv2HttpClient);
141 private CmpMessageBuilder<CreateCertRequest> getCmpMessageBuilderWithCommonRequestValues(CsrModel csrModel,
142 Cmpv2Server cmpv2Server) {
143 KeyPair keyPair = new KeyPair(csrModel.getPublicKey(), csrModel.getPrivateKey());
144 return CmpMessageBuilder.of(CreateCertRequest::new)
145 .with(CreateCertRequest::setIssuerDn, cmpv2Server.getIssuerDN())
146 .with(CreateCertRequest::setSubjectDn, csrModel.getSubjectData())
147 .with(CreateCertRequest::setSansArray, csrModel.getSans())
148 .with(CreateCertRequest::setSubjectKeyPair, keyPair);
151 private SignatureProtection getSignatureProtection(CertificateUpdateModel certificateUpdateModel)
152 throws CmpClientException {
154 PrivateKey oldPrivateKey = certificateUpdateModel.getOldPrivateKeyObject();
155 return new SignatureProtection(oldPrivateKey);
156 } catch (NoSuchAlgorithmException | KeyDecryptionException | InvalidKeySpecException e) {
157 throw new CmpClientException("Cannot parse old private key ", e);
162 private CMPCertificate[] getCMPCertificateFromPem(String encodedCertPem) throws CmpClientException {
164 Certificate certificate = Certificate.getInstance(
166 new InputStreamReader(
167 new ByteArrayInputStream(
168 Base64.decodeBase64(encodedCertPem))))
169 .readPemObject().getContent());
170 CMPCertificate cert = new CMPCertificate(certificate);
171 return new CMPCertificate[]{cert};
172 } catch (IOException | NullPointerException e ) {
173 throw new CmpClientException("Cannot parse old certificate", e);
177 private void checkCmpResponse(
178 final PKIMessage respPkiMessage, final PublicKey publicKey, final String initAuthPassword)
179 throws CmpClientException {
180 final PKIHeader header = respPkiMessage.getHeader();
181 final AlgorithmIdentifier protectionAlgo = header.getProtectionAlg();
182 verifySignatureWithPublicKey(respPkiMessage, publicKey);
183 if (isPasswordBasedMacAlgorithm(protectionAlgo)) {
184 LOG.info("CMP response is protected by Password Base Mac Algorithm. Attempt to verify protection");
185 verifyPasswordBasedMacProtection(respPkiMessage, initAuthPassword, header, protectionAlgo);
189 private boolean isPasswordBasedMacAlgorithm(AlgorithmIdentifier protectionAlgo) throws CmpClientException {
190 if (Objects.isNull(protectionAlgo)) {
191 LOG.error("CMP response does not contain Protection Algorithm field");
192 throw new CmpClientException("CMP response does not contain Protection Algorithm field");
194 return PASSWORD_BASED_MAC.equals(protectionAlgo.getAlgorithm());
197 private void verifySignatureWithPublicKey(PKIMessage respPkiMessage, PublicKey publicKey)
198 throws CmpClientException {
199 if (Objects.nonNull(publicKey)) {
200 LOG.debug("Verifying signature of the response.");
201 verifySignature(respPkiMessage, publicKey);
203 LOG.error("Public Key is not available, therefore cannot verify signature");
204 throw new CmpClientException(
205 "Public Key is not available, therefore cannot verify signature");
209 private void verifyPasswordBasedMacProtection(PKIMessage respPkiMessage, String initAuthPassword,
210 PKIHeader header, AlgorithmIdentifier protectionAlgo)
211 throws CmpClientException {
212 LOG.debug("Verifying PasswordBased Protection of the Response.");
213 verifyPasswordBasedProtection(respPkiMessage, initAuthPassword, protectionAlgo);
214 checkImplicitConfirm(header);
217 private Cmpv2CertificationModel checkCmpCertRepMessage(final PKIMessage respPkiMessage)
218 throws CmpClientException {
219 final PKIBody pkiBody = respPkiMessage.getBody();
220 if (Objects.nonNull(pkiBody) && pkiBody.getContent() instanceof CertRepMessage) {
221 final CertRepMessage certRepMessage = (CertRepMessage) pkiBody.getContent();
222 if (Objects.nonNull(certRepMessage)) {
224 CertResponse certResponse = getCertificateResponseContainingNewCertificate(certRepMessage);
225 checkServerResponse(certResponse);
226 return verifyReturnCertChainAndTrustStore(respPkiMessage, certRepMessage, certResponse);
227 } catch (IOException | CertificateParsingException ex) {
228 CmpClientException cmpClientException =
229 new CmpClientException(
230 "Exception occurred while retrieving Certificates from response", ex);
231 LOG.error("Exception occurred while retrieving Certificates from response", ex);
232 throw cmpClientException;
235 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
238 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
241 private void checkServerResponse(CertResponse certResponse) {
242 if (certResponse.getStatus() != null && certResponse.getStatus().getStatus() != null) {
243 logServerResponse(certResponse);
244 if (certResponse.getStatus().getStatus().intValue() == PkiStatus.REJECTED.getCode()) {
245 String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
246 throw new CmpServerException(Optional.ofNullable(serverMessage).orElse("N/A"));
251 private void logServerResponse(CertResponse certResponse) {
252 if (LOG.isInfoEnabled()) {
253 LOG.info("Response status code: {}", certResponse.getStatus().getStatus());
255 if (certResponse.getStatus().getStatusString() != null) {
256 String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
257 LOG.warn("Response status text: {}", serverMessage);
259 if (LOG.isWarnEnabled() && certResponse.getStatus().getFailInfo() != null) {
260 LOG.warn("Response fail info: {}", certResponse.getStatus().getFailInfo());
264 private Cmpv2CertificationModel verifyReturnCertChainAndTrustStore(
265 PKIMessage respPkiMessage, CertRepMessage certRepMessage, CertResponse certResponse)
266 throws CertificateParsingException, CmpClientException, IOException {
267 LOG.info("Verifying certificates returned as part of CertResponse.");
268 final CMPCertificate cmpCertificate =
269 certResponse.getCertifiedKeyPair().getCertOrEncCert().getCertificate();
270 final Optional<X509Certificate> leafCertificate =
271 getCertFromByteArray(cmpCertificate.getEncoded(), X509Certificate.class);
272 if (leafCertificate.isPresent()) {
273 return verifyAndReturnCertChainAndTrustSTore(
274 respPkiMessage, certRepMessage, leafCertificate.get());
276 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
279 private CertResponse getCertificateResponseContainingNewCertificate(
280 CertRepMessage certRepMessage) {
281 return certRepMessage.getResponse()[0];
285 * Validate inputs for Certificate Creation.
287 * @param csrModel Certificate Signing Request model. Must not be {@code null}.
288 * @param server CMPv2 Server. Must not be {@code null}.
289 * @throws IllegalArgumentException if Before Date is set after the After Date.
291 private static void validate(
292 final CsrModel csrModel,
293 final Cmpv2Server server,
294 final CloseableHttpClient httpClient,
295 final Date notBefore,
296 final Date notAfter) {
298 String caName = CmpUtil.isNullOrEmpty(server.getCaName()) ? server.getCaName() : DEFAULT_CA_NAME;
299 String profile = server.getCaMode() != null ? server.getCaMode().getProfile() : DEFAULT_PROFILE;
301 "Validate before creating Certificate Request for CA :{} in Mode {} ", caName, profile);
303 CmpUtil.notNull(csrModel, "CsrModel Instance");
304 CmpUtil.notNull(csrModel.getSubjectData(), "Subject DN");
305 CmpUtil.notNull(csrModel.getPrivateKey(), "Subject private key");
306 CmpUtil.notNull(csrModel.getPublicKey(), "Subject public key");
307 CmpUtil.notNull(server.getIssuerDN(), "Issuer DN");
308 CmpUtil.notNull(server.getUrl(), "External CA URL");
309 CmpUtil.notNull(server.getAuthentication().getIak(), "IAK/RV Password");
310 CmpUtil.notNull(httpClient, "Closeable Http Client");
312 if (notBefore != null && notAfter != null && notBefore.compareTo(notAfter) > 0) {
313 throw new IllegalArgumentException("Before Date is set after the After Date");
317 private Cmpv2CertificationModel retrieveCertificates(
318 CsrModel csrModel, Cmpv2Server server, PKIMessage pkiMessage, Cmpv2HttpClient cmpv2HttpClient)
319 throws CmpClientException {
320 final byte[] respBytes = cmpv2HttpClient.postRequest(pkiMessage, server.getUrl(), server.getCaName());
322 final PKIMessage respPkiMessage = PKIMessage.getInstance(respBytes);
323 LOG.info("Received response from Server");
324 checkIfCmpResponseContainsError(respPkiMessage);
325 checkCmpResponse(respPkiMessage, csrModel.getPublicKey(), server.getAuthentication().getIak());
326 return checkCmpCertRepMessage(respPkiMessage);
327 } catch (IllegalArgumentException iae) {
328 CmpClientException cmpClientException =
329 new CmpClientException(
330 "Error encountered while processing response from CA server ", iae);
331 LOG.error("Error encountered while processing response from CA server ", iae);
332 throw cmpClientException;