diff --git a/jsign-cli/src/test/java/net/jsign/JsignCLITest.java b/jsign-cli/src/test/java/net/jsign/JsignCLITest.java index cff9d852..f6d95b1e 100644 --- a/jsign-cli/src/test/java/net/jsign/JsignCLITest.java +++ b/jsign-cli/src/test/java/net/jsign/JsignCLITest.java @@ -55,7 +55,7 @@ public class JsignCLITest { private JsignCLI cli; private File sourceFile = new File("target/test-classes/wineyes.exe"); private File targetFile = new File("target/test-classes/wineyes-signed-with-cli.exe"); - + private String keystore = "keystore.jks"; private String alias = "test"; private String keypass = "password"; @@ -65,12 +65,12 @@ public class JsignCLITest { @Before public void setUp() throws Exception { cli = new JsignCLI(); - + // remove the files signed previously if (targetFile.exists()) { assertTrue("Unable to remove the previously signed file", targetFile.delete()); } - + assertEquals("Source file CRC32", SOURCE_FILE_CRC32, FileUtils.checksumCRC32(sourceFile)); Thread.sleep(100); FileUtils.copyFile(sourceFile, targetFile); @@ -219,7 +219,7 @@ public void testSigningMultipleFiles() throws Exception { public void testSigningMultipleFilesWithListFile() throws Exception { File listFile = new File("target/test-classes/files.txt"); Files.write(listFile.toPath(), Arrays.asList("# first file", '"' + targetFile.getPath() + '"', " ", "# second file", targetFile.getAbsolutePath())); - + cli.execute("--name=WinEyes", "--url=http://www.steelblue.com/WinEyes", "--alg=SHA-1", "--keystore=target/test-classes/keystores/" + keystore, "--keypass=" + keypass, "@" + listFile); assertTrue("The file " + targetFile + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile)); @@ -271,7 +271,7 @@ public void testSigningPowerShell() throws Exception { File sourceFile = new File("target/test-classes/hello-world.ps1"); File targetFile = new File("target/test-classes/hello-world-signed-with-cli.ps1"); FileUtils.copyFile(sourceFile, targetFile); - + cli.execute("--alg=SHA-1", "--replace", "--encoding=ISO-8859-1", "--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "" + targetFile); PowerShellScript script = new PowerShellScript(targetFile); @@ -284,7 +284,7 @@ public void testSigningPowerShellWithDefaultEncoding() throws Exception { File sourceFile = new File("target/test-classes/hello-world.ps1"); File targetFile = new File("target/test-classes/hello-world-signed-with-cli.ps1"); FileUtils.copyFile(sourceFile, targetFile); - + cli.execute("--alg=SHA-1", "--replace", "--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "" + targetFile); PowerShellScript script = new PowerShellScript(targetFile); @@ -297,7 +297,7 @@ public void testSigningMSI() throws Exception { File sourceFile = new File("target/test-classes/minimal.msi"); File targetFile = new File("target/test-classes/minimal-signed-with-cli.msi"); FileUtils.copyFile(sourceFile, targetFile); - + cli.execute("--alg=SHA-1", "--replace", "--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "" + targetFile); try (MSIFile file = new MSIFile(targetFile)) { @@ -308,7 +308,7 @@ public void testSigningMSI() throws Exception { @Test public void testSigningPKCS12() throws Exception { cli.execute("--name=WinEyes", "--url=http://www.steelblue.com/WinEyes", "--alg=SHA-256", "--keystore=target/test-classes/keystores/keystore.p12", "--alias=test", "--storepass=password", "" + targetFile); - + assertTrue("The file " + targetFile + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile)); try (PEFile peFile = new PEFile(targetFile)) { @@ -341,7 +341,7 @@ public void testSigningJKS() throws Exception { @Test public void testSigningPVKSPC() throws Exception { cli.execute("--url=http://www.steelblue.com/WinEyes", "--certfile=target/test-classes/keystores/jsign-test-certificate-full-chain.spc", "--keyfile=target/test-classes/keystores/privatekey-encrypted.pvk", "--storepass=password", "" + targetFile); - + assertTrue("The file " + targetFile + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile)); try (PEFile peFile = new PEFile(targetFile)) { @@ -352,7 +352,7 @@ public void testSigningPVKSPC() throws Exception { @Test public void testSigningPEM() throws Exception { cli.execute("--certfile=target/test-classes/keystores/jsign-test-certificate.pem", "--keyfile=target/test-classes/keystores/privatekey.pkcs8.pem", "--keypass=password", "" + targetFile); - + assertTrue("The file " + targetFile + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile)); try (PEFile peFile = new PEFile(targetFile)) { @@ -363,7 +363,7 @@ public void testSigningPEM() throws Exception { @Test public void testSigningEncryptedPEM() throws Exception { cli.execute("--certfile=target/test-classes/keystores/jsign-test-certificate.pem", "--keyfile=target/test-classes/keystores/privatekey-encrypted.pkcs1.pem", "--keypass=password", "" + targetFile); - + assertTrue("The file " + targetFile + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile)); try (PEFile peFile = new PEFile(targetFile)) { @@ -373,7 +373,7 @@ public void testSigningEncryptedPEM() throws Exception { @Test public void testSigningWithYubikey() throws Exception { - Assume.assumeTrue("No Yubikey detected", YubiKey.isPresent()); + Assume.assumeTrue("No Yubikey detected", YubiKeyKeyStore.isPresent()); cli.execute("--storetype=YUBIKEY", "--certfile=target/test-classes/keystores/jsign-test-certificate-full-chain.spc", "--storepass=123456", "--alias=X.509 Certificate for Digital Signature", "" + targetFile, "" + targetFile); } @@ -383,7 +383,7 @@ public void testTimestampingAuthenticode() throws Exception { File targetFile2 = new File("target/test-classes/wineyes-timestamped-with-cli-authenticode.exe"); FileUtils.copyFile(sourceFile, targetFile2); cli.execute("--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "--tsaurl=http://timestamp.sectigo.com", "--tsmode=authenticode", "" + targetFile2); - + assertTrue("The file " + targetFile2 + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile2)); try (PEFile peFile = new PEFile(targetFile2)) { @@ -416,7 +416,7 @@ public HttpFilters filterRequest(HttpRequest originalRequest) { } }) .start(); - + try { File targetFile2 = new File("target/test-classes/wineyes-timestamped-with-cli-rfc3161-proxy-unauthenticated.exe"); FileUtils.copyFile(sourceFile, targetFile2); @@ -424,10 +424,10 @@ public HttpFilters filterRequest(HttpRequest originalRequest) { "--tsaurl=http://timestamp.sectigo.com", "--tsmode=rfc3161", "--tsretries=1", "--tsretrywait=1", "--proxyUrl=localhost:" + proxy.getListenAddress().getPort(), "" + targetFile2); - + assertTrue("The file " + targetFile2 + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile2)); assertTrue("The proxy wasn't used", proxyUsed.get()); - + try (PEFile peFile = new PEFile(targetFile2)) { SignatureAssert.assertSigned(peFile, SHA256); } @@ -469,10 +469,10 @@ public String getRealm() { "--proxyUser=jsign", "--proxyPass=jsign", "" + targetFile2); - + assertTrue("The file " + targetFile2 + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile2)); assertTrue("The proxy wasn't used", proxyUsed.get()); - + try (PEFile peFile = new PEFile(targetFile2)) { SignatureAssert.assertSigned(peFile, SHA256); } @@ -486,11 +486,11 @@ public void testReplaceSignature() throws Exception { File targetFile2 = new File("target/test-classes/wineyes-re-signed.exe"); FileUtils.copyFile(sourceFile, targetFile2); cli.execute("--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "" + targetFile2); - + assertTrue("The file " + targetFile2 + " wasn't changed", SOURCE_FILE_CRC32 != FileUtils.checksumCRC32(targetFile2)); - + cli.execute("--keystore=target/test-classes/keystores/" + keystore, "--alias=" + alias, "--keypass=" + keypass, "--alg=SHA-512", "--replace", "" + targetFile2); - + try (PEFile peFile = new PEFile(targetFile2)) { SignatureAssert.assertSigned(peFile, SHA512); } @@ -526,7 +526,7 @@ public Integer getStatus() { } public void checkPermission(Permission perm) { } - + public void checkPermission(Permission perm, Object context) { } public void checkExit(int status) { diff --git a/jsign-core/src/main/java/net/jsign/SignerHelper.java b/jsign-core/src/main/java/net/jsign/SignerHelper.java index c9526031..92ca3d14 100644 --- a/jsign-core/src/main/java/net/jsign/SignerHelper.java +++ b/jsign-core/src/main/java/net/jsign/SignerHelper.java @@ -261,7 +261,7 @@ public SignerHelper param(String key, String value) { if (value == null) { return this; } - + switch (key) { case PARAM_COMMAND: return command(value); case PARAM_KEYSTORE: return keystore(value); @@ -328,7 +328,7 @@ private AuthenticodeSigner build() throws SignerException { } catch (KeyStoreException e) { throw new SignerException("Failed to load the keystore " + (ksparams.keystore() != null ? ksparams.keystore() : ""), e); } - KeyStoreType storetype = ksparams.storetype(); + JsignKeyStore storetype = ksparams.storetype(); Provider provider = ksparams.provider(); Set aliases = null; @@ -403,12 +403,12 @@ private AuthenticodeSigner build() throws SignerException { } // enable timestamping with Azure Trusted Signing - if (tsaurl == null && storetype == KeyStoreType.TRUSTEDSIGNING) { + if ((tsaurl == null) && (storetype instanceof AzureTrustedSigningKeyStore)) { tsaurl = "http://timestamp.acs.microsoft.com/"; tsmode = TimestampingMode.RFC3161.name(); tsretries = 3; } - + // configure the signer return new AuthenticodeSigner(chain, privateKey) .withProgramName(name) @@ -434,7 +434,7 @@ public void sign(File file) throws SignerException { if (!file.exists()) { throw new SignerException("The file " + file + " couldn't be found"); } - + try (Signable signable = Signable.of(file, encoding)) { File detachedSignature = getDetachedSignature(file); if (detached && detachedSignature.exists()) { @@ -638,7 +638,7 @@ private void timestamp(File file) throws SignerException { SignerId signerId = signerInformation.getSID(); X509CertificateHolder certificate = (X509CertificateHolder) signature.getCertificates().getMatches(signerId).iterator().next(); - String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); + String digestAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(signerInformation.getDigestAlgorithmID()); String keyAlgorithmName = new DefaultAlgorithmNameFinder().getAlgorithmName(new ASN1ObjectIdentifier(signerInformation.getEncryptionAlgOID())); String name = digestAlgorithmName + "/" + keyAlgorithmName + " signature from '" + certificate.getSubject() + "'"; diff --git a/jsign-core/src/test/java/net/jsign/PESignerTest.java b/jsign-core/src/test/java/net/jsign/PESignerTest.java index 37d75c00..514236b6 100644 --- a/jsign-core/src/test/java/net/jsign/PESignerTest.java +++ b/jsign-core/src/test/java/net/jsign/PESignerTest.java @@ -66,7 +66,7 @@ private KeyStore getKeyStore() throws Exception { public void testSign() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed.exe"); - + FileUtils.copyFile(sourceFile, targetFile); PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD) @@ -96,7 +96,7 @@ public void testSignWithUnknownKeyStoreEntry() { public void testSigningWithKeyAndChain() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed-key-chain.exe"); - + FileUtils.copyFile(sourceFile, targetFile); Certificate[] chain; @@ -132,7 +132,7 @@ public void testSigningWithKeyAndChain() throws Exception { @Test public void testSigningWithYubikey() throws Exception { - Assume.assumeTrue("No Yubikey detected", YubiKey.isPresent()); + Assume.assumeTrue("No Yubikey detected", YubiKeyKeyStore.isPresent()); File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed-yubikey.exe"); @@ -166,7 +166,7 @@ public void testNullChain() throws Exception { public void testSigningWithMismatchingKeyAndCertificate() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed-mismatching-key-certificate.exe"); - + FileUtils.copyFile(sourceFile, targetFile); Certificate[] chain; @@ -202,7 +202,7 @@ public void testTimestampRFC3161() throws Exception { public void testTimestamp(TimestampingMode mode, DigestAlgorithm alg) throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-timestamped-" + mode.name().toLowerCase() + ".exe"); - + FileUtils.copyFile(sourceFile, targetFile); PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD); @@ -234,7 +234,7 @@ public void testWithTimestamper() throws Exception { signer.withDigestAlgorithm(SHA1); signer.withTimestamping(true); signer.withTimestamper(new AuthenticodeTimestamper() { - + @Override protected CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) throws IOException, TimestampingException { called.add(true); @@ -257,7 +257,7 @@ protected CMSSignedData timestamp(DigestAlgorithm algo, byte[] encryptedDigest) public void testSignTwice() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed-twice.exe"); - + FileUtils.copyFile(sourceFile, targetFile); try (PEFile peFile = new PEFile(targetFile)) { @@ -286,7 +286,7 @@ public void testSignTwice() throws Exception { public void testSignThreeTimes() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed-three-times.exe"); - + FileUtils.copyFile(sourceFile, targetFile); try (PEFile peFile = new PEFile(targetFile)) { @@ -323,7 +323,7 @@ public void testSignThreeTimes() throws Exception { public void testReplaceSignature() throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-re-signed.exe"); - + FileUtils.copyFile(sourceFile, targetFile); try (PEFile peFile = new PEFile(targetFile)) { @@ -359,16 +359,16 @@ public void testInvalidRFC3161TimestampingAuthority() throws Exception { public void testInvalidTimestampingAuthority(TimestampingMode mode) throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-timestamped-unavailable-" + mode.name().toLowerCase() + ".exe"); - + FileUtils.copyFile(sourceFile, targetFile); - + PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD); signer.withDigestAlgorithm(SHA1); signer.withTimestamping(true); signer.withTimestampingMode(mode); signer.withTimestampingAuthority("http://www.google.com/" + mode.name().toLowerCase()); signer.withTimestampingRetries(1); - + try (PEFile peFile = new PEFile(targetFile)) { Exception e = assertThrows(TimestampingException.class, () -> signer.sign(peFile)); assertTrue("Missing suppressed IOException", e.getSuppressed() != null && e.getSuppressed().length > 0 && e.getSuppressed()[0].getClass().equals(IOException.class)); @@ -390,16 +390,16 @@ public void testBrokenRFC3161TimestampingAuthority() throws Exception { public void testBrokenTimestampingAuthority(TimestampingMode mode) throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-timestamped-broken-" + mode.name().toLowerCase() + ".exe"); - + FileUtils.copyFile(sourceFile, targetFile); - + PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD); signer.withDigestAlgorithm(SHA1); signer.withTimestamping(true); signer.withTimestampingMode(mode); signer.withTimestampingAuthority("http://github.com"); signer.withTimestampingRetries(1); - + try (PEFile peFile = new PEFile(targetFile)) { assertThrows(TimestampingException.class, () -> signer.sign(peFile)); } @@ -434,7 +434,7 @@ public void testRFC3161TimestampingFailover() throws Exception { public void testTimestampingFailover(TimestampingMode mode, String validURL) throws Exception { File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-timestamped-failover-" + mode.name().toLowerCase() + ".exe"); - + FileUtils.copyFile(sourceFile, targetFile); PESigner signer = new PESigner(getKeyStore(), ALIAS, PRIVATE_KEY_PASSWORD); @@ -490,7 +490,7 @@ public void testWithSignatureAlgorithmSHA1withRSA() throws Exception { @Test public void testWithSignatureAlgorithmSHA256withRSAandMGF1() throws Exception { Security.addProvider(new BouncyCastleProvider()); - + File sourceFile = new File("target/test-classes/wineyes.exe"); File targetFile = new File("target/test-classes/wineyes-signed.exe"); diff --git a/jsign-crypto/src/main/java/net/jsign/AbstractJsignKeyStore.java b/jsign-crypto/src/main/java/net/jsign/AbstractJsignKeyStore.java new file mode 100644 index 00000000..88106c97 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/AbstractJsignKeyStore.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public abstract class AbstractJsignKeyStore implements JsignKeyStore { + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return null; + } + + @Override + public KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException { + KeyStore ks; + try { + if (provider != null) { + ks = KeyStore.getInstance(getType(), provider); + } else { + ks = KeyStore.getInstance(getType()); + } + } catch (KeyStoreException e) { + throw new KeyStoreException("keystore type '" + getType() + "' is not supported" + (provider != null ? " with security provider " + provider.getName() : ""), e); + } + + try { + boolean fileBased = this instanceof FileBasedKeyStore; + try (FileInputStream in = fileBased ? new FileInputStream(params.createFile(params.keystore())) : null) { + ks.load(in, params.storepass() != null ? params.storepass().toCharArray() : null); + } + } catch (Exception e) { + throw new KeyStoreException("Unable to load the keystore " + params.keystore(), e); + } + + return ks; + } + + @Override + public Set getAliases(KeyStore keystore) throws KeyStoreException { + return new LinkedHashSet<>(Collections.list(keystore.aliases())); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/AwsKeyStore.java b/jsign-crypto/src/main/java/net/jsign/AwsKeyStore.java new file mode 100644 index 00000000..a09dc754 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/AwsKeyStore.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.AmazonCredentials; +import net.jsign.jca.AmazonSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.io.IOException; +import java.net.UnknownServiceException; +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * AWS Key Management Service (KMS). AWS KMS stores only the private key, the certificate must be provided + * separately. The keystore parameter references the AWS region. + * + *

The AWS access key, secret key, and optionally the session token, are concatenated and used as + * the storepass parameter; if the latter is not provided, Jsign attempts to fetch the credentials from + * the environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and + * AWS_SESSION_TOKEN) or from the IMDSv2 service when running on an AWS EC2 instance.

+ * + *

In any case, the credentials must allow the following actions: kms:ListKeys, + * kms:DescribeKey and kms:Sign.

+ */ +@MetaInfServices(JsignKeyStore.class) +public class AwsKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "AWS"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the AWS region"); + } + if (params.certfile() == null) { + throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + AmazonCredentials credentials; + if (params.storepass() != null) { + credentials = AmazonCredentials.parse(params.storepass()); + } else { + try { + credentials = AmazonCredentials.getDefault(); + } catch (UnknownServiceException e) { + throw new IllegalArgumentException("storepass " + params.parameterName() + + " must specify the AWS credentials: |[|]" + + ", when not running from an EC2 instance (" + e.getMessage() + ")", e); + } catch (IOException e) { + throw new RuntimeException("An error occurred while fetching temporary credentials from IMDSv2 service", e); + } + } + + return new SigningServiceJcaProvider(new AmazonSigningService(params.keystore(), credentials, getCertificateStore(params))); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/AzureKeyVaultKeyStore.java b/jsign-crypto/src/main/java/net/jsign/AzureKeyVaultKeyStore.java new file mode 100644 index 00000000..64d18e58 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/AzureKeyVaultKeyStore.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.AzureKeyVaultSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +/** + * Azure Key Vault. The keystore parameter specifies the name of the key vault, either the short name + * (e.g. myvault), or the full URL (e.g. https://myvault.vault.azure.net). + * The Azure API access token is used as the keystore password. + */ +@MetaInfServices(JsignKeyStore.class) +public class AzureKeyVaultKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "AZUREKEYVAULT"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Azure vault name"); + } + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Azure API access token"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return new SigningServiceJcaProvider(new AzureKeyVaultSigningService(params.keystore(), params.storepass())); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/AzureTrustedSigningKeyStore.java b/jsign-crypto/src/main/java/net/jsign/AzureTrustedSigningKeyStore.java new file mode 100644 index 00000000..4bfebfad --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/AzureTrustedSigningKeyStore.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.AzureTrustedSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +/** + * Azure Trusted Signing Service. The keystore parameter specifies the API endpoint (for example + * weu.codesigning.azure.net). The Azure API access token is used as the keystore password, + * it can be obtained using the Azure CLI with: + * + *
  az account get-access-token --resource https://codesigning.azure.net
