From 3749a060b3a84444956b2256ddb3db846d1a6ae4 Mon Sep 17 00:00:00 2001 From: Loes Immens Date: Wed, 24 Jan 2024 11:45:25 +0100 Subject: [PATCH] FDP-1737: kafka message signing from lsm added to gxf-java-utilities Signed-off-by: Loes Immens --- kafka-message-signing/build.gradle.kts | 12 + .../kafka/message/signing/MessageSigner.java | 459 ++++++++++++++++++ .../signing/UncheckedSecurityException.java | 36 ++ .../wrapper/SignableMessageWrapper.java | 23 + .../message/signing/MessageSignerTest.java | 206 ++++++++ .../src/test/resources/rsa-private.pem | 25 + .../src/test/resources/rsa-public.pem | 9 + 7 files changed, 770 insertions(+) create mode 100644 kafka-message-signing/build.gradle.kts create mode 100644 kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/MessageSigner.java create mode 100644 kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/UncheckedSecurityException.java create mode 100644 kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/wrapper/SignableMessageWrapper.java create mode 100644 kafka-message-signing/src/test/java/com/alliander/osgp/kafka/message/signing/MessageSignerTest.java create mode 100644 kafka-message-signing/src/test/resources/rsa-private.pem create mode 100644 kafka-message-signing/src/test/resources/rsa-public.pem diff --git a/kafka-message-signing/build.gradle.kts b/kafka-message-signing/build.gradle.kts new file mode 100644 index 0000000..3a1249e --- /dev/null +++ b/kafka-message-signing/build.gradle.kts @@ -0,0 +1,12 @@ + +dependencies { + implementation("org.springframework:spring-context") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.assertj:assertj-core") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/MessageSigner.java b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/MessageSigner.java new file mode 100644 index 0000000..7fa6d67 --- /dev/null +++ b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/MessageSigner.java @@ -0,0 +1,459 @@ +/* + * Copyright 2022 Alliander N.V. + */ + +package com.alliander.osgp.kafka.message.signing; + +import com.alliander.osgp.kafka.message.wrapper.SignableMessageWrapper; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +public class MessageSigner { + public static final String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withRSA"; + public static final String DEFAULT_SIGNATURE_PROVIDER = "SunRsaSign"; + public static final String DEFAULT_SIGNATURE_KEY_ALGORITHM = "RSA"; + public static final int DEFAULT_SIGNATURE_KEY_SIZE = 2048; + + // Two magic bytes (0xC3, 0x01) followed by an 8-byte fingerprint + public static final int AVRO_HEADER_LENGTH = 10; + + private final boolean signingEnabled; + + private boolean stripHeaders; + + private String signatureAlgorithm; + private String signatureProvider; + private String signatureKeyAlgorithm; + private int signatureKeySize; + + private Signature signingSignature; + private Signature verificationSignature; + + private PrivateKey signingKey; + private PublicKey verificationKey; + + private MessageSigner(final Builder builder) { + this.signingEnabled = builder.signingEnabled; + if (!this.signingEnabled) { + return; + } + this.stripHeaders = builder.stripHeaders; + this.signatureAlgorithm = builder.signatureAlgorithm; + this.signatureKeyAlgorithm = builder.signatureKeyAlgorithm; + this.signatureKeySize = builder.signatureKeySize; + if (builder.signingKey == null && builder.verificationKey == null) { + throw new IllegalArgumentException( + "A signing key (PrivateKey) or verification key (PublicKey) must be provided"); + } + this.signingKey = builder.signingKey; + this.verificationKey = builder.verificationKey; + this.signingSignature = + signatureInstance( + builder.signatureAlgorithm, builder.signatureProvider, builder.signingKey); + this.verificationSignature = + signatureInstance( + builder.signatureAlgorithm, builder.signatureProvider, builder.verificationKey); + if (builder.signatureProvider != null) { + this.signatureProvider = builder.signatureProvider; + } else if (this.signingSignature != null) { + this.signatureProvider = this.signingSignature.getProvider().getName(); + } else if (this.verificationSignature != null) { + this.signatureProvider = this.verificationSignature.getProvider().getName(); + } else { + // Should not happen, set to null and ignore. + this.signatureProvider = null; + } + } + + public boolean canSignMessages() { + return this.signingEnabled && this.signingSignature != null; + } + + /** + * Signs the provided {@code message}, overwriting an existing signature, if a non-null value is + * already set. + * + * @param message the message to be signed + * @throws IllegalStateException if this message signer has a public key for signature + * verification, but does not have the private key needed for signing messages. + * @throws UncheckedIOException if determining the bytes for the message throws an IOException. + * @throws UncheckedSecurityException if the signing process throws a SignatureException. + */ + public void sign(final SignableMessageWrapper message) { + if (this.signingEnabled) { + final byte[] signatureBytes = this.signature(message); + message.setSignature(ByteBuffer.wrap(signatureBytes)); + } + } + + /** + * Determines the signature for the given {@code message}. + * + *

The value for the signature in the message will be set to {@code null} to properly determine + * the signature, but is restored to its original value before this method returns. + * + * @param message the message to be signed + * @return the signature for the message + * @throws IllegalStateException if this message signer has a public key for signature + * verification, but does not have the private key needed for signing messages. + * @throws UncheckedIOException if determining the bytes for the message throws an IOException. + * @throws UncheckedSecurityException if the signing process throws a SignatureException. + */ + public byte[] signature(final SignableMessageWrapper message) { + if (!this.canSignMessages()) { + throw new IllegalStateException( + "This MessageSigner is not configured for signing, it can only be used for verification"); + } + final ByteBuffer oldSignature = message.getSignature(); + try { + message.setSignature(null); + synchronized (this.signingSignature) { + final byte[] messageBytes; + if (this.stripHeaders) { + messageBytes = this.stripHeaders(this.toByteBuffer(message)); + } else { + messageBytes = this.toByteBuffer(message).array(); + } + this.signingSignature.update(messageBytes); + return this.signingSignature.sign(); + } + } catch (final SignatureException e) { + throw new UncheckedSecurityException("Unable to sign message", e); + } finally { + message.setSignature(oldSignature); + } + } + + public boolean canVerifyMessageSignatures() { + return this.signingEnabled && this.verificationSignature != null; + } + + /** + * Verifies the signature of the provided {@code message}. + * + * @param message the message to be verified + * @return {@code true} if the signature of the given {@code message} was verified; {@code false} + * if not. + * @throws IllegalStateException if this message signer has a private key needed for signing + * messages, but does not have the public key for signature verification. + * @throws UncheckedIOException if determining the bytes for the message throws an IOException. + * @throws UncheckedSecurityException if the signature verification process throws a + * SignatureException. + */ + public boolean verify(final SignableMessageWrapper message) { + if (!this.canVerifyMessageSignatures()) { + throw new IllegalStateException( + "This MessageSigner is not configured for verification, it can only be used for signing"); + } + + final ByteBuffer messageSignature = message.getSignature(); + if (messageSignature == null) { + return false; + } + messageSignature.mark(); + final byte[] signatureBytes = new byte[messageSignature.remaining()]; + messageSignature.get(signatureBytes); + + try { + message.setSignature(null); + synchronized (this.verificationSignature) { + final byte[] messageBytes; + if (this.stripHeaders) { + messageBytes = this.stripHeaders(this.toByteBuffer(message)); + } else { + messageBytes = this.toByteBuffer(message).array(); + } + this.verificationSignature.update(messageBytes); + return this.verificationSignature.verify(signatureBytes); + } + } catch (final SignatureException e) { + throw new UncheckedSecurityException("Unable to verify message signature", e); + } finally { + messageSignature.reset(); + message.setSignature(messageSignature); + } + } + + private boolean hasAvroHeader(final byte[] bytes) { + return bytes.length >= AVRO_HEADER_LENGTH + && (bytes[0] & 0xFF) == 0xC3 + && (bytes[1] & 0xFF) == 0x01; + } + + private byte[] stripHeaders(final ByteBuffer byteBuffer) { + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + if (this.hasAvroHeader(bytes)) { + return Arrays.copyOfRange(bytes, AVRO_HEADER_LENGTH, bytes.length); + } + return bytes; + } + + private ByteBuffer toByteBuffer(final SignableMessageWrapper message) { + try { + return message.toByteBuffer(); + } catch (final IOException e) { + throw new UncheckedIOException("Unable to determine ByteBuffer for Message", e); + } + } + + public boolean isSigningEnabled() { + return this.signingEnabled; + } + + public Optional signingKey() { + return Optional.ofNullable(this.signingKey); + } + + public Optional signingKeyPem() { + return this.signingKey().map(key -> this.keyAsMem(key, key.getAlgorithm() + " PRIVATE KEY")); + } + + public Optional verificationKey() { + return Optional.ofNullable(this.verificationKey); + } + + public Optional verificationKeyPem() { + return this.verificationKey() + .map(key -> this.keyAsMem(key, key.getAlgorithm() + " PUBLIC KEY")); + } + + private String keyAsMem(final Key key, final String label) { + final StringBuilder sb = new StringBuilder(); + sb.append("-----BEGIN ").append(label).append("-----").append("\r\n"); + sb.append(Base64.getMimeEncoder().encodeToString(key.getEncoded())).append("\r\n"); + sb.append("-----END ").append(label).append("-----").append("\r\n"); + return sb.toString(); + } + + @Override + public String toString() { + return String.format( + "MessageSigner[algorithm=\"%s\"-\"%s\", provider=\"%s\", keySize=%d, sign=%b, verify=%b]", + this.signatureAlgorithm, + this.signatureKeyAlgorithm, + this.signatureProvider, + this.signatureKeySize, + this.canSignMessages(), + this.canVerifyMessageSignatures()); + } + + public String descriptionWithKeys() { + final StringBuilder sb = new StringBuilder(this.toString()); + this.signingKeyPem().ifPresent(key -> sb.append(System.lineSeparator()).append(key)); + this.verificationKeyPem().ifPresent(key -> sb.append(System.lineSeparator()).append(key)); + return sb.toString(); + } + + private static Signature signatureInstance( + final String signatureAlgorithm, + final String signatureProvider, + final PrivateKey signingKey) { + + if (signingKey == null) { + return null; + } + + final Signature signature = signatureInstance(signatureAlgorithm, signatureProvider); + try { + signature.initSign(signingKey); + } catch (final InvalidKeyException e) { + throw new UncheckedSecurityException(e); + } + return signature; + } + + private static Signature signatureInstance( + final String signatureAlgorithm, + final String signatureProvider, + final PublicKey verificationKey) { + + if (verificationKey == null) { + return null; + } + + final Signature signature = signatureInstance(signatureAlgorithm, signatureProvider); + try { + signature.initVerify(verificationKey); + } catch (final InvalidKeyException e) { + throw new UncheckedSecurityException(e); + } + return signature; + } + + private static Signature signatureInstance( + final String signatureAlgorithm, final String signatureProvider) { + try { + if (signatureProvider == null) { + return Signature.getInstance(signatureAlgorithm); + } + return Signature.getInstance(signatureAlgorithm, signatureProvider); + } catch (final GeneralSecurityException e) { + throw new UncheckedSecurityException("Unable to create Signature for Avro Messages", e); + } + } + + public static KeyPair generateKeyPair( + final String signatureKeyAlgorithm, + final String signatureProvider, + final int signatureKeySize) { + final KeyPairGenerator keyPairGenerator; + try { + if (signatureProvider == null) { + keyPairGenerator = KeyPairGenerator.getInstance(signatureKeyAlgorithm); + } else { + keyPairGenerator = KeyPairGenerator.getInstance(signatureKeyAlgorithm, signatureProvider); + } + } catch (final GeneralSecurityException e) { + throw new UncheckedSecurityException(e); + } + keyPairGenerator.initialize(signatureKeySize); + return keyPairGenerator.generateKeyPair(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + + private static final Pattern PEM_REMOVAL_PATTERN = + Pattern.compile("-----(?:BEGIN|END) .*?-----|\\r|\\n"); + + private boolean signingEnabled; + + private boolean stripHeaders; + + private String signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM; + private String signatureProvider = DEFAULT_SIGNATURE_PROVIDER; + private String signatureKeyAlgorithm = DEFAULT_SIGNATURE_KEY_ALGORITHM; + private int signatureKeySize = DEFAULT_SIGNATURE_KEY_SIZE; + + private PrivateKey signingKey = null; + private PublicKey verificationKey = null; + + public Builder signingEnabled(final boolean signingEnabled) { + this.signingEnabled = signingEnabled; + return this; + } + + public Builder stripHeaders(final boolean stripHeaders) { + this.stripHeaders = stripHeaders; + return this; + } + + public Builder signatureAlgorithm(final String signatureAlgorithm) { + this.signatureAlgorithm = Objects.requireNonNull(signatureAlgorithm); + return this; + } + + public Builder signatureProvider(final String signatureProvider) { + this.signatureProvider = signatureProvider; + return this; + } + + public Builder signatureKeyAlgorithm(final String signatureKeyAlgorithm) { + this.signatureKeyAlgorithm = Objects.requireNonNull(signatureKeyAlgorithm); + return this; + } + + public Builder signatureKeySize(final int signatureKeySize) { + this.signatureKeySize = signatureKeySize; + return this; + } + + public Builder signingKey(final PrivateKey signingKey) { + this.signingKey = signingKey; + return this; + } + + public Builder signingKey(final String signingKeyPem) { + if (signingKeyPem == null) { + this.signingKey = null; + return this; + } + final String base64 = PEM_REMOVAL_PATTERN.matcher(signingKeyPem).replaceAll(""); + final byte[] bytes = Base64.getDecoder().decode(base64); + return this.signingKey(bytes); + } + + public Builder signingKey(final byte[] signingKeyBytes) { + if (signingKeyBytes == null) { + this.signingKey = null; + return this; + } + final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(signingKeyBytes); + try { + this.signingKey = + KeyFactory.getInstance(this.signatureKeyAlgorithm).generatePrivate(keySpec); + } catch (final GeneralSecurityException e) { + throw new UncheckedSecurityException(e); + } + return this; + } + + public Builder verificationKey(final PublicKey verificationKey) { + this.verificationKey = verificationKey; + return this; + } + + public Builder verificationKey(final String verificationKeyPem) { + if (verificationKeyPem == null) { + this.verificationKey = null; + return this; + } + final String base64 = PEM_REMOVAL_PATTERN.matcher(verificationKeyPem).replaceAll(""); + final byte[] bytes = Base64.getDecoder().decode(base64); + return this.verificationKey(bytes); + } + + public Builder verificationKey(final byte[] verificationKeyBytes) { + if (verificationKeyBytes == null) { + this.verificationKey = null; + return this; + } + final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(verificationKeyBytes); + try { + this.verificationKey = + KeyFactory.getInstance(this.signatureKeyAlgorithm).generatePublic(keySpec); + } catch (final GeneralSecurityException e) { + throw new UncheckedSecurityException(e); + } + return this; + } + + public Builder keyPair(final KeyPair keyPair) { + this.signingKey = keyPair.getPrivate(); + this.verificationKey = keyPair.getPublic(); + return this; + } + + public Builder generateKeyPair() { + return this.keyPair( + MessageSigner.generateKeyPair( + this.signatureKeyAlgorithm, this.signatureProvider, this.signatureKeySize)); + } + + public MessageSigner build() { + return new MessageSigner(this); + } + } +} diff --git a/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/UncheckedSecurityException.java b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/UncheckedSecurityException.java new file mode 100644 index 0000000..838377c --- /dev/null +++ b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/signing/UncheckedSecurityException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Alliander N.V. + */ + +package com.alliander.osgp.kafka.message.signing; + +import java.security.GeneralSecurityException; +import java.util.Objects; + +/** Wraps a {@link GeneralSecurityException} with an unchecked exception. */ +public class UncheckedSecurityException extends RuntimeException { + + private static final long serialVersionUID = 5152038114753546167L; + + /** + * @throws NullPointerException if the cause is {@code null} + */ + public UncheckedSecurityException(final String message, final GeneralSecurityException cause) { + super(message, Objects.requireNonNull(cause)); + } + + /** + * @throws NullPointerException if the cause is {@code null} + */ + public UncheckedSecurityException(final GeneralSecurityException cause) { + super(Objects.requireNonNull(cause)); + } + + /** + * @return the {@code GeneralSecurityException} wrapped by this exception. + */ + @Override + public synchronized GeneralSecurityException getCause() { + return (GeneralSecurityException) super.getCause(); + } +} diff --git a/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/wrapper/SignableMessageWrapper.java b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/wrapper/SignableMessageWrapper.java new file mode 100644 index 0000000..464cfed --- /dev/null +++ b/kafka-message-signing/src/main/java/com/alliander/osgp/kafka/message/wrapper/SignableMessageWrapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Alliander N.V. + */ + +package com.alliander.osgp.kafka.message.wrapper; + +public abstract class SignableMessageWrapper { + protected final T message; + + protected SignableMessageWrapper(final T message) { + this.message = message; + } + + public T getMessage() { + return this.message; + } + + public abstract java.nio.ByteBuffer toByteBuffer() throws java.io.IOException; + + public abstract java.nio.ByteBuffer getSignature(); + + public abstract void setSignature(java.nio.ByteBuffer signature); +} diff --git a/kafka-message-signing/src/test/java/com/alliander/osgp/kafka/message/signing/MessageSignerTest.java b/kafka-message-signing/src/test/java/com/alliander/osgp/kafka/message/signing/MessageSignerTest.java new file mode 100644 index 0000000..a2fa045 --- /dev/null +++ b/kafka-message-signing/src/test/java/com/alliander/osgp/kafka/message/signing/MessageSignerTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2022 Alliander N.V. + */ + +package com.alliander.osgp.kafka.message.signing; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.alliander.osgp.kafka.message.wrapper.SignableMessageWrapper; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.Random; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class MessageSignerTest { + + private static final boolean SIGNING_ENABLED = true; + + private static final boolean STRIP_HEADERS = true; + + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; + private static final String SIGNATURE_PROVIDER = "SunRsaSign"; + private static final String SIGNATURE_KEY_ALGORITHM = "RSA"; + private static final int SIGNATURE_KEY_SIZE = 2048; + private static final int SIGNATURE_KEY_SIZE_BYTES = SIGNATURE_KEY_SIZE / 8; + + private static final KeyPair KEY_PAIR = + MessageSigner.generateKeyPair( + SIGNATURE_KEY_ALGORITHM, SIGNATURE_PROVIDER, SIGNATURE_KEY_SIZE); + + private static final Random RANDOM = new SecureRandom(); + + private final MessageSigner messageSigner = + MessageSigner.newBuilder() + .signingEnabled(SIGNING_ENABLED) + .stripHeaders(STRIP_HEADERS) + .signatureAlgorithm(SIGNATURE_ALGORITHM) + .signatureProvider(SIGNATURE_PROVIDER) + .signatureKeyAlgorithm(SIGNATURE_KEY_ALGORITHM) + .signatureKeySize(SIGNATURE_KEY_SIZE) + .keyPair(KEY_PAIR) + .build(); + + @Test + void signsMessageWithoutSignature() { + final SignableMessageWrapper messageWrapper = this.messageWrapper(); + + this.messageSigner.sign(messageWrapper); + + assertThat(messageWrapper.getSignature()).isNotNull(); + } + + @Test + void signsMessageReplacingSignature() { + final byte[] randomSignature = this.randomSignature(); + final TestableWrapper messageWrapper = this.messageWrapper(); + + this.messageSigner.sign(messageWrapper); + + final byte[] actualSignature = this.bytes(messageWrapper.getSignature()); + assertThat(actualSignature).isNotNull().isNotEqualTo(randomSignature); + } + + @Test + void verifiesMessagesWithValidSignature() { + final TestableWrapper message = this.properlySignedMessage(); + + final boolean signatureWasVerified = this.messageSigner.verify(message); + + assertThat(signatureWasVerified).isTrue(); + } + + @Test + void doesNotVerifyMessagesWithoutSignature() { + final TestableWrapper messageWrapper = this.messageWrapper(); + + final boolean signatureWasVerified = this.messageSigner.verify(messageWrapper); + + assertThat(signatureWasVerified).isFalse(); + } + + @Test + void doesNotVerifyMessagesWithIncorrectSignature() { + final byte[] randomSignature = this.randomSignature(); + final TestableWrapper messageWrapper = this.messageWrapper(randomSignature); + + final boolean signatureWasVerified = this.messageSigner.verify(messageWrapper); + + assertThat(signatureWasVerified).isFalse(); + } + + @Test + void verifiesMessagesPreservingTheSignatureAndItsProperties() { + final TestableWrapper message = this.properlySignedMessage(); + final ByteBuffer originalSignature = message.getSignature(); + final int originalPosition = originalSignature.position(); + final int originalLimit = originalSignature.limit(); + final int originalRemaining = originalSignature.remaining(); + + this.messageSigner.verify(message); + + final ByteBuffer verifiedSignature = message.getSignature(); + assertThat(verifiedSignature).isEqualTo(originalSignature); + assertThat(verifiedSignature.position()).isEqualTo(originalPosition); + assertThat(verifiedSignature.limit()).isEqualTo(originalLimit); + assertThat(verifiedSignature.remaining()).isEqualTo(originalRemaining); + } + + private String fromPemResource(final String name) { + return new BufferedReader( + new InputStreamReader( + this.getClass().getResourceAsStream(name), StandardCharsets.ISO_8859_1)) + .lines() + .collect(Collectors.joining(System.lineSeparator())); + } + + @Test + void worksWithKeysFromPemEncodedResources() { + + final MessageSigner messageSignerWithKeysFromResources = + MessageSigner.newBuilder() + .signingEnabled(SIGNING_ENABLED) + .signatureAlgorithm(SIGNATURE_ALGORITHM) + .signatureProvider(SIGNATURE_PROVIDER) + .signatureKeyAlgorithm(SIGNATURE_KEY_ALGORITHM) + .signatureKeySize(SIGNATURE_KEY_SIZE) + .signingKey(this.fromPemResource("/rsa-private.pem")) + .verificationKey(this.fromPemResource("/rsa-public.pem")) + .build(); + + final TestableWrapper messageWrapper = this.messageWrapper(); + messageSignerWithKeysFromResources.sign(messageWrapper); + final boolean signatureWasVerified = messageSignerWithKeysFromResources.verify(messageWrapper); + + assertThat(signatureWasVerified).isTrue(); + } + + @Test + void signingCanBeDisabled() { + final MessageSigner messageSignerSigningDisabled = + MessageSigner.newBuilder().signingEnabled(!SIGNING_ENABLED).build(); + + assertThat(messageSignerSigningDisabled.canSignMessages()).isFalse(); + assertThat(messageSignerSigningDisabled.canVerifyMessageSignatures()).isFalse(); + } + + private TestableWrapper messageWrapper() { + return new TestableWrapper(); + } + + private TestableWrapper messageWrapper(final byte[] signature) { + final TestableWrapper testableWrapper = new TestableWrapper(); + testableWrapper.setSignature(ByteBuffer.wrap(signature)); + return testableWrapper; + } + + private TestableWrapper properlySignedMessage() { + final TestableWrapper messageWrapper = this.messageWrapper(); + this.messageSigner.sign(messageWrapper); + return messageWrapper; + } + + private byte[] randomSignature() { + final byte[] signature = new byte[SIGNATURE_KEY_SIZE_BYTES]; + RANDOM.nextBytes(signature); + return signature; + } + + private byte[] bytes(final ByteBuffer byteBuffer) { + if (byteBuffer == null) { + return null; + } + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return bytes; + } + + private static class TestableWrapper extends SignableMessageWrapper { + private ByteBuffer signature; + + protected TestableWrapper() { + super("Some test message"); + } + + @Override + public ByteBuffer toByteBuffer() throws IOException { + return ByteBuffer.wrap(this.message.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public ByteBuffer getSignature() { + return this.signature; + } + + @Override + public void setSignature(final ByteBuffer signature) { + this.signature = signature; + } + } +} diff --git a/kafka-message-signing/src/test/resources/rsa-private.pem b/kafka-message-signing/src/test/resources/rsa-private.pem new file mode 100644 index 0000000..1f84db9 --- /dev/null +++ b/kafka-message-signing/src/test/resources/rsa-private.pem @@ -0,0 +1,25 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2nu5a6Ys3QHhGLW8UjpBxxpPw +Icuvogm8RBL7q0nZ7YspaZhBRpEb1ygUB9EXNsNcIugf4P7EzgIzozd4oFwCOI/bbPm7oPBAJgXf +l3jrpknxI/TcRJXBNqDopXelRgaK1R8hG5BMf6NSxzUBMQEodNy1W6lL6umtGmLUY09W/kdPue8/ +G7cj0/ftp5gZSA48clOyh//h9x9CRz/4fQeOFpnTCpl+6JYQ7u+FHsCaRfS6ZSymJWjHmjI0YSsj +ykeJ7F17smwlOamGTjRuGePvG5VQgd8MWkvCgYp5PgfsPDgGTMSsnztPum5cfbOuLJV7M1vAjmqc +XlmPkaOLxw3DAgMBAAECggEBAIvl0pjIgkKQW9L+6TJpSFQwmJIDgcMJMcYMrDIpdMjCxbGy19Vh +lrYqK+S0XEQZSq1hfEs3lFP1sRAXv93jkriM1f91SxamYoXx2tv/cL2tRMW7EtBOph4+mCPA5pgw +vcBLJa66K9++g8JdIsjH3qg8Zft0vYuP6PUX2o/ziAsOLqTmlTljRAM86pST9wo4B1CAJpSIUctH +eavbwp8igGgOl5suG8bwiTYKMeY4660nM3ywyl1fLP5k1rPwBOkgoicT6Ky8exjxFduOz0ct77Y+ +sTj7pI+XDFzn2dM2ZlCegURb1sIkQYpMj1Ik9tz0FUvgTd1VLhBeoh5iDMwvHckCgYEA/pGVNmnb +TotR6APaQVKdgPSGk7Q0njWHRSIJBA7ZkjsVTNE39ZnaXdxYWFxIMKPaFQ24N55IH4LGg0tZdF3z +ti615UBN5anPi020LWAgQCDaFVi3LzeYO2g6wArxlDSuKF6Ww0Ch6TtmM4RY6ZEZPmwdjDdY6/sU +ylCNF6j5T20CgYEAt6XJ/POzbUJDKSGBxbifeqF/2P71YMnTpD374dZzMp1zrNnRGGyRRUV+qSWh +uJD4bpzkexxNzQq1UVFK76McTTQKjWuo74QjXTJcsP9LNJ0qMdNzgT6ctUnFCRVFvSoNB3d3x90F +ZORpjmOtz/PUSduKXfnVGW+KyOBD+m2SI+8CgYEAinMtHsHlt0sISdJGkn5XEPpscspwT5c3MX84 +Pg/BfslJZVToRVfermuXVL8jt+h1RDwI857PBOxAAMorJaGvWWcAIGWfuAdpzA5/rqn4AEidszxj +rHdlAPJH+Yg6KOuZyHThM+Hj7RAUHnKdVLJIc22jiE3Vu8n7Xaj/g12v8eUCgYA9SLgFD5Y6ybf7 +y9CwmJGvrKErWrmr2O4liwG5NYUvyNdHQVDDo8c+pJhF/ebf3pDo6LZeVu2nlQE457XoDjhtkwZK +dzji5OegPCQudKM2JZRlGDkdUjWdUcbM5ypkm9nJOhbgvWMFbivDdoQUNzwKgZbFEZAJcu2PZzeI +JHR2RQKBgBNHh7bmzlaxDLbM7lFQEOpxhdA9GCykYOw6rVJAE8Y7yVjY8+haQuoV8/jrtwPdmdBk +S6jWD25tkhGHbaCnhK+wU7++H1QMEpLhQyhmgFDBobfJt1GsbOr2b91tF11N5FKq+IWSyWSU+zS8 +7Vt5sL3neHodJNoZ3bTL2rMjyPml +-----END RSA PRIVATE KEY----- + diff --git a/kafka-message-signing/src/test/resources/rsa-public.pem b/kafka-message-signing/src/test/resources/rsa-public.pem new file mode 100644 index 0000000..fd57d14 --- /dev/null +++ b/kafka-message-signing/src/test/resources/rsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtp7uWumLN0B4Ri1vFI6QccaT8CHLr6IJ +vEQS+6tJ2e2LKWmYQUaRG9coFAfRFzbDXCLoH+D+xM4CM6M3eKBcAjiP22z5u6DwQCYF35d466ZJ +8SP03ESVwTag6KV3pUYGitUfIRuQTH+jUsc1ATEBKHTctVupS+rprRpi1GNPVv5HT7nvPxu3I9P3 +7aeYGUgOPHJTsof/4fcfQkc/+H0HjhaZ0wqZfuiWEO7vhR7AmkX0umUspiVox5oyNGErI8pHiexd +e7JsJTmphk40bhnj7xuVUIHfDFpLwoGKeT4H7Dw4BkzErJ87T7puXH2zriyVezNbwI5qnF5Zj5Gj +i8cNwwIDAQAB +-----END RSA PUBLIC KEY----- +