Skip to content

Commit

Permalink
WIP PKCS#11
Browse files Browse the repository at this point in the history
  • Loading branch information
dnl50 committed Dec 20, 2024
1 parent 298e6da commit dadb834
Show file tree
Hide file tree
Showing 15 changed files with 523 additions and 138 deletions.
109 changes: 92 additions & 17 deletions app/src/main/java/dev/mieser/tsa/signing/config/TsaConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,47 +1,65 @@
package dev.mieser.tsa.signing.config;

import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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 lombok.extern.slf4j.Slf4j;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import dev.mieser.tsa.datetime.api.CurrentDateService;
import dev.mieser.tsa.datetime.api.DateConverter;
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.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.mapper.TimeStampResponseMapper;
import dev.mieser.tsa.signing.impl.mapper.TimeStampValidationResultMapper;
import dev.mieser.tsa.signing.impl.serial.RandomSerialNumberGenerator;

@Slf4j
public class TsaConfiguration {

@Produces
@ApplicationScoped
TimeStampAuthority timeStampAuthority(TsaProperties tsaProperties,
TspParser tspParser,
TimeStampAuthority timeStampAuthority(Provider jceProvider,
TsaProperties tsaProperties,
SigningKeystoreLoader signingKeystoreLoader,
CurrentDateService currentDateService,
DateConverter dateConverter) {
return new BouncyCastleTimeStampAuthority(tsaProperties,
tspParser,
signingKeystoreLoader,
currentDateService,
return new BouncyCastleTimeStampAuthority(jceProvider,
tsaProperties,
new TspParser(),
signingKeystoreLoader, currentDateService,
new RandomSerialNumberGenerator(),
new TimeStampResponseMapper(dateConverter),
new DigestAlgorithmConverter());
}

@Produces
@ApplicationScoped
TimeStampValidator timeStampValidator(TspParser tspParser,
SigningKeystoreLoader signingKeystoreLoader,
TimeStampValidator timeStampValidator(SigningKeystoreLoader signingKeystoreLoader,
DateConverter dateConverter) {
return new BouncyCastleTimeStampValidator(tspParser,
return new BouncyCastleTimeStampValidator(new TspParser(),
signingKeystoreLoader,
new TimeStampValidationResultMapper(dateConverter),
new SigningCertificateExtractor(),
Expand All @@ -50,18 +68,75 @@ TimeStampValidator timeStampValidator(TspParser tspParser,

@Produces
@ApplicationScoped
TspParser tspParser() {
return new TspParser();
SigningKeystoreLoader signingCertificateLoader(TsaProperties tsaProperties, Provider jceProvider) {
Optional<KeystoreProperties> keystoreProperties = tsaProperties.keystore();
if (keystoreProperties.isPresent()) {
char[] password = keystoreProperties.get().password()
.map(String::toCharArray)
.orElse(new char[0]);

return new Pkcs12SigningKeystoreLoader(
keystoreProperties.get().path(),
password,
keystoreProperties.get().alias().orElse(null));
} else {
char[] pin = tsaProperties.pkcs11().orElseThrow().pin()
.map(String::toCharArray).orElse(new char[0]);

return new Pkcs11SigningKeystoreLoader(jceProvider, pin);
}
}

@Produces
@ApplicationScoped
SigningKeystoreLoader signingCertificateLoader(TsaProperties tsaProperties) {
char[] password = tsaProperties.keystore().password()
.map(String::toCharArray)
.orElse(new char[0]);
Provider jceProvider(TsaProperties tsaProperties) {
if (tsaProperties.pkcs11().isPresent()) {
log.info("Using SunPKCS11 as JCE provider");
return configureSunPkcs11Provider(tsaProperties.pkcs11().orElseThrow().configuration());
} 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) {
throw new IllegalStateException("SunPKCS11 provider is not available!");
}

Path tempFile = createTempFile();
try {
try (
Writer writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING)) {
var properties = new Properties();
properties.putAll(configuration);
properties.store(writer, null);
}

pkcs11JceProvider.configure(tempFile.toAbsolutePath().toString());
} catch (Exception e) {
throw new IllegalStateException("Failed to configure SunPKCS11 provider", e);
} finally {
deleteTempFile(tempFile);
}

return pkcs11JceProvider;
}

private Path createTempFile() {
try {
return Files.createTempFile("sunpkcs11", ".cfg");
} catch (IOException e) {
throw new IllegalStateException("Failed to create temporary file for SunPKCS11 configuration", e);
}
}

return new Pkcs12SigningKeystoreLoader(tsaProperties.keystore().path(), password);
private void deleteTempFile(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.debug("Failed to delete temporary file '{}'", path, e);
}
}

}
37 changes: 33 additions & 4 deletions app/src/main/java/dev/mieser/tsa/signing/config/TsaProperties.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package dev.mieser.tsa.signing.config;

import java.util.Map;
import java.util.Optional;
import java.util.Set;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import dev.mieser.tsa.signing.config.validator.EitherKeystoreOrPkcs11;
import dev.mieser.tsa.signing.config.validator.ValidDigestAlgorithmIdentifier;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

@EitherKeystoreOrPkcs11
@ConfigMapping(prefix = "tsa")
public interface TsaProperties {

Expand Down Expand Up @@ -61,18 +65,23 @@ public interface TsaProperties {
boolean includeTsaName();

/**
* Encapsulates the properties for configuring the TSA keystore.
* Encapsulates the properties for configuring a local keystore.
*/
KeystoreLoaderProperties keystore();
Optional<@Valid KeystoreProperties> keystore();

interface KeystoreLoaderProperties {
/**
* Encapsulates the properties for using a PKCS#11 device.
*/
Optional<@Valid Pkcs11Properties> pkcs11();

interface KeystoreProperties {

/**
* The path to the PKCS#12 file containing the certificate and private key. When the path begins with {@code classpath:}
* the keystore is read from the classpath. Loading a keystore from the classpath is a convenience feature for
* development purposes and will not work in a GraalVM native image.
*/
@NotEmpty
@NotBlank
String path();

/**
Expand All @@ -82,6 +91,26 @@ interface KeystoreLoaderProperties {
*/
Optional<String> password();

/**
* The alias of the keystore entry to use.
*/
Optional<String> alias();

}

interface Pkcs11Properties {

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

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

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.mieser.tsa.signing.config.validator;

import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

/**
* Custom type-level validation constraint, which supports for the {@link dev.mieser.tsa.signing.config.TsaProperties}.
* Validates that either a Keystore or PKCS#11 has been configured.
*
* @see EitherKeystoreOrPkcs11Validator
*/
@Target(TYPE_USE)
@Retention(RUNTIME)
@Constraint(validatedBy = EitherKeystoreOrPkcs11Validator.class)
@Documented
public @interface EitherKeystoreOrPkcs11 {

String message() default "Either a Keystore or a PKCS#11 device must be configured.";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.mieser.tsa.signing.config.validator;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import dev.mieser.tsa.signing.config.TsaProperties;

/**
* {@link ConstraintValidator} for the {@link EitherKeystoreOrPkcs11} annotation.
*
* @see EitherKeystoreOrPkcs11
*/
public class EitherKeystoreOrPkcs11Validator implements ConstraintValidator<EitherKeystoreOrPkcs11, TsaProperties> {

@Override
public boolean isValid(TsaProperties value, ConstraintValidatorContext context) {
boolean keystoreConfigured = value.keystore().isPresent();
boolean pkcs11Configured = value.pkcs11().isPresent();

return (keystoreConfigured && !pkcs11Configured) || (!keystoreConfigured && pkcs11Configured);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;
Expand All @@ -17,9 +18,12 @@
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampRequest;
Expand Down Expand Up @@ -50,6 +54,8 @@
@RequiredArgsConstructor
public class BouncyCastleTimeStampAuthority implements TimeStampAuthority {

private final Provider jceProvider;

private final TsaProperties tsaProperties;

private final TspParser tspParser;
Expand Down Expand Up @@ -136,29 +142,40 @@ private DigestCalculator buildSignerCertDigestCalculator() throws Exception {
String hashAlgorithmOid = tsaProperties.essCertIdAlgorithm().getObjectIdentifier();
var hashAlgorithmIdentifier = new AlgorithmIdentifier(new ASN1ObjectIdentifier(hashAlgorithmOid));

return new JcaDigestCalculatorProviderBuilder().build()
return new JcaDigestCalculatorProviderBuilder()
.setProvider(jceProvider)
.build()
.get(hashAlgorithmIdentifier);
}

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

PrivateKey signingPrivateKey = signingKeystoreLoader.loadPrivateKey();
String signingAlgorithmName = bouncyCastleSignatureAlgorithmName(publicKeyAlgorithm);
PrivateKey signaturePrivateKey = signingKeystoreLoader.loadPrivateKey();
String signatureAlgorithmName = bouncyCastleSignatureAlgorithmName(publicKeyAlgorithm);
log.info("Public key algorithm is '{}', using signature algorithm '{}'.", publicKeyAlgorithm.getJcaName(),
signingAlgorithmName);
signatureAlgorithmName);

DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder()
.setProvider(jceProvider)
.build();
ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithmName)
.setProvider(jceProvider)
.build(signaturePrivateKey);

return new JcaSimpleSignerInfoGeneratorBuilder().build(signingAlgorithmName, signingPrivateKey, signingCertificate);
return new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
.build(contentSigner, signatureCertificate);
}

/**
* @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 Down
Loading

0 comments on commit dadb834

Please sign in to comment.