diff options
author | Bogumil Zebek <bogumil.zebek@nokia.com> | 2020-02-20 17:04:58 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2020-02-20 17:04:58 +0000 |
commit | 7831ee84ae44f14964739fe0d291074a885768dd (patch) | |
tree | bc1bc76f208435a3e5d1ef7ae017f223fd176a57 | |
parent | 18acead623826c43da43da6d0e55e81e1f2953a1 (diff) | |
parent | 153a7ac15d804178e7c52f69117e1a9478862df1 (diff) |
Merge "Refactoring of Cmpv2Client code for sending CertRequest"
18 files changed, 1693 insertions, 2 deletions
diff --git a/certService/pom.xml b/certService/pom.xml index 20988436..5fbd5b1c 100644 --- a/certService/pom.xml +++ b/certService/pom.xml @@ -13,7 +13,7 @@ ============LICENSE_END========================================================= --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.onap.aaf.certservice</groupId> @@ -67,6 +67,15 @@ <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + </dependencies> <build> diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/api/CmpClient.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/api/CmpClient.java new file mode 100644 index 00000000..feee3eed --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/api/CmpClient.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.api; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Date; +import org.apache.http.impl.client.CloseableHttpClient; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.onap.aaf.certservice.cmpv2client.exceptions.PkiErrorException; +import org.onap.aaf.certservice.cmpv2client.external.CSRMeta; + +/** + * This class represent CmpV2Client Interface for obtaining X.509 Digital Certificates in a Public + * Key Infrastructure (PKI), making use of Certificate Management Protocol (CMPv2) operating on + * newest version: cmp2000(2). + */ +public interface CmpClient { + + /** + * Requests for a External Root CA Certificate to be created for the passed public keyPair wrapped + * in a CSRMeta with common details, accepts self-signed certificate. Basic Authentication using + * IAK/RV, Verification of the signature (proof-of-possession) on the request is performed and an + * Exception thrown if verification fails or issue encountered in fetching certificate from CA. + * + * @param caName Information about the External Root Certificate Authority (CA) performing the + * event CA Name. Could be {@code null}. + * @param profile Profile on CA server Client/RA Mode configuration on Server. Could be {@code + * null}. + * @param csrMeta Certificate Signing Request Meta Data. Must not be {@code null}. + * @param csr Certificate Signing Request {.cer} file. Must not be {@code null}. + * @param notBefore An optional validity to set in the created certificate, Certificate not valid + * before this date. + * @param notAfter An optional validity to set in the created certificate, Certificate not valid + * after this date. + * @return {@link X509Certificate} The newly created Certificate. + * @throws CmpClientException if client error occurs. + */ + X509Certificate createCertificate( + String caName, + String profile, + CSRMeta csrMeta, + X509Certificate csr, + Date notBefore, + Date notAfter) + throws CmpClientException; + + /** + * Requests for a External Root CA Certificate to be created for the passed public keyPair wrapped + * in a CSRMeta with common details, accepts self-signed certificate. Basic Authentication using + * IAK/RV, Verification of the signature (proof-of-possession) on the request is performed and an + * Exception thrown if verification fails or issue encountered in fetching certificate from CA. + * + * @param caName Information about the External Root Certificate Authority (CA) performing the + * event CA Name. Could be {@code null}. + * @param profile Profile on CA server Client/RA Mode configuration on Server. Could be {@code + * null}. + * @param csrMeta Certificate Signing Request Meta Data. Must not be {@code null}. + * @param csr Certificate Signing Request {.cer} file. Must not be {@code null}. + * @return {@link X509Certificate} The newly created Certificate. + * @throws CmpClientException if client error occurs. + */ + X509Certificate createCertificate( + String caName, + String profile, + CSRMeta csrMeta, + X509Certificate csr) + throws CmpClientException; +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/CmpClientException.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/CmpClientException.java new file mode 100644 index 00000000..7f7d4ae6 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/CmpClientException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.exceptions; + +/** The CmpClientException wraps all exceptions occur internally to Cmpv2Client Api code. */ +public class CmpClientException extends Exception { + + private static final long serialVersionUID = 1L; + + /** Creates a new instance with detail message. */ + public CmpClientException(String message) { + super(message); + } + + /** Creates a new instance with detail Throwable cause. */ + public CmpClientException(Throwable cause) { + super(cause); + } + + /** Creates a new instance with detail message and Throwable cause. */ + public CmpClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/PkiErrorException.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/PkiErrorException.java new file mode 100644 index 00000000..965ce6fb --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/PkiErrorException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.exceptions; + +public class PkiErrorException extends Exception { + + private static final long serialVersionUID = 1L; + + /** Creates a new instance with detail message. */ + public PkiErrorException(String message) { + super(message); + } + + /** Creates a new instance with detail Throwable cause. */ + public PkiErrorException(Throwable cause) { + super(cause); + } + + /** Creates a new instance with detail message and Throwable cause. */ + public PkiErrorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/CSRMeta.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/CSRMeta.java new file mode 100644 index 00000000..7655b025 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/CSRMeta.java @@ -0,0 +1,202 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.aaf + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * + * Modifications Copyright (C) 2019 IBM. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.aaf.certservice.cmpv2client.external; + +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.Certificate; + +public class CSRMeta { + + private String cn; + private String mechID; + private String environment; + private String email; + private String challenge; + private String issuerCn; + private String issuerEmail; + private String password; + private String CaUrl; + private List<RDN> rdns; + private ArrayList<String> sanList = new ArrayList<>(); + private KeyPair keyPair; + private X500Name name; + private X500Name issuerName; + private Certificate certificate; + private SecureRandom random = new SecureRandom(); + + public CSRMeta(List<RDN> rdns) { + this.rdns = rdns; + } + + public X500Name x500Name() { + if (name == null) { + X500NameBuilder xnb = new X500NameBuilder(); + xnb.addRDN(BCStyle.CN, cn); + xnb.addRDN(BCStyle.E, email); + if (mechID != null) { + if (environment == null) { + xnb.addRDN(BCStyle.OU, mechID); + } else { + xnb.addRDN(BCStyle.OU, mechID + ':' + environment); + } + } + for (RDN rdn : rdns) { + xnb.addRDN(rdn.aoi, rdn.value); + } + name = xnb.build(); + } + return name; + } + + public X500Name issuerx500Name() { + if (issuerName == null) { + X500NameBuilder xnb = new X500NameBuilder(); + xnb.addRDN(BCStyle.CN, issuerCn); + if (issuerEmail != null) { + xnb.addRDN(BCStyle.E, issuerEmail); + } + issuerName = xnb.build(); + } + return issuerName; + } + + public CSRMeta san(String v) { + sanList.add(v); + return this; + } + + public List<String> sans() { + return sanList; + } + + public KeyPair keypair() { + if (keyPair == null) { + keyPair = Factory.generateKeyPair(); + } + return keyPair; + } + + public KeyPair keyPair() { + return keyPair; + } + + public void keyPair(KeyPair keyPair) { + this.keyPair = keyPair; + } + + /** @return the cn */ + public String cn() { + return cn; + } + + /** @param cn the cn to set */ + public void cn(String cn) { + this.cn = cn; + } + + /** Environment of Service MechID is good for */ + public void environment(String env) { + environment = env; + } + + /** @return */ + public String environment() { + return environment; + } + + /** @return the mechID */ + public String mechID() { + return mechID; + } + + /** @param mechID the mechID to set */ + public void mechID(String mechID) { + this.mechID = mechID; + } + + /** @return the email */ + public String email() { + return email; + } + + /** @param email the email to set */ + public void email(String email) { + this.email = email; + } + + /** @return the challenge */ + public String challenge() { + return challenge; + } + + /** @param challenge the challenge to set */ + public void challenge(String challenge) { + this.challenge = challenge; + } + + public void password(String password) { + this.password = password; + } + + public String password() { + return password; + } + + public void certificate(Certificate certificate) { + this.certificate = certificate; + } + + public Certificate certificate() { + return certificate; + } + + public void issuerCn(String issuerCn) { + this.issuerCn = issuerCn; + } + + public String caUrl() { + return CaUrl; + } + + public void caUrl(String caUrl) { + CaUrl = caUrl; + } + + public String issuerCn() { + return issuerCn; + } + + public String issuerEmail() { + return issuerEmail; + } + + public void issuerEmail(String issuerEmail) { + this.issuerEmail = issuerEmail; + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Factory.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Factory.java new file mode 100644 index 00000000..7072abfd --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Factory.java @@ -0,0 +1,54 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.aaf + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * + * Modifications Copyright (C) 2019 IBM. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.aaf.certservice.cmpv2client.external; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class Factory { + + private static final KeyPairGenerator keygen; + private static final SecureRandom random; + private static final String KEY_ALGO = "RSA"; + private static final int KEY_LENGTH = 2048; + private static final int SUB = 0x08; + + static { + random = new SecureRandom(); + KeyPairGenerator tempKeygen; + try { + tempKeygen = KeyPairGenerator.getInstance(KEY_ALGO); // ,"BC"); + tempKeygen.initialize(KEY_LENGTH, random); + } catch (NoSuchAlgorithmException e) { + tempKeygen = null; + e.printStackTrace(System.err); + } + keygen = tempKeygen; + } + + public static KeyPair generateKeyPair() { + return keygen.generateKeyPair(); + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/RDN.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/RDN.java new file mode 100644 index 00000000..512a76e1 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/RDN.java @@ -0,0 +1,145 @@ +/** + * ============LICENSE_START==================================================== + * org.onap.aaf + * =========================================================================== + * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. + * + * Modifications Copyright (C) 2019 IBM. + * =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END==================================================== + * + */ +package org.onap.aaf.certservice.cmpv2client.external; + +import java.util.ArrayList; +import java.util.List; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.CertException; + +public class RDN { + + public String tag; + public String value; + public ASN1ObjectIdentifier aoi; + + public RDN(final String tagValue) throws CertException { + String[] tv = Split.splitTrim('=', tagValue); + switch (tv[0]) { + case "cn": + case "CN": + aoi = BCStyle.CN; + break; + case "c": + case "C": + aoi = BCStyle.C; + break; + case "st": + case "ST": + aoi = BCStyle.ST; + break; + case "l": + case "L": + aoi = BCStyle.L; + break; + case "o": + case "O": + aoi = BCStyle.O; + break; + case "ou": + case "OU": + aoi = BCStyle.OU; + break; + case "dc": + case "DC": + aoi = BCStyle.DC; + break; + case "gn": + case "GN": + aoi = BCStyle.GIVENNAME; + break; + case "sn": + case "SN": + aoi = BCStyle.SN; + break; // surname + case "email": + case "EMAIL": + case "emailaddress": + case "EMAILADDRESS": + aoi = BCStyle.EmailAddress; + break; // should be SAN extension + case "initials": + aoi = BCStyle.INITIALS; + break; + case "pseudonym": + aoi = BCStyle.PSEUDONYM; + break; + case "generationQualifier": + aoi = BCStyle.GENERATION; + break; + case "serialNumber": + aoi = BCStyle.SERIALNUMBER; + break; + default: + throw new CertException( + "Unknown ASN1ObjectIdentifier for " + tv[0] + " in " + tagValue); + } + tag = tv[0]; + value = tv[1]; + } + + /** + * Parse various forms of DNs into appropriate RDNs, which have the ASN1ObjectIdentifier + * + * @param delim + * @param dnString + * @return + * @throws CertException + */ + public static List<RDN> parse(final char delim, final String dnString) throws CertException { + List<RDN> lrnd = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + boolean inQuotes = false; + for (int i = 0; i < dnString.length(); ++i) { + char c = dnString.charAt(i); + if (inQuotes) { + if ('"' == c) { + inQuotes = false; + } else { + sb.append(dnString.charAt(i)); + } + } else { + if ('"' == c) { + inQuotes = true; + } else if (delim == c) { + if (sb.length() > 0) { + lrnd.add(new RDN(sb.toString())); + sb.setLength(0); + } + } else { + sb.append(dnString.charAt(i)); + } + } + } + if (sb.indexOf("=") > 0) { + lrnd.add(new RDN(sb.toString())); + } + return lrnd; + } + + @Override + public String toString() { + return tag + '=' + value; + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Split.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Split.java new file mode 100644 index 00000000..e531f2d2 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Split.java @@ -0,0 +1,127 @@ +/** + * ============LICENSE_START==================================================== org.onap.aaf + * =========================================================================== Copyright (c) 2018 + * AT&T Intellectual Property. All rights reserved. + * + * Modifications Copyright (C) 2019 IBM. =========================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. ============LICENSE_END==================================================== + */ +package org.onap.aaf.certservice.cmpv2client.external; + +/** + * Split by Char, optional Trim + * + * <p>Note: Copied from Inno to avoid linking issues. Note: I read the String split and Pattern + * split code, and we can do this more efficiently for a single Character + * + * <p>8/20/2015 + */ +public class Split { + + private static final String[] EMPTY = new String[0]; + + public static String[] split(char c, String value) { + if (value == null) { + return EMPTY; + } + + return split(c, value, 0, value.length()); + } + + public static String[] split(char c, String value, int start, int end) { + if (value == null) { + return EMPTY; + } + + // Count items to preallocate Array (memory alloc is more expensive than counting twice) + int count, idx; + for (count = 1, idx = value.indexOf(c, start); + idx >= 0 && idx < end; + idx = value.indexOf(c, ++idx), ++count) { + ; + } + String[] rv = new String[count]; + if (count == 1) { + rv[0] = value.substring(start, end); + } else { + int last = 0; + count = -1; + for (idx = value.indexOf(c, start); idx >= 0 && idx < end; + idx = value.indexOf(c, idx)) { + rv[++count] = value.substring(last, idx); + last = ++idx; + } + rv[++count] = value.substring(last, end); + } + return rv; + } + + public static String[] splitTrim(char c, String value, int start, int end) { + if (value == null) { + return EMPTY; + } + + // Count items to preallocate Array (memory alloc is more expensive than counting twice) + int count, idx; + for (count = 1, idx = value.indexOf(c, start); + idx >= 0 && idx < end; + idx = value.indexOf(c, ++idx), ++count) { + ; + } + String[] rv = new String[count]; + if (count == 1) { + rv[0] = value.substring(start, end).trim(); + } else { + int last = start; + count = -1; + for (idx = value.indexOf(c, start); idx >= 0 && idx < end; + idx = value.indexOf(c, idx)) { + rv[++count] = value.substring(last, idx).trim(); + last = ++idx; + } + rv[++count] = value.substring(last, end).trim(); + } + return rv; + } + + public static String[] splitTrim(char c, String value) { + if (value == null) { + return EMPTY; + } + return splitTrim(c, value, 0, value.length()); + } + + public static String[] splitTrim(char c, String value, int size) { + if (value == null) { + return EMPTY; + } + + int idx; + String[] rv = new String[size]; + if (size == 1) { + rv[0] = value.trim(); + } else { + int last = 0; + int count = -1; + size -= 2; + for (idx = value.indexOf(c); idx >= 0 && count < size; idx = value.indexOf(c, idx)) { + rv[++count] = value.substring(last, idx).trim(); + last = ++idx; + } + if (idx > 0) { + rv[++count] = value.substring(last, idx).trim(); + } else { + rv[++count] = value.substring(last).trim(); + } + } + return rv; + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpClientImpl.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpClientImpl.java new file mode 100644 index 00000000..fb43e3e8 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpClientImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import java.security.cert.X509Certificate; +import java.util.Date; +import org.apache.http.impl.client.CloseableHttpClient; +import org.bouncycastle.asn1.cmp.PKIMessage; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.onap.aaf.certservice.cmpv2client.api.CmpClient; +import org.onap.aaf.certservice.cmpv2client.external.CSRMeta; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the CmpClient Interface conforming to RFC4210 (Certificate Management Protocol + * (CMP)) and RFC4211 (Certificate Request Message Format (CRMF)) standards. + */ +public class CmpClientImpl implements CmpClient { + + private final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class); + private final CloseableHttpClient httpClient; + + private static final String DEFAULT_PROFILE = "RA"; + private static final String DEFAULT_CA_NAME = "Certification Authority"; + + public CmpClientImpl(CloseableHttpClient httpClient){ + this.httpClient = httpClient; + } + + @Override + public X509Certificate createCertificate( + String caName, + String profile, + CSRMeta csrMeta, + X509Certificate cert, + Date notBefore, + Date notAfter) + throws CmpClientException { + // Validate inputs for Certificate Request + validate(csrMeta, cert, caName, profile, httpClient, notBefore, notAfter); + + final CreateCertRequest certRequest = + CmpMessageBuilder.of(CreateCertRequest::new) + .with(CreateCertRequest::setIssuerDn, csrMeta.issuerx500Name()) + .with(CreateCertRequest::setSubjectDn, csrMeta.x500Name()) + .with(CreateCertRequest::setSansList, csrMeta.sans()) + .with(CreateCertRequest::setSubjectKeyPair, csrMeta.keyPair()) + .with(CreateCertRequest::setNotBefore, notBefore) + .with(CreateCertRequest::setNotAfter, notAfter) + .with(CreateCertRequest::setInitAuthPassword, csrMeta.password()) + .build(); + + final PKIMessage pkiMessage = certRequest.generateCertReq(); + Cmpv2HttpClient cmpv2HttpClient = new Cmpv2HttpClient(httpClient); + final byte[] respBytes = + cmpv2HttpClient.postRequest(pkiMessage, csrMeta.caUrl(), caName); + final PKIMessage respPkiMessage = PKIMessage.getInstance(respBytes); + // todo: add response validation and return Certificate + return null; + } + + @Override + public X509Certificate createCertificate( + String caName, + String profile, + CSRMeta csrMeta, + X509Certificate csr) + throws CmpClientException { + return createCertificate(caName, profile, csrMeta, csr, null, null); + } + + /** + * Validate inputs for Certificate Creation. + * + * @param csrMeta CSRMeta Object containing variables for creating a Certificate Request. + * @param cert Certificate object needed to validate response from CA server. + * @param incomingCaName Date specifying certificate is not valid before this date. + * @param incomingProfile Date specifying certificate is not valid after this date. + * @throws IllegalArgumentException if Before Date is set after the After Date. + */ + private void validate( + final CSRMeta csrMeta, + final X509Certificate cert, + final String incomingCaName, + final String incomingProfile, + final CloseableHttpClient httpClient, + final Date notBefore, + final Date notAfter) + throws IllegalArgumentException { + + String caName; + String caProfile; + caName = CmpUtil.isNullOrEmpty(incomingCaName) ? incomingCaName : DEFAULT_CA_NAME; + caProfile = CmpUtil.isNullOrEmpty(incomingProfile) ? incomingProfile : DEFAULT_PROFILE; + LOG.info( + "Validate before creating Certificate Request for CA :{} in Mode {} ", caName, caProfile); + + CmpUtil.notNull(csrMeta, "CSRMeta Instance"); + CmpUtil.notNull(csrMeta.x500Name(), "Subject DN"); + CmpUtil.notNull(csrMeta.issuerx500Name(), "Issuer DN"); + CmpUtil.notNull(csrMeta.password(), "IAK/RV Password"); + CmpUtil.notNull(cert, "Certificate Signing Request (CSR)"); + CmpUtil.notNull(csrMeta.caUrl(), "External CA URL"); + CmpUtil.notNull(csrMeta.keypair(), "Subject KeyPair"); + CmpUtil.notNull(httpClient, "Closeable Http Client"); + + if (notBefore != null && notAfter != null && notBefore.compareTo(notAfter) > 0) { + throw new IllegalArgumentException("Before Date is set after the After Date"); + } + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageBuilder.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageBuilder.java new file mode 100644 index 00000000..ee8129c6 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** Generic Builder Class for creating CMP Message. */ +public class CmpMessageBuilder<T> { + + private final Supplier<T> instantiator; + private final List<Consumer<T>> instanceModifiers = new ArrayList<>(); + + public CmpMessageBuilder(Supplier<T> instantiator) { + this.instantiator = instantiator; + } + + public static <T> CmpMessageBuilder<T> of(Supplier<T> instantiator) { + return new CmpMessageBuilder<>(instantiator); + } + + public <U> CmpMessageBuilder<T> with(BiConsumer<T, U> consumer, U value) { + Consumer<T> c = instance -> consumer.accept(instance, value); + instanceModifiers.add(c); + return this; + } + + public T build() { + T value = instantiator.get(); + instanceModifiers.forEach(modifier -> modifier.accept(value)); + instanceModifiers.clear(); + return value; + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageHelper.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageHelper.java new file mode 100644 index 00000000..8c470c7f --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageHelper.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import static org.onap.aaf.certservice.cmpv2client.impl.CmpUtil.generateProtectedBytes; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Signature; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.cmp.PBMParameter; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIHeader; +import org.bouncycastle.asn1.cmp.PKIMessage; +import org.bouncycastle.asn1.crmf.CertRequest; +import org.bouncycastle.asn1.crmf.OptionalValidity; +import org.bouncycastle.asn1.crmf.POPOSigningKey; +import org.bouncycastle.asn1.crmf.ProofOfPossession; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.ExtensionsGenerator; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.Time; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class CmpMessageHelper { + + private static final Logger LOG = LoggerFactory.getLogger(CmpMessageHelper.class); + private static final AlgorithmIdentifier OWF_ALGORITHM = + new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.3.14.3.2.26")); + private static final AlgorithmIdentifier MAC_ALGORITHM = + new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.2.9")); + private static final ASN1ObjectIdentifier PASSWORD_BASED_MAC = + new ASN1ObjectIdentifier("1.2.840.113533.7.66.13"); + + private CmpMessageHelper() {} + + /** + * Creates an Optional Validity, which is used to specify how long the returned cert should be + * valid for. + * + * @param notBefore Date specifying certificate is not valid before this date. + * @param notAfter Date specifying certificate is not valid after this date. + * @return {@link OptionalValidity} that can be set for certificate on external CA. + */ + public static OptionalValidity generateOptionalValidity( + final Date notBefore, final Date notAfter) { + LOG.info("Generating Optional Validity from Date objects"); + ASN1EncodableVector optionalValidityV = new ASN1EncodableVector(); + if (notBefore != null) { + Time nb = new Time(notBefore); + optionalValidityV.add(new DERTaggedObject(true, 0, nb)); + } + if (notAfter != null) { + Time na = new Time(notAfter); + optionalValidityV.add(new DERTaggedObject(true, 1, na)); + } + return OptionalValidity.getInstance(new DERSequence(optionalValidityV)); + } + + /** + * Create Extensions from Subject Alternative Names. + * + * @return {@link Extensions}. + */ + public static Extensions generateExtension(final List<String> sansList) + throws CmpClientException { + LOG.info("Generating Extensions from Subject Alternative Names"); + final ExtensionsGenerator extGenerator = new ExtensionsGenerator(); + final GeneralName[] sansGeneralNames = getGeneralNames(sansList); + // KeyUsage + try { + final KeyUsage keyUsage = + new KeyUsage( + KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.nonRepudiation); + extGenerator.addExtension(Extension.keyUsage, false, new DERBitString(keyUsage)); + extGenerator.addExtension( + Extension.subjectAlternativeName, false, new GeneralNames(sansGeneralNames)); + } catch (IOException ioe) { + CmpClientException cmpClientException = + new CmpClientException( + "Exception occurred while creating proof of possession for PKIMessage", ioe); + LOG.error("Exception occurred while creating proof of possession for PKIMessage"); + throw cmpClientException; + } + return extGenerator.generate(); + } + + public static GeneralName[] getGeneralNames(List<String> sansList) { + final List<GeneralName> nameList = new ArrayList<>(); + for (String san : sansList) { + nameList.add(new GeneralName(GeneralName.dNSName, san)); + } + final GeneralName[] sansGeneralNames = new GeneralName[nameList.size()]; + nameList.toArray(sansGeneralNames); + return sansGeneralNames; + } + + /** + * Method generates Proof-of-Possession (POP) of Private Key. To allow a CA/RA to properly + * validity binding between an End Entity and a Key Pair, the PKI Operations specified here make + * it possible for an End Entity to prove that it has possession of the Private Key corresponding + * to the Public Key for which a Certificate is requested. + * + * @param certRequest Certificate request that requires proof of possession + * @param keypair keypair associated with the subject sending the certificate request + * @return {@link ProofOfPossession}. + * @throws CmpClientException A general-purpose Cmp client exception. + */ + public static ProofOfPossession generateProofOfPossession( + final CertRequest certRequest, final KeyPair keypair) throws CmpClientException { + ProofOfPossession proofOfPossession; + try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + final DEROutputStream derOutputStream = new DEROutputStream(byteArrayOutputStream); + derOutputStream.writeObject(certRequest); + + byte[] popoProtectionBytes = byteArrayOutputStream.toByteArray(); + final String sigalg = PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(); + final Signature signature = Signature.getInstance(sigalg, BouncyCastleProvider.PROVIDER_NAME); + signature.initSign(keypair.getPrivate()); + signature.update(popoProtectionBytes); + DERBitString bs = new DERBitString(signature.sign()); + + proofOfPossession = + new ProofOfPossession( + new POPOSigningKey( + null, new AlgorithmIdentifier(new ASN1ObjectIdentifier(sigalg)), bs)); + } catch (IOException + | NoSuchProviderException + | NoSuchAlgorithmException + | InvalidKeyException + | SignatureException ex) { + CmpClientException cmpClientException = + new CmpClientException( + "Exception occurred while creating proof " + "of possession for PKIMessage", ex); + LOG.error("Exception occurred while creating proof of possession for PKIMessage"); + throw cmpClientException; + } + return proofOfPossession; + } + + /** + * Generic code to create Algorithm Identifier for protection of PKIMessage. + * + * @return Algorithm Identifier + */ + public static AlgorithmIdentifier protectionAlgoIdentifier(int iterations, byte[] salt) { + ASN1Integer iteration = new ASN1Integer(iterations); + DEROctetString derSalt = new DEROctetString(salt); + + PBMParameter pp = new PBMParameter(derSalt, OWF_ALGORITHM, iteration, MAC_ALGORITHM); + return new AlgorithmIdentifier(PASSWORD_BASED_MAC, pp); + } + + /** + * Adds protection to the PKIMessage via a specified protection algorithm. + * + * @param password password used to authenticate PkiMessage with external CA + * @param pkiHeader Header of PKIMessage containing generic details for any PKIMessage + * @param pkiBody Body of PKIMessage containing specific details for certificate request + * @return Protected Pki Message + * @throws CmpClientException Wraps several exceptions into one general-purpose exception. + */ + public static PKIMessage protectPkiMessage( + PKIHeader pkiHeader, PKIBody pkiBody, String password, int iterations, byte[] salt) + throws CmpClientException { + + byte[] raSecret = password.getBytes(); + byte[] basekey = new byte[raSecret.length + salt.length]; + System.arraycopy(raSecret, 0, basekey, 0, raSecret.length); + System.arraycopy(salt, 0, basekey, raSecret.length, salt.length); + byte[] out; + try { + MessageDigest dig = + MessageDigest.getInstance( + OWF_ALGORITHM.getAlgorithm().getId(), BouncyCastleProvider.PROVIDER_NAME); + for (int i = 0; i < iterations; i++) { + basekey = dig.digest(basekey); + dig.reset(); + } + byte[] protectedBytes = generateProtectedBytes(pkiHeader, pkiBody); + Mac mac = + Mac.getInstance(MAC_ALGORITHM.getAlgorithm().getId(), BouncyCastleProvider.PROVIDER_NAME); + SecretKey key = new SecretKeySpec(basekey, MAC_ALGORITHM.getAlgorithm().getId()); + mac.init(key); + mac.reset(); + mac.update(protectedBytes, 0, protectedBytes.length); + out = mac.doFinal(); + } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException ex) { + CmpClientException cmpClientException = + new CmpClientException( + "Exception occurred while generating " + "proof of possession for PKIMessage", ex); + LOG.error("Exception occured while generating the proof of possession for PKIMessage"); + throw cmpClientException; + } + DERBitString bs = new DERBitString(out); + + return new PKIMessage(pkiHeader, pkiBody, bs); + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpUtil.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpUtil.java new file mode 100644 index 00000000..b7452fcf --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpUtil.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Date; +import java.util.Objects; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIHeader; +import org.bouncycastle.asn1.cmp.PKIHeaderBuilder; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.GeneralName; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class CmpUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(CmpUtil.class); + private static final SecureRandom secureRandom = new SecureRandom(); + + private CmpUtil() {} + + /** + * Validates specified object reference is not null. + * + * @param argument T - the type of the reference. + * @param message message - detail message to be used in the event that a NullPointerException is + * thrown. + * @return The Object if not null + */ + public static <T> T notNull(T argument, String message) { + return Objects.requireNonNull(argument, message + " must not be null"); + } + + /** + * Validates String object reference is not null and not empty. + * + * @param stringArg String Object that need to be validated. + * @return boolean + */ + public static boolean isNullOrEmpty(String stringArg) { + return (stringArg != null && !stringArg.trim().isEmpty()); + } + + /** + * Creates a random number than can be used for sendernonce, transactionId and salts. + * + * @return bytes containing a random number string representing a nonce + */ + static byte[] createRandomBytes() { + LOGGER.info("Generating random array of bytes"); + byte[] randomBytes = new byte[16]; + secureRandom.nextBytes(randomBytes); + return randomBytes; + } + + /** + * Creates a random integer than can be used to represent a transactionId or determine the number + * iterations in a protection algorithm. + * + * @return bytes containing a random number string representing a nonce + */ + static int createRandomInt(int range) { + LOGGER.info("Generating random integer"); + return secureRandom.nextInt(range) + 1000; + } + + /** + * Generates protected bytes of a combined PKIHeader and PKIBody. + * + * @param header Header of PKIMessage containing common parameters + * @param body Body of PKIMessage containing specific information for message + * @return bytes representing the PKIHeader and PKIBody thats to be protected + */ + static byte[] generateProtectedBytes(PKIHeader header, PKIBody body) throws CmpClientException { + LOGGER.info("Generating array of bytes representing PkiHeader and PkiBody"); + byte[] res; + ASN1EncodableVector vector = new ASN1EncodableVector(); + vector.add(header); + vector.add(body); + ASN1Encodable protectedPart = new DERSequence(vector); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + DEROutputStream out = new DEROutputStream(baos); + out.writeObject(protectedPart); + res = baos.toByteArray(); + } catch (IOException ioe) { + CmpClientException cmpClientException = + new CmpClientException("IOException occurred while creating protectedBytes", ioe); + LOGGER.error("IOException occurred while creating protectedBytes"); + throw cmpClientException; + } + return res; + } + + /** + * Generates a PKIHeader Builder object. + * + * @param subjectDn distinguished name of Subject + * @param issuerDn distinguished name of external CA + * @param protectionAlg protection Algorithm used to protect PKIMessage + * @return PKIHeaderBuilder + */ + static PKIHeader generatePkiHeader( + X500Name subjectDn, X500Name issuerDn, AlgorithmIdentifier protectionAlg) { + LOGGER.info("Generating a Pki Header Builder"); + PKIHeaderBuilder pkiHeaderBuilder = + new PKIHeaderBuilder( + PKIHeader.CMP_2000, new GeneralName(subjectDn), new GeneralName(issuerDn)); + + pkiHeaderBuilder.setMessageTime(new ASN1GeneralizedTime(new Date())); + pkiHeaderBuilder.setSenderNonce(new DEROctetString(createRandomBytes())); + pkiHeaderBuilder.setTransactionID(new DEROctetString(createRandomBytes())); + pkiHeaderBuilder.setProtectionAlg(protectionAlg); + + return pkiHeaderBuilder.build(); + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/Cmpv2HttpClient.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/Cmpv2HttpClient.java new file mode 100644 index 00000000..b1f96333 --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/Cmpv2HttpClient.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.bouncycastle.asn1.cmp.PKIMessage; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Cmpv2HttpClient { + + private static final Logger LOG = LoggerFactory.getLogger(Cmpv2HttpClient.class); + + private static final String CONTENT_TYPE = "Content-type"; + private static final String CMP_REQUEST_MIMETYPE = "application/pkixcmp"; + private CloseableHttpClient httpClient; + + public Cmpv2HttpClient(CloseableHttpClient httpClient){ + this.httpClient = httpClient; + } + + public byte[] postRequest( + final PKIMessage pkiMessage, + final String urlString, + final String caName) + throws CmpClientException { + try (final ByteArrayOutputStream byteArrOutputStream = new ByteArrayOutputStream()) { + final HttpPost postRequest = new HttpPost(urlString); + final byte[] requestBytes = pkiMessage.getEncoded(); + + postRequest.setEntity(new ByteArrayEntity(requestBytes)); + postRequest.setHeader(CONTENT_TYPE, CMP_REQUEST_MIMETYPE); + + try (CloseableHttpResponse response = httpClient.execute(postRequest)) { + response.getEntity().writeTo(byteArrOutputStream); + } + return byteArrOutputStream.toByteArray(); + } catch (IOException ioe) { + CmpClientException cmpClientException = + new CmpClientException("IOException error while trying to connect CA " + caName, ioe); + LOG.error("IOException error {}, while trying to connect CA {}", ioe.getMessage(), caName); + throw cmpClientException; + } + } +} diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CreateCertRequest.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CreateCertRequest.java new file mode 100644 index 00000000..aa544e7f --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CreateCertRequest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import static org.onap.aaf.certservice.cmpv2client.impl.CmpUtil.createRandomBytes; +import static org.onap.aaf.certservice.cmpv2client.impl.CmpUtil.createRandomInt; +import static org.onap.aaf.certservice.cmpv2client.impl.CmpUtil.generatePkiHeader; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIHeader; +import org.bouncycastle.asn1.cmp.PKIMessage; +import org.bouncycastle.asn1.crmf.AttributeTypeAndValue; +import org.bouncycastle.asn1.crmf.CRMFObjectIdentifiers; +import org.bouncycastle.asn1.crmf.CertReqMessages; +import org.bouncycastle.asn1.crmf.CertReqMsg; +import org.bouncycastle.asn1.crmf.CertRequest; +import org.bouncycastle.asn1.crmf.CertTemplateBuilder; +import org.bouncycastle.asn1.crmf.ProofOfPossession; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the CmpClient Interface conforming to RFC4210 (Certificate Management Protocol + * (CMP)) and RFC4211 (Certificate Request Message Format (CRMF)) standards. + */ +class CreateCertRequest { + + private static final Logger LOG = LoggerFactory.getLogger(CreateCertRequest.class); + + private X500Name issuerDn; + private X500Name subjectDn; + private List<String> sansList; + private KeyPair subjectKeyPair; + private Date notBefore; + private Date notAfter; + private String initAuthPassword; + + private static final int iterations = createRandomInt(5000); + private static final byte[] salt = createRandomBytes(); + private final int certReqId = createRandomInt(Integer.MAX_VALUE); + + public void setIssuerDn(X500Name issuerDn) { + this.issuerDn = issuerDn; + } + + public void setSubjectDn(X500Name subjectDn) { + this.subjectDn = subjectDn; + } + + public void setSansList(List<String> sansList) { + this.sansList = sansList; + } + + public void setSubjectKeyPair(KeyPair subjectKeyPair) { + this.subjectKeyPair = subjectKeyPair; + } + + public void setNotBefore(Date notBefore) { + this.notBefore = notBefore; + } + + public void setNotAfter(Date notAfter) { + this.notAfter = notAfter; + } + + public void setInitAuthPassword(String initAuthPassword) { + this.initAuthPassword = initAuthPassword; + } + + /** + * Method to create {@link PKIMessage} from {@link CertRequest},{@link ProofOfPossession}, {@link + * CertReqMsg}, {@link CertReqMessages}, {@link PKIHeader} and {@link PKIBody}. + * + * @return {@link PKIMessage} + */ + public PKIMessage generateCertReq() throws CmpClientException { + final CertTemplateBuilder certTemplateBuilder = + new CertTemplateBuilder() + .setIssuer(issuerDn) + .setSubject(subjectDn) + .setExtensions(CmpMessageHelper.generateExtension(sansList)) + .setValidity(CmpMessageHelper.generateOptionalValidity(notBefore, notAfter)) + .setPublicKey( + SubjectPublicKeyInfo.getInstance(subjectKeyPair.getPublic().getEncoded())); + + final CertRequest certRequest = new CertRequest(certReqId, certTemplateBuilder.build(), null); + final ProofOfPossession proofOfPossession = + CmpMessageHelper.generateProofOfPossession(certRequest, subjectKeyPair); + + final AttributeTypeAndValue[] attrTypeVal = { + new AttributeTypeAndValue( + CRMFObjectIdentifiers.id_regCtrl_regToken, new DERUTF8String(initAuthPassword)) + }; + + final CertReqMsg certReqMsg = new CertReqMsg(certRequest, proofOfPossession, attrTypeVal); + final CertReqMessages certReqMessages = new CertReqMessages(certReqMsg); + + final PKIHeader pkiHeader = + generatePkiHeader( + subjectDn, issuerDn, CmpMessageHelper.protectionAlgoIdentifier(iterations, salt)); + final PKIBody pkiBody = new PKIBody(PKIBody.TYPE_CERT_REQ, certReqMessages); + + return CmpMessageHelper.protectPkiMessage( + pkiHeader, pkiBody, initAuthPassword, iterations, salt); + } +} diff --git a/certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java b/certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java new file mode 100644 index 00000000..74eb098f --- /dev/null +++ b/certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2019 Ericsson Software Technology AB. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.onap.aaf.certservice.cmpv2Client; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.bouncycastle.cert.CertException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; +import org.onap.aaf.certservice.cmpv2client.external.CSRMeta; +import org.onap.aaf.certservice.cmpv2client.external.RDN; +import org.onap.aaf.certservice.cmpv2client.impl.CmpClientImpl; + +class Cmpv2ClientTest { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private CSRMeta csrMeta; + private Date notBefore; + private Date notAfter; + + @Mock KeyPairGenerator kpg; + + @Mock X509Certificate cert; + + @Mock CloseableHttpClient httpClient; + + @Mock CloseableHttpResponse httpResponse; + + @Mock HttpEntity httpEntity; + + private static KeyPair keyPair; + private static ArrayList<RDN> rdns; + + @BeforeEach + void setUp() throws NoSuchProviderException, NoSuchAlgorithmException { + KeyPairGenerator keyGenerator; + keyGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + keyGenerator.initialize(2048); + keyPair = keyGenerator.generateKeyPair(); + rdns = new ArrayList<>(); + try { + rdns.add(new RDN("O=CommonCompany")); + } catch (CertException e) { + e.printStackTrace(); + } + initMocks(this); + } + + @Test + void shouldReturnValidPkiMessageWhenCreateCertificateRequestMessageMethodCalledWithValidCsr() + throws Exception { + // given + Date beforeDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); + Date afterDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2020/11/11 12:00:00"); + setCsrMetaValuesAndDateValues( + rdns, + "CN=CommonName", + "CN=ManagementCA", + "CommonName.com", + "CommonName@cn.com", + "password", + "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + beforeDate, + afterDate); + when(httpClient.execute(any())).thenReturn(httpResponse); + when(httpResponse.getEntity()).thenReturn(httpEntity); + + try (final InputStream is = + this.getClass().getResourceAsStream("/ReturnedSuccessPKIMessageWithCertificateFile"); + BufferedInputStream bis = new BufferedInputStream(is)) { + + byte[] ba = IOUtils.toByteArray(bis); + doAnswer( + invocation -> { + OutputStream os = (ByteArrayOutputStream) invocation.getArguments()[0]; + os.write(ba); + return null; + }) + .when(httpEntity) + .writeTo(any(OutputStream.class)); + } + CmpClientImpl cmpClient = spy(new CmpClientImpl(httpClient)); + // when + Certificate certificate = + cmpClient.createCertificate("data", "RA", csrMeta, cert, notBefore, notAfter); + // then + assertNull(certificate); + } + + @Test + void shouldThrowIllegalArgumentExceptionWhencreateCertificateCalledWithInvalidCsr() + throws ParseException { + // given + Date beforeDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2020/11/11 12:00:00"); + Date afterDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); + setCsrMetaValuesAndDateValues( + rdns, + "CN=CommonName", + "CN=ManagementCA", + "CommonName.com", + "CommonName@cn.com", + "password", + "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + beforeDate, + afterDate); + CmpClientImpl cmpClient = new CmpClientImpl(httpClient); + // then + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + cmpClient.createCertificate( + "data", "RA", csrMeta, cert, notBefore, notAfter)); + } + + @Test + void shouldThrowIOExceptionWhenCreateCertificateCalledWithNoServerAvailable() + throws IOException, ParseException { + // given + Date beforeDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); + Date afterDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2020/11/11 12:00:00"); + setCsrMetaValuesAndDateValues( + rdns, + "CN=Common", + "CN=CommonCA", + "Common.com", + "Common@cn.com", + "myPassword", + "http://127.0.0.1/ejbca/publicweb/cmp/cmpTest", + beforeDate, + afterDate); + when(httpClient.execute(any())).thenThrow(IOException.class); + CmpClientImpl cmpClient = spy(new CmpClientImpl(httpClient)); + // then + Assertions.assertThrows( + CmpClientException.class, + () -> + cmpClient.createCertificate( + "data", "RA", csrMeta, cert, notBefore, notAfter)); + } + + private void setCsrMetaValuesAndDateValues( + List<RDN> rdns, + String cn, + String issuerCn, + String san, + String email, + String password, + String externalCaUrl, + Date notBefore, + Date notAfter) { + csrMeta = new CSRMeta(rdns); + csrMeta.cn(cn); + csrMeta.san(san); + csrMeta.password(password); + csrMeta.email(email); + csrMeta.issuerCn(issuerCn); + when(kpg.generateKeyPair()).thenReturn(keyPair); + csrMeta.keypair(); + csrMeta.caUrl(externalCaUrl); + + this.notBefore = notBefore; + this.notAfter = notAfter; + } +} diff --git a/certService/src/test/resources/ReturnedFailurePKIMessageBadPassword b/certService/src/test/resources/ReturnedFailurePKIMessageBadPassword new file mode 100644 index 00000000..7d815814 --- /dev/null +++ b/certService/src/test/resources/ReturnedFailurePKIMessageBadPassword @@ -0,0 +1,2 @@ +0‚00ä010UManagementCA¤T0R10U
CN=CommonName1 0 *†H†÷
CommonName@cn.com10U +
CommonCompany 20191127135043Z¤oxeå×Öpî1Â`ï¥
›ˆ¢ŠSI\q–eè#«¦eþCÑÁrZÇÊ’ˆa®·h0f0d0[YFailed to verify message using both Global Shared Secret and CMP RA Authentication Secret
\ No newline at end of file diff --git a/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile Binary files differnew file mode 100644 index 00000000..94cc3461 --- /dev/null +++ b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile @@ -13,7 +13,7 @@ ============LICENSE_END========================================================= --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -51,6 +51,8 @@ <docker-maven-plugin.version>0.33.0</docker-maven-plugin.version> <springdoc-openapi-maven-plugin.version>0.2</springdoc-openapi-maven-plugin.version> <gson.version>2.8.6</gson.version> + <httpcomponents.version>4.5.6</httpcomponents.version> + <commons-io.version>2.6</commons-io.version> <docker-maven-plugin.version>0.33.0</docker-maven-plugin.version> <junit.version>5.5.2</junit.version> <mockito-junit-jupiter.version>2.17.0</mockito-junit-jupiter.version> @@ -96,6 +98,24 @@ </configuration> </plugin> <plugin> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-maven-plugin</artifactId> + <version>${springdoc-openapi-maven-plugin.version}</version> + <executions> + <execution> + <phase>integration-test</phase> + <goals> + <goal>generate</goal> + </goals> + </execution> + </executions> + <configuration> + <apiDocsUrl>${springdoc-openapi-maven-plugin.apiDocsUrl}</apiDocsUrl> + <outputFileName>api-docs.json</outputFileName> + <outputDir>${project.build.directory}</outputDir> + </configuration> + </plugin> + <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot-starter.version}</version> @@ -228,6 +248,16 @@ <version>${gson.version}</version> </dependency> <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>${httpcomponents.version}</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${commons-io.version}</version> + </dependency> + <dependency> <!-- Import dependency management from Spring Boot --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> |