+ */ +@MetaInfServices(JsignKeyStore.class) +public class AzureTrustedSigningKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "TRUSTEDSIGNING"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Azure endpoint (.codesigning.azure.net)"); + } + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Azure API access token"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return new SigningServiceJcaProvider(new AzureTrustedSigningService(params.keystore(), params.storepass())); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/DigiCertOneKeyStore.java b/jsign-crypto/src/main/java/net/jsign/DigiCertOneKeyStore.java new file mode 100644 index 00000000..cd464cae --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/DigiCertOneKeyStore.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.DigiCertOneSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +/** + * DigiCert ONE. Certificates and keys stored in the DigiCert ONE Secure Software Manager can be used directly + * without installing the DigiCert client tools. The API key, the PKCS#12 keystore holding the client certificate + * and its password are combined to form the storepass parameter: <api-key>|<keystore>|<password>. + */ +@MetaInfServices(JsignKeyStore.class) +public class DigiCertOneKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "DIGICERTONE"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.storepass() == null || params.storepass().split("\\|").length != 3) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the DigiCert ONE API key and the client certificate: ||"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + String[] elements = params.storepass().split("\\|"); + return new SigningServiceJcaProvider(new DigiCertOneSigningService(params.keystore(), elements[0], params.createFile(elements[1]), elements[2])); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/ESignerKeyStore.java b/jsign-crypto/src/main/java/net/jsign/ESignerKeyStore.java new file mode 100644 index 00000000..1ed75b34 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/ESignerKeyStore.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.ESignerSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.io.IOException; +import java.security.Provider; + +/** + * SSL.com eSigner. The SSL.com username and password are used as the keystore password (<username>|<password>), + * and the base64 encoded TOTP secret is used as the key password. + */ +@MetaInfServices(JsignKeyStore.class) +public class ESignerKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "ESIGNER"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.storepass() == null || !params.storepass().contains("|")) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SSL.com username and password: |"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + String[] elements = params.storepass().split("\\|", 2); + String endpoint = params.keystore() != null ? params.keystore() : "https://cs.ssl.com"; + try { + return new SigningServiceJcaProvider(new ESignerSigningService(endpoint, elements[0], elements[1])); + } catch (IOException e) { + throw new IllegalStateException("Authentication failed with SSL.com", e); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/FileBasedKeyStore.java b/jsign-crypto/src/main/java/net/jsign/FileBasedKeyStore.java new file mode 100644 index 00000000..3db89ae6 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/FileBasedKeyStore.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +abstract public class FileBasedKeyStore extends AbstractJsignKeyStore { + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); + } + if (!params.createFile(params.keystore()).exists()) { + throw new IllegalArgumentException("The keystore " + params.keystore() + " couldn't be found"); + } + if (params.keypass() == null && params.storepass() != null) { + // reuse the storepass as the keypass + params.keypass(params.storepass()); + } + } + + boolean hasSignature(File file, long signature, long mask) { + if (file.exists()) { + try (FileInputStream in = new FileInputStream(file)) { + byte[] header = new byte[4]; + in.read(header); + ByteBuffer buffer = ByteBuffer.wrap(header); + if ((buffer.getInt(0) & mask) == signature) { + return true; + } + } catch (IOException e) { + throw new RuntimeException("Unable to load the keystore " + file, e); + } + } + + return false; + } + + /** + * Tells if the specified file is a keystore of this type. + * + * @param file the path to the keystore + */ + abstract boolean isSupported(File file); +} diff --git a/jsign-crypto/src/main/java/net/jsign/GaraSignKeyStore.java b/jsign-crypto/src/main/java/net/jsign/GaraSignKeyStore.java new file mode 100644 index 00000000..7f43492b --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/GaraSignKeyStore.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.GaraSignCredentials; +import net.jsign.jca.GaraSignSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +@MetaInfServices(JsignKeyStore.class) +public class GaraSignKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "GARASIGN"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.storepass() == null || params.storepass().split("\\|").length > 3) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the GaraSign username/password and/or the path to the keystore containing the TLS client certificate: |, , or ||"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + String[] elements = params.storepass().split("\\|"); + String username = null; + String password = null; + String certificate = null; + if (elements.length == 1) { + certificate = elements[0]; + } else if (elements.length == 2) { + username = elements[0]; + password = elements[1]; + } else if (elements.length == 3) { + username = elements[0]; + password = elements[1]; + certificate = elements[2]; + } + + GaraSignCredentials credentials = new GaraSignCredentials(username, password, certificate, params.keypass()); + return new SigningServiceJcaProvider(new GaraSignSigningService(params.keystore(), credentials)); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/GoogleCloudKeyStore.java b/jsign-crypto/src/main/java/net/jsign/GoogleCloudKeyStore.java new file mode 100644 index 00000000..495d8966 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/GoogleCloudKeyStore.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.GoogleCloudSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * Google Cloud KMS. Google Cloud KMS stores only the private key, the certificate must be provided separately. + * The keystore parameter references the path of the keyring. The alias can specify either the full path of the key, + * or only the short name. If the version is omitted the most recent one will be picked automatically. + */ +@MetaInfServices(JsignKeyStore.class) +public class GoogleCloudKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "GOOGLECLOUD"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Goole Cloud keyring"); + } + if (!params.keystore().matches("projects/[^/]+/locations/[^/]+/keyRings/[^/]+")) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the path of the keyring (projects/{projectName}/locations/{location}/keyRings/{keyringName})"); + } + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Goole Cloud API access token"); + } + if (params.certfile() == null) { + throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return new SigningServiceJcaProvider(new GoogleCloudSigningService(params.keystore(), params.storepass(), getCertificateStore(params))); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/HashiCorpVaultKeyStore.java b/jsign-crypto/src/main/java/net/jsign/HashiCorpVaultKeyStore.java new file mode 100644 index 00000000..24a339cd --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/HashiCorpVaultKeyStore.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.HashiCorpVaultSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * HashiCorp Vault secrets engine (Transit or GCPKMS). The certificate must be provided separately. The keystore + * parameter references the URL of the HashiCorp Vault secrets engine (https://vault.example.com/v1/gcpkms). + * The alias parameter specifies the name of the key in Vault. For the Google Cloud KMS secrets engine, the version + * of the Google Cloud key is appended to the key name, separated by a colon character. (mykey:1). + */ +@MetaInfServices(JsignKeyStore.class) +public class HashiCorpVaultKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "HASHICORPVAULT"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the HashiCorp Vault secrets engine URL"); + } + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the HashiCorp Vault token"); + } + if (params.certfile() == null) { + throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return new SigningServiceJcaProvider(new HashiCorpVaultSigningService(params.keystore(), params.storepass(), getCertificateStore(params))); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/JavaKeyStore.java b/jsign-crypto/src/main/java/net/jsign/JavaKeyStore.java new file mode 100644 index 00000000..904f3d4f --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/JavaKeyStore.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.io.File; + +/** + * Java keystore + */ +@MetaInfServices(JsignKeyStore.class) +public class JavaKeyStore extends FileBasedKeyStore { + @Override + public String getType() { + return "JKS"; + } + + @Override + boolean isSupported(File file) { + String filename = file.getName().toLowerCase(); + return hasSignature(file, 0xFEEDFEEDL, 0xFFFFFFFFL) || filename.endsWith(".jks"); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/JceKeyStore.java b/jsign-crypto/src/main/java/net/jsign/JceKeyStore.java new file mode 100644 index 00000000..9388dbf9 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/JceKeyStore.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.io.File; + +/** + * JCE keystore + */ +@MetaInfServices(JsignKeyStore.class) +public class JceKeyStore extends FileBasedKeyStore { + @Override + public String getType() { + return "JCEKS"; + } + + @Override + boolean isSupported(File file) { + String filename = file.getName().toLowerCase(); + return hasSignature(file, 0xCECECECEL, 0xFFFFFFFFL) || filename.endsWith(".jceks"); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/JsignKeyStore.java b/jsign-crypto/src/main/java/net/jsign/JsignKeyStore.java new file mode 100644 index 00000000..4022cc23 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/JsignKeyStore.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.Set; +import java.util.function.Function; + +public interface JsignKeyStore { + + /** + * The keystore type identifier used to select a specific keystore type. + */ + String getType(); + + /** + * Validates the keystore parameters. + */ + void validate(KeyStoreBuilder params) throws IllegalArgumentException; + + /** + * Returns the security provider to use the keystore. + */ + Provider getProvider(KeyStoreBuilder params); + + /** + * Build the keystore. + */ + KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException; + + /** + * Returns the aliases of the keystore available for signing. + */ + Set getAliases(KeyStore keystore) throws KeyStoreException; + + static Function getCertificateStore(KeyStoreBuilder params) { + return alias -> { + if (alias == null || alias.isEmpty()) { + return null; + } + + try { + return CertificateUtils.loadCertificateChain(params.certfile()); + } catch (IOException | CertificateException e) { + throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e); + } + }; + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/JsignKeyStoreDiscovery.java b/jsign-crypto/src/main/java/net/jsign/JsignKeyStoreDiscovery.java new file mode 100644 index 00000000..1fecbb43 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/JsignKeyStoreDiscovery.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +public class JsignKeyStoreDiscovery { + private static Map keyStoresByType = new HashMap<>(); + + static { + Map keyStoresByType = new HashMap<>(); + for (JsignKeyStore keyStore : ServiceLoader.load(JsignKeyStore.class)) { + if (keyStoresByType.put(keyStore.getType(), keyStore) != null) { + throw new IllegalStateException("Duplicate key store type: " + keyStore.getType()); + } + } + JsignKeyStoreDiscovery.keyStoresByType = keyStoresByType; + } + + private JsignKeyStoreDiscovery() { + } + + public static JsignKeyStore getKeyStore(KeyStoreType type) { + return keyStoresByType.get(type.name()); + } + + public static JsignKeyStore getKeyStore(String type) { + return keyStoresByType.get(type); + } + + public static Set getKeyStoreTypes() { + return keyStoresByType.keySet(); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/KeyStoreBuilder.java b/jsign-crypto/src/main/java/net/jsign/KeyStoreBuilder.java index 51d75f0b..51eac708 100644 --- a/jsign-crypto/src/main/java/net/jsign/KeyStoreBuilder.java +++ b/jsign-crypto/src/main/java/net/jsign/KeyStoreBuilder.java @@ -25,9 +25,8 @@ import java.security.KeyStoreException; import java.security.Provider; import java.util.stream.Collectors; -import java.util.stream.Stream; -import static net.jsign.KeyStoreType.*; +import static net.jsign.KeyStoreType.NONE; /** * Keystore builder. @@ -47,7 +46,7 @@ public class KeyStoreBuilder { private String keystore; private String storepass; - private KeyStoreType storetype; + private JsignKeyStore storetype; private String keypass; private File keyfile; private File certfile; @@ -64,7 +63,7 @@ public KeyStoreBuilder() { this.parameterName = parameterName; } - String parameterName() { + public String parameterName() { return parameterName; } @@ -84,7 +83,7 @@ public KeyStoreBuilder keystore(String keystore) { return this; } - String keystore() { + public String keystore() { return keystore; } @@ -98,7 +97,7 @@ public KeyStoreBuilder storepass(String storepass) { return this; } - String storepass() { + public String storepass() { storepass = readPassword("storepass", storepass); return storepass; } @@ -106,11 +105,19 @@ String storepass() { /** * Sets the type of the keystore. */ - public KeyStoreBuilder storetype(KeyStoreType storetype) { + public KeyStoreBuilder storetype(JsignKeyStore storetype) { this.storetype = storetype; return this; } + /** + * Sets the type of the keystore. + */ + public KeyStoreBuilder storetype(KeyStoreType storetype) { + this.storetype = storetype.getJsignKeyStore(); + return this; + } + /** * Sets the type of the keystore. * @@ -118,29 +125,35 @@ public KeyStoreBuilder storetype(KeyStoreType storetype) { * @throws IllegalArgumentException if the type is not recognized */ public KeyStoreBuilder storetype(String storetype) { - try { - this.storetype = storetype != null ? KeyStoreType.valueOf(storetype.toUpperCase()) : null; - } catch (IllegalArgumentException e) { - String expectedTypes = Stream.of(KeyStoreType.values()) - .filter(type -> type != NONE).map(KeyStoreType::name) - .collect(Collectors.joining(", ")); - throw new IllegalArgumentException("Unknown keystore type '" + storetype + "' (expected types: " + expectedTypes + ")"); + if (storetype == null) { + this.storetype = null; + } else { + this.storetype = JsignKeyStoreDiscovery.getKeyStore(storetype.toUpperCase()); + if (this.storetype == null) { + String noneType = NONE.getJsignKeyStore().getType(); + String expectedTypes = JsignKeyStoreDiscovery + .getKeyStoreTypes() + .stream() + .filter(type -> !noneType.equals(type)) + .collect(Collectors.joining(", ")); + throw new IllegalArgumentException("Unknown keystore type '" + storetype + "' (expected types: " + expectedTypes + ")"); + } } return this; } - KeyStoreType storetype() { + public JsignKeyStore storetype() { if (storetype == null) { if (keystore == null) { // no keystore specified, keyfile and certfile are expected - storetype = NONE; + storetype = NONE.getJsignKeyStore(); } else { // the keystore type wasn't specified, let's try to guess it File file = createFile(keystore); if (!file.isFile()) { throw new IllegalArgumentException("Keystore file '" + keystore + "' not found"); } - storetype = KeyStoreType.of(file); + storetype = getType(file); if (storetype == null) { throw new IllegalArgumentException("Keystore type of '" + keystore + "' not recognized"); } @@ -149,6 +162,22 @@ KeyStoreType storetype() { return storetype; } + /** + * Guess the type of the keystore from the header or the extension of the file. + * + * @param file the path to the keystore + */ + static JsignKeyStore getType(File file) { + for (String type : JsignKeyStoreDiscovery.getKeyStoreTypes()) { + JsignKeyStore keyStore = JsignKeyStoreDiscovery.getKeyStore(type); + if (keyStore instanceof FileBasedKeyStore && ((FileBasedKeyStore) keyStore).isSupported(file)) { + return keyStore; + } + } + + return null; + } + /** * Sets the password to access the private key. The password can be loaded from a file by using the file: * prefix followed by the path of the file, or from an environment variable by using the env: prefix @@ -159,7 +188,7 @@ public KeyStoreBuilder keypass(String keypass) { return this; } - String keypass() { + public String keypass() { keypass = readPassword("keypass", keypass); return keypass; } @@ -179,7 +208,7 @@ public KeyStoreBuilder keyfile(File keyfile) { return this; } - File keyfile() { + public File keyfile() { return keyfile; } @@ -198,7 +227,7 @@ public KeyStoreBuilder certfile(File certfile) { return this; } - File certfile() { + public File certfile() { return certfile; } @@ -206,7 +235,7 @@ void setBaseDir(File basedir) { this.basedir = basedir; } - File createFile(String file) { + public File createFile(String file) { if (file == null) { return null; } diff --git a/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java b/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java index 3f92aa29..99a64ec6 100644 --- a/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java +++ b/jsign-crypto/src/main/java/net/jsign/KeyStoreType.java @@ -16,42 +16,6 @@ package net.jsign; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.UnknownServiceException; -import java.nio.ByteBuffer; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.PrivateKey; -import java.security.Provider; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.function.Function; -import javax.smartcardio.CardException; - -import net.jsign.jca.AmazonCredentials; -import net.jsign.jca.AmazonSigningService; -import net.jsign.jca.AzureKeyVaultSigningService; -import net.jsign.jca.AzureTrustedSigningService; -import net.jsign.jca.DigiCertOneSigningService; -import net.jsign.jca.ESignerSigningService; -import net.jsign.jca.GaraSignCredentials; -import net.jsign.jca.GaraSignSigningService; -import net.jsign.jca.GoogleCloudSigningService; -import net.jsign.jca.HashiCorpVaultSigningService; -import net.jsign.jca.OpenPGPCardSigningService; -import net.jsign.jca.OracleCloudCredentials; -import net.jsign.jca.OracleCloudSigningService; -import net.jsign.jca.PIVCardSigningService; -import net.jsign.jca.SignServerCredentials; -import net.jsign.jca.SignServerSigningService; -import net.jsign.jca.SigningServiceJcaProvider; - /** * Type of a keystore. * @@ -60,135 +24,23 @@ public enum KeyStoreType { /** Not a keystore, a private key file and a certificate file are provided separately and assembled into an in-memory keystore */ - NONE(true, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keyfile() == null) { - throw new IllegalArgumentException("keyfile " + params.parameterName() + " must be set"); - } - if (!params.keyfile().exists()) { - throw new IllegalArgumentException("The keyfile " + params.keyfile() + " couldn't be found"); - } - if (params.certfile() == null) { - throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); - } - if (!params.certfile().exists()) { - throw new IllegalArgumentException("The certfile " + params.certfile() + " couldn't be found"); - } - } - - @Override - KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException { - // load the certificate chain - Certificate[] chain; - try { - chain = CertificateUtils.loadCertificateChain(params.certfile()); - } catch (Exception e) { - throw new KeyStoreException("Failed to load the certificate from " + params.certfile(), e); - } - - // load the private key - PrivateKey privateKey; - try { - privateKey = PrivateKeyUtils.load(params.keyfile(), params.keypass() != null ? params.keypass() : params.storepass()); - } catch (Exception e) { - throw new KeyStoreException("Failed to load the private key from " + params.keyfile(), e); - } - - // build the in-memory keystore - KeyStore ks = KeyStore.getInstance("JKS"); - try { - ks.load(null, null); - String keypass = params.keypass(); - ks.setKeyEntry("jsign", privateKey, keypass != null ? keypass.toCharArray() : new char[0], chain); - } catch (Exception e) { - throw new KeyStoreException(e); - } - - return ks; - } - }, + NONE, /** Java keystore */ - JKS(true, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); - } - if (!params.createFile(params.keystore()).exists()) { - throw new IllegalArgumentException("The keystore " + params.keystore() + " couldn't be found"); - } - if (params.keypass() == null && params.storepass() != null) { - // reuse the storepass as the keypass - params.keypass(params.storepass()); - } - } - }, + JKS, /** JCE keystore */ - JCEKS(true, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); - } - if (!params.createFile(params.keystore()).exists()) { - throw new IllegalArgumentException("The keystore " + params.keystore() + " couldn't be found"); - } - if (params.keypass() == null && params.storepass() != null) { - // reuse the storepass as the keypass - params.keypass(params.storepass()); - } - } - }, + JCEKS, /** PKCS#12 keystore */ - PKCS12(true, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); - } - if (!params.createFile(params.keystore()).exists()) { - throw new IllegalArgumentException("The keystore " + params.keystore() + " couldn't be found"); - } - if (params.keypass() == null && params.storepass() != null) { - // reuse the storepass as the keypass - params.keypass(params.storepass()); - } - } - }, + PKCS12, /** * PKCS#11 hardware token. The keystore parameter specifies either the name of the provider defined * in jre/lib/security/java.security or the path to the * SunPKCS11 configuration file. */ - PKCS11(false, true) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - // the keystore parameter is either the provider name or the SunPKCS11 configuration file - if (params.createFile(params.keystore()).exists()) { - return ProviderUtils.createSunPKCS11Provider(params.keystore()); - } else if (params.keystore().startsWith("SunPKCS11-")) { - Provider provider = Security.getProvider(params.keystore()); - if (provider == null) { - throw new IllegalArgumentException("Security provider " + params.keystore() + " not found"); - } - return provider; - } else { - 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"); - } - } - }, + PKCS11, /** * OpenPGP card. OpenPGP cards contain up to 3 keys, one for signing, one for encryption, and one for authentication. @@ -198,35 +50,14 @@ Provider getProvider(KeyStoreBuilder params) { * the keystore parameter can be used to specify the name of the one to use. This keystore type doesn't require * any external library to be installed. */ - OPENPGP(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the PIN"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - try { - return new SigningServiceJcaProvider(new OpenPGPCardSigningService(params.keystore(), params.storepass(), params.certfile() != null ? getCertificateStore(params) : null)); - } catch (CardException e) { - throw new IllegalStateException("Failed to initialize the OpenPGP card", e); - } - } - }, + OPENPGP, /** * OpenSC supported smart card. * This keystore requires the installation of OpenSC. * If multiple devices are connected, the keystore parameter can be used to specify the name of the one to use. */ - OPENSC(false, true) { - @Override - Provider getProvider(KeyStoreBuilder params) { - return OpenSC.getProvider(params.keystore()); - } - }, + OPENSC, /** * PIV card. PIV cards contain up to 24 private keys and certificates. The alias to select the key is either, @@ -235,23 +66,7 @@ Provider getProvider(KeyStoreBuilder params) { * signature key). If multiple devices are connected, the keystore parameter can be used to specify the name * of the one to use. This keystore type doesn't require any external library to be installed. */ - PIV(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the PIN"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - try { - return new SigningServiceJcaProvider(new PIVCardSigningService(params.keystore(), params.storepass(), params.certfile() != null ? getCertificateStore(params) : null)); - } catch (CardException e) { - throw new IllegalStateException("Failed to initialize the PIV card", e); - } - } - }, + PIV, /** * Nitrokey HSM. This keystore requires the installation of OpenSC. @@ -259,32 +74,14 @@ Provider getProvider(KeyStoreBuilder params) { * certificate must be imported into the Nitrokey (using the gnupg writecert command). Keys without certificates * are ignored. Otherwise the {@link #OPENPGP} type should be used. */ - NITROKEY(false, true) { - @Override - Provider getProvider(KeyStoreBuilder params) { - return OpenSC.getProvider(params.keystore() != null ? params.keystore() : "Nitrokey"); - } - }, + NITROKEY, /** * YubiKey PIV. This keystore requires the ykcs11 library from the Yubico PIV Tool * to be installed at the default location. On Windows, the path to the library must be specified in the * PATH environment variable. */ - YUBIKEY(false, true) { - @Override - Provider getProvider(KeyStoreBuilder params) { - return YubiKey.getProvider(); - } - - @Override - Set getAliases(KeyStore keystore) throws KeyStoreException { - Set aliases = super.getAliases(keystore); - // the attestation certificate is never used for signing - aliases.remove("X.509 Certificate for PIV Attestation"); - return aliases; - } - }, + YUBIKEY, /** * AWS Key Management Service (KMS). AWS KMS stores only the private key, the certificate must be provided @@ -298,131 +95,34 @@ Set getAliases(KeyStore keystore) throws KeyStoreException { *

In any case, the credentials must allow the following actions: kms:ListKeys, * kms:DescribeKey and kms:Sign.

* */ - AWS(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the AWS region"); - } - if (params.certfile() == null) { - throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - AmazonCredentials credentials; - if (params.storepass() != null) { - credentials = AmazonCredentials.parse(params.storepass()); - } else { - try { - credentials = AmazonCredentials.getDefault(); - } catch (UnknownServiceException e) { - throw new IllegalArgumentException("storepass " + params.parameterName() - + " must specify the AWS credentials: |[|]" - + ", when not running from an EC2 instance (" + e.getMessage() + ")", e); - } catch (IOException e) { - throw new RuntimeException("An error occurred while fetching temporary credentials from IMDSv2 service", e); - } - } - - return new SigningServiceJcaProvider(new AmazonSigningService(params.keystore(), credentials, getCertificateStore(params))); - } - }, + AWS, /** * Azure Key Vault. The keystore parameter specifies the name of the key vault, either the short name * (e.g. myvault), or the full URL (e.g. https://myvault.vault.azure.net). * The Azure API access token is used as the keystore password. */ - AZUREKEYVAULT(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Azure vault name"); - } - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Azure API access token"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - return new SigningServiceJcaProvider(new AzureKeyVaultSigningService(params.keystore(), params.storepass())); - } - }, + AZUREKEYVAULT, /** * DigiCert ONE. Certificates and keys stored in the DigiCert ONE Secure Software Manager can be used directly * without installing the DigiCert client tools. The API key, the PKCS#12 keystore holding the client certificate * and its password are combined to form the storepass parameter: <api-key>|<keystore>|<password>. */ - DIGICERTONE(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.storepass() == null || params.storepass().split("\\|").length != 3) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the DigiCert ONE API key and the client certificate: ||"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - String[] elements = params.storepass().split("\\|"); - return new SigningServiceJcaProvider(new DigiCertOneSigningService(params.keystore(), elements[0], params.createFile(elements[1]), elements[2])); - } - }, + DIGICERTONE, /** * SSL.com eSigner. The SSL.com username and password are used as the keystore password (<username>|<password>), * and the base64 encoded TOTP secret is used as the key password. */ - ESIGNER(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.storepass() == null || !params.storepass().contains("|")) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SSL.com username and password: |"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - String[] elements = params.storepass().split("\\|", 2); - String endpoint = params.keystore() != null ? params.keystore() : "https://cs.ssl.com"; - try { - return new SigningServiceJcaProvider(new ESignerSigningService(endpoint, elements[0], elements[1])); - } catch (IOException e) { - throw new IllegalStateException("Authentication failed with SSL.com", e); - } - } - }, + ESIGNER, /** * Google Cloud KMS. Google Cloud KMS stores only the private key, the certificate must be provided separately. * The keystore parameter references the path of the keyring. The alias can specify either the full path of the key, * or only the short name. If the version is omitted the most recent one will be picked automatically. */ - GOOGLECLOUD(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Goole Cloud keyring"); - } - if (!params.keystore().matches("projects/[^/]+/locations/[^/]+/keyRings/[^/]+")) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the path of the keyring (projects/{projectName}/locations/{location}/keyRings/{keyringName})"); - } - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Goole Cloud API access token"); - } - if (params.certfile() == null) { - throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - return new SigningServiceJcaProvider(new GoogleCloudSigningService(params.keystore(), params.storepass(), getCertificateStore(params))); - } - }, + GOOGLECLOUD, /** * HashiCorp Vault secrets engine (Transit or GCPKMS). The certificate must be provided separately. The keystore @@ -430,36 +130,13 @@ Provider getProvider(KeyStoreBuilder params) { * The alias parameter specifies the name of the key in Vault. For the Google Cloud KMS secrets engine, the version * of the Google Cloud key is appended to the key name, separated by a colon character. (mykey:1). */ - HASHICORPVAULT(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the HashiCorp Vault secrets engine URL"); - } - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the HashiCorp Vault token"); - } - if (params.certfile() == null) { - throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - return new SigningServiceJcaProvider(new HashiCorpVaultSigningService(params.keystore(), params.storepass(), getCertificateStore(params))); - } - }, + HASHICORPVAULT, /** * SafeNet eToken * This keystore requires the installation of the SafeNet Authentication Client. */ - ETOKEN(false, true) { - @Override - Provider getProvider(KeyStoreBuilder params) { - return SafeNetEToken.getProvider(); - } - }, + ETOKEN, /** * Oracle Cloud Infrastructure Key Management Service. This keystore requires the configuration file @@ -473,38 +150,7 @@ Provider getProvider(KeyStoreBuilder params) { *

