Skip to content

Commit

Permalink
WIP: Further implement PKCS#11 connection
Browse files Browse the repository at this point in the history
  • Loading branch information
dnl50 committed Dec 21, 2024
1 parent 29ea405 commit db1fcb9
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
import java.nio.file.StandardOpenOption;
import java.security.Provider;
import java.security.Security;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;

import lombok.extern.slf4j.Slf4j;

Expand All @@ -24,14 +24,15 @@
import dev.mieser.tsa.signing.api.TimeStampAuthority;
import dev.mieser.tsa.signing.api.TimeStampValidator;
import dev.mieser.tsa.signing.config.TsaProperties.KeystoreProperties;
import dev.mieser.tsa.signing.config.TsaProperties.Pkcs11Properties;
import dev.mieser.tsa.signing.impl.BouncyCastleTimeStampAuthority;
import dev.mieser.tsa.signing.impl.BouncyCastleTimeStampValidator;
import dev.mieser.tsa.signing.impl.TspParser;
import dev.mieser.tsa.signing.impl.cert.CertificateParser;
import dev.mieser.tsa.signing.impl.cert.Pkcs11SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.Pkcs12SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.SigningCertificateExtractor;
import dev.mieser.tsa.signing.impl.cert.SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.keystore.Pkcs11SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.keystore.Pkcs12SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.keystore.SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.mapper.TimeStampResponseMapper;
import dev.mieser.tsa.signing.impl.mapper.TimeStampValidationResultMapper;
import dev.mieser.tsa.signing.impl.serial.RandomSerialNumberGenerator;
Expand Down Expand Up @@ -88,19 +89,20 @@ SigningKeystoreLoader signingCertificateLoader(TsaProperties tsaProperties, Prov
}

@Produces
@Singleton
Provider jceProvider(TsaProperties tsaProperties) {
if (tsaProperties.pkcs11().isPresent()) {
log.info("Using SunPKCS11 as JCE provider");
return configureSunPkcs11Provider(tsaProperties.pkcs11().orElseThrow().configuration());
return configureSunPkcs11Provider(tsaProperties.pkcs11().orElseThrow());
} else {
log.info("Using Bouncy Castle as JCE provider");
return new BouncyCastleProvider();
}
}

