CmsValidator.java
/*
* 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.CollectionCertStoreParameters;
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.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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** The Cms validator. */
public class CmsValidator {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(CmsValidator.class);
private final KeyStoreWrapper truststore;
/**
* Instantiates a new Cms validator.
*
* @param truststore the truststore
*/
public CmsValidator(final KeyStoreWrapper truststore) {
this.truststore = truststore;
}
/**
* 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) {
if (cms == null || cms.isEmpty()) {
return false;
}
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) {
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) {
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)
createCRLCertStore(truststore)
.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);
}
}
private Optional<CertStore> createCRLCertStore(final KeyStoreWrapper truststore) {
if (truststore.getCRLs() != null) {
try {
return Optional.of(
CertStore.getInstance(
"Collection", new CollectionCertStoreParameters(truststore.getCRLs())));
} catch (final Exception e) {
throw new RuntimeException("Error loading CRLs from Truststore", e);
}
} else {
return Optional.empty();
}
}
}