diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractGenerator.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractGenerator.java new file mode 100644 index 000000000..b65121ed1 --- /dev/null +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractGenerator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * 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 dev.dsf.maven.dev; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.hsheilbronn.mi.utils.crypto.io.PemReader; +import de.hsheilbronn.mi.utils.crypto.io.PemWriter; + +public class AbstractGenerator +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractGenerator.class); + + public static final String POSTFIX_PRIVATE_KEY = ".key"; + + private final Path baseDir; + private final char[] privateKeyPassword; + + public AbstractGenerator(Path baseDir, char[] privateKeyPassword) + { + Objects.requireNonNull(baseDir, "baseDir"); + Objects.requireNonNull(privateKeyPassword, "privateKeyPassword"); + + this.baseDir = baseDir; + this.privateKeyPassword = privateKeyPassword; + } + + protected void writePrivateKey(String commonName, PrivateKey privateKey) throws RuntimeException + { + Path file = toPath(commonName, POSTFIX_PRIVATE_KEY); + + try + { + PemWriter.writePrivateKey(privateKey).asPkcs8().encryptedAes128(privateKeyPassword).toFile(file); + } + catch (IOException e) + { + logger.error("Unable to write private-key {}: {} - {}", file.toAbsolutePath().normalize(), + e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } + + protected Optional readPrivateKey(String commonName) throws RuntimeException + { + Path file = toPath(commonName, POSTFIX_PRIVATE_KEY); + + if (!Files.isReadable(file)) + return Optional.empty(); + + try + { + return Optional.of(PemReader.readPrivateKey(file, privateKeyPassword)); + } + catch (IOException e) + { + logger.error("Unable to read private-key {}: {} - {}", file.toAbsolutePath().normalize(), + e.getClass().getName(), e.getMessage()); + + throw new RuntimeException(e); + } + } + + protected Path toPath(String id, String postFix) + { + return baseDir.resolve(id.replaceAll(" ", "_") + postFix); + } +} diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractIo.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractIo.java index e9333b348..8b510fb0f 100644 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractIo.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/AbstractIo.java @@ -16,17 +16,36 @@ package dev.dsf.maven.dev; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.hsheilbronn.mi.utils.crypto.io.PemWriter; import dev.dsf.maven.exception.RuntimeIOException; public abstract class AbstractIo { + private static final Logger logger = LoggerFactory.getLogger(AbstractIo.class); + protected static interface RunnableWithIoException { void run() throws IOException; } - protected final void toRuntimeException(RunnableWithIoException runnable) + protected final Path projectBasedir; + protected final char[] privateKeyPassword; + + public AbstractIo(Path projectBasedir, char[] privateKeyPassword) + { + this.projectBasedir = Objects.requireNonNull(projectBasedir, "projectBasedir"); + this.privateKeyPassword = privateKeyPassword; + } + + protected final void toRuntimeException(RunnableWithIoException runnable) throws RuntimeIOException { try { @@ -37,4 +56,25 @@ protected final void toRuntimeException(RunnableWithIoException runnable) throw new RuntimeIOException(e); } } + + protected void writePrivateKey(String type, String id, PrivateKey privateKey, Path target) throws IOException + { + logger.info("Writing private-key encrypted ({}: {}) to {}", type, id, projectBasedir.relativize(target)); + + PemWriter.writePrivateKey(privateKey).asPkcs8().encryptedAes128(privateKeyPassword).toFile(target); + } + + protected void writePrivateKeyPlain(String type, String id, PrivateKey privateKey, Path target) throws IOException + { + logger.info("Writing private-key unencrypted ({}: {}) to {}", type, id, projectBasedir.relativize(target)); + + PemWriter.writePrivateKey(privateKey).asPkcs8().notEncrypted().toFile(target); + } + + protected final void writePassword(String type, String id, Path target) throws IOException + { + logger.info("Writing key password ({}: {}) to {}", type, id, projectBasedir.relativize(target)); + + Files.writeString(target, new String(privateKeyPassword)); + } } diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateGenerator.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateGenerator.java old mode 100755 new mode 100644 index fea48d429..e7d62315f --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateGenerator.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateGenerator.java @@ -53,7 +53,7 @@ import de.hsheilbronn.mi.utils.crypto.io.PemWriter; import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairValidator; -public class CertificateGenerator +public class CertificateGenerator extends AbstractGenerator { private static final Logger logger = LoggerFactory.getLogger(CertificateGenerator.class); @@ -109,7 +109,6 @@ public CertificateAndPrivateKey sign(CertificateAuthority ca) } } - public static final String POSTFIX_PRIVATE_KEY = ".key"; public static final String POSTFIX_CERTIFICATE = ".crt"; private static final String SUBJECT_C = "DE"; @@ -121,8 +120,6 @@ public CertificateAndPrivateKey sign(CertificateAuthority ca) private static final CertificationRequestConfig CERTIFICATION_REQUEST_ISSUING_CA = new CertificationRequestConfig( CertificateAuthority::signClientServerIssuingCaCertificate, SUBJECT_CN_ISSUING_CA, null); - private final Path certDir; - private final char[] privateKeyPassword; private final List certificationRequestConfigs = new ArrayList<>(); private CertificateAuthority rootCa; @@ -132,11 +129,7 @@ public CertificateAndPrivateKey sign(CertificateAuthority ca) public CertificateGenerator(Path certDir, char[] privateKeyPassword, List certificationRequestConfigs) { - Objects.requireNonNull(certDir, "certDir"); - Objects.requireNonNull(privateKeyPassword, "privateKeyPassword"); - - this.certDir = certDir; - this.privateKeyPassword = privateKeyPassword; + super(certDir, privateKeyPassword); if (certificationRequestConfigs != null) this.certificationRequestConfigs.addAll(certificationRequestConfigs); @@ -216,11 +209,6 @@ private String toHexThumbprint(X509Certificate certificate) } } - private Path toPath(String commonName, String postFix) - { - return certDir.resolve(commonName.replaceAll(" ", "_") + postFix); - } - private Optional readCertificate(String commonName) { Path file = toPath(commonName, POSTFIX_CERTIFICATE); @@ -241,26 +229,6 @@ private Optional readCertificate(String commonName) } } - private Optional readPrivateKey(String commonName) - { - Path file = toPath(commonName, POSTFIX_PRIVATE_KEY); - - if (!Files.isReadable(file)) - return Optional.empty(); - - try - { - return Optional.of(PemReader.readPrivateKey(file, privateKeyPassword)); - } - catch (IOException e) - { - logger.error("Unable to read private-key {}: {} - {}", file.toAbsolutePath().normalize(), - e.getClass().getName(), e.getMessage()); - - throw new RuntimeException(e); - } - } - private Optional readCertificateAndPrivateKey(String commonName) { Optional crt = readCertificate(commonName); @@ -333,21 +301,6 @@ private void writeCertificate(String commonName, X509Certificate crt) } } - private void writePrivateKey(String commonName, PrivateKey privateKey) - { - Path file = toPath(commonName, POSTFIX_PRIVATE_KEY); - - try - { - PemWriter.writePrivateKey(privateKey).asPkcs8().encryptedAes128(privateKeyPassword).toFile(file); - } - catch (IOException e) - { - logger.error("Unable to write private-key {}: {} - {}", file.toAbsolutePath().normalize(), - e.getClass().getName(), e.getMessage()); - throw new RuntimeException(e); - } - } private void writeCertificateAndPrivateKey(String commonName, CertificateAndPrivateKey certificateAndPrivateKey) { diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateWriter.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateWriter.java index 14d047a39..e3acb67e6 100644 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateWriter.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CertificateWriter.java @@ -17,7 +17,6 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; import java.util.List; @@ -31,29 +30,28 @@ import de.hsheilbronn.mi.utils.crypto.io.PemWriter; import de.hsheilbronn.mi.utils.crypto.keystore.KeyStoreCreator; import dev.dsf.maven.dev.CertificateGenerator.CertificateAndPrivateKey; +import dev.dsf.maven.exception.RuntimeIOException; public class CertificateWriter extends AbstractIo { private static final Logger logger = LoggerFactory.getLogger(CertificateWriter.class); - private final Path projectBasedir; private final CertificateGenerator generator; - private final char[] privateKeyPassword; - public CertificateWriter(Path projectBasedir, CertificateGenerator generator, char[] privateKeyPassword) + public CertificateWriter(Path projectBasedir, char[] privateKeyPassword, CertificateGenerator generator) { - this.projectBasedir = Objects.requireNonNull(projectBasedir, "projectBasedir"); + super(projectBasedir, privateKeyPassword); + this.generator = Objects.requireNonNull(generator, "generator"); - this.privateKeyPassword = Objects.requireNonNull(privateKeyPassword, "privateKeyPassword"); } - public void write(List certs) + public void write(List certs) throws RuntimeIOException { if (certs != null) certs.forEach(this::write); } - private void write(Cert cert) + private void write(Cert cert) throws RuntimeIOException { Optional certificateAndPrivateKey = generator .getCertificateAndPrivateKey(cert.getCn()); @@ -64,11 +62,11 @@ private void write(Cert cert) else if (target.getFileName().toString().endsWith(".crt")) toRuntimeException(() -> writeCertificate(cert.getCn(), capk, target)); else if (target.getFileName().toString().endsWith(".key")) - toRuntimeException(() -> writePrivateKey(cert.getCn(), capk, target)); + toRuntimeException(() -> writePrivateKey("cn", cert.getCn(), capk.privateKey(), target)); else if (target.getFileName().toString().endsWith(".key.plain")) - toRuntimeException(() -> writePrivateKeyPlain(cert.getCn(), capk, target)); + toRuntimeException(() -> writePrivateKeyPlain("cn", cert.getCn(), capk.privateKey(), target)); else if (target.getFileName().toString().endsWith(".key.password")) - toRuntimeException(() -> writePassword(cert.getCn(), target)); + toRuntimeException(() -> writePassword("cn", cert.getCn(), target)); else if (target.getFileName().toString().endsWith(".p12")) toRuntimeException(() -> writePkcs12(cert.getCn(), capk, target)); else @@ -76,7 +74,7 @@ else if (target.getFileName().toString().endsWith(".p12")) })); } - public void write(RootCa rootCa) + public void write(RootCa rootCa) throws RuntimeIOException { if (rootCa == null) return; @@ -87,12 +85,14 @@ public void write(RootCa rootCa) toRuntimeException(() -> writeRootCa(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> writeRootCaJks(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> writeRootCaPkcs12(target)); else logger.warn("RootCa target filetype not supported: {}", target.getFileName()); }); } - public void write(IssuingCa issuingCa) + public void write(IssuingCa issuingCa) throws RuntimeIOException { if (issuingCa == null) return; @@ -103,12 +103,14 @@ public void write(IssuingCa issuingCa) toRuntimeException(() -> writeIssuingCa(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> writeIssuingCaJks(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> writeIssuingCaPkcs12(target)); else logger.warn("IssuingCa target filetype not supported: {}", target.getFileName()); }); } - public void write(CaChain caChain) + public void write(CaChain caChain) throws RuntimeIOException { if (caChain == null) return; @@ -119,6 +121,8 @@ public void write(CaChain caChain) toRuntimeException(() -> writeCaChain(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> writeCaChainJks(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> writeCaChainPkcs12(target)); else logger.warn("CaChain target filetype not supported: {}", target.getFileName()); }); @@ -138,27 +142,6 @@ private void writeCertificateChain(String cn, CertificateAndPrivateKey capk, Pat PemWriter.writeCertificates(List.of(capk.certificate(), generator.getIssuingCaCertificate()), true, target); } - private void writePrivateKey(String cn, CertificateAndPrivateKey capk, Path target) throws IOException - { - logger.info("Writing private-key encrypted (cn: {}) to {}", cn, projectBasedir.relativize(target)); - - PemWriter.writePrivateKey(capk.privateKey()).asPkcs8().encryptedAes128(privateKeyPassword).toFile(target); - } - - private void writePrivateKeyPlain(String cn, CertificateAndPrivateKey capk, Path target) throws IOException - { - logger.info("Writing private-key unencrypted (cn: {}) to {}", cn, projectBasedir.relativize(target)); - - PemWriter.writePrivateKey(capk.privateKey()).asPkcs8().notEncrypted().toFile(target); - } - - private void writePassword(String cn, Path target) throws IOException - { - logger.info("Writing key password (cn: {}) to {}", cn, projectBasedir.relativize(target)); - - Files.writeString(target, new String(privateKeyPassword)); - } - private void writePkcs12(String cn, CertificateAndPrivateKey capk, Path target) throws IOException { logger.info("Writing pkcs12 key-store (cn: {}) to {}", cn, projectBasedir.relativize(target)); @@ -219,4 +202,32 @@ private void writeCaChainJks(Path target) throws IOException KeyStoreWriter.write(keyStore, privateKeyPassword, target); } + + private void writeRootCaPkcs12(Path target) throws IOException + { + KeyStore keyStore = KeyStoreCreator.pkcs12ForTrustedCertificates(generator.getRootCaCertificate()); + + logger.info("Writing rootCa to {}", projectBasedir.relativize(target)); + + KeyStoreWriter.write(keyStore, privateKeyPassword, target); + } + + private void writeIssuingCaPkcs12(Path target) throws IOException + { + KeyStore keyStore = KeyStoreCreator.pkcs12ForTrustedCertificates(generator.getIssuingCaCertificate()); + + logger.info("Writing issuingCa to {}", projectBasedir.relativize(target)); + + KeyStoreWriter.write(keyStore, privateKeyPassword, target); + } + + private void writeCaChainPkcs12(Path target) throws IOException + { + KeyStore keyStore = KeyStoreCreator.pkcs12ForTrustedCertificates(generator.getIssuingCaCertificate(), + generator.getRootCaCertificate()); + + logger.info("Writing caChain to {}", projectBasedir.relativize(target)); + + KeyStoreWriter.write(keyStore, privateKeyPassword, target); + } } \ No newline at end of file diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CleanDevSetupCertFilesMojo.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CleanDevSetupCertFilesMojo.java index e0065714d..a2170541a 100644 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CleanDevSetupCertFilesMojo.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/CleanDevSetupCertFilesMojo.java @@ -32,6 +32,9 @@ * Cleans up certificate files for a local DSF development setup. *

