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