GoogleCloudSigningServiceTest.java

/*
 * Copyright 2024 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.jca;

import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static net.jadler.Jadler.*;
import static org.junit.Assert.*;

public class GoogleCloudSigningServiceTest {

    @Before
    public void setUp() {
        initJadler().withDefaultResponseStatus(404);
    }

    @After
    public void tearDown() {
        closeJadler();
    }

    private SigningService getTestService() {
        return getTestService(true);
    }

    private SigningService getTestService(boolean certificate) {
        return new GoogleCloudSigningService("http://localhost:" + port() + "/", "projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring", "token", alias -> {
            if (!certificate) {
                return null;
            }
            try (FileInputStream in = new FileInputStream("target/test-classes/keystores/jsign-test-certificate-full-chain.pem")) {
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
                return certificates.toArray(new Certificate[0]);
            } catch (IOException | CertificateException e) {
                throw new RuntimeException("Failed to load the certificate", e);
            }
        });
    }

    @Test
    public void testGetAliases() throws Exception {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys")
                .respond()
                .withStatus(200)
                .withContentType("application/json")
                .withBody(new FileReader("target/test-classes/services/googlecloud-cryptokeys.json"));

        SigningService service = getTestService();
        List<String> aliases = service.aliases();

        assertEquals("aliases", Arrays.asList("hsmkey", "jsign-encrypt", "jsign-rsa-4096-raw", "jsign-rsa-2048", "jsign-rsa-4096"), aliases);
    }

    @Test
    public void testGetAliasesWithError() {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys")
                .respond()
                .withStatus(404)
                .withContentType("application/json")
                .withBody("{\"error\": {\"code\": 404,\"message\": \"KeyRing projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring not found.\", \"status\": \"NOT_FOUND\"}}");

        SigningService service = getTestService();

        Exception e = assertThrows(KeyStoreException.class, service::aliases);
        assertEquals("message", "404 - NOT_FOUND: KeyRing projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring not found.", e.getCause().getMessage());
    }

    @Test
    public void testGetCertificateChain() throws Exception {
        SigningService service = getTestService();
        Certificate[] chain = service.getCertificateChain("key1");
        assertNotNull("chain", chain);
        assertEquals("number of certificates", 3, chain.length);
    }

    @Test
    public void testGetPrivateKey() throws Exception {
        testGetPrivateKey("jsign-rsa-2048", true);
    }

    @Test
    public void testGetPrivateKeyWithFullName() throws Exception {
        testGetPrivateKey("projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048", true);
    }

    @Test
    public void testGetPrivateKeyWithVersion() throws Exception {
        testGetPrivateKey("jsign-rsa-2048/cryptoKeyVersions/2", false);
    }

    @Test
    public void testGetPrivateKeyWithVersionAndCertificate() throws Exception {
        testGetPrivateKey("jsign-rsa-2048/cryptoKeyVersions/2", true);
    }

    @Test
    public void testGetPrivateKeyWithVersionAndAlgorithm() throws Exception {
        testGetPrivateKey("jsign-rsa-2048/cryptoKeyVersions/2:RSA", false);
    }

    public void testGetPrivateKey(String alias, boolean certificate) throws Exception {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions")
                .respond()
                .withStatus(200)
                .withContentType("application/json")
                .withBody(new FileReader("target/test-classes/services/googlecloud-cryptokey-versions.json"));
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions/2")
                .respond()
                .withStatus(200)
                .withContentType("application/json")
                .withBody(new FileReader("target/test-classes/services/googlecloud-cryptokey-version.json"));

        SigningService service = getTestService(certificate);

        SigningServicePrivateKey key = service.getPrivateKey(alias, null);
        assertNotNull("null key", key);
        assertEquals("id", "projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions/2", key.getId());
        assertEquals("algorithm", "RSA", key.getAlgorithm());

        // check if the key is cached
        SigningServicePrivateKey key2 = service.getPrivateKey(alias, null);
        assertSame("private key not cached", key, key2);
    }

    @Test
    public void testGetPrivateKeyWithError() {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign/cryptoKeyVersions")
                .respond()
                .withStatus(404)
                .withContentType("application/json")
                .withBody("{\"error\": {\"code\": 404, \"message\": \"CryptoKey projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign not found.\", \"status\": \"NOT_FOUND\"}}");

        SigningService service = getTestService();

        Exception e = assertThrows(UnrecoverableKeyException.class, () -> service.getPrivateKey("jsign", null));
        assertEquals("message", "Unable to fetch Google Cloud private key 'projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign'", e.getMessage());
        assertEquals("root cause", "404 - NOT_FOUND: CryptoKey projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign not found.", e.getCause().getMessage());
    }

    @Test
    public void testGetPrivateKeyWithNoEnabledVersion() {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign/cryptoKeyVersions")
                .respond()
                .withStatus(200)
                .withContentType("application/json")
                .withBody("{}");

        SigningService service = getTestService();

        Exception e = assertThrows(UnrecoverableKeyException.class, () -> service.getPrivateKey("jsign", null));
        assertEquals("message", "Unable to fetch Google Cloud private key 'projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign', no version found", e.getMessage());
    }

    @Test
    public void testSign() throws Exception {
        onRequest()
                .havingMethodEqualTo("POST")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions/2:asymmetricSign")
                .respond()
                .withStatus(200)
                .withContentType("application/json")
                .withBody(new FileReader("target/test-classes/services/googlecloud-sign.json"));

        SigningService service = getTestService();
        SigningServicePrivateKey privateKey = service.getPrivateKey("jsign-rsa-2048/cryptoKeyVersions/2:RSA", null);
        String signature = Base64.getEncoder().encodeToString(service.sign(privateKey, "SHA256withRSA", "Hello".getBytes()));
        assertEquals("signature", "MiZ/YXfluqyuMfR3cnChG7+K7JmU2b8SzBAc6+WOpWQwIV4GfkLcRe0A68H45Lf+XPiMPPLrs7EqOv1EAnkYDFx5AqZBTWBfoaBeqKpy30OBvNbxIsaTLsaJYGypwmHOUTP+Djz7FxQUyM0uWVfUnHUDT564gQLz0cta6PKE/oMUo9fZhpv5VQcgfrbdUlPaD/cSAOb833ZSRzPWbnqztWO6py5sUugvqGFHKhsEXesx5yrPvJTKu5HVF3QM3E8YrgnVfFK14W8oyTJmXIWQxfYpwm/CW037UmolDMqwc3mjx1758kR+9lOcf8c/LSmD/SVD18SDSK4FyLQWOmn16A==", signature);
    }

    @Test
    public void testSignWithInvalidKey() throws Exception {
        onRequest()
                .havingMethodEqualTo("POST")
                .havingPathEqualTo("/projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions/2:asymmetricSign")
                .respond()
                .withStatus(400)
                .withContentType("application/json")
                .withBody(new FileReader("target/test-classes/services/googlecloud-sign-error.json"));

        SigningService service = getTestService();
        SigningServicePrivateKey privateKey = service.getPrivateKey("jsign-rsa-2048/cryptoKeyVersions/2:RSA", null);

        Exception e = assertThrows(GeneralSecurityException.class, () -> service.sign(privateKey, "SHA256withRSA", "Hello".getBytes()));
        assertEquals("message", "400 - FAILED_PRECONDITION: projects/fifth-glider-316809/locations/global/keyRings/jsignkeyring/cryptoKeys/jsign-rsa-2048/cryptoKeyVersions/2 is not enabled, current state is: DESTROYED.", e.getCause().getMessage());
    }
}