diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 52 | ||||
-rw-r--r-- | README.md | 84 | ||||
-rw-r--r-- | certService/src/main/java/org/onap/oom/certservice/cmpv2client/impl/CmpClientImpl.java | 37 | ||||
-rw-r--r-- | certService/src/main/resources/application.properties | 3 | ||||
-rw-r--r-- | certService/src/test/java/org/onap/oom/certservice/cmpv2client/Cmpv2ClientTest.java | 89 | ||||
-rw-r--r-- | compose-resources/cmpServers.json | 4 | ||||
-rwxr-xr-x | compose-resources/ejbca-configuration.sh | 10 | ||||
-rw-r--r-- | docker-compose.yml | 1 | ||||
-rwxr-xr-x | parseCertServiceResponse.sh | 4 |
10 files changed, 263 insertions, 22 deletions
@@ -3,6 +3,7 @@ target/ !**/src/test/** **/var compose-resources/client-volume +compose-resources/certs-from-curl ### STS ### .apt_generated @@ -32,3 +32,55 @@ stop-backend: @echo "##### Stop Cert Service #####" docker-compose down @echo "##### DONE #####" + +send-initialization-request: + @echo "##### Create folder for certificates from curl: `pwd`/compose-resources/certs-from-curl/ #####" + mkdir -p `pwd`/compose-resources/certs-from-curl/ + @echo "##### Generate CSR and Key #####" + openssl req -new -newkey rsa:2048 -nodes -keyout `pwd`/compose-resources/certs-from-curl/ir.key \ + -out `pwd`/compose-resources/certs-from-curl/ir.csr \ + -subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=onap.org" \ + -addext "subjectAltName = DNS:test.onap.org" + @echo "##### Send Initialization Request #####" + curl -sN https://localhost:8443/v1/certificate/RA -H "PK: $$(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "CSR: $$(cat ./compose-resources/certs-from-curl/ir.csr | base64 | tr -d \\n)" \ + --cert `pwd`/certs/cmpv2Issuer-cert.pem \ + --key `pwd`/certs/cmpv2Issuer-key.pem \ + --cacert `pwd`/certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "ir" + +send-key-update-request: verify-initialization-request-files-exist + @echo "##### Generate CSR and Key #####" + openssl req -new -newkey rsa:2048 -nodes -keyout `pwd`/compose-resources/certs-from-curl/kur.key \ + -out `pwd`/compose-resources/certs-from-curl/kur.csr \ + -subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=onap.org" \ + -addext "subjectAltName = DNS:test.onap.org" + @echo "##### Send Key Update Request #####" + curl -sN https://localhost:8443/v1/certificate-update/RA -H "PK: $$(cat ./compose-resources/certs-from-curl/kur.key | base64 | tr -d \\n)" \ + -H "CSR: $$(cat ./compose-resources/certs-from-curl/kur.csr | base64 | tr -d \\n)" \ + -H "OLD_PK: $$(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "OLD_CERT: $$(cat ./compose-resources/certs-from-curl/ir-cert.pem | base64 | tr -d \\n)" \ + --cert `pwd`/certs/cmpv2Issuer-cert.pem \ + --key `pwd`/certs/cmpv2Issuer-key.pem \ + --cacert `pwd`/certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "kur" + +send-certification-request: verify-initialization-request-files-exist + @echo "##### Generate CSR and Key #####" + openssl req -new -newkey rsa:2048 -nodes -keyout `pwd`/compose-resources/certs-from-curl/cr.key \ + -out `pwd`/compose-resources/certs-from-curl/cr.csr \ + -subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=new-onap.org" \ + -addext "subjectAltName = DNS:test.onap.org" + @echo "##### Send Key Update Request #####" + curl -sN https://localhost:8443/v1/certificate-update/RA -H "PK: $$(cat ./compose-resources/certs-from-curl/cr.key | base64 | tr -d \\n)" \ + -H "CSR: $$(cat ./compose-resources/certs-from-curl/cr.csr | base64 | tr -d \\n)" \ + -H "OLD_PK: $$(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "OLD_CERT: $$(cat ./compose-resources/certs-from-curl/ir-cert.pem | base64 | tr -d \\n)" \ + --cert `pwd`/certs/cmpv2Issuer-cert.pem \ + --key `pwd`/certs/cmpv2Issuer-key.pem \ + --cacert `pwd`/certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "cr" + +verify-initialization-request-files-exist: + ifeq (,$(wildcard compose-resources/certs-from-curl/ir.key)) + ifeq (,$(wildcard compose-resources/certs-from-curl/ir-cert.pem)) + $(error Execute send-initialization-request first) + endif + endif @@ -54,6 +54,90 @@ make run-client make stop-backend ``` +### Generating certificates via REST Api +#### Requirements +* OpenSSL +* cURL +* jq (for parseCertServiceResponse.sh script) +#### Initialization Request +1. Create Certificate Signing Request and Private Key +``` +openssl req -new -newkey rsa:2048 -nodes -keyout ./compose-resources/certs-from-curl/ir.key \ + -out ./compose-resources/certs-from-curl/ir.csr \ + -subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=onap.org" \ + -addext "subjectAltName = DNS:test.onap.org" +``` +2. Send Initialization Request +``` +curl -s https://localhost:8443/v1/certificate/RA -H "PK: $(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "CSR: $(cat ./compose-resources/certs-from-curl/ir.csr | base64 | tr -d \\n)" \ + --cert ./certs/cmpv2Issuer-cert.pem \ + --key ./certs/cmpv2Issuer-key.pem \ + --cacert ./certs/cacert.pem +``` +to parse the response pipe the output to `parseCertserviceResponse.sh` script, providing prefix as argument +``` +curl -sN https://localhost:8443/v1/certificate/RA -H "PK: $(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "CSR: $(cat ./compose-resources/certs-from-curl/ir.csr | base64 | tr -d \\n)" \ + --cert ./certs/cmpv2Issuer-cert.pem \ + --key ./certs/cmpv2Issuer-key.pem \ + --cacert ./certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "ir" +``` + +#### Update Request +1. Create Certificate Signing Request and Private Key - same as for Initialization Request. +When CSR data (like Subject and SANS) is unchanged, Key Update Request will be performed. +Otherwise Certification Request will be performed. +Example for KUR: +``` +openssl req -new -newkey rsa:2048 -nodes -keyout ./compose-resources/certs-from-curl/kur.key \ +-out ./compose-resources/certs-from-curl/kur.csr \ +-subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=onap.org" \ +-addext "subjectAltName = DNS:test.onap.org" +``` +Example for CR: +``` +openssl req -new -newkey rsa:2048 -nodes -keyout ./compose-resources/certs-from-curl/cr.key \ +-out ./compose-resources/certs-from-curl/cr.csr \ +-subj "/C=US/ST=California/L=San-Francisco/O=ONAP/OU=Linux-Foundation/CN=new-onap.org" \ +-addext "subjectAltName = DNS:test.onap.org" +``` +2. Send Update Request. +Example for KUR: +``` +curl -sN https://localhost:8443/v1/certificate-update/RA -H "PK: $(cat ./compose-resources/certs-from-curl/kur.key | base64 | tr -d \\n)" \ + -H "CSR: $(cat ./compose-resources/certs-from-curl/kur.csr | base64 | tr -d \\n)" \ + -H "OLDPK: $(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "OLDCERT: $(cat ./compose-resources/certs-from-curl/ir-cert.pem | base64 | tr -d \\n)" \ + --cert ./certs/cmpv2Issuer-cert.pem \ + --key ./certs/cmpv2Issuer-key.pem \ + --cacert ./certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "kur" +``` +Example CR: +``` +curl -sN https://localhost:8443/v1/certificate-update/RA -H "PK: $$(cat ./compose-resources/certs-from-curl/cr.key | base64 | tr -d \\n)" \ + -H "CSR: $$(cat ./compose-resources/certs-from-curl/cr.csr | base64 | tr -d \\n)" \ + -H "OLD_PK: $$(cat ./compose-resources/certs-from-curl/ir.key | base64 | tr -d \\n)" \ + -H "OLD_CERT: $$(cat ./compose-resources/certs-from-curl/ir-cert.pem | base64 | tr -d \\n)" \ + --cert ./certs/cmpv2Issuer-cert.pem \ + --key ./certs/cmpv2Issuer-key.pem \ + --cacert ./certs/cacert.pem | `pwd`/parseCertServiceResponse.sh "cr" +``` + +#### Using makefile +1. Perform Initialization Request: +``` +make send-initialization-request +``` +2. Perform Update Request: +``` +make send-key-update-request +``` +or: +``` +make send-certification-request +``` + ### OOM CertService CSITs #### CSIT repository ``` diff --git a/certService/src/main/java/org/onap/oom/certservice/cmpv2client/impl/CmpClientImpl.java b/certService/src/main/java/org/onap/oom/certservice/cmpv2client/impl/CmpClientImpl.java index 68b78f23..06e785ac 100644 --- a/certService/src/main/java/org/onap/oom/certservice/cmpv2client/impl/CmpClientImpl.java +++ b/certService/src/main/java/org/onap/oom/certservice/cmpv2client/impl/CmpClientImpl.java @@ -39,6 +39,7 @@ import java.util.Date; import java.util.Objects; import java.util.Optional; import org.apache.http.impl.client.CloseableHttpClient; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.cmp.CMPCertificate; import org.bouncycastle.asn1.cmp.CertRepMessage; import org.bouncycastle.asn1.cmp.CertResponse; @@ -67,6 +68,7 @@ public class CmpClientImpl implements CmpClient { private static final String DEFAULT_CA_NAME = "Certification Authority"; private static final String DEFAULT_PROFILE = CaMode.RA.getProfile(); + private static final ASN1ObjectIdentifier PASSWORD_BASED_MAC = new ASN1ObjectIdentifier("1.2.840.113533.7.66.13"); public CmpClientImpl(CloseableHttpClient httpClient) { this.httpClient = httpClient; @@ -114,7 +116,18 @@ public class CmpClientImpl implements CmpClient { final PKIHeader header = respPkiMessage.getHeader(); final AlgorithmIdentifier protectionAlgo = header.getProtectionAlg(); verifySignatureWithPublicKey(respPkiMessage, publicKey); - verifyProtectionWithProtectionAlgo(respPkiMessage, initAuthPassword, header, protectionAlgo); + if (isPasswordBasedMacAlgorithm(protectionAlgo)) { + LOG.info("CMP response is protected by Password Base Mac Algorithm. Attempt to verify protection"); + verifyPasswordBasedMacProtection(respPkiMessage, initAuthPassword, header, protectionAlgo); + } + } + + private boolean isPasswordBasedMacAlgorithm(AlgorithmIdentifier protectionAlgo) throws CmpClientException { + if (Objects.isNull(protectionAlgo)) { + LOG.error("CMP response does not contain Protection Algorithm field"); + throw new CmpClientException("CMP response does not contain Protection Algorithm field"); + } + return PASSWORD_BASED_MAC.equals(protectionAlgo.getAlgorithm()); } private void verifySignatureWithPublicKey(PKIMessage respPkiMessage, PublicKey publicKey) @@ -129,22 +142,12 @@ public class CmpClientImpl implements CmpClient { } } - private void verifyProtectionWithProtectionAlgo( - PKIMessage respPkiMessage, - String initAuthPassword, - PKIHeader header, - AlgorithmIdentifier protectionAlgo) - throws CmpClientException { - if (Objects.nonNull(protectionAlgo)) { - LOG.debug("Verifying PasswordBased Protection of the Response."); - verifyPasswordBasedProtection(respPkiMessage, initAuthPassword, protectionAlgo); - checkImplicitConfirm(header); - } else { - LOG.error( - "Protection Algorithm is not available when expecting PBE protected response containing protection algorithm"); - throw new CmpClientException( - "Protection Algorithm is not available when expecting PBE protected response containing protection algorithm"); - } + private void verifyPasswordBasedMacProtection(PKIMessage respPkiMessage, String initAuthPassword, + PKIHeader header, AlgorithmIdentifier protectionAlgo) + throws CmpClientException { + LOG.debug("Verifying PasswordBased Protection of the Response."); + verifyPasswordBasedProtection(respPkiMessage, initAuthPassword, protectionAlgo); + checkImplicitConfirm(header); } private Cmpv2CertificationModel checkCmpCertRepMessage(final PKIMessage respPkiMessage) diff --git a/certService/src/main/resources/application.properties b/certService/src/main/resources/application.properties index a7f5eea8..8698a314 100644 --- a/certService/src/main/resources/application.properties +++ b/certService/src/main/resources/application.properties @@ -10,6 +10,9 @@ springdoc.swagger-ui.path=/docs # OOM CertService app specific configuration app.config.path=/etc/onap/oom/certservice +# HTTP Configuration +server.max-http-header-size=16384 + # Mutual TLS configuration server.ssl.enabled=true server.ssl.client-auth=need diff --git a/certService/src/test/java/org/onap/oom/certservice/cmpv2client/Cmpv2ClientTest.java b/certService/src/test/java/org/onap/oom/certservice/cmpv2client/Cmpv2ClientTest.java index 6a5a37f6..337ed8c1 100644 --- a/certService/src/test/java/org/onap/oom/certservice/cmpv2client/Cmpv2ClientTest.java +++ b/certService/src/test/java/org/onap/oom/certservice/cmpv2client/Cmpv2ClientTest.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2019 Ericsson Software Technology AB. All rights reserved. + * Copyright (C) 2021 Nokia. 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. @@ -16,6 +17,7 @@ package org.onap.oom.certservice.cmpv2client; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -24,6 +26,7 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -35,7 +38,7 @@ import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; -import java.security.cert.X509Certificate; + import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; @@ -47,6 +50,18 @@ 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.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIHeader; +import org.bouncycastle.asn1.cmp.PKIHeaderBuilder; +import org.bouncycastle.asn1.cmp.PKIMessage; +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.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; @@ -76,8 +91,6 @@ class Cmpv2ClientTest { private Date notAfter; private X500Name dn; - @Mock - X509Certificate cert; @Mock CloseableHttpClient httpClient; @@ -233,6 +246,47 @@ class Cmpv2ClientTest { () -> cmpClient.createCertificate(csrModel, server, notBefore, notAfter)); } + + @Test + void shouldThrowExceptionWhenResponseNotContainProtectionAlgorithmField() + throws IOException, ParseException { + + 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"); + setCsrModelAndServerValues( + "password", + "senderKID", + "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + beforeDate, + afterDate); + + when(httpClient.execute(any())).thenReturn(httpResponse); + when(httpResponse.getEntity()).thenReturn(httpEntity); + + try ( + BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream( + preparePKIMessageWithoutProtectionAlgorithm().getEncoded() + ))) { + + byte[] ba = IOUtils.toByteArray(bis); + doAnswer( + invocation -> { + OutputStream os = invocation.getArgument(0); + os.write(ba); + return null; + }) + .when(httpEntity) + .writeTo(any(OutputStream.class)); + } + + CmpClientImpl cmpClient = new CmpClientImpl(httpClient); + + assertThatExceptionOfType(CmpClientException.class) + .isThrownBy(() -> cmpClient.createCertificate(csrModel, server, notBefore, notAfter)) + .withMessageContaining("CMP response does not contain Protection Algorithm field"); + + } + @Test void shouldThrowIllegalArgumentExceptionWhencreateCertificateCalledWithInvalidCsr() throws ParseException { @@ -285,4 +339,33 @@ class Cmpv2ClientTest { this.notBefore = notBefore; this.notAfter = notAfter; } + + private PKIMessage preparePKIMessageWithoutProtectionAlgorithm() { + + CertTemplateBuilder certTemplateBuilder = new CertTemplateBuilder(); + X500Name issuerDN = getTestIssuerDN(); + + certTemplateBuilder.setIssuer(issuerDN); + certTemplateBuilder.setSerialNumber(new ASN1Integer(0L)); + + CertRequest certRequest = new CertRequest(4, certTemplateBuilder.build(), null); + CertReqMsg certReqMsg = new CertReqMsg(certRequest, new ProofOfPossession(), null); + CertReqMessages certReqMessages = new CertReqMessages(certReqMsg); + + PKIHeaderBuilder pkiHeaderBuilder = new PKIHeaderBuilder(PKIHeader.CMP_2000, new GeneralName(issuerDN), new GeneralName(issuerDN)); + pkiHeaderBuilder.setMessageTime(new ASN1GeneralizedTime(new Date())); + pkiHeaderBuilder.setProtectionAlg(null); + + PKIBody pkiBody = new PKIBody(PKIBody.TYPE_INIT_REQ, certReqMessages); + return new PKIMessage(pkiHeaderBuilder.build(), pkiBody, new DERBitString("test".getBytes())); + } + + private X500Name getTestIssuerDN() { + return new X500NameBuilder() + .addRDN(BCStyle.O, "Test_Organization") + .addRDN(BCStyle.UID, "Test_UID") + .addRDN(BCStyle.CN, "Test_CA") + .build(); + } + } diff --git a/compose-resources/cmpServers.json b/compose-resources/cmpServers.json index 72564949..8972fd4d 100644 --- a/compose-resources/cmpServers.json +++ b/compose-resources/cmpServers.json @@ -3,7 +3,7 @@ { "caName": "Client", "url": "http://oomcert-ejbca:8080/ejbca/publicweb/cmp/cmp", - "issuerDN": "CN=ManagementCA", + "issuerDN": "O=EJBCA Container Quickstart,CN=ManagementCA,UID=12345", "caMode": "CLIENT", "authentication": { "iak": "mypassword", @@ -13,7 +13,7 @@ { "caName": "RA", "url": "http://oomcert-ejbca:8080/ejbca/publicweb/cmp/cmpRA", - "issuerDN": "CN=ManagementCA", + "issuerDN": "O=EJBCA Container Quickstart,CN=ManagementCA,UID=12345", "caMode": "RA", "authentication": { "iak": "mypassword", diff --git a/compose-resources/ejbca-configuration.sh b/compose-resources/ejbca-configuration.sh index 8e6bd038..42e3f6bd 100755 --- a/compose-resources/ejbca-configuration.sh +++ b/compose-resources/ejbca-configuration.sh @@ -1,6 +1,16 @@ #!/bin/bash configureEjbca() { + ejbca.sh ca init \ + --caname ManagementCA \ + --dn "O=EJBCA Container Quickstart,CN=ManagementCA,UID=12345" \ + --tokenType soft \ + --keyspec 3072 \ + --keytype RSA \ + -v 3652 \ + --policy null \ + -s SHA256WithRSA \ + -type "x509" ejbca.sh config cmp addalias --alias cmpRA ejbca.sh config cmp updatealias --alias cmpRA --key operationmode --value ra ejbca.sh ca editca --caname ManagementCA --field cmpRaAuthSecret --value mypassword diff --git a/docker-compose.yml b/docker-compose.yml index 3e55c29b..1b154f4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - "443:8443" environment: - INITIAL_ADMIN=;PublicAccessAuthenticationToken:TRANSPORT_ANY; + - NO_CREATE_CA=true volumes: - ./compose-resources/ejbca-configuration.sh:/opt/primekey/scripts/ejbca-configuration.sh - ./compose-resources/certprofile_CUSTOM_ENDUSER-1834889499.xml:/opt/primekey/custom_profiles/certprofile_CUSTOM_ENDUSER-1834889499.xml diff --git a/parseCertServiceResponse.sh b/parseCertServiceResponse.sh new file mode 100755 index 00000000..dff867fa --- /dev/null +++ b/parseCertServiceResponse.sh @@ -0,0 +1,4 @@ +#!/bin/bash +read -r RESPONSE +echo "$RESPONSE" | jq -r '.certificateChain[]' > ./compose-resources/certs-from-curl/$1-cert.pem +echo "$RESPONSE" | jq -r '.trustedCertificates[]' > ./compose-resources/certs-from-curl/$1-cacert.pem |