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.