The certificate must be provided separately using the certfile parameter. The alias specifies the OCID * of the key.

*/ - ORACLECLOUD(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.certfile() == null) { - throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - OracleCloudCredentials credentials = new OracleCloudCredentials(); - try { - File config = null; - String profile = null; - if (params.storepass() != null) { - String[] elements = params.storepass().split("\\|", 2); - config = new File(elements[0]); - if (elements.length > 1) { - profile = elements[1]; - } - } - credentials.load(config, profile); - credentials.loadFromEnvironment(); - if (params.keypass() != null) { - credentials.setPassphrase(params.keypass()); - } - } catch (IOException e) { - throw new RuntimeException("An error occurred while fetching the Oracle Cloud credentials", e); - } - return new SigningServiceJcaProvider(new OracleCloudSigningService(credentials, getCertificateStore(params))); - } - }, + ORACLECLOUD, /** * Azure Trusted Signing Service. The keystore parameter specifies the API endpoint (for example @@ -513,201 +159,13 @@ Provider getProvider(KeyStoreBuilder params) { * *
  az account get-access-token --resource https://codesigning.azure.net
*/ - TRUSTEDSIGNING(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the Azure endpoint (.codesigning.azure.net)"); - } - if (params.storepass() == null) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the Azure API access token"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - return new SigningServiceJcaProvider(new AzureTrustedSigningService(params.keystore(), params.storepass())); - } - }, - - GARASIGN(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.storepass() == null || params.storepass().split("\\|").length > 3) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the GaraSign username/password and/or the path to the keystore containing the TLS client certificate: |, , or ||"); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - String[] elements = params.storepass().split("\\|"); - String username = null; - String password = null; - String certificate = null; - if (elements.length == 1) { - certificate = elements[0]; - } else if (elements.length == 2) { - username = elements[0]; - password = elements[1]; - } else if (elements.length == 3) { - username = elements[0]; - password = elements[1]; - certificate = elements[2]; - } - - GaraSignCredentials credentials = new GaraSignCredentials(username, password, certificate, params.keypass()); - return new SigningServiceJcaProvider(new GaraSignSigningService(params.keystore(), credentials)); - } - }, - - /** - * Keyfactor SignServer. This keystore requires a Plain Signer worker configured to allow client-side hashing (with - * the properties CLIENTSIDEHASHING or ALLOW_CLIENTSIDEHASHING_OVERRIDE set to true), and - * the SIGNATUREALGORITHM property set to NONEwithRSA or NONEwithECDSA. - * - *

