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);
96 final CreateCertRequest certRequest = getIakRvRequest(csrModel, server, notBefore, notAfter, PKIBody.TYPE_INIT_REQ);
97 return executeCmpRequest(csrModel, server, certRequest);
101 public Cmpv2CertificationModel createCertificate(CsrModel csrModel, Cmpv2Server server)
102 throws CmpClientException {
103 return createCertificate(csrModel, server, null, null);
107 public Cmpv2CertificationModel updateCertificate(CsrModel csrModel, Cmpv2Server cmpv2Server,
108 CertificateUpdateModel certificateUpdateModel) throws CmpClientException {
109 validate(csrModel, cmpv2Server, httpClient, null, null);
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)
119 return executeCmpRequest(csrModel, cmpv2Server, certRequest);
124 public Cmpv2CertificationModel certificationRequest(CsrModel csrModel, Cmpv2Server cmpv2Server) throws CmpClientException {
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);
131 private CreateCertRequest getIakRvRequest(
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)
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);
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);
166 private SignatureProtection getSignatureProtection(CertificateUpdateModel certificateUpdateModel)
167 throws CmpClientException {
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);
177 private CMPCertificate[] getCMPCertificateFromPem(String encodedCertPem) throws CmpClientException {
179 Certificate certificate = Certificate.getInstance(
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);
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);
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");
209 return PASSWORD_BASED_MAC.equals(protectionAlgo.getAlgorithm());
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);
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");
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);
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)) {
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;
250 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
253 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
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"));
266 private void logServerResponse(CertResponse certResponse) {
267 if (LOG.isInfoEnabled()) {
268 LOG.info("Response status code: {}", certResponse.getStatus().getStatus());
270 if (certResponse.getStatus().getStatusString() != null) {
271 String serverMessage = certResponse.getStatus().getStatusString().getStringAt(0).getString();
272 LOG.warn("Response status text: {}", serverMessage);
274 if (LOG.isWarnEnabled() && certResponse.getStatus().getFailInfo() != null) {
275 LOG.warn("Response fail info: {}", certResponse.getStatus().getFailInfo());
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());
291 return new Cmpv2CertificationModel(Collections.emptyList(), Collections.emptyList());
294 private CertResponse getCertificateResponseContainingNewCertificate(
295 CertRepMessage certRepMessage) {
296 return certRepMessage.getResponse()[0];
300 * Validate inputs for Certificate Creation.
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.
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) {
313 String caName = CmpUtil.isNullOrEmpty(server.getCaName()) ? server.getCaName() : DEFAULT_CA_NAME;
314 String profile = server.getCaMode() != null ? server.getCaMode().getProfile() : DEFAULT_PROFILE;
316 "Validate before creating Certificate Request for CA :{} in Mode {} ", caName, profile);
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");
327 if (notBefore != null && notAfter != null && notBefore.compareTo(notAfter) > 0) {
328 throw new IllegalArgumentException("Before Date is set after the After Date");
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());
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;