HashiCorpVaultSigningServiceTest.java

/*
 * Copyright 2023 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.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 net.jsign.DigestAlgorithm;

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

public class HashiCorpVaultSigningServiceTest {

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

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

    @Test
    public void testGetCertificateChain() throws Exception {
        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port() + "/", "token", alias -> {
            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);
            }
        });

        Certificate[] chain = service.getCertificateChain("key1");
        assertNotNull("chain", chain);
        assertEquals("number of certificates", 3, chain.length);
    }

    @Test
    public void testGetAliases() throws Exception {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys")
                .havingQueryStringEqualTo("list=true")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"keys\": [\"key1\", \"key2\", \"key3\"]" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        List<String> aliases = service.aliases();

        assertEquals("aliases", Arrays.asList("key1", "key2", "key3"), aliases);
    }

    @Test
    public void testGetAliasesError() {
        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);

        assertThrows(KeyStoreException.class, service::aliases);
    }

    @Test
    public void testMissingKeyVersion() {
        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);

        Exception e = assertThrows(UnrecoverableKeyException.class, () -> service.getPrivateKey("key1", null));
        assertEquals("message", "Unable to fetch HashiCorp Vault private key 'key1' (missing key version)", e.getMessage());
    }

    @Test
    public void testGetPrivateKeyGCPKMS() throws Exception {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"id\": \"projects/first-rain-123/locations/global/keyRings/mykeyring/cryptoKeys/key1\", " +
                        "    \"algorithm\": \"rsa_sign_pkcs1_2048_sha256\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);
        assertNotNull("privateKey", privateKey);
        assertEquals("keyId", "key1:7", privateKey.getId());
        assertEquals("algorithm", "RSA", privateKey.getAlgorithm());

        // check if the key is cached
        SigningServicePrivateKey privateKey2 = service.getPrivateKey("key1:7", null);
        assertSame("privateKey", privateKey, privateKey2);
    }

    @Test
    public void testGetPrivateKeyTransit() throws Exception {
        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"type\": \"rsa-2048\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);
        assertNotNull("privateKey", privateKey);
        assertEquals("keyId", "key1:7", privateKey.getId());
        assertEquals("algorithm", "RSA", privateKey.getAlgorithm());

        // check if the key is cached
        SigningServicePrivateKey privateKey2 = service.getPrivateKey("key1:7", null);
        assertSame("privateKey", privateKey, privateKey2);
    }

    @Test
    public void testGetPrivateKeyError() {
        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);

        Exception e = assertThrows(UnrecoverableKeyException.class, () -> service.getPrivateKey("key1:7", null));
        assertEquals("message", "Unable to fetch HashiCorp Vault private key 'key1:7'", e.getMessage());
    }

    @Test
    public void testSignGCPKMS() throws Exception {
        byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes();
        byte[] digest = DigestAlgorithm.SHA256.getMessageDigest().digest(data);

        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"id\": \"projects/first-rain-123/locations/global/keyRings/mykeyring/cryptoKeys/key1\", " +
                        "    \"algorithm\": \"rsa_sign_pkcs1_2048_sha256\"" +
                        "  }" +
                        "}");

        onRequest()
                .havingMethodEqualTo("POST")
                .havingPathEqualTo("/sign/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .havingBodyEqualTo("{\"key_version\":\"7\",\"digest\":\"" + Base64.getEncoder().encodeToString(digest) + "\"}")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"signature\": \"" + Base64.getEncoder().encodeToString(new byte[32]) + "\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);

        byte[] signature = service.sign(privateKey, "SHA256withRSA", data);
        assertNotNull("signature", signature);
        assertArrayEquals("signature", new byte[32], signature);
    }

    @Test
    public void testSignTransit() throws Exception {
        byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes();
        byte[] digest = DigestAlgorithm.SHA384.getMessageDigest().digest(data);

        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"type\": \"rsa-2048\"" +
                        "  }" +
                        "}");

        onRequest()
                .havingMethodEqualTo("POST")
                .havingPathEqualTo("/sign/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .havingBodyEqualTo("{\"prehashed\":true,\"input\":\"" + Base64.getEncoder().encodeToString(digest) +"\",\"key_version\":\"7\",\"hash_algorithm\":\"sha2-384\",\"signature_algorithm\":\"pkcs1v15\"}")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"signature\": \"vault:v7:" + Base64.getEncoder().encodeToString(new byte[32]) + "\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port() , "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);

        byte[] signature = service.sign(privateKey, "SHA384withRSA", data);
        assertNotNull("signature", signature);
    }

    @Test
    public void testSignErrorGCPKMS() throws Exception {
        byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes();

        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"id\": \"projects/first-rain-123/locations/global/keyRings/mykeyring/cryptoKeys/key1\", " +
                        "    \"algorithm\": \"rsa_sign_pkcs1_2048_sha256\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);

        assertThrows(GeneralSecurityException.class, () -> service.sign(privateKey, "SHA256withRSA", data));
    }

    @Test
    public void testSignErrorTransit() throws Exception {
        byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes();

        onRequest()
                .havingMethodEqualTo("GET")
                .havingPathEqualTo("/keys/key1")
                .havingHeaderEqualTo("Authorization", "Bearer token")
                .respond()
                .withStatus(200)
                .withBody("{" +
                        "  \"data\": {" +
                        "    \"type\": \"rsa-2048\"" +
                        "  }" +
                        "}");

        SigningService service = new HashiCorpVaultSigningService("http://localhost:" + port(), "token", null);
        SigningServicePrivateKey privateKey = service.getPrivateKey("key1:7", null);

        assertThrows(GeneralSecurityException.class, () -> service.sign(privateKey, "SHA256withRSA", data));
    }
}