* This goal deletes all generated certificate files (client, server, CA chain) from the configured target directories. + *

+ * Use -Ddsf.includeCertDir=true and/or -Ddsf.includeKeyDir=true to also delete the plugin + * cache files. */ @Mojo(name = "clean-dev-setup-cert-files", defaultPhase = LifecyclePhase.CLEAN, requiresDependencyResolution = ResolutionScope.NONE, threadSafe = true, aggregator = true) public class CleanDevSetupCertFilesMojo extends AbstractMojo @@ -48,12 +51,24 @@ public class CleanDevSetupCertFilesMojo extends AbstractMojo @Parameter(required = true, property = "dsf.certDir", defaultValue = "cert") private File certDir; + /** + * The directory to write the generated key files to. + */ + @Parameter(required = true, property = "dsf.keyDir", defaultValue = "key") + private File keyDir; + /** * The certificates to generate. See usage for details. */ @Parameter private List certs; + /** + * The key-pairs to generate. See usage for details. + */ + @Parameter + private List keys; + /** * The root CA configuration. */ @@ -84,19 +99,28 @@ public class CleanDevSetupCertFilesMojo extends AbstractMojo @Parameter(required = true, property = "dsf.includeCertDir", defaultValue = "false") private boolean includeCertDir; + /** + * Whether to delete the key directory with its contents as well. + */ + @Parameter(required = true, property = "dsf.includeKeyDir", defaultValue = "false") + private boolean includeKeyDir; + @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().debug("projectBasedir: " + projectBasedir); getLog().debug("certDir: " + certDir); + getLog().debug("keyDir: " + keyDir); getLog().debug("certs: " + certs); + getLog().debug("keys: " + keys); getLog().debug("rootCa: " + rootCa); getLog().debug("issuingCa: " + issuingCa); getLog().debug("caChain: " + caChain); getLog().debug("templates: " + templates); getLog().debug("includeCertDir: " + includeCertDir); + getLog().debug("includeKeyDir: " + includeKeyDir); - FileRemover fileRemover = new FileRemover(projectBasedir.toPath(), certDir.toPath()); + FileRemover fileRemover = new FileRemover(projectBasedir.toPath(), certDir.toPath(), keyDir.toPath()); try { @@ -105,9 +129,12 @@ public void execute() throws MojoExecutionException, MojoFailureException fileRemover.delete(issuingCa); fileRemover.delete(caChain); fileRemover.deleteTemplates(templates); + fileRemover.deleteKeys(keys); if (includeCertDir) fileRemover.deleteFilesInCertDir(certs); + if (includeKeyDir) + fileRemover.deleteFilesInKeyDir(keys); } catch (RuntimeIOException e) { diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/FileRemover.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/FileRemover.java index 1306fe6a0..7c77df68e 100644 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/FileRemover.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/FileRemover.java @@ -30,13 +30,15 @@ public class FileRemover extends AbstractIo { private static final Logger logger = LoggerFactory.getLogger(FileRemover.class); - private final Path projectBasedir; private final Path certDir; + private final Path keyDir; - public FileRemover(Path projectBasedir, Path certDir) + public FileRemover(Path projectBasedir, Path certDir, Path keyDir) { - this.projectBasedir = Objects.requireNonNull(projectBasedir, "projectBasedir"); + super(projectBasedir, null); + this.certDir = Objects.requireNonNull(certDir, "certDir"); + this.keyDir = Objects.requireNonNull(keyDir, "keyDir"); } public void deleteCerts(List certs) @@ -76,6 +78,8 @@ public void delete(RootCa rootCa) toRuntimeException(() -> delete(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> delete(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> delete(target)); else logger.warn("RootCa target filetype not supported: {}", target.getFileName()); }); @@ -92,6 +96,8 @@ public void delete(IssuingCa issuingCa) toRuntimeException(() -> delete(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> delete(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> delete(target)); else logger.warn("IssuingCa target filetype not supported: {}", target.getFileName()); }); @@ -108,6 +114,8 @@ public void delete(CaChain caChain) toRuntimeException(() -> delete(target)); else if (target.getFileName().toString().endsWith(".jks")) toRuntimeException(() -> delete(target)); + else if (target.getFileName().toString().endsWith(".p12")) + toRuntimeException(() -> delete(target)); else logger.warn("CaChain target filetype not supported: {}", target.getFileName()); }); @@ -134,7 +142,7 @@ private void delete(Path target) throws IOException } } - private Path toPath(String commonName, String postFix) + private Path toCertPath(String commonName, String postFix) { return certDir.resolve(commonName.replaceAll(" ", "_") + postFix); } @@ -147,8 +155,49 @@ public void deleteFilesInCertDir(List certs) commonNamesToDelete.forEach(cn -> { - toRuntimeException(() -> delete(toPath(cn, CertificateGenerator.POSTFIX_PRIVATE_KEY))); - toRuntimeException(() -> delete(toPath(cn, CertificateGenerator.POSTFIX_CERTIFICATE))); + toRuntimeException(() -> delete(toCertPath(cn, CertificateGenerator.POSTFIX_PRIVATE_KEY))); + toRuntimeException(() -> delete(toCertPath(cn, CertificateGenerator.POSTFIX_CERTIFICATE))); + }); + } + + private Path toKeyPath(String commonName, String postFix) + { + return keyDir.resolve(commonName.replaceAll(" ", "_") + postFix); + } + + public void deleteFilesInKeyDir(List Keys) + { + Stream idsToDelete = Keys == null ? Stream.empty() + : Keys.stream().map(Key::getId).filter(Objects::nonNull); + + idsToDelete.forEach(id -> + { + toRuntimeException(() -> delete(toKeyPath(id, KeyGenerator.POSTFIX_PRIVATE_KEY))); + toRuntimeException(() -> delete(toKeyPath(id, KeyGenerator.POSTFIX_PUBLIC_KEY))); + }); + } + + public void deleteKeys(List keys) + { + if (keys != null) + keys.forEach(this::delete); + } + + private void delete(Key key) + { + if (key == null) + return; + + key.getTargets().stream().filter(Objects::nonNull).map(File::toPath).forEach(target -> + { + if (target.getFileName().toString().endsWith(".key")) + toRuntimeException(() -> delete(target)); + else if (target.getFileName().toString().endsWith(".key.plain")) + toRuntimeException(() -> delete(target)); + else if (target.getFileName().toString().endsWith(".pub")) + toRuntimeException(() -> delete(target)); + else + logger.warn("Key (id: {}) target filetype not supported: {}", key.getId(), target.getFileName()); }); } } diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/GenerateDevSetupCertFilesMojo.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/GenerateDevSetupCertFilesMojo.java index 0ef711dc8..6cb5fd850 100644 --- a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/GenerateDevSetupCertFilesMojo.java +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/GenerateDevSetupCertFilesMojo.java @@ -31,10 +31,13 @@ import dev.dsf.maven.exception.RuntimeIOException; /** - * Generates certificates for a local DSF development setup. + * Generates certificates and other files for a local DSF development setup. *

* This goal creates all required certificate files (client, server, CA chain) and copies them to the configured target * directories. + *

+ * Arbitrary files can be used as templates to fill in certificate thumbprints, private / public key pairs can be + * generated. */ @Mojo(name = "generate-dev-setup-cert-files", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.NONE, threadSafe = true, aggregator = true) public class GenerateDevSetupCertFilesMojo extends AbstractMojo @@ -57,6 +60,12 @@ public class GenerateDevSetupCertFilesMojo extends AbstractMojo @Parameter(required = true, property = "dsf.certDir", defaultValue = "cert") private File certDir; + /** + * The directory to write the generated key files to. + */ + @Parameter(required = true, property = "dsf.keyDir", defaultValue = "key") + private File keyDir; + /** * The password to protect the private keys. */ @@ -69,6 +78,12 @@ public class GenerateDevSetupCertFilesMojo extends AbstractMojo @Parameter private List certs; + /** + * The key-pairs to generate. See usage for details. + */ + @Parameter + private List keys; + /** * The root CA configuration. */ @@ -99,9 +114,11 @@ public void execute() throws MojoExecutionException, MojoFailureException getLog().debug("projectBasedir: " + projectBasedir); getLog().debug("encoding: " + encoding); getLog().debug("certDir: " + certDir); + getLog().debug("keyDir: " + keyDir); getLog().debug("privateKeyPassword: " + (privateKeyPassword == null ? null : !privateKeyPassword.isEmpty() ? "***" : "")); getLog().debug("certs: " + certs); + getLog().debug("keys: " + keys); getLog().debug("rootCa: " + rootCa); getLog().debug("issuingCa: " + issuingCa); getLog().debug("caChain: " + caChain); @@ -121,8 +138,8 @@ public void execute() throws MojoExecutionException, MojoFailureException CertificateGenerator certificateGenerator = new CertificateGenerator(certDir.toPath(), privateKeyPassword.toCharArray(), certs.stream().map(Cert::toCertificationRequestConfig).toList()); - CertificateWriter certificateWriter = new CertificateWriter(projectBasedir.toPath(), certificateGenerator, - privateKeyPassword.toCharArray()); + CertificateWriter certificateWriter = new CertificateWriter(projectBasedir.toPath(), + privateKeyPassword.toCharArray(), certificateGenerator); TemplateHandler templateHandler = new TemplateHandler(projectBasedir.toPath(), certificateGenerator, encoding); certificateGenerator.initialize(); @@ -140,5 +157,19 @@ public void execute() throws MojoExecutionException, MojoFailureException { throw new MojoFailureException(e); } + + KeyGenerator keyGenerator = new KeyGenerator(keyDir.toPath(), privateKeyPassword.toCharArray(), keys); + keyGenerator.initialize(); + + KeyWriter keyWriter = new KeyWriter(projectBasedir.toPath(), privateKeyPassword.toCharArray(), keyGenerator); + + try + { + keyWriter.write(keys); + } + catch (RuntimeIOException e) + { + throw new MojoFailureException(e); + } } } diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/Key.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/Key.java new file mode 100644 index 000000000..cdb7ee218 --- /dev/null +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/Key.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * 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 dev.dsf.maven.dev; + +import java.io.File; +import java.util.List; + +import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairGeneratorFactory; + +public class Key +{ + public static enum Type + { + RSA1024(KeyPairGeneratorFactory.rsa1024(), "RSA 1024"), + + RSA2048(KeyPairGeneratorFactory.rsa2048(), "RSA 2048"), + + RSA3072(KeyPairGeneratorFactory.rsa3072(), "RSA 3072"), + + RSA4096(KeyPairGeneratorFactory.rsa4096(), "RSA 4096"), + + SECP256R1(KeyPairGeneratorFactory.secp256r1(), "Secp256r1"), + + SECP384R1(KeyPairGeneratorFactory.secp384r1(), "Secp384r1"), + + SECP521R1(KeyPairGeneratorFactory.secp521r1(), "Secp521r1"), + + ED25519(KeyPairGeneratorFactory.ed25519(), "ED25519"), + + ED448(KeyPairGeneratorFactory.ed448(), "ED448"), + + X25519(KeyPairGeneratorFactory.x25519(), "X25519"), + + X448(KeyPairGeneratorFactory.x448(), "X448"); + + private final KeyPairGeneratorFactory keyPairGeneratorFactory; + private final String keyType; + + private Type(KeyPairGeneratorFactory keyPairGeneratorFactory, String keyType) + { + this.keyPairGeneratorFactory = keyPairGeneratorFactory; + this.keyType = keyType; + } + + public KeyPairGeneratorFactory getKeyPairGeneratorFactory() + { + return keyPairGeneratorFactory; + } + + public String getKeyType() + { + return keyType; + } + } + + private String id; + private Type type; + private List targets; + + public String getId() + { + return id; + } + + public Type getType() + { + return type; + } + + public List getTargets() + { + return targets; + } + + @Override + public String toString() + { + return "Key [" + (id != null ? "id=" + id + ", " : "") + (type != null ? "type=" + type + ", " : "") + + (targets != null ? "targets=" + targets + ", " : "") + "]"; + } +} diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyGenerator.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyGenerator.java new file mode 100644 index 000000000..0b6d606f5 --- /dev/null +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyGenerator.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * 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 dev.dsf.maven.dev; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.hsheilbronn.mi.utils.crypto.io.PemReader; +import de.hsheilbronn.mi.utils.crypto.io.PemWriter; +import de.hsheilbronn.mi.utils.crypto.keypair.KeyPairValidator; + +public class KeyGenerator extends AbstractGenerator +{ + private static final Logger logger = LoggerFactory.getLogger(KeyGenerator.class); + + public static final String POSTFIX_PUBLIC_KEY = ".pub"; + + private final List keys = new ArrayList<>(); + + private Map keyPairsById; + + public KeyGenerator(Path keyDir, char[] privateKeyPassword, List keys) + { + super(keyDir, privateKeyPassword); + + if (keys != null) + this.keys.addAll(keys); + } + + public void initialize() + { + logger.info("Initializing key generator ..."); + + keyPairsById = keys.stream().collect(Collectors.toMap(Key::getId, this::initKeyPair)); + } + + public boolean isInitialized() + { + return keyPairsById != null; + } + + private void checkInitialized() + { + if (!isInitialized()) + throw new IllegalStateException("not initialized"); + } + + private KeyPair initKeyPair(Key key) + { + return readKeyPair(key).orElseGet(() -> createKeyPair(key)); + } + + public Optional getKeyPair(Key key) + { + checkInitialized(); + + return Optional.ofNullable(keyPairsById.get(key.getId())); + } + + private Optional readKeyPair(Key key) + { + Optional publicKey = readPublicKey(key.getId()); + if (publicKey.isEmpty()) + { + logger.debug("{} public-key for '{}' not found", key.getType().getKeyType(), key.getId()); + return Optional.empty(); + } + + Optional privateKey = readPrivateKey(key.getId()); + if (privateKey.isEmpty()) + { + logger.debug("{} private-key for '{}' not found", key.getType().getKeyType(), key.getId()); + return Optional.empty(); + } + + if (!KeyPairValidator.matches(privateKey.get(), publicKey.get())) + { + logger.warn("Found {} public-key and private-key for '{}' not matching", key.getType().getKeyType(), + key.getId()); + return Optional.empty(); + } + + logger.info("Using existing {} key pair for '{}'", key.getType().getKeyType(), key.getId()); + return Optional.of(new KeyPair(publicKey.get(), privateKey.get())); + } + + private KeyPair createKeyPair(Key key) + { + logger.info("Creating {} key pair '{}'", key.getType().getKeyType(), key.getId()); + + KeyPair keyPair = key.getType().getKeyPairGeneratorFactory().initialize().generateKeyPair(); + + writePrivateKey(key.getId(), keyPair.getPrivate()); + writePublicKey(key.getId(), keyPair.getPublic()); + + return keyPair; + } + + private void writePublicKey(String id, PublicKey publicKey) throws RuntimeException + { + Path file = toPath(id, POSTFIX_PUBLIC_KEY); + + try + { + PemWriter.writePublicKey(publicKey, file); + } + catch (IOException e) + { + logger.error("Unable to write public-key {}: {} - {}", file.toAbsolutePath().normalize(), + e.getClass().getName(), e.getMessage()); + throw new RuntimeException(e); + } + } + + private Optional readPublicKey(String id) throws RuntimeException + { + Path file = toPath(id, POSTFIX_PUBLIC_KEY); + + if (!Files.isReadable(file)) + return Optional.empty(); + + try + { + return Optional.of(PemReader.readPublicKey(file)); + } + catch (IOException e) + { + logger.error("Unable to read public-key {}: {} - {}", file.toAbsolutePath().normalize(), + e.getClass().getName(), e.getMessage()); + + throw new RuntimeException(e); + } + } +} diff --git a/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyWriter.java b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyWriter.java new file mode 100644 index 000000000..08a91cbfd --- /dev/null +++ b/dsf-maven/dsf-maven-plugin/src/main/java/dev/dsf/maven/dev/KeyWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018-2025 Heilbronn University of Applied Sciences + * + * 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 dev.dsf.maven.dev; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.hsheilbronn.mi.utils.crypto.io.PemWriter; +import dev.dsf.maven.exception.RuntimeIOException; + +public class KeyWriter extends AbstractIo +{ + private static final Logger logger = LoggerFactory.getLogger(KeyWriter.class); + + private final KeyGenerator keyGenerator; + + public KeyWriter(Path projectBasedir, char[] privateKeyPassword, KeyGenerator keyGenerator) + { + super(projectBasedir, privateKeyPassword); + + this.keyGenerator = Objects.requireNonNull(keyGenerator, "keyGenerator"); + } + + public void write(List keys) + { + if (keys != null) + keys.forEach(this::write); + } + + private void write(Key key) throws RuntimeIOException + { + Optional keyPair = keyGenerator.getKeyPair(key); + keyPair.ifPresent(kp -> key.getTargets().stream().map(File::toPath).forEach(target -> + { + if (target.getFileName().toString().endsWith(".key")) + toRuntimeException(() -> writePrivateKey("id", key.getId(), kp.getPrivate(), target)); + else if (target.getFileName().toString().endsWith(".key.plain")) + toRuntimeException(() -> writePrivateKeyPlain("id", key.getId(), kp.getPrivate(), target)); + else if (target.getFileName().toString().endsWith(".key.password")) + toRuntimeException(() -> writePassword("id", key.getId(), target)); + else if (target.getFileName().toString().endsWith(".pub")) + toRuntimeException(() -> writePublicKey(key.getId(), kp.getPublic(), target)); + else + logger.warn("Key (id: {}) target filetype not supported: {}", key.getId(), target.getFileName()); + })); + } + + private void writePublicKey(String id, PublicKey publicKey, Path target) throws IOException + { + logger.info("Writing public-key (id: {}) to {}", id, projectBasedir.relativize(target)); + + PemWriter.writePublicKey(publicKey, target); + } +} diff --git a/dsf-maven/dsf-maven-plugin/src/site/markdown/index.md b/dsf-maven/dsf-maven-plugin/src/site/markdown/index.md index 77c2558cb..75122706d 100644 --- a/dsf-maven/dsf-maven-plugin/src/site/markdown/index.md +++ b/dsf-maven/dsf-maven-plugin/src/site/markdown/index.md @@ -63,6 +63,7 @@ Generates certificates and keys for local DSF development setups (e.g. FHIR, BPE * Creates Root, Issuing, and CA Chain certificates * Generates client/server certificates +* Generate key pairs * Copies files to configured target locations * Create configuration files through templates * Supports optional cleanup via `clean-dev-setup-cert-files` @@ -171,7 +172,7 @@ mvn dsf:help -Ddetail=true -Dgoal=generate-dev-setup-cert-files ``` -### Example 2: Configuration for Dev Setup Certificates +### Example 2: Configuration for Dev Setup Certificates and Related Files ```xml @@ -248,6 +249,10 @@ mvn dsf:help -Ddetail=true -Dgoal=generate-dev-setup-cert-files dsf-docker-dev-setup/fhir/secrets/root_ca.crt dsf-docker-dev-setup-3dic-ttp/secrets/root_ca.crt dsf-fhir/dsf-fhir-server-jetty/cert/root_ca.crt + + root_ca.jks + + root_ca.p12 @@ -257,19 +262,25 @@ mvn dsf:help -Ddetail=true -Dgoal=generate-dev-setup-cert-files dsf-docker-dev-setup/bpe/secrets/issuing_ca.crt dsf-docker-dev-setup/fhir/secrets/issuing_ca.crt dsf-docker-dev-setup-3dic-ttp/secrets/issuing_ca.crt - - dsf-docker-dev-setup-3dic-ttp/secrets/keycloak_trust_store.jks dsf-fhir/dsf-fhir-server-jetty/cert/issuing_ca.crt + + issuing_ca.jks + + issuing_ca.p12 - + dsf-bpe/dsf-bpe-server-jetty/cert/ca_chain.crt dsf-docker-dev-setup/bpe/secrets/ca_chain.crt dsf-docker-dev-setup/fhir/secrets/ca_chain.crt dsf-docker-dev-setup-3dic-ttp/secrets/ca_chain.crt dsf-fhir/dsf-fhir-server-jetty/cert/ca_chain.crt + + ca_chain.jks + + ca_chain.p12 @@ -282,6 +293,26 @@ mvn dsf:help -Ddetail=true -Dgoal=generate-dev-setup-cert-files + + + foo + + + + docker-dev-setup/secrets/foo.key + + docker-dev-setup/secrets/foo.key.password + + docker-dev-setup/secrets/foo.key.plain + + docker-dev-setup/secrets/foo.pub + + second/location/foo.pub + + + false @@ -297,6 +328,9 @@ DIC1_THUMBPRINT=${dic1.thumbprint} DIC2_THUMBPRINT=${dic2.thumbprint} DIC3_THUMBPRINT=${dic3.thumbprint} ``` + +Templates can be used to insert certificate SHA-512 thumbprints. Syntax: `${cn.thumbprint}` with `cn` as configured in the pom. + --- ## Configuration @@ -305,11 +339,12 @@ All goals support configuration through plugin parameters in the POM or system p Common parameters include: -| Parameter | Description | Default | -| ------------------- | --------------------------------------------------------------------- | -------------------------------------------- | -| `certFolder` | Root folder containing certificate resources | `${project.basedir}/src/main/resources/cert` | -| `configDocPackages` | Package list to scan for annotated DSF configuration classes | — | -| `includeCertDir` | Whether to remove the original certificate directory when cleaning up | `false` | +| Parameter | Description | Default | +| -------------------------- | --------------------------------------------------------------------- | ---------------------------------------------| +| `dsf.configDocPackages` | Package list to scan for annotated DSF configuration classes | — | +| `dsf.certFolder` | Root folder containing certificate resources | `${project.basedir}/src/main/resources/cert` | +| `dsf.includeCertDir` | Whether to remove the original certificate directory when cleaning up | `false` | +| `dsf.includeKeyDir` | Whether to remove the original key pair directory when cleaning up | `false` | Refer to [Plugin Details](plugin-info.html) for the complete parameter list. @@ -324,7 +359,7 @@ This plugin is used by various DSF components, including: * **dsf-bpe-test-plugin-v1/v2** * **dsf-fhir-validation** -It can be included your process plugins to build documentation and help using your dev setup. +It can be included in process plugins repositories to build documentation and help create DSF dev setups. --- diff --git a/pom.xml b/pom.xml index 0ab5b8de4..921eb32cb 100755 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 1.84 3.8.0 5.2.1 - 5.2.1 + 6.1.0 DSF Parent POM