CertificateVerifier.java
/*
* Copyright 2026 Emmanuel Bourg
*
* 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.
*/
package net.jsign;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import static net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers.*;
/**
* Certificate verifier.
*
* @since 8.0
*/
public class CertificateVerifier {
private final Provider provider = new BouncyCastleProvider();
/** The list of trusted certificate keys, identified by the SHA-256 hash of the public key */
private Set<PublicKeyHash> trustedKeys = new HashSet<>();
/** The list of trusted certificates */
private List<X509Certificate> trustedCertificates = new ArrayList<>();
/** The store of certificates to build the chain from */
private CertStore certificateStore;
/** The purpose of the certificate */
private KeyPurposeId keyPurposeId;
/** The date to use for certificate validation (optional) */
private Date date;
/** Tells if the revocation status of the certificates should be checked (OCSP/CRL) */
private boolean checkRevocation;
public CertificateVerifier() {
}
public CertificateVerifier withTrustedKey(PublicKeyHash publicKeyHash) {
trustedKeys.add(publicKeyHash);
return this;
}
public CertificateVerifier withTrustedKeys(Collection<PublicKeyHash> publicKeyHashes) {
this.trustedKeys = new HashSet<>(publicKeyHashes);
return this;
}
public CertificateVerifier withTrustedCertificates(List<X509Certificate> additionalTrustedCerts) {
this.trustedCertificates = additionalTrustedCerts != null ? additionalTrustedCerts : new ArrayList<>();
return this;
}
public CertificateVerifier withCertificateStore(CertStore certificateStore) {
this.certificateStore = certificateStore;
return this;
}
public CertificateVerifier withKeyPurposeId(KeyPurposeId keyPurposeId) {
this.keyPurposeId = keyPurposeId;
return this;
}
public CertificateVerifier withDate(Date date) {
this.date = date;
return this;
}
public CertificateVerifier withRevocationCheck(boolean checkRevocation) {
if (checkRevocation) {
Security.setProperty("ocsp.enable", "true");
}
this.checkRevocation = checkRevocation;
return this;
}
private Set<TrustAnchor> getTrustAnchors() {
Set<TrustAnchor> trustAnchors = new HashSet<>();
for (X509Certificate cert : trustedCertificates) {
trustAnchors.add(new TrustAnchor(cert, null));
}
return trustAnchors;
}
/**
* Tells if the key of the specified certificate is trusted.
*/
private boolean isTrustedKey(X509Certificate certificate) {
return trustedKeys.contains(new PublicKeyHash(certificate));
}
private void checkKeyPurpose(X509Certificate certificate) throws CertificateException {
if (keyPurposeId != null) {
List<String> eku = certificate.getExtendedKeyUsage();
if (eku == null || !eku.contains(keyPurposeId.getId())) {
String purpose;
if (keyPurposeId.equals(KeyPurposeId.id_kp_codeSigning)) {
purpose = "code signing";
} else if (keyPurposeId.equals(KeyPurposeId.id_kp_timeStamping)) {
purpose = "time stamping";
} else {
purpose = "key purpose " + keyPurposeId.getId();
}
throw new CertificateException("The '" + getCommonName(certificate) + "' certificate is not authorized for " + purpose + ".");
}
}
}
public CertPath verify(List<X509Certificate> chain) throws GeneralSecurityException {
if (chain == null || chain.isEmpty()) {
throw new IllegalArgumentException("The certificate chain is null or empty.");
}
withCertificateStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(chain)));
return verify(chain.get(0));
}
public CertPath verify(X509Certificate certificate) throws GeneralSecurityException {
checkKeyPurpose(certificate);
// look for trust anchors in the provided certificate store
Set<TrustAnchor> trustAnchors = getTrustAnchors();
if (certificateStore != null) {
for (X509Certificate cert : (Collection<X509Certificate>) certificateStore.getCertificates(new X509CertSelector())) {
if (isTrustedKey(cert)) {
trustAnchors.add(new TrustAnchor(cert, null));
}
}
}
if (trustAnchors.isEmpty()) {
List<X509Certificate> chain = buildChain(certificate);
if (chain.size() == 1) {
throw new CertificateException("No trust anchor could be established for the certificate:\n" + getCommonName(certificate));
} else {
StringBuilder indentedChain = new StringBuilder();
for (int i = 0; i < chain.size(); i++) {
char[] indentation = new char[2 * i];
Arrays.fill(indentation, ' ');
indentedChain.append(new String(indentation)).append(getCommonName(chain.get(i))).append("\n");
}
throw new CertificateException("No trust anchor could be established for the chain:\n" + indentedChain);
}
}
X509CertSelector selector = new X509CertSelector();
selector.setCertificate(certificate);
PKIXBuilderParameters params = new PKIXBuilderParameters(trustAnchors, selector);
params.addCertStore(certificateStore);
params.setDate(date);
params.addCertPathChecker(new PKIXCertPathChecker() {
public void init(boolean forward) {
}
public boolean isForwardCheckingSupported() {
return true;
}
public Set<String> getSupportedExtensions() {
return Collections.singleton(OID_APPLICATION_CERT_POLICIES.getId());
}
public void check(Certificate cert, Collection<String> unresolvedCritExts) {
if (unresolvedCritExts != null) {
unresolvedCritExts.remove(OID_APPLICATION_CERT_POLICIES.getId());
}
}
});
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", provider);
if (checkRevocation) {
PKIXRevocationChecker revChecker = (PKIXRevocationChecker) builder.getRevocationChecker();
//revChecker.setOptions(EnumSet.of(PKIXRevocationChecker.Option.NO_FALLBACK));
params.addCertPathChecker(revChecker);
params.setRevocationEnabled(true);
} else {
params.setRevocationEnabled(false);
}
return builder.build(params).getCertPath();
}
private String getCommonName(X509Certificate certificate) {
X500Name name = X500Name.getInstance(certificate.getSubjectX500Principal().getEncoded());
RDN[] rdns = name.getRDNs(BCStyle.CN);
if (rdns.length == 0) {
return certificate.getSubjectX500Principal().getName();
}
return rdns[0].getFirst().getValue().toString();
}
private List<X509Certificate> buildChain(X509Certificate certificate) throws CertStoreException {
List<X509Certificate> chain = new ArrayList<>();
X509Certificate current = certificate;
chain.add(current);
while (current != null && !current.getSubjectX500Principal().equals(current.getIssuerX500Principal())) {
X509Certificate issuer = getIssuerCertificate(current);
if (issuer != null) {
if (chain.contains(issuer)) {
break;
}
chain.add(issuer);
}
current = issuer;
}
return chain;
}
private X509Certificate getIssuerCertificate(X509Certificate certificate) throws CertStoreException {
Collection<?> matches = certificateStore.getCertificates(getIssuerSelector(certificate));
return !matches.isEmpty() ? (X509Certificate) matches.iterator().next() : null;
}
private X509CertSelector getIssuerSelector(X509Certificate certificate) throws CertStoreException {
X509CertSelector selector = new X509CertSelector();
selector.setSubject(certificate.getIssuerX500Principal());
ASN1OctetString aki = getAuthorityKeyIdentifier(certificate);
if (aki != null) {
try {
selector.setSubjectKeyIdentifier(aki.getEncoded());
} catch (IOException e) {
throw new CertStoreException(e);
}
}
return selector;
}
private ASN1OctetString getAuthorityKeyIdentifier(X509Certificate certificate) {
byte[] extension = certificate.getExtensionValue(Extension.authorityKeyIdentifier.getId());
if (extension != null) {
return AuthorityKeyIdentifier.getInstance(ASN1OctetString.getInstance(extension).getOctets()).getKeyIdentifierObject();
} else {
return null;
}
}
}