mirror of https://github.com/hyperledger/besu
CMS creation/validation logic (#2340)
* CMS creation/validation logic Signed-off-by: Lucas Saldanha <lucascrsaldanha@gmail.com>pull/2463/head
parent
ec4db5b7a7
commit
e92c9bc697
@ -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<X509Certificate> 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); |
||||
} |
||||
} |
||||
} |
@ -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<CertStore> 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<X509CertificateHolder> 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<X509CertificateHolder> 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); |
||||
} |
||||
} |
||||
} |
@ -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<X509CRL> 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); |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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<X509Certificate> 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); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue