001/**
002 * Copyright 2023 Emmanuel Bourg
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;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.net.UnknownServiceException;
023import java.nio.ByteBuffer;
024import java.security.KeyStore;
025import java.security.KeyStoreException;
026import java.security.PrivateKey;
027import java.security.Provider;
028import java.security.Security;
029import java.security.cert.Certificate;
030import java.security.cert.CertificateException;
031import java.util.Collections;
032import java.util.LinkedHashSet;
033import java.util.Set;
034import java.util.function.Function;
035import javax.smartcardio.CardException;
036
037import net.jsign.jca.AmazonCredentials;
038import net.jsign.jca.AmazonSigningService;
039import net.jsign.jca.AzureKeyVaultSigningService;
040import net.jsign.jca.DigiCertOneSigningService;
041import net.jsign.jca.ESignerSigningService;
042import net.jsign.jca.GoogleCloudSigningService;
043import net.jsign.jca.HashiCorpVaultSigningService;
044import net.jsign.jca.OpenPGPCardSigningService;
045import net.jsign.jca.SigningServiceJcaProvider;
046
047/**
048 * Type of a keystore.
049 *
050 * @since 5.0
051 */
052public enum KeyStoreType {
053
054    /** Not a keystore, a private key file and a certificate file are provided separately and assembled into an in-memory keystore */
055    NONE(true, false, false) {
056        @Override
057        void validate(KeyStoreBuilder params) {
058            if (params.keyfile() == null) {
059                throw new IllegalArgumentException("keyfile " + params.parameterName() + " must be set");
060            }
061            if (!params.keyfile().exists()) {
062                throw new IllegalArgumentException("The keyfile " + params.keyfile() + " couldn't be found");
063            }
064            if (params.certfile() == null) {
065                throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set");
066            }
067            if (!params.certfile().exists()) {
068                throw new IllegalArgumentException("The certfile " + params.certfile() + " couldn't be found");
069            }
070        }
071
072        @Override
073        KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException {
074            // load the certificate chain
075            Certificate[] chain;
076            try {
077                chain = CertificateUtils.loadCertificateChain(params.certfile());
078            } catch (Exception e) {
079                throw new KeyStoreException("Failed to load the certificate from " + params.certfile(), e);
080            }
081
082            // load the private key
083            PrivateKey privateKey;
084            try {
085                privateKey = PrivateKeyUtils.load(params.keyfile(), params.keypass() != null ? params.keypass() : params.storepass());
086            } catch (Exception e) {
087                throw new KeyStoreException("Failed to load the private key from " + params.keyfile(), e);
088            }
089
090            // build the in-memory keystore
091            KeyStore ks = KeyStore.getInstance("JKS");
092            try {
093                ks.load(null, null);
094                String keypass = params.keypass();
095                if (keypass == null) {
096                    keypass = params.storepass();
097                }
098                ks.setKeyEntry("jsign", privateKey, keypass != null ? keypass.toCharArray() : new char[0], chain);
099            } catch (Exception e) {
100                throw new KeyStoreException(e);
101            }
102
103            return ks;
104        }
105    },
106
107    /** Java keystore */
108    JKS(true, true, false) {
109        @Override
110        void validate(KeyStoreBuilder params) {
111            if (params.keystore() == null) {
112                throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set");
113            }
114        }
115    },
116
117    /** JCE keystore */
118    JCEKS(true, true, false) {
119        @Override
120        void validate(KeyStoreBuilder params) {
121            if (params.keystore() == null) {
122                throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set");
123            }
124        }
125    },
126
127    /** PKCS#12 keystore */
128    PKCS12(true, true, false) {
129        @Override
130        void validate(KeyStoreBuilder params) {
131            if (params.keystore() == null) {
132                throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set");
133            }
134        }
135    },
136
137    /**
138     * PKCS#11 hardware token. The keystore parameter specifies either the name of the provider defined
139     * in <code>jre/lib/security/java.security</code> or the path to the
140     * <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html#Config">SunPKCS11 configuration file</a>.
141     */
142    PKCS11(false, true, true) {
143        @Override
144        void validate(KeyStoreBuilder params) {
145            if (params.keystore() == null) {
146                throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set");
147            }
148        }
149
150        @Override
151        Provider getProvider(KeyStoreBuilder params) {
152            // the keystore parameter is either the provider name or the SunPKCS11 configuration file
153            if (params.createFile(params.keystore()).exists()) {
154                return ProviderUtils.createSunPKCS11Provider(params.keystore());
155            } else if (params.keystore().startsWith("SunPKCS11-")) {
156                Provider provider = Security.getProvider(params.keystore());
157                if (provider == null) {
158                    throw new IllegalArgumentException("Security provider " + params.keystore() + " not found");
159                }
160                return provider;
161            } else {
162                throw new IllegalArgumentException("keystore " + params.parameterName() + " should either refer to the SunPKCS11 configuration file or to the name of the provider configured in jre/lib/security/java.security");
163            }
164        }
165    },
166
167    /**
168     * OpenPGP card. OpenPGP cards contain up to 3 keys, one for signing, one for encryption, and one for authentication.
169     * All of them can be used for code signing (except encryption keys based on an elliptic curve). The alias
170     * to select the key is either, <code>SIGNATURE</code>, <code>ENCRYPTION</code> or <code>AUTHENTICATION</code>.
171     * This keystore can be used with a Nitrokey (non-HSM models) or a Yubikey. It doesn't require any external library
172     * to be installed.
173     */
174    OPENPGP(false, false, false) {
175        @Override
176        void validate(KeyStoreBuilder params) {
177            if (params.storepass() == null) {
178                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the PIN");
179            }
180        }
181
182        @Override
183        Provider getProvider(KeyStoreBuilder params) {
184            try {
185                Function<String, Certificate[]> certificateStore = alias -> {
186                    try {
187                        return CertificateUtils.loadCertificateChain(params.certfile());
188                    } catch (IOException | CertificateException e) {
189                        throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e);
190                    }
191                };
192                return new SigningServiceJcaProvider(new OpenPGPCardSigningService(params.storepass(), params.certfile() != null ? certificateStore : null));
193            } catch (CardException e) {
194                throw new IllegalStateException("Failed to initialize the OpenPGP card", e);
195            }
196        }
197    },
198
199    /**
200     * OpenSC supported smart card.
201     * This keystore requires the installation of <a href="https://github.com/OpenSC/OpenSC">OpenSC</a>.
202     * If multiple devices are connected, the keystore parameter can be used to specify the name of the one to use.
203     */
204    OPENSC(false, true, true) {
205        @Override
206        Provider getProvider(KeyStoreBuilder params) {
207            return OpenSC.getProvider(params.keystore());
208        }
209    },
210
211    /**
212     * Nitrokey HSM. This keystore requires the installation of <a href="https://github.com/OpenSC/OpenSC">OpenSC</a>.
213     * Other Nitrokeys based on the OpenPGP card standard are also supported with this storetype, but an X.509
214     * certificate must be imported into the Nitrokey (using the gnupg writecert command). Keys without certificates
215     * are ignored. Otherwise the {@link #OPENPGP} type should be used.
216     */
217    NITROKEY(false, true, true) {
218        @Override
219        Provider getProvider(KeyStoreBuilder params) {
220            return OpenSC.getProvider(params.keystore() != null ? params.keystore() : "Nitrokey");
221        }
222    },
223
224    /**
225     * YubiKey PIV. This keystore requires the ykcs11 library from the <a href="https://developers.yubico.com/yubico-piv-tool/">Yubico PIV Tool</a>
226     * to be installed at the default location. On Windows, the path to the library must be specified in the
227     * <code>PATH</code> environment variable.
228     */
229    YUBIKEY(false, true, true) {
230        @Override
231        Provider getProvider(KeyStoreBuilder params) {
232            return YubiKey.getProvider();
233        }
234
235        @Override
236        Set<String> getAliases(KeyStore keystore) throws KeyStoreException {
237            Set<String> aliases = super.getAliases(keystore);
238            // the attestation certificate is never used for signing
239            aliases.remove("X.509 Certificate for PIV Attestation");
240            return aliases;
241        }
242    },
243
244    /**
245     * AWS Key Management Service (KMS). AWS KMS stores only the private key, the certificate must be provided
246     * separately. The keystore parameter references the AWS region.
247     *
248     * <p>The AWS access key, secret key, and optionally the session token, are concatenated and used as
249     * the storepass parameter; if the latter is not provided, Jsign attempts to fetch the credentials from
250     * the environment variables (<code>AWS_ACCESS_KEY_ID</code>, <code>AWS_SECRET_ACCESS_KEY</code> and
251     * <code>AWS_SESSION_TOKEN</code>) or from the IMDSv2 service when running on an AWS EC2 instance.</p>
252     *
253     * <p>In any case, the credentials must allow the following actions: <code>kms:ListKeys</code>,
254     * <code>kms:DescribeKey</code> and <code>kms:Sign</code>.</p>
255     * */
256    AWS(false, false, false) {
257        @Override
258        void validate(KeyStoreBuilder params) {
259            if (params.keystore() == null) {
260                throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the AWS region");
261            }
262            if (params.certfile() == null) {
263                throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set");
264            }
265        }
266
267        @Override
268        Provider getProvider(KeyStoreBuilder params) {
269            AmazonCredentials credentials;
270            if (params.storepass() != null) {
271                credentials = AmazonCredentials.parse(params.storepass());
272            } else {
273                try {
274                    credentials = AmazonCredentials.getDefault();
275                } catch (UnknownServiceException e) {
276                    throw new IllegalArgumentException("storepass " + params.parameterName()
277                            + " must specify the AWS credentials: <accessKey>|<secretKey>[|<sessionToken>]"
278                            + ", when not running from an EC2 instance (" + e.getMessage() + ")", e);
279                } catch (IOException e) {
280                    throw new RuntimeException("An error occurred while fetching temporary credentials from IMDSv2 service", e);
281                }
282            }
283
284            return new SigningServiceJcaProvider(new AmazonSigningService(params.keystore(), credentials, alias -> {
285                try {
286                    return CertificateUtils.loadCertificateChain(params.certfile());
287                } catch (IOException | CertificateException e) {
288                    throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e);
289                }
290            }));
291        }
292    },
293
294    /**
295     * Azure Key Vault. The keystore parameter specifies the name of the key vault, either the short name
296     * (e.g. <code>myvault</code>), or the full URL (e.g. <code>https://myvault.vault.azure.net</code>).
297     * The Azure API access token is used as the keystore password.
298     */
299    AZUREKEYVAULT(false, true, false) {
300        @Override
301        void validate(KeyStoreBuilder params) {
302            if (params.keystore() == null) {
303                throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Azure vault name");
304            }
305            if (params.storepass() == null) {
306                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Azure API access token");
307            }
308        }
309
310        @Override
311        Provider getProvider(KeyStoreBuilder params) {
312            return new SigningServiceJcaProvider(new AzureKeyVaultSigningService(params.keystore(), params.storepass()));
313        }
314    },
315
316    /**
317     * DigiCert ONE. Certificates and keys stored in the DigiCert ONE Secure Software Manager can be used directly
318     * without installing the DigiCert client tools. The API key, the PKCS#12 keystore holding the client certificate
319     * and its password are combined to form the storepass parameter: <code>&lt;api-key&gt;|&lt;keystore&gt;|&lt;password&gt;</code>.
320     */
321    DIGICERTONE(false, true, false) {
322        @Override
323        void validate(KeyStoreBuilder params) {
324            if (params.storepass() == null || params.storepass().split("\\|").length != 3) {
325                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the DigiCert ONE API key and the client certificate: <apikey>|<keystore>|<password>");
326            }
327        }
328
329        @Override
330        Provider getProvider(KeyStoreBuilder params) {
331            String[] elements = params.storepass().split("\\|");
332            return new SigningServiceJcaProvider(new DigiCertOneSigningService(elements[0], params.createFile(elements[1]), elements[2]));
333        }
334    },
335
336    /**
337     * SSL.com eSigner. The SSL.com username and password are used as the keystore password (<code>&lt;username&gt;|&lt;password&gt;</code>),
338     * and the base64 encoded TOTP secret is used as the key password.
339     */
340    ESIGNER(false, true, false) {
341        @Override
342        void validate(KeyStoreBuilder params) {
343            if (params.storepass() == null || !params.storepass().contains("|")) {
344                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SSL.com username and password: <username>|<password>");
345            }
346        }
347
348        @Override
349        Provider getProvider(KeyStoreBuilder params) {
350            String[] elements = params.storepass().split("\\|", 2);
351            String endpoint = params.keystore() != null ? params.keystore() : "https://cs.ssl.com";
352            try {
353                return new SigningServiceJcaProvider(new ESignerSigningService(endpoint, elements[0], elements[1]));
354            } catch (IOException e) {
355                throw new IllegalStateException("Authentication failed with SSL.com", e);
356            }
357        }
358
359        @Override
360        boolean reuseKeyStorePassword() {
361            return false;
362        }
363    },
364
365    /**
366     * Google Cloud KMS. Google Cloud KMS stores only the private key, the certificate must be provided separately.
367     * The keystore parameter references the path of the keyring. The alias can specify either the full path of the key,
368     * or only the short name. If the version is omitted the most recent one will be picked automatically.
369     */
370    GOOGLECLOUD(false, false, false) {
371        @Override
372        void validate(KeyStoreBuilder params) {
373            if (params.keystore() == null) {
374                throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Goole Cloud keyring");
375            }
376            if (params.storepass() == null) {
377                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Goole Cloud API access token");
378            }
379            if (params.certfile() == null) {
380                throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set");
381            }
382        }
383
384        @Override
385        Provider getProvider(KeyStoreBuilder params) {
386            return new SigningServiceJcaProvider(new GoogleCloudSigningService(params.keystore(), params.storepass(), alias -> {
387                try {
388                    return CertificateUtils.loadCertificateChain(params.certfile());
389                } catch (IOException | CertificateException e) {
390                    throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e);
391                }
392            }));
393        }
394    },
395
396    /**
397     * HashiCorp Vault secrets engine (GCP only). Since Google Cloud KMS stores only the private key, the certificate
398     * must be provided separately. The keystore parameter references the URL of the HashiCorp Vault secrets engine
399     * (<code>https://vault.example.com/v1/gcpkms</code>). The alias specifies the name of the key in Vault and the key version
400     * in Google Cloud separated by a colon character (<code>mykey:1</code>).
401     */
402    HASHICORPVAULT(false, false, false) {
403        @Override
404        void validate(KeyStoreBuilder params) {
405            if (params.keystore() == null) {
406                throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the HashiCorp Vault secrets engine URL");
407            }
408            if (params.storepass() == null) {
409                throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the HashiCorp Vault token");
410            }
411            if (params.certfile() == null) {
412                throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set");
413            }
414        }
415
416        @Override
417        Provider getProvider(KeyStoreBuilder params) {
418            return new SigningServiceJcaProvider(new HashiCorpVaultSigningService(params.keystore(), params.storepass(), alias -> {
419                try {
420                    return CertificateUtils.loadCertificateChain(params.certfile());
421                } catch (IOException | CertificateException e) {
422                    throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e);
423                }
424            }));
425        }
426    };
427
428
429    /** Tells if the keystore is contained in a local file */
430    private final boolean fileBased;
431
432    /** Tells if the keystore contains the certificate */
433    private final boolean certificate;
434
435    /** Tells if the keystore is actually a PKCS#11 keystore */
436    private final boolean pkcs11;
437
438    KeyStoreType(boolean fileBased, boolean certificate, boolean pkcs11) {
439        this.fileBased = fileBased;
440        this.certificate = certificate;
441        this.pkcs11 = pkcs11;
442    }
443
444    boolean hasCertificate() {
445        return certificate;
446    }
447
448    /**
449     * Validates the keystore parameters.
450     */
451    void validate(KeyStoreBuilder params) throws IllegalArgumentException {
452    }
453
454    /**
455     * Returns the security provider to use the keystore.
456     */
457    Provider getProvider(KeyStoreBuilder params) {
458        return null;
459    }
460
461    /**
462     * Build the keystore.
463     */
464    KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException {
465        KeyStore ks;
466        try {
467            KeyStoreType storetype = pkcs11 ? PKCS11 : this;
468            if (provider != null) {
469                ks = KeyStore.getInstance(storetype.name(), provider);
470            } else {
471                ks = KeyStore.getInstance(storetype.name());
472            }
473        } catch (KeyStoreException e) {
474            throw new KeyStoreException("keystore type '" + name() + "' is not supported" + (provider != null ? " with security provider " + provider.getName() : ""), e);
475        }
476
477        if (fileBased && (params.keystore() == null || !params.createFile(params.keystore()).exists())) {
478            throw new KeyStoreException("The keystore " + params.keystore() + " couldn't be found");
479        }
480
481        try {
482            try (FileInputStream in = fileBased ? new FileInputStream(params.createFile(params.keystore())) : null) {
483                ks.load(in, params.storepass() != null ? params.storepass().toCharArray() : null);
484            }
485        } catch (Exception e) {
486            throw new KeyStoreException("Unable to load the keystore " + params.keystore(), e);
487        }
488
489        return ks;
490    }
491
492    /**
493     * Returns the aliases of the keystore available for signing.
494     */
495    Set<String> getAliases(KeyStore keystore) throws KeyStoreException {
496        return new LinkedHashSet<>(Collections.list(keystore.aliases()));
497    }
498
499    /**
500     * Tells if the keystore password can be reused as the key password.
501     */
502    boolean reuseKeyStorePassword() {
503        return true;
504    }
505
506    /**
507     * Guess the type of the keystore from the header or the extension of the file.
508     *
509     * @param path   the path to the keystore
510     */
511    static KeyStoreType of(File path) {
512        // guess the type of the keystore from the header of the file
513        if (path.exists()) {
514            try (FileInputStream in = new FileInputStream(path)) {
515                byte[] header = new byte[4];
516                in.read(header);
517                ByteBuffer buffer = ByteBuffer.wrap(header);
518                if (buffer.get(0) == 0x30) {
519                    return PKCS12;
520                } else if ((buffer.getInt(0) & 0xFFFFFFFFL) == 0xCECECECEL) {
521                    return JCEKS;
522                } else if ((buffer.getInt(0) & 0xFFFFFFFFL) == 0xFEEDFEEDL) {
523                    return JKS;
524                }
525            } catch (IOException e) {
526                throw new RuntimeException("Unable to load the keystore " + path, e);
527            }
528        }
529
530        // guess the type of the keystore from the extension of the file
531        String filename = path.getName().toLowerCase();
532        if (filename.endsWith(".p12") || filename.endsWith(".pfx")) {
533            return PKCS12;
534        } else if (filename.endsWith(".jceks")) {
535            return JCEKS;
536        } else if (filename.endsWith(".jks")) {
537            return JKS;
538        } else {
539            return null;
540        }
541    }
542}