From 9b682503a32af10dd6335c897e73e0e63f688210 Mon Sep 17 00:00:00 2001 From: EmmettCox Date: Thu, 27 Feb 2020 17:19:47 +0000 Subject: [PATCH] Authenticate response from CMP server Issue-ID: AAF-1037 Signed-off-by: EmmettCox Change-Id: I6f52627a169359067ddd928d1e895e8d6237c7b5 --- .../certservice/cmpv2client/external/CSRMeta.java | 10 +- .../cmpv2client/impl/CmpClientImpl.java | 60 +++++- .../cmpv2client/impl/CmpResponseHelper.java | 66 ++++-- .../impl/CmpResponseValidationHelper.java | 238 +++++++++++++++++++++ .../aaf/certservice/cmpv2client/impl/CmpUtil.java | 6 +- .../cmpv2client/impl/Cmpv2HttpClient.java | 5 +- .../cmpv2client/impl/CreateCertRequest.java | 12 +- .../certservice/cmpv2Client/Cmpv2ClientTest.java | 68 ++++-- .../ReturnedSuccessPKIMessageWithCertificateFile | Bin 4297 -> 2759 bytes certService/src/test/resources/privateKey | Bin 0 -> 1218 bytes certService/src/test/resources/publicKey | Bin 0 -> 294 bytes 11 files changed, 417 insertions(+), 48 deletions(-) create mode 100644 certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseValidationHelper.java create mode 100644 certService/src/test/resources/privateKey create mode 100644 certService/src/test/resources/publicKey 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 index 7655b025..e9f7a483 100644 --- 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 @@ -48,7 +48,7 @@ public class CSRMeta { private X500Name name; private X500Name issuerName; private Certificate certificate; - private SecureRandom random = new SecureRandom(); + private String senderKid; public CSRMeta(List rdns) { this.rdns = rdns; @@ -188,6 +188,14 @@ public class CSRMeta { CaUrl = caUrl; } + public String senderKid() { + return senderKid; + } + + public void senderKid(String senderKid) { + this.senderKid = senderKid; + } + public String issuerCn() { return issuerCn; } 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 index 7dacfc80..29bd671d 100644 --- 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 @@ -20,9 +20,13 @@ package org.onap.aaf.certservice.cmpv2client.impl; +import java.security.PublicKey; import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseHelper.checkIfCmpResponseContainsError; import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseHelper.getCertfromByteArray; import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseHelper.verifyAndReturnCertChainAndTrustSTore; +import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseValidationHelper.checkImplicitConfirm; +import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseValidationHelper.verifyPasswordBasedProtection; +import static org.onap.aaf.certservice.cmpv2client.impl.CmpResponseValidationHelper.verifySignature; import java.io.IOException; import java.security.cert.CertificateParsingException; @@ -38,7 +42,9 @@ import org.bouncycastle.asn1.cmp.CMPCertificate; import org.bouncycastle.asn1.cmp.CertRepMessage; import org.bouncycastle.asn1.cmp.CertResponse; import org.bouncycastle.asn1.cmp.PKIBody; +import org.bouncycastle.asn1.cmp.PKIHeader; import org.bouncycastle.asn1.cmp.PKIMessage; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.onap.aaf.certservice.cmpv2client.exceptions.CmpClientException; import org.onap.aaf.certservice.cmpv2client.api.CmpClient; import org.onap.aaf.certservice.cmpv2client.external.CSRMeta; @@ -51,7 +57,7 @@ import org.slf4j.LoggerFactory; */ public class CmpClientImpl implements CmpClient { - private final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(CmpClientImpl.class); private final CloseableHttpClient httpClient; private static final String DEFAULT_PROFILE = "RA"; @@ -82,6 +88,7 @@ public class CmpClientImpl implements CmpClient { .with(CreateCertRequest::setNotBefore, notBefore) .with(CreateCertRequest::setNotAfter, notAfter) .with(CreateCertRequest::setInitAuthPassword, csrMeta.password()) + .with(CreateCertRequest::setSenderKid, csrMeta.senderKid()) .build(); final PKIMessage pkiMessage = certRequest.generateCertReq(); @@ -96,6 +103,45 @@ public class CmpClientImpl implements CmpClient { return createCertificate(caName, profile, csrMeta, csr, null, null); } + private void checkCmpResponse( + final PKIMessage respPkiMessage, final PublicKey publicKey, final String initAuthPassword) + throws CmpClientException { + final PKIHeader header = respPkiMessage.getHeader(); + final AlgorithmIdentifier protectionAlgo = header.getProtectionAlg(); + verifySignatureWithPublicKey(respPkiMessage, publicKey); + verifyProtectionWithProtectionAlgo(respPkiMessage, initAuthPassword, header, protectionAlgo); + } + + private void verifySignatureWithPublicKey(PKIMessage respPkiMessage, PublicKey publicKey) + throws CmpClientException { + if (Objects.nonNull(publicKey)) { + LOG.debug("Verifying signature of the response."); + verifySignature(respPkiMessage, publicKey); + } else { + LOG.error("Public Key is not available, therefore cannot verify signature"); + throw new CmpClientException( + "Public Key is not available, therefore cannot verify signature"); + } + } + + 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 List> checkCmpCertRepMessage(final PKIMessage respPkiMessage) throws CmpClientException { final PKIBody pkiBody = respPkiMessage.getBody(); @@ -128,10 +174,11 @@ public class CmpClientImpl implements CmpClient { certResponse.getCertifiedKeyPair().getCertOrEncCert().getCertificate(); final Optional leafCertificate = getCertfromByteArray(cmpCertificate.getEncoded(), X509Certificate.class); - ArrayList certChain = new ArrayList<>(); - ArrayList trustStore = new ArrayList<>(); - return verifyAndReturnCertChainAndTrustSTore( - respPkiMessage, certRepMessage, leafCertificate.get(), certChain, trustStore); + if (leafCertificate.isPresent()) { + return verifyAndReturnCertChainAndTrustSTore( + respPkiMessage, certRepMessage, leafCertificate.get()); + } + return Collections.emptyList(); } private CertResponse getCertificateResponseContainingNewCertificate( @@ -184,8 +231,9 @@ public class CmpClientImpl implements CmpClient { final byte[] respBytes = cmpv2HttpClient.postRequest(pkiMessage, csrMeta.caUrl(), caName); try { final PKIMessage respPkiMessage = PKIMessage.getInstance(respBytes); - LOG.info("Recieved response from Server"); + LOG.info("Received response from Server"); checkIfCmpResponseContainsError(respPkiMessage); + checkCmpResponse(respPkiMessage, csrMeta.keypair().getPublic(), csrMeta.password()); return checkCmpCertRepMessage(respPkiMessage); } catch (IllegalArgumentException iae) { CmpClientException cmpClientException = diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseHelper.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseHelper.java index 8fdee2ed..60d91c5f 100644 --- a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseHelper.java +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseHelper.java @@ -56,7 +56,9 @@ import org.slf4j.LoggerFactory; public class CmpResponseHelper { - private static final Logger LOG = LoggerFactory.getLogger(CmpMessageHelper.class); + private static final Logger LOG = LoggerFactory.getLogger(CmpResponseHelper.class); + + private CmpResponseHelper() {} public static void checkIfCmpResponseContainsError(PKIMessage respPkiMessage) throws CmpClientException { @@ -192,7 +194,7 @@ public class CmpResponseHelper { CertPathValidator.getInstance("PKIX", BouncyCastleProvider.PROVIDER_NAME); PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) cpv.validate(cp, params); if (LOG.isDebugEnabled()) { - LOG.debug("Certificate verify result:{} ", result.toString()); + LOG.debug("Certificate verify result:{} ", result); } } @@ -276,25 +278,23 @@ public class CmpResponseHelper { * @param respPkiMessage PKIMessage that may contain extra certs used for certchain * @param certRepMessage CertRepMessage that should contain rootCA for certchain * @param leafCertificate certificate returned from our original Cert Request - * @param certChain Array of certificates to be used for KeyStore - * @param trustStore Array of certificates to be used for TrustStore * @return list of two lists, CertChain and TrustStore * @throws CertificateParsingException thrown if error occurs while parsing certificate * @throws IOException thrown if IOException occurs while parsing certificate * @throws CmpClientException thrown if error occurs during the verification of the certChain */ public static List> verifyAndReturnCertChainAndTrustSTore( - PKIMessage respPkiMessage, - CertRepMessage certRepMessage, - X509Certificate leafCertificate, - ArrayList certChain, - ArrayList trustStore) + PKIMessage respPkiMessage, CertRepMessage certRepMessage, X509Certificate leafCertificate) throws CertificateParsingException, IOException, CmpClientException { + List certChain = + addExtraCertsToChain(respPkiMessage, certRepMessage, leafCertificate); List certNames = getNamesOfCerts(certChain); LOG.debug("Verifying the following certificates in the cert chain: {}", certNames); - certChain.add(leafCertificate); - addExtraCertsToChain(respPkiMessage, certRepMessage, certChain); verify(certChain); + ArrayList trustStore = new ArrayList<>(); + final int rootCaIndex = certChain.size() - 1; + trustStore.add(certChain.get(rootCaIndex)); + certChain.remove(rootCaIndex); List> listOfArray = new ArrayList<>(); listOfArray.add(certChain); listOfArray.add(trustStore); @@ -303,7 +303,7 @@ public class CmpResponseHelper { public static List getNamesOfCerts(List certChain) { List certNames = new ArrayList<>(); - certChain.forEach((cert) -> certNames.add(cert.getSubjectDN().getName())); + certChain.forEach(cert -> certNames.add(cert.getSubjectDN().getName())); return certNames; } @@ -312,30 +312,56 @@ public class CmpResponseHelper { * * @param respPkiMessage PKIMessage that may contain extra certs used for certchain * @param certRepMessage CertRepMessage that should contain rootCA for certchain - * @param certChain Array of certificates to be used for KeyStore + * @param leafCert certificate at top of certChain. * @throws CertificateParsingException thrown if error occurs while parsing certificate * @throws IOException thrown if IOException occurs while parsing certificate * @throws CmpClientException thrown if there are errors creating CertificateFactory */ - public static void addExtraCertsToChain( - PKIMessage respPkiMessage, - CertRepMessage certRepMessage, - ArrayList certChain) + public static List addExtraCertsToChain( + PKIMessage respPkiMessage, CertRepMessage certRepMessage, X509Certificate leafCert) throws CertificateParsingException, IOException, CmpClientException { + List certChain = new ArrayList<>(); + certChain.add(leafCert); if (respPkiMessage.getExtraCerts() != null) { final CMPCertificate[] extraCerts = respPkiMessage.getExtraCerts(); for (CMPCertificate cmpCert : extraCerts) { Optional cert = getCertfromByteArray(cmpCert.getEncoded(), X509Certificate.class); - LOG.debug("Adding certificate {} to cert chain", cert.get().getSubjectDN().getName()); - certChain.add(cert.get()); + certChain = + ifCertPresent( + certChain, + cert, + "Adding certificate from extra certs {} to cert chain", + "Couldn't add certificate from extra certs, certificate wasn't an X509Certificate"); + return certChain; } } else { final CMPCertificate respCmpCaCert = getRootCa(certRepMessage); Optional cert = getCertfromByteArray(respCmpCaCert.getEncoded(), X509Certificate.class); - LOG.debug("Adding certificate {} to TrustStore", cert.get().getSubjectDN().getName()); + certChain = + ifCertPresent( + certChain, + cert, + "Adding certificate from CaPubs {} to TrustStore", + "Couldn't add certificate from CaPubs, certificate wasn't an X509Certificate"); + return certChain; + } + return Collections.emptyList(); + } + + public static List ifCertPresent( + List certChain, + Optional cert, + String certPresentString, + String certUnavailableString) { + if (cert.isPresent()) { + LOG.debug(certPresentString, cert.get().getSubjectDN().getName()); certChain.add(cert.get()); + return certChain; + } else { + LOG.debug(certUnavailableString); + return certChain; } } diff --git a/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseValidationHelper.java b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseValidationHelper.java new file mode 100644 index 00000000..8191c69d --- /dev/null +++ b/certService/src/main/java/org/onap/aaf/certservice/cmpv2client/impl/CmpResponseValidationHelper.java @@ -0,0 +1,238 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2020 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.aaf.certservice.cmpv2client.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.Objects; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.DEROutputStream; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.cmp.CMPObjectIdentifiers; +import org.bouncycastle.asn1.cmp.InfoTypeAndValue; +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.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +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 CmpResponseValidationHelper { + + private static final Logger LOG = LoggerFactory.getLogger(CmpResponseValidationHelper.class); + + private CmpResponseValidationHelper() {} + + /** + * Create a base key to use for verifying the PasswordBasedMac on a PKIMessage + * + * @param pbmParamSeq parameters recieved in PKIMessage used with password + * @param initAuthPassword password used to decrypt the basekey + * @return bytes representing the basekey + * @throws CmpClientException thrown if algorithem exceptions occur for the message digest + */ + public static byte[] getBaseKeyFromPbmParameters( + PBMParameter pbmParamSeq, String initAuthPassword) throws CmpClientException { + final int iterationCount = pbmParamSeq.getIterationCount().getPositiveValue().intValue(); + LOG.info("Iteration count is: {}", iterationCount); + final AlgorithmIdentifier owfAlg = pbmParamSeq.getOwf(); + LOG.info("One Way Function type is: {}", owfAlg.getAlgorithm().getId()); + final byte[] salt = pbmParamSeq.getSalt().getOctets(); + final byte[] raSecret = initAuthPassword != null ? initAuthPassword.getBytes() : new byte[0]; + 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); + try { + final MessageDigest messageDigest = + MessageDigest.getInstance( + owfAlg.getAlgorithm().getId(), BouncyCastleProvider.PROVIDER_NAME); + for (int i = 0; i < iterationCount; i++) { + basekey = messageDigest.digest(basekey); + messageDigest.reset(); + } + } catch (NoSuchAlgorithmException | NoSuchProviderException ex) { + LOG.error("ProtectionBytes don't match passwordBasedProtection, authentication failed"); + throw new CmpClientException( + "ProtectionBytes don't match passwordBasedProtection, authentication failed", ex); + } + return basekey; + } + + /** + * Verifies the signature of the response message using our public key + * + * @param respPkiMessage PKIMessage we wish to verify signature for + * @param pk public key used to verify signature. + * @throws CmpClientException + */ + public static void verifySignature(PKIMessage respPkiMessage, PublicKey pk) + throws CmpClientException { + final byte[] protBytes = getProtectedBytes(respPkiMessage); + final DERBitString derBitString = respPkiMessage.getProtection(); + try { + final Signature signature = + Signature.getInstance( + PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), + BouncyCastleProvider.PROVIDER_NAME); + signature.initVerify(pk); + signature.update(protBytes); + signature.verify(derBitString.getBytes()); + } catch (NoSuchAlgorithmException + | NoSuchProviderException + | InvalidKeyException + | SignatureException e) { + CmpClientException clientException = + new CmpClientException("Signature Verification failed", e); + LOG.error("Signature Verification failed", e); + throw clientException; + } + } + /** + * Converts the header and the body of a PKIMessage to an ASN1Encodable and returns the as a byte + * array + * + * @param msg PKIMessage to get protected bytes from + * @return the PKIMessage's header and body in byte array + */ + public static byte[] getProtectedBytes(PKIMessage msg) throws CmpClientException { + return getProtectedBytes(msg.getHeader(), msg.getBody()); + } + + /** + * Converts the header and the body of a PKIMessage to an ASN1Encodable and returns the as a byte + * array + * + * @param header PKIHeader to be converted + * @param body PKIMessage to be converted + * @return the PKIMessage's header and body in byte array + */ + public static byte[] getProtectedBytes(PKIHeader header, PKIBody body) throws CmpClientException { + byte[] res; + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(header); + v.add(body); + ASN1Encodable protectedPart = new DERSequence(v); + try { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DEROutputStream out = new DEROutputStream(bao); + out.writeObject(protectedPart); + res = bao.toByteArray(); + } catch (IOException ioe) { + CmpClientException cmpClientException = + new CmpClientException("Error occured while getting protected bytes", ioe); + LOG.error("Error occured while getting protected bytes", ioe); + throw cmpClientException; + } + return res; + } + + /** + * verify the password based protection within the response message + * + * @param respPkiMessage PKIMessage we want to verify password based protection for + * @param initAuthPassword password used to decrypt protection + * @param protectionAlgo protection algorithm we can use to decrypt protection + * @throws CmpClientException + */ + public static void verifyPasswordBasedProtection( + PKIMessage respPkiMessage, String initAuthPassword, AlgorithmIdentifier protectionAlgo) + throws CmpClientException { + final byte[] protectedBytes = getProtectedBytes(respPkiMessage); + final PBMParameter pbmParamSeq = PBMParameter.getInstance(protectionAlgo.getParameters()); + if (Objects.nonNull(pbmParamSeq)) { + try { + byte[] basekey = getBaseKeyFromPbmParameters(pbmParamSeq, initAuthPassword); + final Mac mac = getMac(protectedBytes, pbmParamSeq, basekey); + final byte[] outBytes = mac.doFinal(); + final byte[] protectionBytes = respPkiMessage.getProtection().getBytes(); + if (!Arrays.equals(outBytes, protectionBytes)) { + LOG.error("protectionBytes don't match passwordBasedProtection, authentication failed"); + throw new CmpClientException( + "protectionBytes don't match passwordBasedProtection, authentication failed"); + } + } catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidKeyException ex) { + CmpClientException cmpClientException = + new CmpClientException("Error while validating CMP response ", ex); + LOG.error("Error while validating CMP response ", ex); + throw cmpClientException; + } + } + } + + public static void checkImplicitConfirm(PKIHeader header) { + InfoTypeAndValue[] infos = header.getGeneralInfo(); + if (Objects.nonNull(infos)) { + if (CMPObjectIdentifiers.it_implicitConfirm.equals(getImplicitConfirm(infos))) { + LOG.info("Implicit Confirm on certificate from server."); + } else { + LOG.debug("No Implicit confirm in Response"); + } + } else { + LOG.debug("No general Info in header of response, cannot verify implicit confirm"); + } + } + + public static ASN1ObjectIdentifier getImplicitConfirm(InfoTypeAndValue[] info) { + return info[0].getInfoType(); + } + + /** + * Get cryptographical Mac we can use to decrypt our PKIMessage + * + * @param protectedBytes Protected bytes representing the PKIMessage + * @param pbmParamSeq Parameters used to decrypt PKIMessage, including mac algorithm used + * @param basekey Key used alongside mac Oid to create secret key for decrypting PKIMessage + * @return Mac that's ready to return decrypted bytes + * @throws NoSuchAlgorithmException Possibly thrown trying to get mac instance + * @throws NoSuchProviderException Possibly thrown trying to get mac instance + * @throws InvalidKeyException Possibly thrown trying to initialize mac using secretkey + */ + public static Mac getMac(byte[] protectedBytes, PBMParameter pbmParamSeq, byte[] basekey) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException { + final AlgorithmIdentifier macAlg = pbmParamSeq.getMac(); + LOG.info("Mac type is: {}", macAlg.getAlgorithm().getId()); + final String macOid = macAlg.getAlgorithm().getId(); + final Mac mac = Mac.getInstance(macOid, BouncyCastleProvider.PROVIDER_NAME); + final SecretKey key = new SecretKeySpec(basekey, macOid); + mac.init(key); + mac.reset(); + mac.update(protectedBytes, 0, protectedBytes.length); + return mac; + } +} 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 index 86935135..1bda4ac1 100644 --- 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 @@ -31,6 +31,8 @@ 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.CMPObjectIdentifiers; +import org.bouncycastle.asn1.cmp.InfoTypeAndValue; import org.bouncycastle.asn1.cmp.PKIBody; import org.bouncycastle.asn1.cmp.PKIHeader; import org.bouncycastle.asn1.cmp.PKIHeaderBuilder; @@ -129,7 +131,7 @@ public final class CmpUtil { * @return PKIHeaderBuilder */ static PKIHeader generatePkiHeader( - X500Name subjectDn, X500Name issuerDn, AlgorithmIdentifier protectionAlg) { + X500Name subjectDn, X500Name issuerDn, AlgorithmIdentifier protectionAlg, String senderKid) { LOGGER.info("Generating a Pki Header Builder"); PKIHeaderBuilder pkiHeaderBuilder = new PKIHeaderBuilder( @@ -139,6 +141,8 @@ public final class CmpUtil { pkiHeaderBuilder.setSenderNonce(new DEROctetString(createRandomBytes())); pkiHeaderBuilder.setTransactionID(new DEROctetString(createRandomBytes())); pkiHeaderBuilder.setProtectionAlg(protectionAlg); + pkiHeaderBuilder.setGeneralInfo(new InfoTypeAndValue(CMPObjectIdentifiers.it_implicitConfirm)); + pkiHeaderBuilder.setSenderKID(new DEROctetString(senderKid.getBytes())); 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 index db86a1e3..682e410d 100644 --- 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 @@ -54,7 +54,7 @@ class Cmpv2HttpClient { * @param pkiMessage PKIMessage to send to server * @param urlString url for the server we're sending request * @param caName name of CA server - * @return + * @return PKIMessage received from CMPServer * @throws CmpClientException thrown if problems with connecting or parsing response to server */ public byte[] postRequest( @@ -73,7 +73,8 @@ class Cmpv2HttpClient { return byteArrOutputStream.toByteArray(); } catch (IOException ioe) { CmpClientException cmpClientException = - new CmpClientException(String.format("IOException error while trying to connect CA %s",caName), ioe); + new CmpClientException( + String.format("IOException error while trying to connect CA %s", 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 index 25943321..b185c92a 100644 --- 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 @@ -55,6 +55,7 @@ class CreateCertRequest { private Date notBefore; private Date notAfter; private String initAuthPassword; + private String senderKid; private static final int ITERATIONS = createRandomInt(5000); private static final byte[] SALT = createRandomBytes(); @@ -88,6 +89,10 @@ class CreateCertRequest { this.initAuthPassword = initAuthPassword; } + public void setSenderKid(String senderKid) { + this.senderKid = senderKid; + } + /** * Method to create {@link PKIMessage} from {@link CertRequest},{@link ProofOfPossession}, {@link * CertReqMsg}, {@link CertReqMessages}, {@link PKIHeader} and {@link PKIBody}. @@ -118,8 +123,11 @@ class CreateCertRequest { final PKIHeader pkiHeader = generatePkiHeader( - subjectDn, issuerDn, CmpMessageHelper.protectionAlgoIdentifier(ITERATIONS, SALT)); - final PKIBody pkiBody = new PKIBody(PKIBody.TYPE_CERT_REQ, certReqMessages); + subjectDn, + issuerDn, + CmpMessageHelper.protectionAlgoIdentifier(ITERATIONS, SALT), + senderKid); + final PKIBody pkiBody = new PKIBody(PKIBody.TYPE_INIT_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 index 26cf7e2d..713a2d00 100644 --- a/certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java +++ b/certService/src/test/java/org/onap/aaf/certservice/cmpv2Client/Cmpv2ClientTest.java @@ -27,12 +27,18 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; 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; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -77,11 +83,13 @@ class Cmpv2ClientTest { private static ArrayList rdns; @BeforeEach - void setUp() throws NoSuchProviderException, NoSuchAlgorithmException { + void setUp() + throws NoSuchProviderException, NoSuchAlgorithmException, IOException, + InvalidKeySpecException { KeyPairGenerator keyGenerator; keyGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); keyGenerator.initialize(2048); - keyPair = keyGenerator.generateKeyPair(); + keyPair = LoadKeyPair(); rdns = new ArrayList<>(); try { rdns.add(new RDN("O=CommonCompany")); @@ -91,6 +99,27 @@ class Cmpv2ClientTest { initMocks(this); } + public KeyPair LoadKeyPair() + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, + NoSuchProviderException { + + final InputStream privateInputStream = this.getClass().getResourceAsStream("/privateKey"); + final InputStream publicInputStream = this.getClass().getResourceAsStream("/publicKey"); + BufferedInputStream bis = new BufferedInputStream(privateInputStream); + byte[] privateBytes = IOUtils.toByteArray(bis); + bis = new BufferedInputStream(publicInputStream); + byte[] publicBytes = IOUtils.toByteArray(bis); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicBytes); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateBytes); + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + + return new KeyPair(publicKey, privateKey); + } + @Test void shouldReturnValidPkiMessageWhenCreateCertificateRequestMessageMethodCalledWithValidCsr() throws Exception { @@ -103,8 +132,9 @@ class Cmpv2ClientTest { "CN=ManagementCA", "CommonName.com", "CommonName@cn.com", - "password", + "mypassword", "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + "senderKID", beforeDate, afterDate); when(httpClient.execute(any())).thenReturn(httpResponse); @@ -133,8 +163,9 @@ class Cmpv2ClientTest { } @Test - void shouldReturnValidPkiMessageWhenCreateCertificateRequestMessageMethodCalledWithValidCsr2() - throws Exception { + void + shouldThrowCmpClientExceptionWhenCreateCertificateRequestMessageMethodCalledWithWrongProtectedBytesInResponse() + 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"); @@ -146,35 +177,35 @@ class Cmpv2ClientTest { "CommonName@cn.com", "password", "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + "senderKID", beforeDate, afterDate); when(httpClient.execute(any())).thenReturn(httpResponse); when(httpResponse.getEntity()).thenReturn(httpEntity); try (final InputStream is = - this.getClass().getResourceAsStream("/ReturnedSuccessPKIMessageWithCertificateFile"); + 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; - }) + 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 - List> cmpClientResult = - cmpClient.createCertificate("data", "RA", csrMeta, cert, notBefore, notAfter); // then - assertNotNull(cmpClientResult); + Assertions.assertThrows( + CmpClientException.class, + () -> cmpClient.createCertificate("data", "RA", csrMeta, cert, notBefore, notAfter)); } @Test - void shouldReturnCmpClientExceptionWithPkiErrorExceptionWhenCmpClientCalledWithBadPassword() + void shouldThrowCmpClientExceptionWithPkiErrorExceptionWhenCmpClientCalledWithBadPassword() throws Exception { // given Date beforeDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse("2019/11/11 12:00:00"); @@ -187,6 +218,7 @@ class Cmpv2ClientTest { "CommonName@cn.com", "password", "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + "senderKID", beforeDate, afterDate); when(httpClient.execute(any())).thenReturn(httpResponse); @@ -228,6 +260,7 @@ class Cmpv2ClientTest { "CommonName@cn.com", "password", "http://127.0.0.1/ejbca/publicweb/cmp/cmp", + "senderKID", beforeDate, afterDate); CmpClientImpl cmpClient = new CmpClientImpl(httpClient); @@ -251,6 +284,7 @@ class Cmpv2ClientTest { "Common@cn.com", "myPassword", "http://127.0.0.1/ejbca/publicweb/cmp/cmpTest", + "sender", beforeDate, afterDate); when(httpClient.execute(any())).thenThrow(IOException.class); @@ -269,6 +303,7 @@ class Cmpv2ClientTest { String email, String password, String externalCaUrl, + String senderKid, Date notBefore, Date notAfter) { csrMeta = new CSRMeta(rdns); @@ -280,6 +315,7 @@ class Cmpv2ClientTest { when(kpg.generateKeyPair()).thenReturn(keyPair); csrMeta.keypair(); csrMeta.caUrl(externalCaUrl); + csrMeta.senderKid(senderKid); this.notBefore = notBefore; this.notAfter = notAfter; diff --git a/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile b/certService/src/test/resources/ReturnedSuccessPKIMessageWithCertificateFile index 94cc3461073c7f72f712d513144720d6cc52ea6f..e4a1d7b946d27d555cece7ab8dade48df83561b0 100644 GIT binary patch delta 1999 zcmZ{kX*3jy8^+DT*q1T(J!6?s%rJI_%F>;&qiZ$QGxhOUhIx zMYuIg8BEtU#iY5HQI<*=f1Ue3_fz-7d*1Ur@B8I>&-uM0T0?RYs2tJfs2qS75CCin zrDOpVq|HK&F-RkG3sX#3NGQf4gu)ZIDUJ6h8>3Lh#u&5(27|WrZLmUF2#Fx`kL7>K zfEK2FpG{sg`TmH(l*M zLMZb~LF4uerLf}Cyhe&0KpmD@y;Ixt2siwpZJi;ejlTUr_!EMX0MPwe6k82-1&M#6 zpe#)>XcJ!r36memO#Uzb>jngJzp#h;p0{kQsP7beok2r>ANAX^Z8?7JU9#1sI#aTIw4DJ_k&7v| zu*WBJ<}^f%BfD-Pee?`F^t-DTk{d_MZ8%G$b_-ixP!HgM*2|AlUP|hjh_x5x27b7q z)@gCTxy7?Ix}rpS9Hk}%I;kcJmIuRrHl_rF;bi9`hD|K(Jy{-z!Zuzl2!ms(2;IQ; zU#>C_RXSgl*ys!3aBWw~E1ezIssJ z8LKh;xwS7T5_;rGtP;Je@lJ*1i=whsGJYZ32d~R{huxS|8XIci&-6|+4bPvQo}c)< zeVyc7{=Xc0k3 z=*-3aC)4WxGL5z{`SG6i5jXz9^goiq6go>j9~i>q!2HV<4M~i%>pqLc;rI%<%A6~! zy#{ZYL4^nxHCDOz_y_;=C(EwWeuam>Xp+r4|GKEMB?NL}%sxm#=JxfdvoGvMHWOtW z3EvStT?i`@U!s|DkXC6b6Z%RrG(0H<&3l7~6{vIePSh^$9;loGQG*U@X-K&Vc#%Jq z*n>zBPi=Qzc+m0_0+I!Ni!RL_ta#~Z8M2cvRE>+sTlbE)VfFO2FF>={HN04A>w_F!+M|>vC>}8&>8vq=tK~e5|2W(35q!!o=WQn- zDMmE+^3-4f_f15`)_1+Ym0i`gEZ{VPf)m#LIUTvejr?KZanS0%M#q7NTzf4vwm-pPL)TxdpbOY>K^W{`RBJmUv=!26 zKIPSfTz+9tp49qXmV+v5BR+?_#Js_qJGN*mou+^vU%K0*3T`u)G>Xdfyd)Min6t^- zQQQXU6Ski$%bw5}q(1=Vk({U#iQfm_+f-+&kVlcOEP>et{5Vy;fo(dpEC<@eE#Bmk z1hw2BEhJY6B9kit?r)xz)aLJe)wMG(>O{nxSDg4l*x8vZ}UG|-6$7PF(cw&h|)o|)0f zLXm%`+@e9ny4gpUJR`2PiWd<;;LoeKVSkhzC){ng>zv3_nDb)lhQ0D$3t>S#Lzz=Z z#tVDcsXgzR*_oyQ1eR)*SgZu^;$lE?M^EeLjiou-(T>xPmMLY^Qb~I8L)9gXCw@(N z*!|cp1kxt(%SRn63AwnhCE?nAK?NXvcz9GuiO{f4jkFmNYA)) R2D(I`s~CEn@_Gg){tv|wfYJZ} literal 4297 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 diff --git a/certService/src/test/resources/privateKey b/certService/src/test/resources/privateKey new file mode 100644 index 0000000000000000000000000000000000000000..216714c9b53b9bb85402baf9ed47770dadf3d9d8 GIT binary patch literal 1218 zcmV;z1U>sOf&{(-0RS)!1_>&LNQUrs4#*Aqyhl|0)hbn0LO=#8ga^2 zIekeQKKY8C%yPe8=LG%4+H3GDds!B|?k8#{CruC*0u|4g@QUTl(peATv(w{u_*L}Q zgUxRsrcLX8v?Wn$DHWL&>uT4~{GISt3%C@|X*?(&H+!KA8Uc_}gU2pJzEyx1k6ST5 zQkoF&qmb=ibW1=UamMOb4vX`j7`5l8aR_p6dckl?UW7%)kD7y7*U$4!=v>i8m8eb; zG9?xBJh#?JBPeU^!rZ*j5=KkjS^gW^7%_WPxuT5sS1O}+Pft$ZSL5w7SruLL%8Phm*08=q^b zVD?Mbw~t~YT;IV-RNh(!oap$;tT`!Er0SE4W?&&r@+wpAY`M(Qcq99qL2j&Vr7)v( zX!?6Z`P&=#TLGhLPp(C*K?XU}0TPf#&6LDi!_Z_I0#>_mU2dR znv+vYlt$hN4fdI@Mq05m^52wCX&Fa*%wDPLho9Ofi^18Q& zW!Hv|XY(GuT!!ek_a8!@aTRoVFXSx9Io6O!);=E#c7#e_yFhE>qRX7S2R)s?M+>I~ z@qg(x8&qImXW-FO3QQ95XA707Lr6FOGff245pKj^*U7L;c?B^4)SwCyL#~mJ@qYq= zfdHk}j9bClv$hS__yq$UToQ#&^~GZ@$ugw@pLDF6g}VGa@)zzx5V}H-3`2j?z9lL3 zMMW(A0u&$Tm6=Fe?4V83bO7&kNq;E~KKaK;EM<}Fkim_KlxzF`Ox@aFq?_|$Uya0n z;(t9~y5nHO>EC#Beu65NEF#aClio1j2jv2RfNE{n`$!!vP>s^PGyx1_0aZ)%pO*ha zaZp(8C15AW)8FUnUj5-touVT1TlkA;301&hNE@4;`fWXs;*r%b(fV(TXBVaYw1*f$ zcQy0JWTv?0Gc3KA?1i}k*?-p;pVqyXGlbtTWUOn~TP9J%Hj5GuTtS>;#lHHV2iqoZ z2?BwE0EX9Ajj40~UL7*3W6x$7`pMc`E}jm1!Xt{2z7~rCYa8~k^bjP-!EYo6a^1g) zc$OKGli!W1DBie=`w97=BoBDYtVY~~=~>auJCr+sD^COi&sT{eG-2yeWs~;Ihy|)* gF9EY>&e8|q=f2WOc1`a__#JiYXEW7AUalX`h7NO7s{jB1 literal 0 HcmV?d00001 diff --git a/certService/src/test/resources/publicKey b/certService/src/test/resources/publicKey new file mode 100644 index 0000000000000000000000000000000000000000..e5c63be86697bbb92c0f8f1f884afef573d00676 GIT binary patch literal 294 zcmV+>0ondAf&n5h4F(A+hDe6@4FLfG1potr0S^E$f&mHwf&l>l$A_94amrRXeMuTV z`HG&*a=%{Z1pUO?Yw#<3Sr)wRCu$}qO%N6W70;OPisjAHSr6f})8lveRrJ<_&2J#4 zP3wKMB~fZA6`2+5YS+;Go$yu*xD?N6JSZPGd!Y*&0gzII$1X&^Re%?dTQNRTnh@`! zknLY|OF$lR#_CrNi}RltwdbdC2y$_T+v3As7?_wB^C2L zx7J7_C~NG(+`P~dMoZpV{u|mDF?&?GqKx-fDx-E!Pfp-h