From e92c9bc697e9db0f0ff3cae6f3e2bf297cf7959a Mon Sep 17 00:00:00 2001 From: Lucas Saldanha Date: Mon, 21 Jun 2021 16:36:12 +1200 Subject: [PATCH] CMS creation/validation logic (#2340) * CMS creation/validation logic Signed-off-by: Lucas Saldanha --- pki/build.gradle | 1 + .../hyperledger/besu/pki/cms/CmsCreator.java | 107 ++++++ .../besu/pki/cms/CmsValidator.java | 181 ++++++++++ .../org/hyperledger/besu/pki/crl/CRLUtil.java | 43 +++ .../pki/keystore/SoftwareKeyStoreWrapper.java | 11 +- .../pki/cms/CmsCreationAndValidationTest.java | 338 ++++++++++++++++++ .../besu/pki/util/TestCertificateUtils.java | 198 ++++++++++ 7 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 pki/src/main/java/org/hyperledger/besu/pki/cms/CmsCreator.java create mode 100644 pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java create mode 100644 pki/src/main/java/org/hyperledger/besu/pki/crl/CRLUtil.java create mode 100644 pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java create mode 100644 pki/src/test/java/org/hyperledger/besu/pki/util/TestCertificateUtils.java diff --git a/pki/build.gradle b/pki/build.gradle index f0e0e2aaaf..8b2dc39f00 100644 --- a/pki/build.gradle +++ b/pki/build.gradle @@ -36,4 +36,5 @@ dependencies { testImplementation 'junit:junit' testImplementation 'org.assertj:assertj-core' testImplementation 'org.mockito:mockito-core' + testImplementation 'org.apache.logging.log4j:log4j-core' } diff --git a/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsCreator.java b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsCreator.java new file mode 100644 index 0000000000..0dcd6bf93a --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsCreator.java @@ -0,0 +1,107 @@ +/* + * Copyright ConsenSys AG. + * + * 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 + */ +package org.hyperledger.besu.pki.cms; + +import org.hyperledger.besu.pki.keystore.KeyStoreWrapper; + +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.util.Store; + +public class CmsCreator { + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private static final Logger LOGGER = LogManager.getLogger(); + + private final String certificateAlias; + private final KeyStoreWrapper keyStore; + + public CmsCreator(final KeyStoreWrapper keyStore, final String certificateAlias) { + this.keyStore = keyStore; + this.certificateAlias = certificateAlias; + } + + /** + * Creates a CMS message with the content parameter, signed with the certificate with alias + * defined in the {@code CmsCreator} constructor. The certificate chain is also included so the + * recipient has the information needed to build a trusted certificate path when validating this + * message. + * + * @param contentToSign the content that will be signed and added to the message + * @return the CMS message bytes + */ + @SuppressWarnings("rawtypes") + public Bytes create(final Bytes contentToSign) { + try { + // Certificates that will be sent + final List x509Certificates = + Stream.of(keyStore.getCertificateChain(certificateAlias)) + .map(X509Certificate.class::cast) + .collect(Collectors.toList()); + final Store certs = new JcaCertStore(x509Certificates); + + // Private key of the certificate that will sign the message + final PrivateKey privateKey = keyStore.getPrivateKey(certificateAlias); + + final ContentSigner contentSigner = + new JcaContentSignerBuilder("SHA256withRSA").build(privateKey); + + final CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); + + // Aditional intermediate certificates for path building + cmsGenerator.addCertificates(certs); + + final DigestCalculatorProvider digestCalculatorProvider = + new JcaDigestCalculatorProviderBuilder().setProvider("BC").build(); + // The first certificate in the list (leaf certificate is the signer) + cmsGenerator.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, x509Certificates.get(0))); + + // Add signed content + final CMSTypedData cmsData = new CMSProcessableByteArray(contentToSign.toArray()); + final CMSSignedData cmsSignedData = cmsGenerator.generate(cmsData, false); + + return Bytes.wrap(cmsSignedData.getEncoded()); + } catch (final Exception e) { + LOGGER.error("Error creating CMS data", e); + throw new RuntimeException("Error creating CMS data", e); + } + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java new file mode 100644 index 0000000000..a7001afe34 --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java @@ -0,0 +1,181 @@ +/* + * Copyright ConsenSys AG. + * + * 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 + */ +package org.hyperledger.besu.pki.cms; + +import org.hyperledger.besu.pki.keystore.KeyStoreWrapper; + +import java.security.Security; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXRevocationChecker; +import java.security.cert.PKIXRevocationChecker.Option; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaCertStoreBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.Store; + +public class CmsValidator { + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + private static final Logger LOGGER = LogManager.getLogger(); + + private final KeyStoreWrapper truststore; + private final Optional crlCertStore; + + public CmsValidator(final KeyStoreWrapper truststore, final CertStore crlCertStore) { + this.truststore = truststore; + this.crlCertStore = Optional.ofNullable(crlCertStore); + } + + /** + * Verifies that a CMS message signed content matched the expected content, and that the message + * signer is from a certificate that is trusted (has permission to propose a block) + * + * @param cms the CMS message bytes + * @param expectedContent the expected signed content in the CMS message + * @return true, if the signed content matched the expected content and the signer's certificate + * is trusted, otherwise returns false. + */ + public boolean validate(final Bytes cms, final Bytes expectedContent) { + try { + LOGGER.trace("Validating CMS message"); + + final CMSSignedData cmsSignedData = + new CMSSignedData(new CMSProcessableByteArray(expectedContent.toArray()), cms.toArray()); + final X509Certificate signerCertificate = getSignerCertificate(cmsSignedData); + + // Validate msg signature and content + if (!isSignatureValid(signerCertificate, cmsSignedData)) { + return false; + } + + // Validate certificate trust + if (!isCertificateTrusted(signerCertificate, cmsSignedData)) { + return false; + } + + return true; + } catch (final Exception e) { + LOGGER.error("Error validating CMS data", e); + throw new RuntimeException("Error validating CMS data", e); + } + } + + @SuppressWarnings("unchecked") + private X509Certificate getSignerCertificate(final CMSSignedData cmsSignedData) { + try { + final Store certificateStore = cmsSignedData.getCertificates(); + + // We don't expect more than one signer on the CMS data + if (cmsSignedData.getSignerInfos().size() != 1) { + throw new RuntimeException("Only one signer is expected on the CMS message"); + } + final SignerInformation signerInformation = + cmsSignedData.getSignerInfos().getSigners().stream().findFirst().get(); + + // Find signer's certificate from CMS data + final Collection signerCertificates = + certificateStore.getMatches(signerInformation.getSID()); + final X509CertificateHolder certificateHolder = signerCertificates.stream().findFirst().get(); + + return new JcaX509CertificateConverter().getCertificate(certificateHolder); + } catch (final Exception e) { + LOGGER.error("Error retrieving signer certificate from CMS data", e); + throw new RuntimeException("Error retrieving signer certificate from CMS data", e); + } + } + + private boolean isSignatureValid( + final X509Certificate signerCertificate, final CMSSignedData cmsSignedData) { + LOGGER.trace("Validating CMS signature"); + try { + return cmsSignedData.verifySignatures( + sid -> new JcaSimpleSignerInfoVerifierBuilder().build(signerCertificate)); + } catch (final CMSException e) { + return false; + } + } + + private boolean isCertificateTrusted( + final X509Certificate signerCertificate, final CMSSignedData cmsSignedData) { + LOGGER.trace("Starting CMS certificate validation"); + + try { + final CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX"); + + // Define CMS signer certificate as the starting point of the path (leaf certificate) + final X509CertSelector targetConstraints = new X509CertSelector(); + targetConstraints.setCertificate(signerCertificate); + + // Set parameters for the certificate path building algorithm + final PKIXBuilderParameters params = + new PKIXBuilderParameters(truststore.getKeyStore(), targetConstraints); + + // Adding CertStore with CRLs (if present, otherwise disabling revocation check) + crlCertStore.ifPresentOrElse( + CRLs -> { + params.addCertStore(CRLs); + PKIXRevocationChecker rc = (PKIXRevocationChecker) cpb.getRevocationChecker(); + rc.setOptions(EnumSet.of(Option.PREFER_CRLS)); + params.addCertPathChecker(rc); + }, + () -> { + LOGGER.warn("No CRL CertStore provided. CRL validation will be disabled."); + params.setRevocationEnabled(false); + }); + + // Read certificates sent on the CMS message and adding it to the path building algorithm + final CertStore cmsCertificates = + new JcaCertStoreBuilder().addCertificates(cmsSignedData.getCertificates()).build(); + params.addCertStore(cmsCertificates); + + // Validate certificate path + try { + cpb.build(params); + return true; + } catch (final CertPathBuilderException cpbe) { + LOGGER.warn("Untrusted certificate chain", cpbe); + LOGGER.trace("Reason for failed validation", cpbe.getCause()); + return false; + } + + } catch (final Exception e) { + LOGGER.error("Error validating certificate chain"); + throw new RuntimeException("Error validating certificate chain", e); + } + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/crl/CRLUtil.java b/pki/src/main/java/org/hyperledger/besu/pki/crl/CRLUtil.java new file mode 100644 index 0000000000..6188382dcd --- /dev/null +++ b/pki/src/main/java/org/hyperledger/besu/pki/crl/CRLUtil.java @@ -0,0 +1,43 @@ +/* + * Copyright ConsenSys AG. + * + * 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 + */ +package org.hyperledger.besu.pki.crl; + +import org.hyperledger.besu.pki.PkiException; + +import java.io.FileInputStream; +import java.nio.file.Paths; +import java.security.cert.CertStore; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.X509CRL; +import java.util.List; +import java.util.stream.Collectors; + +public class CRLUtil { + + public static CertStore loadCRLs(final String path) { + try { + final List crls = + CertificateFactory.getInstance("X509") + .generateCRLs(new FileInputStream(Paths.get(path).toFile())).stream() + .map(X509CRL.class::cast) + .collect(Collectors.toList()); + + return CertStore.getInstance("Collection", new CollectionCertStoreParameters(crls)); + } catch (Exception e) { + throw new PkiException("Error loading CRL file " + path, e); + } + } +} diff --git a/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java b/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java index d3d79374f4..a6df558eca 100644 --- a/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java +++ b/pki/src/main/java/org/hyperledger/besu/pki/keystore/SoftwareKeyStoreWrapper.java @@ -90,7 +90,7 @@ public class SoftwareKeyStoreWrapper extends AbstractKeyStoreWrapper { } @VisibleForTesting - SoftwareKeyStoreWrapper( + public SoftwareKeyStoreWrapper( final KeyStore keystore, final String keystorePassword, final KeyStore truststore, @@ -102,6 +102,15 @@ public class SoftwareKeyStoreWrapper extends AbstractKeyStoreWrapper { this.truststorePassword = truststorePassword.toCharArray(); } + @VisibleForTesting + public SoftwareKeyStoreWrapper(final KeyStore keystore, final String keystorePassword) { + super(null); + this.keystore = keystore; + this.keystorePassword = keystorePassword.toCharArray(); + this.truststore = null; + this.truststorePassword = null; + } + @Override public PrivateKey getPrivateKey(final String keyAlias) { LOG.debug("Retrieving private key for alias: {}", keyAlias); diff --git a/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java b/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java new file mode 100644 index 0000000000..8ea2ec38ba --- /dev/null +++ b/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java @@ -0,0 +1,338 @@ +/* + * Copyright ConsenSys AG. + * + * 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 + */ + +package org.hyperledger.besu.pki.cms; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hyperledger.besu.pki.util.TestCertificateUtils.createCRL; +import static org.hyperledger.besu.pki.util.TestCertificateUtils.createKeyPair; +import static org.hyperledger.besu.pki.util.TestCertificateUtils.createSelfSignedCertificate; +import static org.hyperledger.besu.pki.util.TestCertificateUtils.issueCertificate; + +import org.hyperledger.besu.pki.keystore.KeyStoreWrapper; +import org.hyperledger.besu.pki.keystore.SoftwareKeyStoreWrapper; + +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.CertStore; +import java.security.cert.Certificate; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class CmsCreationAndValidationTest { + + private static KeyStoreWrapper keystoreWrapper; + private static KeyStoreWrapper truststoreWrapper; + private static CertStore CRLs; + + private CmsValidator cmsValidator; + + @BeforeClass + public static void beforeAll() throws Exception { + final Instant notBefore = Instant.now().minus(1, ChronoUnit.DAYS); + final Instant notAfter = Instant.now().plus(1, ChronoUnit.DAYS); + + /* + Create self-signed certificate + */ + final KeyPair selfsignedKeyPair = createKeyPair(); + final X509Certificate selfsignedCertificate = + createSelfSignedCertificate("selfsigned", notBefore, notAfter, selfsignedKeyPair); + + /* + Create trusted chain (ca -> interca -> partneraca -> partneravalidator) + */ + final KeyPair caKeyPair = createKeyPair(); + final X509Certificate caCertificate = + createSelfSignedCertificate("ca", notBefore, notAfter, caKeyPair); + + final KeyPair interCAKeyPair = createKeyPair(); + final X509Certificate interCACertificate = + issueCertificate( + caCertificate, caKeyPair, "interca", notBefore, notAfter, interCAKeyPair, true); + + final KeyPair partnerACAPair = createKeyPair(); + final X509Certificate partnerACACertificate = + issueCertificate( + interCACertificate, + interCAKeyPair, + "partneraca", + notBefore, + notAfter, + partnerACAPair, + true); + + final KeyPair parterAValidatorKeyPair = createKeyPair(); + final X509Certificate partnerAValidatorCertificate = + issueCertificate( + partnerACACertificate, + partnerACAPair, + "partneravalidator", + notBefore, + notAfter, + parterAValidatorKeyPair, + false); + + /* + Create expired certificate + */ + final KeyPair expiredKeyPair = createKeyPair(); + final X509Certificate expiredCertificate = + issueCertificate( + caCertificate, + caKeyPair, + "expired", + notBefore, + notBefore.plus(1, ChronoUnit.SECONDS), + expiredKeyPair, + true); + + /* + Create revoked and revoked certificates + */ + final KeyPair revokedKeyPair = createKeyPair(); + final X509Certificate revokedCertificate = + issueCertificate( + caCertificate, caKeyPair, "revoked", notBefore, notAfter, revokedKeyPair, true); + + /* + Create untrusted chain (untrusted_selfsigned -> unstrusted_partner) + */ + final KeyPair untrustedSelfSignedKeyPair = createKeyPair(); + final X509Certificate untrustedSelfsignedCertificate = + createSelfSignedCertificate( + "untrusted_selfsigned", notBefore, notAfter, untrustedSelfSignedKeyPair); + + final KeyPair untrustedIntermediateKeyPair = createKeyPair(); + final X509Certificate untrustedIntermediateCertificate = + issueCertificate( + untrustedSelfsignedCertificate, + untrustedSelfSignedKeyPair, + "unstrusted_partner", + notBefore, + notAfter, + untrustedIntermediateKeyPair, + true); + + /* + Create truststore wrapper with 3 trusted certificates: 'ca', 'interca' and 'selfsigned' + */ + final KeyStore truststore = KeyStore.getInstance("PKCS12"); + truststore.load(null, null); + + truststore.setCertificateEntry("ca", caCertificate); + truststore.setCertificateEntry("interca", interCACertificate); + truststore.setCertificateEntry("selfsigned", selfsignedCertificate); + + truststoreWrapper = new SoftwareKeyStoreWrapper(truststore, ""); + + /* + Create keystore with certificates used in tests + */ + final KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(null, null); + + keystore.setKeyEntry( + "trusted_selfsigned", + selfsignedKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {selfsignedCertificate}); + keystore.setKeyEntry( + "untrusted_selfsigned", + untrustedSelfSignedKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {untrustedSelfsignedCertificate}); + keystore.setKeyEntry( + "expired", + expiredKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {expiredCertificate}); + keystore.setKeyEntry( + "revoked", + revokedKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {revokedCertificate}); + keystore.setKeyEntry( + "trusted", + parterAValidatorKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {partnerAValidatorCertificate, partnerACACertificate}); + keystore.setKeyEntry( + "untrusted", + untrustedIntermediateKeyPair.getPrivate(), + "".toCharArray(), + new Certificate[] {untrustedIntermediateCertificate, untrustedSelfsignedCertificate}); + keystoreWrapper = new SoftwareKeyStoreWrapper(keystore, ""); + + /* + Create CRLs for all CA certificates (mostly empty, only ca has one revoked certificate) + */ + final X509CRL caCRL = createCRL(caCertificate, caKeyPair, Set.of(revokedCertificate)); + final X509CRL intercaCRL = + createCRL(interCACertificate, interCAKeyPair, Collections.emptyList()); + final X509CRL partnerACACRL = + createCRL(partnerACACertificate, partnerACAPair, Collections.emptyList()); + final X509CRL selfsignedCRL = + createCRL(selfsignedCertificate, selfsignedKeyPair, Collections.emptyList()); + + CRLs = + CertStore.getInstance( + "Collection", + new CollectionCertStoreParameters( + Set.of(caCRL, intercaCRL, partnerACACRL, selfsignedCRL))); + } + + @Before + public void before() { + cmsValidator = new CmsValidator(truststoreWrapper, CRLs); + } + + @Test + public void cmsValidationWithTrustedSelfSignedCertificate() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "trusted_selfsigned"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isTrue(); + } + + @Test + public void cmsValidationWithUntrustedSelfSignedCertificate() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "untrusted_selfsigned"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isFalse(); + } + + @Test + public void cmsValidationWithTrustedChain() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "trusted"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isTrue(); + } + + @Test + public void cmsValidationWithUntrustedChain() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "untrusted"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isFalse(); + } + + @Test + public void cmsValidationWithExpiredCertificate() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "expired"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isFalse(); + } + + @Test + public void cmsValidationWithRevokedCertificate() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "revoked"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + assertThat(cmsValidator.validate(cms, data)).isFalse(); + } + + @Test + public void cmsValidationWithoutCRLConfigDisablesCRLCheck() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "revoked"); + final Bytes data = Bytes.random(32); + + final Bytes cms = cmsCreator.create(data); + + // Overriding validator with instance without CRL CertStore + cmsValidator = new CmsValidator(truststoreWrapper, null); + + // Because we don't have a CRL CertStore, revocation is not checked + assertThat(cmsValidator.validate(cms, data)).isTrue(); + } + + @Test + public void cmsValidationWithWrongSignedData() { + final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "trusted"); + final Bytes otherData = Bytes.random(32); + final Bytes cms = cmsCreator.create(otherData); + + final Bytes expectedData = Bytes.random(32); + assertThat(cmsValidator.validate(cms, expectedData)).isFalse(); + } + + @Test + public void cmsValidationWithInvalidSignature() throws Exception { + // Create a CMS message signed with a certificate, but create SignerInfo using another + // certificate to trigger the signature verification to fail. + + final PrivateKey privateKey = keystoreWrapper.getPrivateKey("trusted"); + final X509Certificate signerCertificate = + (X509Certificate) keystoreWrapper.getCertificate("trusted"); + final X509Certificate otherCertificate = + (X509Certificate) keystoreWrapper.getCertificate("trusted_selfsigned"); + + final ContentSigner contentSigner = + new JcaContentSignerBuilder("SHA256withRSA").build(privateKey); + + final CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator(); + cmsGenerator.addCertificate(new JcaX509CertificateHolder(signerCertificate)); + cmsGenerator.addCertificate(new JcaX509CertificateHolder(otherCertificate)); + + final DigestCalculatorProvider digestCalculatorProvider = + new JcaDigestCalculatorProviderBuilder().setProvider("BC").build(); + cmsGenerator.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider) + .build(contentSigner, otherCertificate)); + + final Bytes expectedData = Bytes.random(32); + final CMSTypedData cmsData = new CMSProcessableByteArray(expectedData.toArray()); + final CMSSignedData cmsSignedData = cmsGenerator.generate(cmsData, true); + final Bytes cmsBytes = Bytes.wrap(cmsSignedData.getEncoded()); + + assertThat(cmsValidator.validate(cmsBytes, expectedData)).isFalse(); + } +} diff --git a/pki/src/test/java/org/hyperledger/besu/pki/util/TestCertificateUtils.java b/pki/src/test/java/org/hyperledger/besu/pki/util/TestCertificateUtils.java new file mode 100644 index 0000000000..3097007e5a --- /dev/null +++ b/pki/src/test/java/org/hyperledger/besu/pki/util/TestCertificateUtils.java @@ -0,0 +1,198 @@ +/* + * Copyright ConsenSys AG. + * + * 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 + */ + +package org.hyperledger.besu.pki.util; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CRLException; +import java.security.cert.CRLReason; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.sql.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Random; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.X509KeyUsage; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/* + This class provides utility method for creating certificates used on tests. + + Based on https://stackoverflow.com/a/18648284/5021783 +*/ +public class TestCertificateUtils { + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + public static KeyPair createKeyPair() { + try { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Error creating KeyPair", e); + } + } + + public static X509Certificate createSelfSignedCertificate( + final String name, final Instant notBefore, final Instant notAfter, final KeyPair keyPair) { + try { + final ContentSigner signer = + new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); + + final X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder( + new X500Name("CN=" + name), + new BigInteger(32, new Random()), + Date.from(notBefore), + Date.from(notAfter), + new X500Name("CN=" + name), + keyPair.getPublic()) + .addExtension( + Extension.authorityKeyIdentifier, + false, + new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic())) + .addExtension( + Extension.subjectKeyIdentifier, + false, + new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic())) + .addExtension( + Extension.basicConstraints, + false, + new BasicConstraints(true)) // true if it is allowed to sign other certs + .addExtension( + Extension.keyUsage, + true, + new X509KeyUsage(X509KeyUsage.keyCertSign | X509KeyUsage.cRLSign)); + + final X509CertificateHolder certHolder = certificateBuilder.build(signer); + + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(certHolder); + + } catch (final Exception e) { + throw new RuntimeException("Error creating CA certificate", e); + } + } + + public static X509Certificate issueCertificate( + final X509Certificate issuer, + final KeyPair issuerKeyPair, + final String subject, + final Instant notBefore, + final Instant notAfter, + final KeyPair keyPair, + final boolean isCa) { + + try { + final ContentSigner signer = + new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(issuerKeyPair.getPrivate()); + + final X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder( + issuer, + new BigInteger(32, new Random()), + Date.from(notBefore), + Date.from(notAfter), + new X500Name("CN=" + subject), + keyPair.getPublic()) + .addExtension( + Extension.authorityKeyIdentifier, + false, + new JcaX509ExtensionUtils() + .createAuthorityKeyIdentifier(issuerKeyPair.getPublic())) + .addExtension( + Extension.basicConstraints, + false, + new BasicConstraints(isCa)) // true if it is allowed to sign other certs + .addExtension( + Extension.keyUsage, + true, + new X509KeyUsage( + X509KeyUsage.digitalSignature + | X509KeyUsage.nonRepudiation + | X509KeyUsage.keyEncipherment + | X509KeyUsage.dataEncipherment + | X509KeyUsage.cRLSign + | X509KeyUsage.keyCertSign)); + + final X509CertificateHolder certHolder = certificateBuilder.build(signer); + + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(certHolder); + } catch (final Exception e) { + throw new RuntimeException("Error creating certificate", e); + } + } + + public static X509CRL createCRL( + final X509Certificate issuer, + final KeyPair issuerKeyPair, + final Collection revokedCertificates) { + try { + final X509CertificateHolder x509CertificateHolder = + new X509CertificateHolder(issuer.getEncoded()); + + final X509v2CRLBuilder crlBuilder = + new X509v2CRLBuilder(x509CertificateHolder.getSubject(), Date.from(Instant.now())); + + revokedCertificates.forEach( + c -> + crlBuilder.addCRLEntry( + c.getSerialNumber(), Date.from(Instant.now()), CRLReason.UNSPECIFIED.ordinal())); + + crlBuilder.setNextUpdate(Date.from(Instant.now().plus(1, ChronoUnit.DAYS))); + + final ContentSigner signer = + new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(issuerKeyPair.getPrivate()); + final X509CRLHolder crlHolder = crlBuilder.build(signer); + return new JcaX509CRLConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCRL(crlHolder); + } catch (OperatorCreationException + | CRLException + | CertificateEncodingException + | IOException e) { + throw new RuntimeException(e); + } + } +}