aboutsummaryrefslogtreecommitdiffstats
path: root/certService/src
diff options
context:
space:
mode:
authorEmmettCox <emmett.cox@est.tech>2020-02-17 13:54:05 +0000
committerEmmettCox <emmett.cox@est.tech>2020-02-20 14:28:00 +0000
commit153a7ac15d804178e7c52f69117e1a9478862df1 (patch)
tree2a574f2a593b55531631ba02c97f318a136f1510 /certService/src
parente93c679bc9c22e034ba93a48460830716e1f7457 (diff)
Refactoring of Cmpv2Client code for sending CertRequest
Issue-ID: AAF-1036 Signed-off-by: EmmettCox <emmett.cox@est.tech> Change-Id: Ic0d95b35abb3ca2406b77bbe6e0cd51da0968684
Diffstat (limited to 'certService/src')
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/api/CmpClient.java83
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/CmpClientException.java38
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/PkiErrorException.java37
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/CSRMeta.java202
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Factory.java54
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/RDN.java145
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Split.java127
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpClientImpl.java126
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageBuilder.java51
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageHelper.java241
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpUtil.java141
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/Cmpv2HttpClient.java65
-rw-r--r--certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CreateCertRequest.java129
-rw-r--r--certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java211
-rw-r--r--certService/src/test/resources/ReturnedFailurePKIMessageBadPassword2
-rw-r--r--certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFilebin0 -> 4297 bytes
16 files changed, 1652 insertions, 0 deletions
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ä010U ManagementCA¤T0R10U CN=CommonName1 0 *†H†÷  CommonName@cn.com10U
+ CommonCompany 20191127135043Z¤ox eå×Ö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
new file mode 100644
index 00000000..94cc3461
--- /dev/null
+++ b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile
Binary files differ