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); + } +}