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><api-key>|<keystore>|<password></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><username>|<password></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}