private Provider configureSunPkcs11Provider(Map<String, String> configuration) {
var pkcs11JceProvider = Security.getProvider("SunPKCS11");
if (pkcs11JceProvider == null) {
private Provider configureSunPkcs11Provider(Pkcs11Properties pkcs11Properties) {
Provider uninitializedPkcs11Provider = Security.getProvider("SunPKCS11");
if (uninitializedPkcs11Provider == null) {
throw new IllegalStateException("SunPKCS11 provider is not available!");
}

Expand All @@ -109,18 +111,21 @@ private Provider configureSunPkcs11Provider(Map<String, String> configuration) {
try (
Writer writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING)) {
var properties = new Properties();
properties.putAll(configuration);
properties.putAll(pkcs11Properties.additionalConfiguration());
properties.put("library", pkcs11Properties.library().toAbsolutePath().toString());
properties.put("name", "TSA");
properties.store(writer, null);
}

pkcs11JceProvider.configure(tempFile.toAbsolutePath().toString());
Provider initializedPkcs11Provider = uninitializedPkcs11Provider.configure(tempFile.toAbsolutePath().toString());
log.info("PKCS#11 provider configured successfully.");

return initializedPkcs11Provider;
} catch (Exception e) {
throw new IllegalStateException("Failed to configure SunPKCS11 provider", e);
} finally {
deleteTempFile(tempFile);
}

return pkcs11JceProvider;
}

private Path createTempFile() {
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/java/dev/mieser/tsa/signing/config/TsaProperties.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.mieser.tsa.signing.config;

import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -100,16 +101,25 @@ interface KeystoreProperties {

interface Pkcs11Properties {

/**
* The PKCS#11 implementation to use.
*/
Path library();

/**
* The PIN of the PKCS#11 device.
*/
Optional<String> pin();

/**
* The {@code CKA_ID} of the key pair to use.
*/
Optional<String> keyIdentifier();

/**
* The configuration of the {@code SunPKCS11} JCE provider.
*/
@NotEmpty
Map<@NotNull String, String> configuration();
Map<@NotNull String, String> additionalConfiguration();

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;

Expand Down Expand Up @@ -42,8 +40,9 @@
import dev.mieser.tsa.signing.api.exception.TspResponseException;
import dev.mieser.tsa.signing.config.DigestAlgorithmConverter;
import dev.mieser.tsa.signing.config.TsaProperties;
import dev.mieser.tsa.signing.impl.cert.CertificateAndPrivateKey;
import dev.mieser.tsa.signing.impl.cert.PublicKeyAlgorithm;
import dev.mieser.tsa.signing.impl.cert.SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.cert.keystore.SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.mapper.TimeStampResponseMapper;
import dev.mieser.tsa.signing.impl.serial.SerialNumberGenerator;

Expand Down Expand Up @@ -116,8 +115,9 @@ public void initialize() {
new ASN1ObjectIdentifier(tsaProperties.policyOid()));
timeStampTokenGenerator.addCertificates(tokenGeneratorCertificateStore());
if (tsaProperties.includeTsaName()) {
X509CertificateHolder signingCertificate = new X509CertificateHolder(signingKeystoreLoader.loadCertificate()
.getEncoded());
X509CertificateHolder signingCertificate = new X509CertificateHolder(
signingKeystoreLoader.loadCertificateAndPrivateKey()
.certificate().getEncoded());
timeStampTokenGenerator.setTSA(new GeneralName(signingCertificate.getSubject()));
}

Expand Down Expand Up @@ -149,13 +149,12 @@ private DigestCalculator buildSignerCertDigestCalculator() throws Exception {
}

private SignerInfoGenerator buildSignerInfoGenerator() throws OperatorCreationException, CertificateEncodingException {
X509Certificate signatureCertificate = signingKeystoreLoader.loadCertificate();
String jcaAlgorithmName = signatureCertificate.getPublicKey().getAlgorithm();
CertificateAndPrivateKey certificateAndPrivateKey = signingKeystoreLoader.loadCertificateAndPrivateKey();
String jcaAlgorithmName = certificateAndPrivateKey.certificate().getPublicKey().getAlgorithm();
PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromJcaName(jcaAlgorithmName)
.orElseThrow(() -> new IllegalArgumentException(
String.format("Public Key algorithm '%s' is not supported.", jcaAlgorithmName)));

PrivateKey signaturePrivateKey = signingKeystoreLoader.loadPrivateKey();
String signatureAlgorithmName = bouncyCastleSignatureAlgorithmName(publicKeyAlgorithm);
log.info("Public key algorithm is '{}', using signature algorithm '{}'.", publicKeyAlgorithm.getJcaName(),
signatureAlgorithmName);
Expand All @@ -165,17 +164,16 @@ private SignerInfoGenerator buildSignerInfoGenerator() throws OperatorCreationEx
.build();
ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithmName)
.setProvider(jceProvider)
.build(signaturePrivateKey);
.build(certificateAndPrivateKey.privateKey());

return new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
.build(contentSigner, signatureCertificate);
.build(contentSigner, certificateAndPrivateKey.certificate());
}

/**
* @param publicKeyAlgorithm
* The algorithm of the public key whose corresponding private key is used to sign the TSP requests with, not
* {@code null}.
*
* @return The name of the Bouncy Castle signature algorithm used to sign TSP requests.
*/
private String bouncyCastleSignatureAlgorithmName(PublicKeyAlgorithm publicKeyAlgorithm) {
Expand All @@ -193,7 +191,7 @@ private String bouncyCastleSignatureAlgorithmName(PublicKeyAlgorithm publicKeyAl
*/
private Store<X509CertificateHolder> tokenGeneratorCertificateStore() throws IOException, CertificateEncodingException {
X509CertificateHolder signingCertificate = new X509CertificateHolder(
signingKeystoreLoader.loadCertificate().getEncoded());
signingKeystoreLoader.loadCertificateAndPrivateKey().certificate().getEncoded());
return new CollectionStore<>(List.of(signingCertificate));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
import dev.mieser.tsa.signing.api.exception.TsaInitializationException;
import dev.mieser.tsa.signing.api.exception.TsaNotInitializedException;
import dev.mieser.tsa.signing.impl.cert.*;
import dev.mieser.tsa.signing.impl.cert.keystore.SigningKeystoreLoader;
import dev.mieser.tsa.signing.impl.mapper.TimeStampValidationResultMapper;

// TODO: use JCE provider?
@Slf4j
@RequiredArgsConstructor
public class BouncyCastleTimeStampValidator implements TimeStampValidator {
Expand All @@ -54,7 +56,8 @@ public void initialize() {
}

try {
defaultSignatureVerifier = buildSignerInformationVerifier(signingKeystoreLoader.loadCertificate());
defaultSignatureVerifier = buildSignerInformationVerifier(
signingKeystoreLoader.loadCertificateAndPrivateKey().certificate());
} catch (InvalidCertificateException e) {
throw new TsaInitializationException("Failed to initialize signature verifier.", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.mieser.tsa.signing.impl.cert;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;

public record CertificateAndPrivateKey(X509Certificate certificate, PrivateKey privateKey) {

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.mieser.tsa.signing.impl.cert.keystore;

import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;

/**
* Interface abstraction of the {@link KeyStore#getCertificate(String)} and {@link KeyStore#getKey(String, char[])}
* methods.
*
* @param <T>
* The type of the certificate or key to be abstracted from the {@link KeyStore}.
*/
@FunctionalInterface
interface KeystoreEntryExtractor<T> {

T extractEntry(KeyStore keyStore,
String alias) throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dev.mieser.tsa.signing.impl.cert.keystore;

import java.io.IOException;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import dev.mieser.tsa.signing.impl.cert.CertificateAndPrivateKey;

/**
* {@link SigningKeystoreLoader} which loads key material from a PKCS#11 token.
*/
@Slf4j
@RequiredArgsConstructor
public class Pkcs11SigningKeystoreLoader implements SigningKeystoreLoader {

private static final String KEYSTORE_TYPE = "pkcs11";

private final Provider pkcs11JceProvider;

private final char[] pin;

// TODO: refactor + unify with Pkcs12 logic
@Override
public CertificateAndPrivateKey loadCertificateAndPrivateKey() {
KeyStore keyStore = loadKeystoreFromToken();
List<String> keyIdentifiers = extractKeyIdentifiers(keyStore);
if (keyIdentifiers.isEmpty()) {
throw new IllegalStateException("The configured PKCS#11 token contains no certificates.");
} else if (keyIdentifiers.size() > 1) {
throw new IllegalStateException("The configured PKCS#11 token contains than one certificate: " + keyIdentifiers);
}

String keyIdentifierToUse = keyIdentifiers.getFirst();
try {
Key key = keyStore.getKey(keyIdentifierToUse, pin);
if (!(key instanceof PrivateKey)) {
throw new IllegalStateException("Key is not a private key!");
}

Certificate certificate = keyStore.getCertificate(keyIdentifierToUse);
if (!(certificate instanceof X509Certificate)) {
throw new IllegalStateException("Certificate is not a X509 certificate!");
}

return new CertificateAndPrivateKey((X509Certificate) certificate, (PrivateKey) key);
} catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

private KeyStore loadKeystoreFromToken() {
try {
KeyStore pkcs11Keystore = KeyStore.getInstance(KEYSTORE_TYPE, pkcs11JceProvider);
pkcs11Keystore.load(null, pin);

return pkcs11Keystore;
} catch (KeyStoreException e) {
throw new IllegalStateException("The pseudo-mechanism of the SunPKCS11 provider to read keystores (PCKM_KEYSTORE) " +
"is not enabled. Please add it to the list of enabled mechanisms.");
} catch (CertificateException | IOException | NoSuchAlgorithmException e) {
throw new IllegalStateException("Failed to query certificates and private keys from PKCS#11 token.", e);
}
}

private List<String> extractKeyIdentifiers(KeyStore keyStore) {
try {
return Collections.list(keyStore.aliases());
} catch (KeyStoreException e) {
throw new IllegalStateException("Failed to query certificates from PKCS#11 token.", e);
}
}

}
Loading

0 comments on commit db1fcb9

Please sign in to comment.