From b64d6e8e55243737bcb0ebef5e2eceea40e2b670 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Apr 2022 00:22:41 +0200 Subject: [PATCH 1/3] Stabilize HashAlgorithm.fromName() --- .../main/java/org/pgpainless/algorithm/HashAlgorithm.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java index bcd69cc0..12feb678 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java @@ -80,7 +80,12 @@ public enum HashAlgorithm { */ @Nullable public static HashAlgorithm fromName(String name) { - return NAME_MAP.get(name); + String algorithmName = name.toUpperCase(); + HashAlgorithm algorithm = NAME_MAP.get(algorithmName); + if (algorithm == null) { + algorithm = NAME_MAP.get(algorithmName.replace("-", "")); + } + return algorithm; } private final int algorithmId; From c3dfb254b1cee1be1a15725e3b37b07809c67230 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Apr 2022 00:23:20 +0200 Subject: [PATCH 2/3] Experimental implementation of signing of existing hash contexts (MessageDigest instances) --- .../HashContextPGPContentSignerBuilder.java | 237 ++++++++++++++++++ .../signature/builder/HashContextSigner.java | 38 +++ .../signature/HashContextSignerTest.java | 125 +++++++++ 3 files changed, 400 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java new file mode 100644 index 00000000..68c24743 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java @@ -0,0 +1,237 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.DSASigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.Ed25519Signer; +import org.bouncycastle.crypto.signers.Ed448Signer; +import org.bouncycastle.crypto.signers.RSADigestSigner; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.operator.PGPContentSigner; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; +import org.bouncycastle.util.Arrays; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; + +/** + * Implementation of {@link PGPContentSignerBuilder} using the BC API, which can be used to sign hash contexts. + * 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 { + + private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); + private final MessageDigest messageDigest; + private final HashAlgorithm hashAlgorithm; + + public HashContextPGPContentSignerBuilder(MessageDigest messageDigest) { + this.messageDigest = messageDigest; + this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); + if (hashAlgorithm == null) { + throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + messageDigest.getAlgorithm()); + } + } + + @Override + public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException { + PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm()); + AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey); + final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam); + signer.init(true, privKeyParam); + + return new PGPContentSigner() { + public int getType() { + return signatureType; + } + + public int getHashAlgorithm() { + return hashAlgorithm.getAlgorithmId(); + } + + public int getKeyAlgorithm() { + return keyAlgorithm.getAlgorithmId(); + } + + public long getKeyID() { + return privateKey.getKeyID(); + } + + public OutputStream getOutputStream() { + return new SignerOutputStream(signer); + } + + public byte[] getSignature() { + try { + return signer.generateSignature(); + } catch (CryptoException e) { + throw new IllegalStateException("unable to create signature"); + } + } + + public byte[] getDigest() { + return messageDigest.digest(); + } + }; + } + + static Signer createSigner( + PublicKeyAlgorithm keyAlgorithm, + MessageDigest messageDigest, + CipherParameters keyParam) + throws PGPException { + ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest); + switch (keyAlgorithm.getAlgorithmId()) { + case PublicKeyAlgorithmTags.RSA_GENERAL: + case PublicKeyAlgorithmTags.RSA_SIGN: + return new RSADigestSigner(staticDigest); + case PublicKeyAlgorithmTags.DSA: + return new DSADigestSigner(new DSASigner(), staticDigest); + case PublicKeyAlgorithmTags.ECDSA: + return new DSADigestSigner(new ECDSASigner(), staticDigest); + case PublicKeyAlgorithmTags.EDDSA: + if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) { + return new EdDsaSigner(new Ed25519Signer(), staticDigest); + } + return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest); + default: + throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm); + } + } + + 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 { + private final Signer signer; + private final Digest digest; + private final byte[] digBuf; + + EdDsaSigner(Signer signer, Digest digest) { + this.signer = signer; + this.digest = digest; + this.digBuf = new byte[digest.getDigestSize()]; + } + + public void init(boolean forSigning, CipherParameters param) { + this.signer.init(forSigning, param); + this.digest.reset(); + } + + public void update(byte b) { + this.digest.update(b); + } + + public void update(byte[] in, int off, int len) { + this.digest.update(in, off, len); + } + + public byte[] generateSignature() + throws CryptoException, DataLengthException { + digest.doFinal(digBuf, 0); + + signer.update(digBuf, 0, digBuf.length); + + return signer.generateSignature(); + } + + public boolean verifySignature(byte[] signature) { + digest.doFinal(digBuf, 0); + + signer.update(digBuf, 0, digBuf.length); + + return signer.verifySignature(signature); + } + + public void reset() { + Arrays.clear(digBuf); + signer.reset(); + digest.reset(); + } + } + + // 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/signature/builder/HashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java new file mode 100644 index 00000000..57d687cc --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java @@ -0,0 +1,38 @@ +// 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/signature/HashContextSignerTest.java new file mode 100644 index 00000000..baa5a73f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +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; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SignatureType; +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; + +public class HashContextSignerTest { + + private static final String message = "Hello, World!\n"; + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62D5 CBED 8BD0 7D3F D167 240D 4364 E4C1 C4ED 8F59\n" + + "Comment: Sigfried \n" + + "\n" + + "lFgEYlnOkRYJKwYBBAHaRw8BAQdA7Kxn/sPIXo44xnxLBL81G5ghGzMikFc5ib9/\n" + + "qgJpZSUAAQCZnJN2l/cfWWh4DijBAwFWoVJOCphKhsJEjKxOzWdqMA2DtBVTaWdm\n" + + "cmllZCA8c2lnQGZyaS5lZD6IjwQTFgoAQQUCYlnOkQkQQ2TkwcTtj1kWIQRi1cvt\n" + + "i9B9P9FnJA1DZOTBxO2PWQKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAd/gEA\n" + + "kiPFDdMGjZV/7Do/3ox46iCH3N1I3BGmA2Jt8PsYwe0BAKe5ahLzCNAXjBQU4iSD\n" + + "A4FGipNaG2ZWgAMkdwVjMLEAnF0EYlnOkRIKKwYBBAGXVQEFAQEHQI3n0cWbBh+7\n" + + "zeuBjMWevsyxLUCExKSj5fxCh/0GuJgAAwEIBwAA/16V22vjZfAvtnUrVtUZQoYZ\n" + + "E8h87Jzj/XxXFy63I6qoER2IdQQYFgoAHQUCYlnOkQKeAQKbDAUWAgMBAAQLCQgH\n" + + "BRUKCQgLAAoJEENk5MHE7Y9ZzhsA+gPb2FNutetjrYUSY7BEsk+PPkCXF9W6JZmb\n" + + "W/zyRxgpAP9zNzpWrO7kKQ0PwMMd3R1F4Rg6GH+vjXsIbd6jT25UBJxYBGJZzpEW\n" + + "CSsGAQQB2kcPAQEHQPOZhITstSj3cPfsTiBEPhtCrc184fkAjl4s+gSB9ttRAAD/\n" + + "RVpdc9BhJ/ZXtqQaCBL65h7Uym7i+HExQphHOiuB3iwQOIjVBBgWCgB9BQJiWc6R\n" + + "Ap4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYlnOkQAKCRDXXcvYX8Ym\n" + + "crh9AP99WWietGWYs2//FYi0bEAWp6D0HmHP42rvC3qsqyMa8wD8D1Pi2atKwQTP\n" + + "JAxQFa06cUIw2POE3llaB0MKQXdTVgQACgkQQ2TkwcTtj1mF+gD+OHo68KeGFUi0\n" + + "VcVV/dx/6ES9GAIf1TI6jEsaU8TPBcMBAOHG+5MMVvyNiVKLA0JgJPF3JXOOEU+5\n" + + "qiHwlVoGncUM\n" + + "=431t\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void signContextWithEdDSAKeys() throws PGPException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithRSAKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Sigfried", RsaLength._3072); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithEcKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Sigfried"); + signWithKeys(secretKeys); + } + + private void signWithKeys(PGPSecretKeyRing secretKeys) throws PGPException, NoSuchAlgorithmException, IOException { + for (HashAlgorithm hashAlgorithm : new HashAlgorithm[] { + HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512 + }) { + signFromContext(secretKeys, hashAlgorithm); + } + } + + 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); + assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(messageIn) + .withOptions(new ConsumerOptions() + .addVerificationCert(certificate) + .addVerificationOfDetachedSignature(signature)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isVerified()); + } + + private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPPrivateKey signingKey) + 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); + } +} From 73b7f1b9bb0e2d8adf2dae79c95051eecfe22efa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 19 Apr 2022 21:07:46 +0200 Subject: [PATCH 3/3] Refactoring --- .../BcHashContextSigner.java | 66 ++++++++++++++ ...BcPGPHashContextContentSignerBuilder.java} | 76 +--------------- .../PGPHashContextContentSignerBuilder.java | 86 +++++++++++++++++++ .../signature/builder/HashContextSigner.java | 38 -------- .../BcHashContextSignerTest.java} | 20 ++--- 5 files changed, 164 insertions(+), 122 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java rename pgpainless-core/src/main/java/org/pgpainless/{signature/builder/HashContextPGPContentSignerBuilder.java => encryption_signing/BcPGPHashContextContentSignerBuilder.java} (74%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java rename pgpainless-core/src/test/java/org/pgpainless/{signature/HashContextSignerTest.java => encryption_signing/BcHashContextSignerTest.java} (87%) 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()); } }