From 536b4bb44a176b66690c000342f07dbec3c305ad Mon Sep 17 00:00:00 2001 From: Martin Vogel <martin.vogel@sinc.de> Date: Mon, 19 Jun 2023 17:27:35 +0200 Subject: [PATCH] feature(#664): add cert generator for writing files, add tests --- .../fitko/fitconnect/cli/util/CertTool.java | 113 ++++++++++++++ .../fitconnect/cli/util/JWKGenerator.java | 113 +++++++++----- .../fitko/fitconnect/cli/util/JWKPair.java | 11 ++ .../fitconnect/cli/util/JWKGeneratorTest.java | 143 ++++++++++++++++++ 4 files changed, 340 insertions(+), 40 deletions(-) create mode 100644 cli/src/main/java/dev/fitko/fitconnect/cli/util/CertTool.java create mode 100644 cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKPair.java create mode 100644 cli/src/test/java/dev/fitko/fitconnect/cli/util/JWKGeneratorTest.java diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/util/CertTool.java b/cli/src/main/java/dev/fitko/fitconnect/cli/util/CertTool.java new file mode 100644 index 000000000..0f8dfbf91 --- /dev/null +++ b/cli/src/main/java/dev/fitko/fitconnect/cli/util/CertTool.java @@ -0,0 +1,113 @@ +package dev.fitko.fitconnect.cli.util; + +import com.nimbusds.jose.jwk.JWK; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class CertTool { + + public static final String OUTPUT_DIR = "outputDir"; + private final Map<String, String> cmdArgs = new HashMap<>(); + + final JWKGenerator jwkGenerator; + + public static void main(final String[] args) { + + new CertTool(new JWKGenerator()).run(args); + + /*final Map<String, Object> data = new LinkedHashMap<>(); + data.put("senderConfig", Map.of("clientSecret", "", "clientId", "")); + data.put("subscriberConfig", Map.of("clientSecret", "", "clientId", "", "privateDecryptionKeyPaths", List.of(), "privateSigningKeyPath", "")); + data.put("activeEnvironment", DefaultEnvironments.TEST.getEnvironmentName().getName()); + + final DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + + final Yaml yaml = new Yaml(options); + final StringWriter writer = new StringWriter(); + + yaml.dump(data, writer); + writer.flush(); + + System.out.println(writer);*/ + } + + public CertTool(final JWKGenerator jwkGenerator){ + this.jwkGenerator = jwkGenerator; + } + + private void run(final String[] args) { + + mapArgs(args); + + final String dir = getDir(); + + final JWKPair encryptionKeyPair = jwkGenerator.generateEncryptionKeyPair(4096); + final JWKPair signatureKeyPair = jwkGenerator.generateSignatureKeyPair(4096); + + writeFile(Path.of(dir, "publicKey_encryption.json"), encryptionKeyPair.getPublicKey()); + writeFile(Path.of(dir, "privateKey_decryption.json"), encryptionKeyPair.getPrivateKey()); + + writeFile(Path.of(dir, "publicKey_signature_verification.json"), signatureKeyPair.getPublicKey()); + writeFile(Path.of(dir, "privateKey_signing.json"), signatureKeyPair.getPrivateKey()); + + } + + private String getDir() { + if (cmdArgs.containsKey(OUTPUT_DIR)) { + return Path.of(cmdArgs.get(OUTPUT_DIR)).toAbsolutePath().toString(); + } else { + return createTempDir(); + } + } + + private static String createTempDir() { + try { + return Files.createTempDirectory("testJWKs").toFile().getAbsolutePath(); + } catch (final IOException e) { + System.out.println(e); + System.exit(0); + } + return null; + } + + private static void writeFile(final Path path, final JWK jwk) { + try { + Files.write(path, toBytes(jwk)); + System.out.println("Wrote " + path.getFileName() + " to " + path); + } catch (final IOException e) { + System.out.println(e); + System.exit(0); + } + } + + private void mapArgs(final String[] args) { + if (args.length > 1) { + System.out.println("Invalid number of arguments \n Use: -o=OutputDirectory"); + System.exit(0); + } else if (args.length == 1) { + final String[] s = args[0].split("="); + if(s.length != 2){ + System.out.println("Invalid option " + args[0] + "\n Use: -o=OutputDirectory"); + System.exit(0); + } + if (Objects.equals(s[0], "-o")) { + cmdArgs.put(OUTPUT_DIR, s[1]); + } else { + System.out.println("Invalid option " + s[0] + "\n Use: -o=OutputDirectory"); + System.exit(0); + } + } + } + + private static byte[] toBytes(final JWK jwk) { + return jwk.toJSONString().getBytes(StandardCharsets.UTF_8); + } +} diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKGenerator.java b/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKGenerator.java index a2ebe76e8..df09d5c5e 100644 --- a/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKGenerator.java +++ b/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKGenerator.java @@ -1,11 +1,12 @@ package dev.fitko.fitconnect.cli.util; +import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWEAlgorithm; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -15,6 +16,7 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.math.BigInteger; +import java.security.InvalidParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; @@ -23,6 +25,7 @@ import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; import java.time.Duration; import java.time.Instant; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; @@ -30,66 +33,96 @@ import java.util.UUID; /** * JWK Test Key Generator. - * - * Generates public and private keys for encryption and signing + * <p> + * Generates public and private keys for encryption and signing with. */ public class JWKGenerator { - public static void main(final String[] args) throws NoSuchAlgorithmException, JOSEException, CertificateException, OperatorCreationException { + /** + * Generate a set of public encryption key and private decryption key. + * + * @param keySize size of the RSA key in bits + * @return JWKPair of public and private key + */ + public JWKPair generateEncryptionKeyPair(final int keySize) { + final KeyPair keyPair = getKeyPair(keySize); + final List<Base64> x509CertChain = getX509CertChain(keyPair); - final JWKSet encryptionKeySet = getEncryptionKeySet(); + final String keyId = UUID.randomUUID().toString(); + final JWEAlgorithm encryptionAlgorithm = JWEAlgorithm.RSA_OAEP_256; - } + final RSAKey publicEncryptionKey = buildRSAKey(keyId, keyPair, KeyOperation.WRAP_KEY, encryptionAlgorithm, x509CertChain); + final RSAKey privateDecryptionKey = buildRSAKey(keyId, keyPair, KeyOperation.UNWRAP_KEY, encryptionAlgorithm); - private static JWKSet getEncryptionKeySet() throws NoSuchAlgorithmException, OperatorCreationException, CertificateException, JOSEException { + return new JWKPair(publicEncryptionKey.toPublicJWK(), privateDecryptionKey); + } - final KeyPair keyPair = getKeyPair(); - final X509Certificate cert = getX509Certificate(keyPair); + /** + * Generate a set of public signature verification key and private signature key. + * + * @param keySize size of the RSA key in bits + * @return JWKPair of signature and verification key + */ + public JWKPair generateSignatureKeyPair(final int keySize) { + final KeyPair keyPair = getKeyPair(keySize); + final List<Base64> x509CertChain = getX509CertChain(keyPair); final String keyId = UUID.randomUUID().toString(); + final JWSAlgorithm signingAlgorithm = JWSAlgorithm.PS512; - final JWK publicKey = new RSAKey.Builder((RSAPublicKey)keyPair.getPublic()) - .privateKey(keyPair.getPrivate()) - .keyID(keyId) - .keyOperations(Set.of(KeyOperation.WRAP_KEY)) - .algorithm(JWEAlgorithm.RSA_OAEP_256) - .x509CertChain(RSAKey.parse(cert).getX509CertChain()) - .build(); + final RSAKey privateSignatureKey = buildRSAKey(keyId, keyPair, KeyOperation.SIGN, signingAlgorithm); + final RSAKey publicSignatureVerificationKey = buildRSAKey(keyId, keyPair, KeyOperation.VERIFY, signingAlgorithm, x509CertChain); + + return new JWKPair(publicSignatureVerificationKey.toPublicJWK(), privateSignatureKey); + } + + private static List<Base64> getX509CertChain(final KeyPair keyPair) { + final X509Certificate cert = getX509Certificate(keyPair); + try { + return RSAKey.parse(cert).getX509CertChain(); + } catch (final JOSEException e) { + throw new RuntimeException(e); + } + } - final JWK privateKey = new RSAKey.Builder((RSAPublicKey)keyPair.getPublic()) + private static RSAKey buildRSAKey(final String keyId, final KeyPair keyPair, final KeyOperation keyOperation, final Algorithm algorithm, final List<Base64> x509CertChain) { + final RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) .privateKey(keyPair.getPrivate()) .keyID(keyId) - .keyOperations(Set.of(KeyOperation.UNWRAP_KEY)) - .algorithm(JWEAlgorithm.RSA_OAEP_256) - .build(); + .keyOperations(Set.of(keyOperation)) + .algorithm(algorithm); + return x509CertChain.isEmpty() ? builder.build() : builder.x509CertChain(x509CertChain).build(); + } - return new JWKSet(List.of(publicKey, privateKey)); + private static RSAKey buildRSAKey(final String keyId, final KeyPair keyPair, final KeyOperation keyOperation, final Algorithm algorithm) { + return buildRSAKey(keyId, keyPair, keyOperation, algorithm, Collections.emptyList()); } - private static X509Certificate getX509Certificate(final KeyPair keyPair) throws OperatorCreationException, CertificateException { + private static X509Certificate getX509Certificate(final KeyPair keyPair) { final Instant now = Instant.now(); final Date notBefore = Date.from(now); final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10))); - final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512withRSA").build(keyPair.getPrivate()); - final X500Name x500Name = new X500Name("CN=localhost"); - final X500Name x500Subject = new X500Name("C=Test"); - - final X509v3CertificateBuilder certificateBuilder = - new JcaX509v3CertificateBuilder(x500Name, - BigInteger.valueOf(now.toEpochMilli()), - notBefore, - notAfter, - x500Subject, - keyPair.getPublic()); - - return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); + try { + final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA512withRSA").build(keyPair.getPrivate()); + final X500Name x500Name = new X500Name("CN=localhost"); + final X500Name x500Subject = new X500Name("C=Test"); + final BigInteger serialNr = BigInteger.valueOf(now.toEpochMilli()); + final X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(x500Name, serialNr, notBefore, notAfter, x500Subject, keyPair.getPublic()); + return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); + } catch (final CertificateException | OperatorCreationException e) { + throw new RuntimeException(e); + } } - private static KeyPair getKeyPair() throws NoSuchAlgorithmException { - final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(4096); - return keyPairGenerator.generateKeyPair(); + private static KeyPair getKeyPair(final int keySize) { + try { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(keySize); + return keyPairGenerator.generateKeyPair(); + } catch (final NoSuchAlgorithmException | InvalidParameterException e) { + throw new RuntimeException(e); + } } } diff --git a/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKPair.java b/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKPair.java new file mode 100644 index 000000000..6c2f54ee3 --- /dev/null +++ b/cli/src/main/java/dev/fitko/fitconnect/cli/util/JWKPair.java @@ -0,0 +1,11 @@ +package dev.fitko.fitconnect.cli.util; + +import com.nimbusds.jose.jwk.JWK; +import lombok.Value; + +@Value +public class JWKPair { + + JWK publicKey; + JWK privateKey; +} diff --git a/cli/src/test/java/dev/fitko/fitconnect/cli/util/JWKGeneratorTest.java b/cli/src/test/java/dev/fitko/fitconnect/cli/util/JWKGeneratorTest.java new file mode 100644 index 000000000..437ed0fe6 --- /dev/null +++ b/cli/src/test/java/dev/fitko/fitconnect/cli/util/JWKGeneratorTest.java @@ -0,0 +1,143 @@ +package dev.fitko.fitconnect.cli.util; + +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyOperation; +import com.nimbusds.jose.jwk.KeyType; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; + +class JWKGeneratorTest { + + private JWKGenerator underTest; + + @BeforeEach + void setup() { + underTest = new JWKGenerator(); + } + + @Test + void testPublicEncryptionKey() { + + // When + final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(2048); + + final JWK publicKey = encryptionKeyPair.getPublicKey(); + + // Then + assertThat(publicKey, is(notNullValue())); + + + assertThat(publicKey.getKeyID(), is(notNullValue())); + + assertThat(publicKey.getKeyOperations(), hasSize(1)); + assertThat(publicKey.getKeyOperations(), contains(KeyOperation.WRAP_KEY)); + + assertThat(publicKey.getX509CertChain(), hasSize(1)); + + assertThat(publicKey.getKeyType(), is(KeyType.RSA)); + assertThat( publicKey.getAlgorithm(), is(JWEAlgorithm.RSA_OAEP_256)); + } + + @Test + void testPrivateDecryptionKey() { + + // When + final JWKPair encryptionKeyPair = underTest.generateEncryptionKeyPair(2048); + + final JWK privateKey = encryptionKeyPair.getPrivateKey(); + + // Then + assertThat(privateKey, is(CoreMatchers.notNullValue())); + + assertThat(privateKey.getKeyID(), is(notNullValue())); + + assertThat(privateKey.getKeyOperations(), hasSize(1)); + assertThat(privateKey.getKeyOperations(), contains(KeyOperation.UNWRAP_KEY)); + + assertThat(privateKey.getX509CertChain(), is(Matchers.nullValue())); + + assertThat(privateKey.getKeyType(), is(KeyType.RSA)); + assertThat(privateKey.getAlgorithm(), is(JWEAlgorithm.RSA_OAEP_256)); + + final Map<String, Object> keyParams = privateKey.toRSAKey().toJSONObject(); + + assertThat(keyParams.get("d"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("dp"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("dq"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("e"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("n"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("p"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("q"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("qi"), is(CoreMatchers.notNullValue())); + } + + @Test + void testPublicSignatureVerificationKey() { + + // When + final JWKPair signatureKeyPair = underTest.generateSignatureKeyPair(2048); + + final JWK publicKey = signatureKeyPair.getPublicKey(); + + // Then + assertThat(publicKey, is(CoreMatchers.notNullValue())); + + assertThat(publicKey.getKeyID(), is(notNullValue())); + + assertThat(publicKey.getKeyOperations(), hasSize(1)); + assertThat(publicKey.getKeyOperations(), contains(KeyOperation.VERIFY)); + + assertThat(publicKey.getX509CertChain(), hasSize(1)); + + assertThat(publicKey.getKeyType(), is(KeyType.RSA)); + assertThat( publicKey.getAlgorithm(), is(JWSAlgorithm.PS512)); + + } + + @Test + void testPrivateSigningKey() { + + // When + final JWKPair signatureKeyPair = underTest.generateSignatureKeyPair(2048); + + final JWK privateKey = signatureKeyPair.getPrivateKey(); + + // Then + assertThat(privateKey, is(notNullValue())); + + + assertThat(privateKey.getKeyID(), is(notNullValue())); + + assertThat(privateKey.getKeyOperations(), hasSize(1)); + assertThat(privateKey.getKeyOperations(), contains(KeyOperation.SIGN)); + + assertThat(privateKey.getX509CertChain(), is(Matchers.nullValue())); + + assertThat(privateKey.getKeyType(), is(KeyType.RSA)); + assertThat(privateKey.getAlgorithm(), is(JWSAlgorithm.PS512)); + + final Map<String, Object> keyParams = privateKey.toRSAKey().toJSONObject(); + + assertThat(keyParams.get("d"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("dp"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("dq"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("e"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("n"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("p"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("q"), is(CoreMatchers.notNullValue())); + assertThat(keyParams.get("qi"), is(CoreMatchers.notNullValue())); + + } +} \ No newline at end of file -- GitLab