The authentication is performed by specifying the username/password or the TLS client certificate in the - * storepass parameter. If the TLS client certificate is stored in a password protected keystore, the password is - * specified in the keypass parameter. The keystore parameter references the URL of the SignServer REST API. The - * alias parameter specifies the id or the name of the worker.

- */ - SIGNSERVER(false, false) { - @Override - void validate(KeyStoreBuilder params) { - if (params.keystore() == null) { - throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the SignServer API endpoint (e.g. https://example.com/signserver/)"); - } - if (params.storepass() != null && params.storepass().split("\\|").length > 2) { - throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SignServer username/password or the path to the keystore containing the TLS client certificate: | or "); - } - } - - @Override - Provider getProvider(KeyStoreBuilder params) { - String username = null; - String password = null; - String certificate = null; - if (params.storepass() != null) { - String[] elements = params.storepass().split("\\|"); - if (elements.length == 1) { - certificate = elements[0]; - } else if (elements.length == 2) { - username = elements[0]; - password = elements[1]; - } - } - - SignServerCredentials credentials = new SignServerCredentials(username, password, certificate, params.keypass()); - return new SigningServiceJcaProvider(new SignServerSigningService(params.keystore(), credentials)); - } - }; - - - /** Tells if the keystore is contained in a local file */ - private final boolean fileBased; - - /** Tells if the keystore is actually a PKCS#11 keystore */ - private final boolean pkcs11; - - KeyStoreType(boolean fileBased, boolean pkcs11) { - this.fileBased = fileBased; - this.pkcs11 = pkcs11; - } - - /** - * Validates the keystore parameters. - */ - void validate(KeyStoreBuilder params) throws IllegalArgumentException { - } - - /** - * Returns the security provider to use the keystore. - */ - Provider getProvider(KeyStoreBuilder params) { - return null; - } - - /** - * Build the keystore. - */ - KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException { - KeyStore ks; - try { - KeyStoreType storetype = pkcs11 ? PKCS11 : this; - if (provider != null) { - ks = KeyStore.getInstance(storetype.name(), provider); - } else { - ks = KeyStore.getInstance(storetype.name()); - } - } catch (KeyStoreException e) { - throw new KeyStoreException("keystore type '" + name() + "' is not supported" + (provider != null ? " with security provider " + provider.getName() : ""), e); - } - - try { - try (FileInputStream in = fileBased ? new FileInputStream(params.createFile(params.keystore())) : null) { - ks.load(in, params.storepass() != null ? params.storepass().toCharArray() : null); - } - } catch (Exception e) { - throw new KeyStoreException("Unable to load the " + name() + " keystore" + (params.keystore() != null ? " " + params.keystore() : ""), e); - } - - return ks; - } - - /** - * Returns the aliases of the keystore available for signing. - */ - Set getAliases(KeyStore keystore) throws KeyStoreException { - return new LinkedHashSet<>(Collections.list(keystore.aliases())); - } + TRUSTEDSIGNING, - /** - * Guess the type of the keystore from the header or the extension of the file. - * - * @param path the path to the keystore - */ - static KeyStoreType of(File path) { - // guess the type of the keystore from the header of the file - if (path.exists()) { - try (FileInputStream in = new FileInputStream(path)) { - byte[] header = new byte[4]; - in.read(header); - ByteBuffer buffer = ByteBuffer.wrap(header); - if (buffer.get(0) == 0x30) { - return PKCS12; - } else if ((buffer.getInt(0) & 0xFFFFFFFFL) == 0xCECECECEL) { - return JCEKS; - } else if ((buffer.getInt(0) & 0xFFFFFFFFL) == 0xFEEDFEEDL) { - return JKS; - } - } catch (IOException e) { - throw new RuntimeException("Unable to load the keystore " + path, e); - } - } - - // guess the type of the keystore from the extension of the file - String filename = path.getName().toLowerCase(); - if (filename.endsWith(".p12") || filename.endsWith(".pfx")) { - return PKCS12; - } else if (filename.endsWith(".jceks")) { - return JCEKS; - } else if (filename.endsWith(".jks")) { - return JKS; - } else { - return null; - } - } + GARASIGN, - private static Function getCertificateStore(KeyStoreBuilder params) { - return alias -> { - if (alias == null || alias.isEmpty()) { - return null; - } + SIGNSERVER; - try { - return CertificateUtils.loadCertificateChain(params.certfile()); - } catch (IOException | CertificateException e) { - throw new RuntimeException("Failed to load the certificate from " + params.certfile(), e); - } - }; + JsignKeyStore getJsignKeyStore() { + return JsignKeyStoreDiscovery.getKeyStore(this); } } diff --git a/jsign-crypto/src/main/java/net/jsign/NitroKeyKeyStore.java b/jsign-crypto/src/main/java/net/jsign/NitroKeyKeyStore.java new file mode 100644 index 00000000..7637d578 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/NitroKeyKeyStore.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +/** + * Nitrokey HSM. This keystore requires the installation of OpenSC. + * Other Nitrokeys based on the OpenPGP card standard are also supported with this storetype, but an X.509 + * certificate must be imported into the Nitrokey (using the gnupg writecert command). Keys without certificates + * are ignored. Otherwise, the {@link OpenPGPKeyStore} type should be used. + */ +@MetaInfServices(JsignKeyStore.class) +public class NitroKeyKeyStore extends Pkcs11KeyStore { + @Override + public String getType() { + return "NITROKEY"; + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return OpenSCKeyStore.getProvider(params.keystore() != null ? params.keystore() : "Nitrokey"); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/NoneKeyStore.java b/jsign-crypto/src/main/java/net/jsign/NoneKeyStore.java new file mode 100644 index 00000000..ce962ae0 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/NoneKeyStore.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.cert.Certificate; + +/** + * Not a keystore, a private key file and a certificate file are provided separately and assembled into an in-memory keystore + */ +@MetaInfServices(JsignKeyStore.class) +public class NoneKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "NONE"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keyfile() == null) { + throw new IllegalArgumentException("keyfile " + params.parameterName() + " must be set"); + } + if (!params.keyfile().exists()) { + throw new IllegalArgumentException("The keyfile " + params.keyfile() + " couldn't be found"); + } + if (params.certfile() == null) { + throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); + } + if (!params.certfile().exists()) { + throw new IllegalArgumentException("The certfile " + params.certfile() + " couldn't be found"); + } + } + + @Override + public KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException { + // load the certificate chain + Certificate[] chain; + try { + chain = CertificateUtils.loadCertificateChain(params.certfile()); + } catch (Exception e) { + throw new KeyStoreException("Failed to load the certificate from " + params.certfile(), e); + } + + // load the private key + PrivateKey privateKey; + try { + privateKey = PrivateKeyUtils.load(params.keyfile(), params.keypass() != null ? params.keypass() : params.storepass()); + } catch (Exception e) { + throw new KeyStoreException("Failed to load the private key from " + params.keyfile(), e); + } + + // build the in-memory keystore + KeyStore ks = KeyStore.getInstance("JKS"); + try { + ks.load(null, null); + String keypass = params.keypass(); + ks.setKeyEntry("jsign", privateKey, keypass != null ? keypass.toCharArray() : new char[0], chain); + } catch (Exception e) { + throw new KeyStoreException(e); + } + + return ks; + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/OpenPGPKeyStore.java b/jsign-crypto/src/main/java/net/jsign/OpenPGPKeyStore.java new file mode 100644 index 00000000..77826112 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/OpenPGPKeyStore.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.OpenPGPCardSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import javax.smartcardio.CardException; +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * OpenPGP card. OpenPGP cards contain up to 3 keys, one for signing, one for encryption, and one for authentication. + * All of them can be used for code signing (except encryption keys based on an elliptic curve). The alias + * to select the key is either, SIGNATURE, ENCRYPTION or AUTHENTICATION. + * This keystore can be used with a Nitrokey (non-HSM models) or a Yubikey. If multiple devices are connected, + * the keystore parameter can be used to specify the name of the one to use. This keystore type doesn't require + * any external library to be installed. + */ +@MetaInfServices(JsignKeyStore.class) +public class OpenPGPKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "OPENPGP"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the PIN"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + try { + return new SigningServiceJcaProvider(new OpenPGPCardSigningService(params.keystore(), params.storepass(), params.certfile() != null ? getCertificateStore(params) : null)); + } catch (CardException e) { + throw new IllegalStateException("Failed to initialize the OpenPGP card", e); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/OpenSC.java b/jsign-crypto/src/main/java/net/jsign/OpenSCKeyStore.java similarity index 88% rename from jsign-crypto/src/main/java/net/jsign/OpenSC.java rename to jsign-crypto/src/main/java/net/jsign/OpenSCKeyStore.java index 17f508e3..5bda687e 100644 --- a/jsign-crypto/src/main/java/net/jsign/OpenSC.java +++ b/jsign-crypto/src/main/java/net/jsign/OpenSCKeyStore.java @@ -1,162 +1,173 @@ -/** - * 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; - -import java.io.File; -import java.io.IOException; -import java.security.Provider; -import java.security.ProviderException; -import java.util.ArrayList; -import java.util.List; - -import sun.security.pkcs11.wrapper.CK_SLOT_INFO; -import sun.security.pkcs11.wrapper.CK_TOKEN_INFO; -import sun.security.pkcs11.wrapper.PKCS11; -import sun.security.pkcs11.wrapper.PKCS11Exception; - -/** - * Helper class for working with OpenSC. - * - * @since 5.0 - */ -class OpenSC { - - /** - * Returns the security provider for OpenSC. - * - * @param name the name of the token - * @return the OpenSC security provider - * @throws ProviderException thrown if the provider can't be initialized - */ - static Provider getProvider(String name) { - return ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration(name)); - } - - /** - * Returns the SunPKCS11 configuration for OpenSC. - * - * @param name the name or the slot id of the token - * @throws ProviderException thrown if the PKCS11 modules cannot be found - */ - static String getSunPKCS11Configuration(String name) { - File library = getOpenSCLibrary(); - if (!library.exists()) { - throw new ProviderException("OpenSC PKCS11 module is not installed (" + library + " is missing)"); - } - String configuration = "--name=opensc\nlibrary = \"" + library.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; - try { - long slot; - try { - slot = Integer.parseInt(name); - } catch (Exception e) { - slot = getTokenSlot(library, name); - } - if (slot >= 0) { - configuration += "slot=" + slot; - } - } catch (Exception e) { - throw new ProviderException(e); - } - return configuration; - } - - /** - * Returns the slot index associated to the token. - * - * @param libraryPath the path to the PKCS11 library - * @param name the partial name of the token - */ - static long getTokenSlot(File libraryPath, String name) throws PKCS11Exception, IOException { - PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); - long[] slots = pkcs11.C_GetSlotList(true); - - List descriptions = new ArrayList<>(); - List matches = new ArrayList<>(); - for (long slot : slots) { - CK_SLOT_INFO info = pkcs11.C_GetSlotInfo(slot); - String description = new String(info.slotDescription).trim(); - if (name == null || description.toLowerCase().contains(name.toLowerCase())) { - CK_TOKEN_INFO tokenInfo = pkcs11.C_GetTokenInfo(slot); - String label = new String(tokenInfo.label).trim(); - if (label.equals("OpenPGP card (User PIN (sig))")) { - // OpenPGP cards such as the Nitrokey 3 are exposed as two slots with the same name by OpenSC. - // Only the first one contains the signing key and the certificate, so the second one is ignored. - continue; - } - - matches.add(slot); - } - descriptions.add(description); - } - - if (matches.size() == 1) { - return matches.get(0); - } - - if (matches.isEmpty()) { - throw new RuntimeException(descriptions.isEmpty() ? "No PKCS11 token found" : "No PKCS11 token found matching '" + name + "' (available tokens: " + String.join(", ", descriptions) + ")"); - } else { - throw new RuntimeException("Multiple PKCS11 tokens found" + (name != null ? " matching '" + name + "'" : "") + ", please specify the name of the token to use (available tokens: " + String.join(", ", descriptions) + ")"); - } - } - - /** - * Attempts to locate the opensc-pkcs11 library on the system. - */ - static File getOpenSCLibrary() { - String osname = System.getProperty("os.name"); - String arch = System.getProperty("sun.arch.data.model"); - - if (osname.contains("Windows")) { - String programfiles; - if ("32".equals(arch) && System.getenv("ProgramFiles(x86)") != null) { - programfiles = System.getenv("ProgramFiles(x86)"); - } else { - programfiles = System.getenv("ProgramFiles"); - } - return new File(programfiles + "/OpenSC Project/OpenSC/pkcs11/opensc-pkcs11.dll"); - - } else if (osname.contains("Mac")) { - return new File("/Library/OpenSC/lib/opensc-pkcs11.so"); - - } else { - // Linux - List paths = new ArrayList<>(); - if ("32".equals(arch)) { - paths.add("/usr/lib/opensc-pkcs11.so"); - paths.add("/usr/lib/i386-linux-gnu/opensc-pkcs11.so"); - paths.add("/usr/lib/arm-linux-gnueabi/opensc-pkcs11.so"); - paths.add("/usr/lib/arm-linux-gnueabihf/opensc-pkcs11.so"); - } else { - paths.add("/usr/lib64/opensc-pkcs11.so"); - paths.add("/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so"); - paths.add("/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so"); - paths.add("/usr/lib/mips64el-linux-gnuabi64/opensc-pkcs11.so"); - paths.add("/usr/lib/riscv64-linux-gnu/opensc-pkcs11.so"); - } - - for (String path : paths) { - File library = new File(path); - if (library.exists()) { - return library; - } - } - - return new File("/usr/local/lib/opensc-pkcs11.so"); - } - } -} +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; +import sun.security.pkcs11.wrapper.CK_SLOT_INFO; +import sun.security.pkcs11.wrapper.CK_TOKEN_INFO; +import sun.security.pkcs11.wrapper.PKCS11; +import sun.security.pkcs11.wrapper.PKCS11Exception; + +import java.io.File; +import java.io.IOException; +import java.security.Provider; +import java.security.ProviderException; +import java.util.ArrayList; +import java.util.List; + +/** + * OpenSC supported smart card. + * This keystore requires the installation of OpenSC. + * If multiple devices are connected, the keystore parameter can be used to specify the name of the one to use. + */ +@MetaInfServices(JsignKeyStore.class) +public class OpenSCKeyStore extends Pkcs11KeyStore { + @Override + public String getType() { + return "OPENSC"; + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return getProvider(params.keystore()); + } + + /** + * Returns the security provider for OpenSC. + * + * @param name the name of the token + * @return the OpenSC security provider + * @throws ProviderException thrown if the provider can't be initialized + */ + static Provider getProvider(String name) { + return ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration(name)); + } + + /** + * Returns the SunPKCS11 configuration for OpenSC. + * + * @param name the name or the slot id of the token + * @throws ProviderException thrown if the PKCS11 modules cannot be found + */ + static String getSunPKCS11Configuration(String name) { + File library = getOpenSCLibrary(); + if (!library.exists()) { + throw new ProviderException("OpenSC PKCS11 module is not installed (" + library + " is missing)"); + } + String configuration = "--name=opensc\nlibrary = \"" + library.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; + try { + long slot; + try { + slot = Integer.parseInt(name); + } catch (Exception e) { + slot = getTokenSlot(library, name); + } + if (slot >= 0) { + configuration += "slot=" + slot; + } + } catch (Exception e) { + throw new ProviderException(e); + } + return configuration; + } + + /** + * Returns the slot index associated to the token. + * + * @param libraryPath the path to the PKCS11 library + * @param name the partial name of the token + */ + static long getTokenSlot(File libraryPath, String name) throws PKCS11Exception, IOException { + PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); + long[] slots = pkcs11.C_GetSlotList(true); + + List descriptions = new ArrayList<>(); + List matches = new ArrayList<>(); + for (long slot : slots) { + CK_SLOT_INFO info = pkcs11.C_GetSlotInfo(slot); + String description = new String(info.slotDescription).trim(); + if (name == null || description.toLowerCase().contains(name.toLowerCase())) { + CK_TOKEN_INFO tokenInfo = pkcs11.C_GetTokenInfo(slot); + String label = new String(tokenInfo.label).trim(); + if (label.equals("OpenPGP card (User PIN (sig))")) { + // OpenPGP cards such as the Nitrokey 3 are exposed as two slots with the same name by OpenSC. + // Only the first one contains the signing key and the certificate, so the second one is ignored. + continue; + } + + matches.add(slot); + } + descriptions.add(description); + } + + if (matches.size() == 1) { + return matches.get(0); + } + + if (matches.isEmpty()) { + throw new RuntimeException(descriptions.isEmpty() ? "No PKCS11 token found" : "No PKCS11 token found matching '" + name + "' (available tokens: " + String.join(", ", descriptions) + ")"); + } else { + throw new RuntimeException("Multiple PKCS11 tokens found" + (name != null ? " matching '" + name + "'" : "") + ", please specify the name of the token to use (available tokens: " + String.join(", ", descriptions) + ")"); + } + } + + /** + * Attempts to locate the opensc-pkcs11 library on the system. + */ + static File getOpenSCLibrary() { + String osname = System.getProperty("os.name"); + String arch = System.getProperty("sun.arch.data.model"); + + if (osname.contains("Windows")) { + String programfiles; + if ("32".equals(arch) && System.getenv("ProgramFiles(x86)") != null) { + programfiles = System.getenv("ProgramFiles(x86)"); + } else { + programfiles = System.getenv("ProgramFiles"); + } + return new File(programfiles + "/OpenSC Project/OpenSC/pkcs11/opensc-pkcs11.dll"); + + } else if (osname.contains("Mac")) { + return new File("/Library/OpenSC/lib/opensc-pkcs11.so"); + + } else { + // Linux + List paths = new ArrayList<>(); + if ("32".equals(arch)) { + paths.add("/usr/lib/opensc-pkcs11.so"); + paths.add("/usr/lib/i386-linux-gnu/opensc-pkcs11.so"); + paths.add("/usr/lib/arm-linux-gnueabi/opensc-pkcs11.so"); + paths.add("/usr/lib/arm-linux-gnueabihf/opensc-pkcs11.so"); + } else { + paths.add("/usr/lib64/opensc-pkcs11.so"); + paths.add("/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so"); + paths.add("/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so"); + paths.add("/usr/lib/mips64el-linux-gnuabi64/opensc-pkcs11.so"); + paths.add("/usr/lib/riscv64-linux-gnu/opensc-pkcs11.so"); + } + + for (String path : paths) { + File library = new File(path); + if (library.exists()) { + return library; + } + } + + return new File("/usr/local/lib/opensc-pkcs11.so"); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/OracleCloudKeyStore.java b/jsign-crypto/src/main/java/net/jsign/OracleCloudKeyStore.java new file mode 100644 index 00000000..659a1044 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/OracleCloudKeyStore.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.OracleCloudCredentials; +import net.jsign.jca.OracleCloudSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.io.File; +import java.io.IOException; +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * Oracle Cloud Infrastructure Key Management Service. This keystore requires the configuration file + * or the environment + * variables used by the OCI CLI. The storepass parameter specifies the path to the configuration file + * (~/.oci/config by default). If the configuration file contains multiple profiles, the name of the + * non-default profile to use is appended to the storepass (for example ~/.oci/config|PROFILE). + * The keypass parameter may be used to specify the passphrase of the key file used for signing the requests to + * the OCI API if it isn't set in the configuration file. + * + *

The certificate must be provided separately using the certfile parameter. The alias specifies the OCID + * of the key.

+ */ +@MetaInfServices(JsignKeyStore.class) +public class OracleCloudKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "ORACLECLOUD"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.certfile() == null) { + throw new IllegalArgumentException("certfile " + params.parameterName() + " must be set"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + OracleCloudCredentials credentials = new OracleCloudCredentials(); + try { + File config = null; + String profile = null; + if (params.storepass() != null) { + String[] elements = params.storepass().split("\\|", 2); + config = new File(elements[0]); + if (elements.length > 1) { + profile = elements[1]; + } + } + credentials.load(config, profile); + credentials.loadFromEnvironment(); + if (params.keypass() != null) { + credentials.setPassphrase(params.keypass()); + } + } catch (IOException e) { + throw new RuntimeException("An error occurred while fetching the Oracle Cloud credentials", e); + } + return new SigningServiceJcaProvider(new OracleCloudSigningService(credentials, getCertificateStore(params))); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/PivKeyStore.java b/jsign-crypto/src/main/java/net/jsign/PivKeyStore.java new file mode 100644 index 00000000..afcaed56 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/PivKeyStore.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.PIVCardSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import javax.smartcardio.CardException; +import java.security.Provider; + +import static net.jsign.JsignKeyStore.getCertificateStore; + +/** + * PIV card. PIV cards contain up to 24 private keys and certificates. The alias to select the key is either, + * AUTHENTICATION, SIGNATURE, KEY_MANAGEMENT, CARD_AUTHENTICATION, + * or RETIRED<1-20>. Slot numbers are also accepted (for example 9c for the digital + * signature key). If multiple devices are connected, the keystore parameter can be used to specify the name + * of the one to use. This keystore type doesn't require any external library to be installed. + */ +@MetaInfServices(JsignKeyStore.class) +public class PivKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "PIV"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.storepass() == null) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the PIN"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + try { + return new SigningServiceJcaProvider(new PIVCardSigningService(params.keystore(), params.storepass(), params.certfile() != null ? getCertificateStore(params) : null)); + } catch (CardException e) { + throw new IllegalStateException("Failed to initialize the PIV card", e); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/Pkcs11KeyStore.java b/jsign-crypto/src/main/java/net/jsign/Pkcs11KeyStore.java new file mode 100644 index 00000000..48a04c2c --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/Pkcs11KeyStore.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import java.security.Security; + +/** + * PKCS#11 hardware token. The keystore parameter specifies either the name of the provider defined + * in jre/lib/security/java.security or the path to the + * SunPKCS11 configuration file. + */ +@MetaInfServices(JsignKeyStore.class) +public class Pkcs11KeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "PKCS11"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must be set"); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + // the keystore parameter is either the provider name or the SunPKCS11 configuration file + if (params.createFile(params.keystore()).exists()) { + return ProviderUtils.createSunPKCS11Provider(params.keystore()); + } else if (params.keystore().startsWith("SunPKCS11-")) { + Provider provider = Security.getProvider(params.keystore()); + if (provider == null) { + throw new IllegalArgumentException("Security provider " + params.keystore() + " not found"); + } + return provider; + } else { + 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"); + } + } + + @Override + public KeyStore getKeystore(KeyStoreBuilder params, Provider provider) throws KeyStoreException { + KeyStore ks; + try { + if (provider != null) { + ks = KeyStore.getInstance("PKCS11", provider); + } else { + ks = KeyStore.getInstance("PKCS11"); + } + } catch (KeyStoreException e) { + throw new KeyStoreException("keystore type '" + getType() + "' is not supported" + (provider != null ? " with security provider " + provider.getName() : ""), e); + } + + try { + ks.load(null, params.storepass() != null ? params.storepass().toCharArray() : null); + } catch (Exception e) { + throw new KeyStoreException("Unable to load the keystore " + params.keystore(), e); + } + + return ks; + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/Pkcs12KeyStore.java b/jsign-crypto/src/main/java/net/jsign/Pkcs12KeyStore.java new file mode 100644 index 00000000..ee2078c4 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/Pkcs12KeyStore.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; + +import java.io.File; + +/** + * PKCS#12 keystore + */ +@MetaInfServices(JsignKeyStore.class) +public class Pkcs12KeyStore extends FileBasedKeyStore { + @Override + public String getType() { + return "PKCS12"; + } + + @Override + boolean isSupported(File file) { + String filename = file.getName().toLowerCase(); + return hasSignature(file, 0x30000000L, 0xFF000000L) || filename.endsWith(".p12") || filename.endsWith(".pfx"); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/SafeNetEToken.java b/jsign-crypto/src/main/java/net/jsign/SafeNetETokenKeyStore.java similarity index 86% rename from jsign-crypto/src/main/java/net/jsign/SafeNetEToken.java rename to jsign-crypto/src/main/java/net/jsign/SafeNetETokenKeyStore.java index 67e40bd8..a9d83103 100644 --- a/jsign-crypto/src/main/java/net/jsign/SafeNetEToken.java +++ b/jsign-crypto/src/main/java/net/jsign/SafeNetETokenKeyStore.java @@ -1,113 +1,113 @@ -/** - * 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; - -import java.io.File; -import java.io.IOException; -import java.security.Provider; -import java.security.ProviderException; -import java.util.ArrayList; -import java.util.List; - -import sun.security.pkcs11.wrapper.PKCS11; -import sun.security.pkcs11.wrapper.PKCS11Exception; - -/** - * Helper class for working with SafeNet eTokens. - * - * @since 6.0 - */ -class SafeNetEToken { - - /** - * Returns the security provider for the SafeNet eToken. - * - * @return the SafeNet eTokens security provider - * @throws ProviderException thrown if the provider can't be initialized - */ - static Provider getProvider() { - return ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration()); - } - - /** - * Returns the SunPKCS11 configuration of the SafeNet eToken. - * - * @throws ProviderException thrown if the PKCS11 modules cannot be found - */ - static String getSunPKCS11Configuration() { - File library = getPKCS11Library(); - if (!library.exists()) { - throw new ProviderException("SafeNet eToken PKCS11 module is not installed (" + library + " is missing)"); - } - String configuration = "--name=\"SafeNet eToken\"\nlibrary = \"" + library.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; - try { - long slot = getTokenSlot(library); - if (slot >= 0) { - configuration += "slot=" + slot; - } - } catch (Exception e) { - throw new ProviderException(e); - } - return configuration; - } - - /** - * Returns the slot index associated to the token. - */ - static long getTokenSlot(File libraryPath) throws PKCS11Exception, IOException { - PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); - long[] slots = pkcs11.C_GetSlotList(true); - return slots.length > 0 ? slots[0] : -1; - } - - /** - * Attempts to locate the SafeNet eToken PKCS11 library on the system. - */ - static File getPKCS11Library() { - String osname = System.getProperty("os.name"); - String arch = System.getProperty("sun.arch.data.model"); - - if (osname.contains("Windows")) { - return new File(System.getenv("windir") + "/system32/eTPKCS11.dll"); - - } else if (osname.contains("Mac")) { - return new File("/usr/local/lib/libeTPkcs11.dylib"); - - } else { - // Linux - List paths = new ArrayList<>(); - if ("64".equals(arch)) { - paths.add("/usr/lib64/pkcs11/libeTPkcs11.so"); - paths.add("/usr/lib64/libeTPkcs11.so"); - paths.add("/usr/lib64/libeToken.so"); - } - paths.add("/usr/lib/pkcs11/libeTPkcs11.so"); - paths.add("/usr/lib/pkcs11/libeToken.so"); - paths.add("/usr/lib/libeTPkcs11.so"); - paths.add("/usr/lib/libeToken.so"); - - for (String path : paths) { - File library = new File(path); - if (library.exists()) { - return library; - } - } - - return new File("/usr/local/lib/libeToken.so"); - } - } -} +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import org.kohsuke.MetaInfServices; +import sun.security.pkcs11.wrapper.PKCS11; +import sun.security.pkcs11.wrapper.PKCS11Exception; + +import java.io.File; +import java.io.IOException; +import java.security.Provider; +import java.security.ProviderException; +import java.util.ArrayList; +import java.util.List; + +/** + * SafeNet eToken + * This keystore requires the installation of the SafeNet Authentication Client. + */ +@MetaInfServices(JsignKeyStore.class) +public class SafeNetETokenKeyStore extends Pkcs11KeyStore { + @Override + public String getType() { + return "ETOKEN"; + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration()); + } + + /** + * Returns the SunPKCS11 configuration of the SafeNet eToken. + * + * @throws ProviderException thrown if the PKCS11 modules cannot be found + */ + static String getSunPKCS11Configuration() { + File library = getPKCS11Library(); + if (!library.exists()) { + throw new ProviderException("SafeNet eToken PKCS11 module is not installed (" + library + " is missing)"); + } + String configuration = "--name=\"SafeNet eToken\"\nlibrary = \"" + library.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; + try { + long slot = getTokenSlot(library); + if (slot >= 0) { + configuration += "slot=" + slot; + } + } catch (Exception e) { + throw new ProviderException(e); + } + return configuration; + } + + /** + * Returns the slot index associated to the token. + */ + static long getTokenSlot(File libraryPath) throws PKCS11Exception, IOException { + PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); + long[] slots = pkcs11.C_GetSlotList(true); + return slots.length > 0 ? slots[0] : -1; + } + + /** + * Attempts to locate the SafeNet eToken PKCS11 library on the system. + */ + static File getPKCS11Library() { + String osname = System.getProperty("os.name"); + String arch = System.getProperty("sun.arch.data.model"); + + if (osname.contains("Windows")) { + return new File(System.getenv("windir") + "/system32/eTPKCS11.dll"); + + } else if (osname.contains("Mac")) { + return new File("/usr/local/lib/libeTPkcs11.dylib"); + + } else { + // Linux + List paths = new ArrayList<>(); + if ("64".equals(arch)) { + paths.add("/usr/lib64/pkcs11/libeTPkcs11.so"); + paths.add("/usr/lib64/libeTPkcs11.so"); + paths.add("/usr/lib64/libeToken.so"); + } + paths.add("/usr/lib/pkcs11/libeTPkcs11.so"); + paths.add("/usr/lib/pkcs11/libeToken.so"); + paths.add("/usr/lib/libeTPkcs11.so"); + paths.add("/usr/lib/libeToken.so"); + + for (String path : paths) { + File library = new File(path); + if (library.exists()) { + return library; + } + } + + return new File("/usr/local/lib/libeToken.so"); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/SignServerKeyStore.java b/jsign-crypto/src/main/java/net/jsign/SignServerKeyStore.java new file mode 100644 index 00000000..461a7cb1 --- /dev/null +++ b/jsign-crypto/src/main/java/net/jsign/SignServerKeyStore.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.SignServerCredentials; +import net.jsign.jca.SignServerSigningService; +import net.jsign.jca.SigningServiceJcaProvider; +import org.kohsuke.MetaInfServices; + +import java.security.Provider; + +/** + * Keyfactor SignServer. This keystore requires a Plain Signer worker configured to allow client-side hashing (with + * the properties CLIENTSIDEHASHING or ALLOW_CLIENTSIDEHASHING_OVERRIDE set to true), and + * the SIGNATUREALGORITHM property set to NONEwithRSA or NONEwithECDSA. + * + *

The authentication is performed by specifying the username/password or the TLS client certificate in the + * storepass parameter. If the TLS client certificate is stored in a password protected keystore, the password is + * specified in the keypass parameter. The keystore parameter references the URL of the SignServer REST API. The + * alias parameter specifies the id or the name of the worker.

+ */ +@MetaInfServices(JsignKeyStore.class) +public class SignServerKeyStore extends AbstractJsignKeyStore { + @Override + public String getType() { + return "SIGNSERVER"; + } + + @Override + public void validate(KeyStoreBuilder params) throws IllegalArgumentException { + if (params.keystore() == null) { + throw new IllegalArgumentException("keystore " + params.parameterName() + " must specify the SignServer API endpoint (e.g. https://example.com/signserver/)"); + } + if (params.storepass() != null && params.storepass().split("\\|").length > 2) { + throw new IllegalArgumentException("storepass " + params.parameterName() + " must specify the SignServer username/password or the path to the keystore containing the TLS client certificate: | or "); + } + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + String username = null; + String password = null; + String certificate = null; + if (params.storepass() != null) { + String[] elements = params.storepass().split("\\|"); + if (elements.length == 1) { + certificate = elements[0]; + } else if (elements.length == 2) { + username = elements[0]; + password = elements[1]; + } + } + + SignServerCredentials credentials = new SignServerCredentials(username, password, certificate, params.keypass()); + return new SigningServiceJcaProvider(new SignServerSigningService(params.keystore(), credentials)); + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/YubiKey.java b/jsign-crypto/src/main/java/net/jsign/YubiKeyKeyStore.java similarity index 78% rename from jsign-crypto/src/main/java/net/jsign/YubiKey.java rename to jsign-crypto/src/main/java/net/jsign/YubiKeyKeyStore.java index 17ff85fd..7a494931 100644 --- a/jsign-crypto/src/main/java/net/jsign/YubiKey.java +++ b/jsign-crypto/src/main/java/net/jsign/YubiKeyKeyStore.java @@ -1,152 +1,162 @@ -/** - * Copyright 2021 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; - -import java.io.File; -import java.io.IOException; -import java.security.AuthProvider; -import java.security.Provider; -import java.security.ProviderException; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; - -import sun.security.pkcs11.wrapper.PKCS11; -import sun.security.pkcs11.wrapper.PKCS11Exception; - -import net.jsign.jca.AutoLoginProvider; - -/** - * Helper class for working with YubiKeys. - * - * @since 4.0 - */ -class YubiKey { - - /** - * Returns the security provider for the YubiKey. - * - * @return the YubiKey security provider - * @throws ProviderException thrown if the provider can't be initialized - * @since 4.0 - */ - static Provider getProvider() { - return new AutoLoginProvider((AuthProvider) ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration())); - } - - /** - * Returns the SunPKCS11 configuration of the YubiKey. - * - * @throws ProviderException thrown if the PKCS11 modules cannot be found - * @since 4.0 - */ - static String getSunPKCS11Configuration() { - File libykcs11 = getYkcs11Library(); - if (!libykcs11.exists()) { - throw new ProviderException("YubiKey PKCS11 module (ykcs11) is not installed (" + libykcs11 + " is missing)"); - } - String configuration = "--name=yubikey\nlibrary = \"" + libykcs11.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; - try { - long slot = getTokenSlot(libykcs11); - if (slot >= 0) { - configuration += "slot=" + slot; - } - } catch (Exception e) { - throw new ProviderException(e); - } - return configuration; - } - - /** - * Returns the slot index associated to the token. - * - * @since 4.1 - */ - static long getTokenSlot(File libraryPath) throws PKCS11Exception, IOException { - PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); - long[] slots = pkcs11.C_GetSlotList(true); - return slots.length > 0 ? slots[0] : -1; - } - - /** - * Tells if a YubiKey is present on the system. - */ - static boolean isPresent() { - try { - return getTokenSlot(getYkcs11Library()) >= 0; - } catch (Exception e) { - return false; - } - } - - /** - * Attempts to locate the ykcs11 library on the system. - * - * @since 4.0 - */ - static File getYkcs11Library() { - String osname = System.getProperty("os.name"); - String arch = System.getProperty("sun.arch.data.model"); - - if (osname.contains("Windows")) { - String programfiles; - if ("32".equals(arch) && System.getenv("ProgramFiles(x86)") != null) { - programfiles = System.getenv("ProgramFiles(x86)"); - } else { - programfiles = System.getenv("ProgramFiles"); - } - File libykcs11 = new File(programfiles + "/Yubico/Yubico PIV Tool/bin/libykcs11.dll"); - - if (!System.getenv("PATH").contains("Yubico PIV Tool\\bin")) { - Logger log = Logger.getLogger(YubiKey.class.getName()); - log.warning("The YubiKey library path (" + libykcs11.getParentFile().getAbsolutePath().replace('/', '\\') + ") is missing from the PATH environment variable"); - } - - return libykcs11; - - } else if (osname.contains("Mac")) { - return new File("/usr/local/lib/libykcs11.dylib"); - - } else { - // Linux - List paths = new ArrayList<>(); - if ("32".equals(arch)) { - paths.add("/usr/lib/libykcs11.so"); - paths.add("/usr/lib/libykcs11.so.1"); - paths.add("/usr/lib/i386-linux-gnu/libykcs11.so"); - paths.add("/usr/lib/arm-linux-gnueabi/libykcs11.so"); - paths.add("/usr/lib/arm-linux-gnueabihf/libykcs11.so"); - } else { - paths.add("/usr/lib64/libykcs11.so"); - paths.add("/usr/lib64/libykcs11.so.1"); - paths.add("/usr/lib/x86_64-linux-gnu/libykcs11.so"); - paths.add("/usr/lib/aarch64-linux-gnu/libykcs11.so"); - paths.add("/usr/lib/mips64el-linux-gnuabi64/libykcs11.so"); - paths.add("/usr/lib/riscv64-linux-gnu/libykcs11.so"); - } - - for (String path : paths) { - File libykcs11 = new File(path); - if (libykcs11.exists()) { - return libykcs11; - } - } - - return new File("/usr/local/lib/libykcs11.so"); - } - } -} +/* + * Copyright 2024 Björn Kautler + * + * 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; + +import net.jsign.jca.AutoLoginProvider; +import org.kohsuke.MetaInfServices; +import sun.security.pkcs11.wrapper.PKCS11; +import sun.security.pkcs11.wrapper.PKCS11Exception; + +import java.io.File; +import java.io.IOException; +import java.security.AuthProvider; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Provider; +import java.security.ProviderException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +/** + * YubiKey PIV. This keystore requires the ykcs11 library from the Yubico PIV Tool + * to be installed at the default location. On Windows, the path to the library must be specified in the + * PATH environment variable. + */ +@MetaInfServices(JsignKeyStore.class) +public class YubiKeyKeyStore extends Pkcs11KeyStore { + @Override + public String getType() { + return "YUBIKEY"; + } + + @Override + public Provider getProvider(KeyStoreBuilder params) { + return new AutoLoginProvider((AuthProvider) ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration())); + } + + @Override + public Set getAliases(KeyStore keystore) throws KeyStoreException { + Set aliases = super.getAliases(keystore); + // the attestation certificate is never used for signing + aliases.remove("X.509 Certificate for PIV Attestation"); + return aliases; + } + + /** + * Returns the SunPKCS11 configuration of the YubiKey. + * + * @throws ProviderException thrown if the PKCS11 modules cannot be found + * @since 4.0 + */ + static String getSunPKCS11Configuration() { + File libykcs11 = getYkcs11Library(); + if (!libykcs11.exists()) { + throw new ProviderException("YubiKey PKCS11 module (ykcs11) is not installed (" + libykcs11 + " is missing)"); + } + String configuration = "--name=yubikey\nlibrary = \"" + libykcs11.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; + try { + long slot = getTokenSlot(libykcs11); + if (slot >= 0) { + configuration += "slot=" + slot; + } + } catch (Exception e) { + throw new ProviderException(e); + } + return configuration; + } + + /** + * Returns the slot index associated to the token. + * + * @since 4.1 + */ + static long getTokenSlot(File libraryPath) throws PKCS11Exception, IOException { + PKCS11 pkcs11 = PKCS11.getInstance(libraryPath.getAbsolutePath(), "C_GetFunctionList", null, false); + long[] slots = pkcs11.C_GetSlotList(true); + return slots.length > 0 ? slots[0] : -1; + } + + /** + * Tells if a YubiKey is present on the system. + */ + public static boolean isPresent() { + try { + return getTokenSlot(getYkcs11Library()) >= 0; + } catch (Exception e) { + return false; + } + } + + /** + * Attempts to locate the ykcs11 library on the system. + * + * @since 4.0 + */ + static File getYkcs11Library() { + String osname = System.getProperty("os.name"); + String arch = System.getProperty("sun.arch.data.model"); + + if (osname.contains("Windows")) { + String programfiles; + if ("32".equals(arch) && System.getenv("ProgramFiles(x86)") != null) { + programfiles = System.getenv("ProgramFiles(x86)"); + } else { + programfiles = System.getenv("ProgramFiles"); + } + File libykcs11 = new File(programfiles + "/Yubico/Yubico PIV Tool/bin/libykcs11.dll"); + + if (!System.getenv("PATH").contains("Yubico PIV Tool\\bin")) { + Logger log = Logger.getLogger(YubiKeyKeyStore.class.getName()); + log.warning("The YubiKey library path (" + libykcs11.getParentFile().getAbsolutePath().replace('/', '\\') + ") is missing from the PATH environment variable"); + } + + return libykcs11; + + } else if (osname.contains("Mac")) { + return new File("/usr/local/lib/libykcs11.dylib"); + + } else { + // Linux + List paths = new ArrayList<>(); + if ("32".equals(arch)) { + paths.add("/usr/lib/libykcs11.so"); + paths.add("/usr/lib/libykcs11.so.1"); + paths.add("/usr/lib/i386-linux-gnu/libykcs11.so"); + paths.add("/usr/lib/arm-linux-gnueabi/libykcs11.so"); + paths.add("/usr/lib/arm-linux-gnueabihf/libykcs11.so"); + } else { + paths.add("/usr/lib64/libykcs11.so"); + paths.add("/usr/lib64/libykcs11.so.1"); + paths.add("/usr/lib/x86_64-linux-gnu/libykcs11.so"); + paths.add("/usr/lib/aarch64-linux-gnu/libykcs11.so"); + paths.add("/usr/lib/mips64el-linux-gnuabi64/libykcs11.so"); + paths.add("/usr/lib/riscv64-linux-gnu/libykcs11.so"); + } + + for (String path : paths) { + File libykcs11 = new File(path); + if (libykcs11.exists()) { + return libykcs11; + } + } + + return new File("/usr/local/lib/libykcs11.so"); + } + } +} diff --git a/jsign-crypto/src/main/java/net/jsign/jca/JsignJcaProvider.java b/jsign-crypto/src/main/java/net/jsign/jca/JsignJcaProvider.java index 0c3adfa3..83402eac 100644 --- a/jsign-crypto/src/main/java/net/jsign/jca/JsignJcaProvider.java +++ b/jsign-crypto/src/main/java/net/jsign/jca/JsignJcaProvider.java @@ -35,13 +35,14 @@ import net.jsign.DigestAlgorithm; import net.jsign.KeyStoreBuilder; -import net.jsign.KeyStoreType; +import net.jsign.JsignKeyStore; +import net.jsign.JsignKeyStoreDiscovery; /** * JCA provider using a Jsign keystore and compatible with jarsigner and apksigner. * *

The provider must be configured with the keystore parameter (the value depends on the keystore type). - * The type of the keystore is one of the names from the {@link KeyStoreType} enum.

+ * The type of the keystore is one of the types from the {@link JsignKeyStore} implementations.

* *

Example:

*
@@ -68,8 +69,8 @@ public JsignJcaProvider() {
         super("Jsign", 1.0, "Jsign security provider");
 
         AccessController.doPrivileged((PrivilegedAction) () -> {
-            for (KeyStoreType type : KeyStoreType.values()) {
-                putService(new ProviderService(this, "KeyStore", type.name(), JsignJcaKeyStore.class.getName(), () -> new JsignJcaKeyStore(type, keystore)));
+            for (String type : JsignKeyStoreDiscovery.getKeyStoreTypes()) {
+                putService(new ProviderService(this, "KeyStore", type, JsignJcaKeyStore.class.getName(), () -> new JsignJcaKeyStore(JsignKeyStoreDiscovery.getKeyStore(type), keystore)));
             }
             for (String alg : new String[]{"RSA", "ECDSA"}) {
                 for (DigestAlgorithm digest : DigestAlgorithm.values()) {
@@ -99,7 +100,7 @@ static class JsignJcaKeyStore extends AbstractKeyStoreSpi {
         private KeyStoreBuilder builder = new KeyStoreBuilder();
         private KeyStore keystore;
 
-        public JsignJcaKeyStore(KeyStoreType type, String keystore) {
+        public JsignJcaKeyStore(JsignKeyStore type, String keystore) {
             builder.storetype(type);
             builder.keystore(keystore);
             builder.certfile("");
diff --git a/jsign-crypto/src/test/java/net/jsign/KeyStoreBuilderTest.java b/jsign-crypto/src/test/java/net/jsign/KeyStoreBuilderTest.java
index bcb029b2..5d28c51f 100644
--- a/jsign-crypto/src/test/java/net/jsign/KeyStoreBuilderTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/KeyStoreBuilderTest.java
@@ -16,20 +16,40 @@
 
 package net.jsign;
 
-import java.io.File;
-import java.nio.file.Files;
-import java.security.KeyStore;
-import java.security.ProviderException;
-
+import net.jsign.jca.OpenPGPCardTest;
+import net.jsign.jca.PIVCardTest;
 import org.apache.commons.io.FileUtils;
 import org.junit.Assume;
 import org.junit.Test;
 
-import net.jsign.jca.OpenPGPCardTest;
-import net.jsign.jca.PIVCardTest;
+import java.io.File;
+import java.nio.file.Files;
+import java.security.KeyStore;
+import java.security.ProviderException;
 
-import static net.jsign.KeyStoreType.*;
-import static org.junit.Assert.*;
+import static net.jsign.KeyStoreType.AWS;
+import static net.jsign.KeyStoreType.AZUREKEYVAULT;
+import static net.jsign.KeyStoreType.DIGICERTONE;
+import static net.jsign.KeyStoreType.ESIGNER;
+import static net.jsign.KeyStoreType.GARASIGN;
+import static net.jsign.KeyStoreType.GOOGLECLOUD;
+import static net.jsign.KeyStoreType.HASHICORPVAULT;
+import static net.jsign.KeyStoreType.JCEKS;
+import static net.jsign.KeyStoreType.JKS;
+import static net.jsign.KeyStoreType.OPENPGP;
+import static net.jsign.KeyStoreType.ORACLECLOUD;
+import static net.jsign.KeyStoreType.PIV;
+import static net.jsign.KeyStoreType.PKCS11;
+import static net.jsign.KeyStoreType.PKCS12;
+import static net.jsign.KeyStoreType.SIGNSERVER;
+import static net.jsign.KeyStoreType.TRUSTEDSIGNING;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.isA;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 public class KeyStoreBuilderTest {
 
@@ -398,7 +418,7 @@ public void testBuildWithoutStoreType() throws Exception {
 
         builder.keystore("target/test-classes/keystores/keystore.p12");
 
-        assertEquals("storetype", PKCS12, builder.storetype());
+        assertThat("storetype", builder.storetype(), isA(Pkcs12KeyStore.class));
 
         KeyStore keystore = builder.build();
         assertNotNull("keystore", keystore);
@@ -455,6 +475,47 @@ public void testBuildPIV() throws Exception {
     @Test
     public void testLowerCaseStoreType() {
         KeyStoreBuilder builder = new KeyStoreBuilder().storetype("pkcs12");
-        assertEquals("storetype", PKCS12, builder.storetype());
+        assertThat("storetype", builder.storetype(), isA(Pkcs12KeyStore.class));
+    }
+
+    @Test
+    public void testGetType() {
+        assertThat(KeyStoreBuilder.getType(new File("keystore.p12")), isA(Pkcs12KeyStore.class));
+        assertThat(KeyStoreBuilder.getType(new File("keystore.pfx")), isA(Pkcs12KeyStore.class));
+        assertThat(KeyStoreBuilder.getType(new File("keystore.jceks")), isA(JceKeyStore.class));
+        assertThat(KeyStoreBuilder.getType(new File("keystore.jks")), isA(JavaKeyStore.class));
+        assertNull(KeyStoreBuilder.getType(new File("keystore.unknown")));
+    }
+
+    @Test
+    public void testGetTypePKCS12FromHeader() throws Exception {
+        File source = new File("target/test-classes/keystores/keystore.p12");
+        File target = new File("target/test-classes/keystores/keystore.p12.ext");
+        FileUtils.copyFile(source, target);
+
+        assertThat(KeyStoreBuilder.getType(target), isA(Pkcs12KeyStore.class));
+    }
+
+    @Test
+    public void testGetTypeJCEKSFromHeader() throws Exception {
+        File source = new File("target/test-classes/keystores/keystore.jceks");
+        File target = new File("target/test-classes/keystores/keystore.jceks.ext");
+        FileUtils.copyFile(source, target);
+
+        assertThat(KeyStoreBuilder.getType(target), isA(JceKeyStore.class));
+    }
+
+    @Test
+    public void testGetTypeJKSFromHeader() throws Exception {
+        File source = new File("target/test-classes/keystores/keystore.jks");
+        File target = new File("target/test-classes/keystores/keystore.jks.ext");
+        FileUtils.copyFile(source, target);
+
+        assertThat(KeyStoreBuilder.getType(target), isA(JavaKeyStore.class));
+    }
+
+    @Test
+    public void testGetTypeUnknown() {
+        assertNull(KeyStoreBuilder.getType(new File("target/test-classes/keystores/jsign-root-ca.pem")));
     }
 }
diff --git a/jsign-crypto/src/test/java/net/jsign/KeyStoreTypeTest.java b/jsign-crypto/src/test/java/net/jsign/KeyStoreTypeTest.java
index 12078819..0df9d47c 100644
--- a/jsign-crypto/src/test/java/net/jsign/KeyStoreTypeTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/KeyStoreTypeTest.java
@@ -16,54 +16,32 @@
 
 package net.jsign;
 
-import java.io.File;
-
-import org.apache.commons.io.FileUtils;
 import org.junit.Test;
 
-import static net.jsign.KeyStoreType.*;
-import static org.junit.Assert.*;
-
-public class KeyStoreTypeTest {
-
-    @Test
-    public void testGetType() {
-        assertEquals(PKCS12, KeyStoreType.of(new File("keystore.p12")));
-        assertEquals(PKCS12, KeyStoreType.of(new File("keystore.pfx")));
-        assertEquals(JCEKS, KeyStoreType.of(new File("keystore.jceks")));
-        assertEquals(JKS, KeyStoreType.of(new File("keystore.jks")));
-        assertNull(KeyStoreType.of(new File("keystore.unknown")));
-    }
-
-    @Test
-    public void testGetTypePKCS12FromHeader() throws Exception {
-        File source = new File("target/test-classes/keystores/keystore.p12");
-        File target = new File("target/test-classes/keystores/keystore.p12.ext");
-        FileUtils.copyFile(source, target);
-
-        assertEquals(PKCS12, KeyStoreType.of(target));
-    }
+import java.util.Arrays;
+import java.util.List;
 
-    @Test
-    public void testGetTypeJCEKSFromHeader() throws Exception {
-        File source = new File("target/test-classes/keystores/keystore.jceks");
-        File target = new File("target/test-classes/keystores/keystore.jceks.ext");
-        FileUtils.copyFile(source, target);
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-        assertEquals(JCEKS, KeyStoreType.of(target));
-    }
+public class KeyStoreTypeTest {
 
     @Test
-    public void testGetTypeJKSFromHeader() throws Exception {
-        File source = new File("target/test-classes/keystores/keystore.jks");
-        File target = new File("target/test-classes/keystores/keystore.jks.ext");
-        FileUtils.copyFile(source, target);
-
-        assertEquals(JKS, KeyStoreType.of(target));
+    public void testValueOf() {
+        assertEquals(KeyStoreType.JCEKS, KeyStoreType.valueOf("JCEKS"));
+        assertEquals(KeyStoreType.OPENSC, KeyStoreType.valueOf("OPENSC"));
+        assertEquals(KeyStoreType.OPENPGP, KeyStoreType.valueOf("OPENPGP"));
+        assertEquals(KeyStoreType.DIGICERTONE, KeyStoreType.valueOf("DIGICERTONE"));
     }
 
     @Test
-    public void testGetTypeUnknown() {
-        assertNull(KeyStoreType.of(new File("target/test-classes/keystores/jsign-root-ca.pem")));
+    public void testValues() {
+        List values = Arrays.asList(KeyStoreType.values());
+        assertTrue(values.contains(KeyStoreType.NONE));
+        assertTrue(values.contains(KeyStoreType.PKCS12));
+        assertTrue(values.contains(KeyStoreType.JKS));
+        assertTrue(values.contains(KeyStoreType.PKCS11));
+        assertTrue(values.contains(KeyStoreType.PIV));
+        assertTrue(values.contains(KeyStoreType.AWS));
     }
 }
diff --git a/jsign-crypto/src/test/java/net/jsign/OpenSCTest.java b/jsign-crypto/src/test/java/net/jsign/OpenSCTest.java
index 45ad0511..588c5124 100644
--- a/jsign-crypto/src/test/java/net/jsign/OpenSCTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/OpenSCTest.java
@@ -37,7 +37,7 @@ private void assumeOpenSC() {
     public void testGetProvider() {
         assumeOpenSC();
         try {
-            Provider provider = OpenSC.getProvider(null);
+            Provider provider = OpenSCKeyStore.getProvider((String) null);
             assertNotNull("provider", provider);
         } catch (RuntimeException e) {
             assertEquals("message", "No PKCS11 token found", ExceptionUtils.getRootCause(e).getMessage());
@@ -47,7 +47,7 @@ public void testGetProvider() {
     @Test
     public void testGetLibrary() {
         assumeOpenSC();
-        File library = OpenSC.getOpenSCLibrary();
+        File library = OpenSCKeyStore.getOpenSCLibrary();
         assertNotNull("native library", library);
         assertTrue("native library not found", library.exists());
     }
diff --git a/jsign-crypto/src/test/java/net/jsign/SafeNetETokenTest.java b/jsign-crypto/src/test/java/net/jsign/SafeNetETokenTest.java
index 413e998b..db470dfd 100644
--- a/jsign-crypto/src/test/java/net/jsign/SafeNetETokenTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/SafeNetETokenTest.java
@@ -23,6 +23,7 @@
 import org.junit.Assume;
 import org.junit.Test;
 
+import static net.jsign.KeyStoreType.ETOKEN;
 import static org.junit.Assert.*;
 
 public class SafeNetETokenTest {
@@ -37,7 +38,7 @@ private void assumeSafeNetEToken() {
     public void testGetProvider() {
         assumeSafeNetEToken();
         try {
-            Provider provider = SafeNetEToken.getProvider();
+            Provider provider = JsignKeyStoreDiscovery.getKeyStore(ETOKEN).getProvider(null);
             assertNotNull("provider", provider);
         } catch (RuntimeException e) {
             assertEquals("message", "No PKCS11 token found", ExceptionUtils.getRootCause(e).getMessage());
@@ -47,7 +48,7 @@ public void testGetProvider() {
     @Test
     public void testGetLibrary() {
         assumeSafeNetEToken();
-        File library = SafeNetEToken.getPKCS11Library();
+        File library = SafeNetETokenKeyStore.getPKCS11Library();
         assertNotNull("native library", library);
         assertTrue("native library not found", library.exists());
     }
diff --git a/jsign-crypto/src/test/java/net/jsign/YubikeyTest.java b/jsign-crypto/src/test/java/net/jsign/YubikeyTest.java
index 9b9892f5..bec501ee 100644
--- a/jsign-crypto/src/test/java/net/jsign/YubikeyTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/YubikeyTest.java
@@ -25,6 +25,7 @@
 import org.junit.Assume;
 import org.junit.Test;
 
+import static net.jsign.KeyStoreType.YUBIKEY;
 import static org.junit.Assert.*;
 
 public class YubikeyTest {
@@ -33,20 +34,20 @@ public static void assumeYubikey() {
         Assume.assumeTrue("libykcs11 isn't installed",
                 new File(System.getenv("ProgramFiles") + "/Yubico/Yubico PIV Tool/bin/libykcs11.dll").exists()
              || new File("/usr/lib/x86_64-linux-gnu/libykcs11.so").exists());
-        Assume.assumeTrue("No Yubikey detected", YubiKey.isPresent());
+        Assume.assumeTrue("No Yubikey detected", YubiKeyKeyStore.isPresent());
     }
 
     @Test
     public void testGetProvider() {
         assumeYubikey();
-        Provider provider = YubiKey.getProvider();
+        Provider provider = JsignKeyStoreDiscovery.getKeyStore(YUBIKEY).getProvider(null);
         assertNotNull("provider", provider);
     }
 
     @Test
     public void testGetLibrary() {
         assumeYubikey();
-        File library = YubiKey.getYkcs11Library();
+        File library = YubiKeyKeyStore.getYkcs11Library();
         assertNotNull("native library", library);
         assertTrue("native library not found", library.exists());
     }
@@ -55,7 +56,7 @@ public void testGetLibrary() {
     public void testAutoLogin() throws Exception {
         assumeYubikey();
 
-        Provider provider = YubiKey.getProvider();
+        Provider provider = JsignKeyStoreDiscovery.getKeyStore(YUBIKEY).getProvider(null);
         KeyStore keystore = KeyStore.getInstance("PKCS11", provider);
         assertEquals("provider", provider, keystore.getProvider());
         keystore.load(() -> new KeyStore.PasswordProtection("123456".toCharArray()));
diff --git a/jsign-crypto/src/test/java/net/jsign/jca/JsignJcaProviderTest.java b/jsign-crypto/src/test/java/net/jsign/jca/JsignJcaProviderTest.java
index 46d3eaca..6a8c6303 100644
--- a/jsign-crypto/src/test/java/net/jsign/jca/JsignJcaProviderTest.java
+++ b/jsign-crypto/src/test/java/net/jsign/jca/JsignJcaProviderTest.java
@@ -20,10 +20,10 @@
 import java.security.PrivateKey;
 import java.security.Signature;
 
+import net.jsign.JsignKeyStoreDiscovery;
 import org.junit.Test;
 
 import net.jsign.DigestAlgorithm;
-import net.jsign.KeyStoreType;
 import net.jsign.YubikeyTest;
 
 import static org.junit.Assert.*;
@@ -34,8 +34,8 @@ public class JsignJcaProviderTest {
     public void testServices() {
         JsignJcaProvider provider = new JsignJcaProvider();
 
-        for (KeyStoreType type : KeyStoreType.values()) {
-            assertNotNull("KeyStore " + type.name(), provider.getService("KeyStore", type.name()));
+        for (String type : JsignKeyStoreDiscovery.getKeyStoreTypes()) {
+            assertNotNull("KeyStore " + type, provider.getService("KeyStore", type));
         }
 
         for (String alg : new String[]{"RSA", "ECDSA"}) {