Refactoring of Cmpv2Client code for sending CertRequest
authorEmmettCox <emmett.cox@est.tech>
Mon, 17 Feb 2020 13:54:05 +0000 (13:54 +0000)
committerEmmettCox <emmett.cox@est.tech>
Thu, 20 Feb 2020 14:28:00 +0000 (14:28 +0000)
Issue-ID: AAF-1036
Signed-off-by: EmmettCox <emmett.cox@est.tech>
Change-Id: Ic0d95b35abb3ca2406b77bbe6e0cd51da0968684

18 files changed:
certService/pom.xml
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/api/CmpClient.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/CmpClientException.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/exceptions/PkiErrorException.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/CSRMeta.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Factory.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/RDN.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/external/Split.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpClientImpl.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageBuilder.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpMessageHelper.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpUtil.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/Cmpv2HttpClient.java [new file with mode: 0644]
certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CreateCertRequest.java [new file with mode: 0644]
certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java [new file with mode: 0644]
certService/src/test/resources/ReturnedFailurePKIMessageBadPassword [new file with mode: 0644]
certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile [new file with mode: 0644]
pom.xml

index 2098843..5fbd5b1 100644 (file)
@@ -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>
             <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 (file)
index 0000000..feee3ee
--- /dev/null
@@ -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 (file)
index 0000000..7f7d4ae
--- /dev/null
@@ -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 (file)
index 0000000..965ce6f
--- /dev/null
@@ -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 (file)
index 0000000..7655b02
--- /dev/null
@@ -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 (file)
index 0000000..7072abf
--- /dev/null
@@ -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 (file)
index 0000000..512a76e
--- /dev/null
@@ -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 (file)
index 0000000..e531f2d
--- /dev/null
@@ -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 (file)
index 0000000..fb43e3e
--- /dev/null
@@ -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 (file)
index 0000000..ee8129c
--- /dev/null
@@ -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 (file)
index 0000000..8c470c7
--- /dev/null
@@ -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 (file)
index 0000000..b7452fc
--- /dev/null
@@ -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 (file)
index 0000000..b1f9633
--- /dev/null
@@ -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 (file)
index 0000000..aa544e7
--- /dev/null
@@ -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 (file)
index 0000000..74eb098
--- /dev/null
@@ -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 (file)
index 0000000..7d81581
--- /dev/null
@@ -0,0 +1,2 @@
+0\82\ 100\81Ã\ 2\ 1\ 2¤\190\171\150\13\ 6\ 3U\ 4\ 3\f\fManagementCA¤T0R1\160\14\ 6\ 3U\ 4\ 3\f\rCN=CommonName1 0\1e\ 6 *\86H\86÷\r\ 1 \ 1\16\11CommonName@cn.com1\160\14\ 6\ 3U\ 4
+\f\rCommonCompany \11\18\ f20191127135043Z¤\12\ 4\10\7fox\veå×Öpî\7f­1Â`ï¥\12\ 4\10\8d\r\9b\88¢\8aSI\ 2\q\96eè#«¦\12\ 4\10eþC\1eÑÁrZÇ\1f\10Ê\92\88a®·h0f0d\ 2\ 1\ 20[\fYFailed to verify message using both Global Shared Secret and CMP RA Authentication Secret\ 3\ 2\ 5 
\ No newline at end of file
diff --git a/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile
new file mode 100644 (file)
index 0000000..94cc346
Binary files /dev/null and b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile differ
diff --git a/pom.xml b/pom.xml
index 5366313..69d8b54 100644 (file)
--- a/pom.xml
+++ b/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>
@@ -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>
     <build>
         <pluginManagement>
             <plugins>
+                <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.springdoc</groupId>
                     <artifactId>springdoc-openapi-maven-plugin</artifactId>
                 <artifactId>gson</artifactId>
                 <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>