CertificateVerifierTest.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.File;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;

import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.junit.Assume;
import org.junit.Test;

import static org.junit.Assert.*;

public class CertificateVerifierTest {

    @Test(expected = IllegalArgumentException.class)
    public void testVerifyEmptyChain() throws Exception {
        CertificateVerifier verifier = new CertificateVerifier();
        verifier.verify(new ArrayList<>());
    }

    @Test
    public void testVerifyWithTrustedRootCertificate() throws Exception {
        X509Certificate rootCertificate = CertificateChain.load(new File("target/test-classes/keystores/jsign-root-ca.pem")).getLeaf();
        CertificateChain chain = CertificateChain.load(new File("target/test-classes/keystores/jsign-test-certificate-full-chain.pem"));

        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedCertificates(Collections.singletonList(rootCertificate));
        verifier.verify(chain.toList());
    }

    @Test
    public void testVerifyWithTrustedIntermediateCertificate() throws Exception {
        X509Certificate intermediateCertificate = CertificateChain.load(new File("target/test-classes/keystores/jsign-code-signing-ca.pem")).getLeaf();
        CertificateChain chain = CertificateChain.load(new File("target/test-classes/keystores/jsign-test-certificate-full-chain.pem"));

        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedCertificates(Collections.singletonList(intermediateCertificate));
        verifier.verify(chain.toList());
    }
    
    @Test(expected = CertificateException.class)
    public void testVerifyWithoutTrustAnchor() throws Exception {
        CertificateVerifier verifier = new CertificateVerifier();

        CertificateChain chain = CertificateChain.load(new File("src/test/resources/keystores/apache-software-foundation-2025.pem")).complete();
        verifier.verify(chain.toList());
    }

    @Test
    public void testVerifyWithTrustAnchorBeforeExpiration() throws Exception {
        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedKeys(PublicKeyHash.load(new File("target/classes/windows-trusted-keys.csv")))
                .withDate(new SimpleDateFormat("yyyy-MM-dd").parse("2028-10-04"));

        CertificateChain chain = CertificateChain.load(new File("src/test/resources/keystores/apache-software-foundation-2025.pem")).complete();
        verifier.verify(chain.toList());
    }

    @Test(expected = CertPathBuilderException.class)
    public void testVerifyWithTrustAnchorAfterExpiration() throws Exception {
        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedKey(new PublicKeyHash("7cd67c248f69d83fc2f9bb01dcb1f7ad67a363d046043796d0984c3a231f6bb0"))
                .withDate(new SimpleDateFormat("yyyy-MM-dd").parse("2028-10-07"));

        CertificateChain chain = CertificateChain.load(new File("src/test/resources/keystores/apache-software-foundation-2025.pem")).complete();
        verifier.verify(chain.toList());
    }

    @Test
    public void testVerifyWithRevocationCheck() throws Exception {
        Assume.assumeTrue("Test certificate has expired", LocalDate.now().isBefore(LocalDate.parse("2028-10-05")));

        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedKey(new PublicKeyHash("7cd67c248f69d83fc2f9bb01dcb1f7ad67a363d046043796d0984c3a231f6bb0"))
                .withRevocationCheck(true);

        CertificateChain chain = CertificateChain.load(new File("src/test/resources/keystores/apache-software-foundation-2025.pem")).complete();
        verifier.verify(chain.toList());
    }

    @Test(expected = CertPathBuilderException.class)
    public void testVerifyWithRevocationCheckFailure() throws Exception {
        CertificateChain chain = CertificateChain.load(new File("src/test/resources/keystores/badssl-revoked.pem")).complete();

        CertificateVerifier verifier = new CertificateVerifier()
                .withTrustedKey(new PublicKeyHash(chain.toList().get(1)))
                .withDate(new SimpleDateFormat("yyyy-MM-dd").parse("2022-10-25"))
                .withRevocationCheck(true);
        verifier.verify(chain.toList());
    }

    @Test
    public void testVerifyMissingCodeSigningKeyUsage() throws Exception {
        CertificateChain chain = CertificateChain.load(new File("target/test-classes/keystores/badssl-revoked.pem"));

        CertificateVerifier verifier = new CertificateVerifier()
                .withKeyPurposeId(KeyPurposeId.id_kp_codeSigning);

        Exception e = assertThrows(CertificateException.class, () -> verifier.verify(chain.toList()));
        assertEquals("The 'revoked.badssl.com' certificate is not authorized for code signing.", e.getMessage());
    }

    @Test
    public void testVerifyMissingTimestampingKeyUsage() throws Exception {
        CertificateChain chain = CertificateChain.load(new File("target/test-classes/keystores/jsign-test-certificate.pem"));

        CertificateVerifier verifier = new CertificateVerifier()
                .withKeyPurposeId(KeyPurposeId.id_kp_timeStamping);

        Exception e = assertThrows(CertificateException.class, () -> verifier.verify(chain.toList()));
        assertEquals("The 'Jsign Code Signing Test Certificate 2024 (RSA)' certificate is not authorized for time stamping.", e.getMessage());
    }

    @Test
    public void testVerifyMissingUnsupportedKeyUsage() throws Exception {
        CertificateChain chain = CertificateChain.load(new File("target/test-classes/keystores/jsign-test-certificate.pem"));

        CertificateVerifier verifier = new CertificateVerifier()
                .withKeyPurposeId(KeyPurposeId.id_kp_serverAuth);

        Exception e = assertThrows(CertificateException.class, () -> verifier.verify(chain.toList()));
        assertEquals("The 'Jsign Code Signing Test Certificate 2024 (RSA)' certificate is not authorized for key purpose 1.3.6.1.5.5.7.3.1.", e.getMessage());
    }
}