diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java new file mode 100644 index 00000000..54c6df9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import java.security.MessageDigest; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +import javax.annotation.Nonnull; + +public class BcHashContextSigner { + + public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext, + @Nonnull SignatureType signatureType, + @Nonnull PGPSecretKeyRing secretKeys, + @Nonnull SecretKeyRingProtector protector) + throws PGPException { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + List signingSubkeyCandidates = info.getSigningSubkeys(); + PGPSecretKey signingKey = null; + for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) { + signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID()); + if (signingKey != null) { + break; + } + } + if (signingKey == null) { + throw new PGPException("Key does not contain suitable signing subkey."); + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); + return signHashContext(hashContext, signatureType, privateKey); + } + + /** + * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) + throws PGPException { + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPHashContextContentSignerBuilder(hashContext) + ); + + sigGen.init(signatureType.getCode(), privateKey); + return sigGen.generate(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java similarity index 74% rename from pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java rename to pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java index 68c24743..474ee9ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -2,9 +2,8 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.signature.builder; +package org.pgpainless.encryption_signing; -import java.io.IOException; import java.io.OutputStream; import java.security.MessageDigest; @@ -37,13 +36,13 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; * This can come in handy to sign data, which was already processed to calculate the hash context, without the * need to process it again to calculate the OpenPGP signature. */ -public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuilder { +class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBuilder { private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); private final MessageDigest messageDigest; private final HashAlgorithm hashAlgorithm; - public HashContextPGPContentSignerBuilder(MessageDigest messageDigest) { + public BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { this.messageDigest = messageDigest; this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); if (hashAlgorithm == null) { @@ -76,7 +75,7 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } public OutputStream getOutputStream() { - return new SignerOutputStream(signer); + return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer); } public byte[] getSignature() { @@ -117,49 +116,6 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } } - static class ExistingMessageDigest implements Digest { - - private final MessageDigest digest; - - ExistingMessageDigest(MessageDigest messageDigest) { - this.digest = messageDigest; - } - - @Override - public void update(byte in) { - digest.update(in); - } - - @Override - public void update(byte[] in, int inOff, int len) { - digest.update(in, inOff, len); - } - - @Override - public int doFinal(byte[] out, int outOff) { - byte[] hash = digest.digest(); - System.arraycopy(hash, 0, out, outOff, hash.length); - return getDigestSize(); - } - - @Override - public void reset() { - // Nope! - // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset - // the messageDigest, losing its state. This would shatter our intention. - } - - @Override - public String getAlgorithmName() { - return digest.getAlgorithm(); - } - - @Override - public int getDigestSize() { - return digest.getDigestLength(); - } - } - // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ private static class EdDsaSigner implements Signer { @@ -210,28 +166,4 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } } - // Copied from BC, required since BCs class is package visible only - static class SignerOutputStream - extends OutputStream { - private Signer sig; - - SignerOutputStream(Signer sig) { - this.sig = sig; - } - - public void write(byte[] bytes, int off, int len) - throws IOException { - sig.update(bytes, off, len); - } - - public void write(byte[] bytes) - throws IOException { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) - throws IOException { - sig.update((byte) b); - } - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java new file mode 100644 index 00000000..2ea36206 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; + +abstract class PGPHashContextContentSignerBuilder implements PGPContentSignerBuilder { + + // Copied from BC, required since BCs class is package visible only + static class SignerOutputStream + extends OutputStream { + private Signer sig; + + SignerOutputStream(Signer sig) { + this.sig = sig; + } + + public void write(byte[] bytes, int off, int len) + throws IOException { + sig.update(bytes, off, len); + } + + public void write(byte[] bytes) + throws IOException { + sig.update(bytes, 0, bytes.length); + } + + public void write(int b) + throws IOException { + sig.update((byte) b); + } + } + + + static class ExistingMessageDigest implements Digest { + + private final MessageDigest digest; + + ExistingMessageDigest(MessageDigest messageDigest) { + this.digest = messageDigest; + } + + @Override + public void update(byte in) { + digest.update(in); + } + + @Override + public void update(byte[] in, int inOff, int len) { + digest.update(in, inOff, len); + } + + @Override + public int doFinal(byte[] out, int outOff) { + byte[] hash = digest.digest(); + System.arraycopy(hash, 0, out, outOff, hash.length); + return getDigestSize(); + } + + @Override + public void reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + + @Override + public String getAlgorithmName() { + return digest.getAlgorithm(); + } + + @Override + public int getDigestSize() { + return digest.getDigestLength(); + } + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java deleted file mode 100644 index 57d687cc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import java.security.MessageDigest; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; - -public class HashContextSigner { - - /** - * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. - * - * WARNING: This method does not yet validate the signing key. - * TODO: Change API to receive and evaluate PGPSecretKeyRing + SecretKeyRingProtector instead. - * - * @param hashContext hash context - * @param privateKey signing-capable key - * @return signature - * @throws PGPException in case of an OpenPGP error - */ - public static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) - throws PGPException { - // TODO: Validate signing key - PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - new HashContextPGPContentSignerBuilder(hashContext) - ); - - sigGen.init(signatureType.getCode(), privateKey); - return sigGen.generate(); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java similarity index 87% rename from pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java rename to pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java index baa5a73f..50b7cbf7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.signature; +package org.pgpainless.encryption_signing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -17,7 +17,6 @@ import java.security.NoSuchAlgorithmException; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; @@ -30,11 +29,9 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.builder.HashContextSigner; -import org.pgpainless.util.Passphrase; +import org.pgpainless.key.protection.SecretKeyRingProtector; -public class HashContextSignerTest { +public class BcHashContextSignerTest { private static final String message = "Hello, World!\n"; private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -88,15 +85,14 @@ public class HashContextSignerTest { } } - private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) throws PGPException, NoSuchAlgorithmException, IOException { + private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) + throws PGPException, NoSuchAlgorithmException, IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); - long signingKeyId = PGPainless.inspectKeyRing(certificate).getSigningSubkeys().get(0).getKeyID(); - PGPPrivateKey signingKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(signingKeyId), Passphrase.emptyPassphrase()); byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); ByteArrayInputStream messageIn = new ByteArrayInputStream(messageBytes); - PGPSignature signature = signMessage(messageBytes, hashAlgorithm, signingKey); + PGPSignature signature = signMessage(messageBytes, hashAlgorithm, secretKeys); assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() @@ -113,13 +109,13 @@ public class HashContextSignerTest { assertTrue(metadata.isVerified()); } - private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPPrivateKey signingKey) + private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPSecretKeyRing secretKeys) throws NoSuchAlgorithmException, PGPException { // Prepare the hash context // This would be done by the caller application MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName(), new BouncyCastleProvider()); messageDigest.update(message); - return HashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, signingKey); + return BcHashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, secretKeys, SecretKeyRingProtector.unprotectedKeys()); } }