001/**
002 * Copyright 2023 Maria Merkel
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package net.jsign.jca;
018
019import java.io.IOException;
020import java.security.GeneralSecurityException;
021import java.security.KeyStoreException;
022import java.security.UnrecoverableKeyException;
023import java.security.cert.Certificate;
024import java.util.ArrayList;
025import java.util.Base64;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.function.Function;
030
031import com.cedarsoftware.util.io.JsonWriter;
032
033import net.jsign.DigestAlgorithm;
034
035/**
036 * Signing service using the HashiCorp Vault API. It supports the Google Cloud KMS secrets engine only.
037 *
038 * @see <a href="https://developer.hashicorp.com/vault/api-docs/secret/gcpkms">HashiCorp Vault API - Google Cloud KMS Secrets Engine</a>
039 * @since 5.0
040 */
041public class HashiCorpVaultSigningService implements SigningService {
042
043    private final Function<String, Certificate[]> certificateStore;
044
045    /** Cache of private keys indexed by id */
046    private final Map<String, SigningServicePrivateKey> keys = new HashMap<>();
047
048    private final RESTClient client;
049
050    /**
051     * Creates a new HashiCorp Vault signing service.
052     *
053     * @param engineURL        the URL of the HashiCorp Vault secrets engine
054     * @param token            the HashiCorp Vault token
055     * @param certificateStore provides the certificate chain for the keys
056     */
057    public HashiCorpVaultSigningService(String engineURL, String token, Function<String, Certificate[]> certificateStore) {
058        this.certificateStore = certificateStore;
059        this.client = new RESTClient(engineURL.endsWith("/") ? engineURL : engineURL + "/", conn -> conn.setRequestProperty("Authorization", "Bearer " + token));
060    }
061
062    @Override
063    public String getName() {
064        return "HashiCorpVault";
065    }
066
067    /**
068     * Returns the list of key names available in the secrets engine.
069     *
070     * NOTE: This will return the key name only, not the key name and version.
071     * HashiCorp Vault does not provide a function to retrieve the key version.
072     * The key version will need to be appended to the key name when using the key.
073     *
074     * @return list of key names
075     */
076    @Override
077    public List<String> aliases() throws KeyStoreException {
078        List<String> aliases = new ArrayList<>();
079
080        try {
081            Map<String, ?> response = client.get("keys?list=true");
082            Object[] keys = ((Map<String, Object[]>) response.get("data")).get("keys");
083            for (Object key : keys) {
084                aliases.add((String) key);
085            }
086        } catch (IOException e) {
087            throw new KeyStoreException(e);
088        }
089
090        return aliases;
091    }
092
093    @Override
094    public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
095        return certificateStore.apply(alias);
096    }
097
098    @Override
099    public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
100        if (keys.containsKey(alias)) {
101            return keys.get(alias);
102        }
103
104        if (!alias.contains(":")) {
105            throw new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "' (missing key version)");
106        }
107
108        String algorithm;
109
110        try {
111            Map<String, ?> response = client.get("keys/" + alias.substring(0, alias.indexOf(":")));
112            algorithm = ((Map<String, String>) response.get("data")).get("algorithm");
113        } catch (IOException e) {
114            throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to fetch HashiCorp Vault Google Cloud private key '" + alias + "'").initCause(e);
115        }
116
117        algorithm = algorithm.substring(0, algorithm.indexOf("_")).toUpperCase();
118
119        SigningServicePrivateKey key = new SigningServicePrivateKey(alias, algorithm);
120        keys.put(alias, key);
121        return key;
122    }
123
124    @Override
125    public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
126        DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
127        data = digestAlgorithm.getMessageDigest().digest(data);
128
129        String alias = privateKey.getId();
130        String keyName = alias.substring(0, alias.indexOf(":"));
131        String keyVersion = alias.substring(alias.indexOf(":") + 1);
132
133        Map<String, Object> request = new HashMap<>();
134        request.put("key_version", keyVersion);
135        request.put("digest", Base64.getEncoder().encodeToString(data));
136
137        try {
138            Map<String, Object> args = new HashMap<>();
139            args.put(JsonWriter.TYPE, "false");
140            Map<String, ?> response = client.post("sign/" + keyName, JsonWriter.objectToJson(request, args));
141            String signature = ((Map<String, String>) response.get("data")).get("signature");
142
143            return Base64.getDecoder().decode(signature);
144        } catch (IOException e) {
145            throw new GeneralSecurityException(e);
146        }
147    }
148}