diff --git a/pg/src/main/java/org/bouncycastle/bcpg/sig/SignatureCreationTime.java b/pg/src/main/java/org/bouncycastle/bcpg/sig/SignatureCreationTime.java index 338ba27e5c..3534b04434 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/sig/SignatureCreationTime.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/sig/SignatureCreationTime.java @@ -41,6 +41,12 @@ public SignatureCreationTime( super(SignatureSubpacketTags.CREATION_TIME, critical, false, timeToBytes(date)); } + public SignatureCreationTime( + Date date) + { + this(true, date); + } + public Date getTime() { long time = Utils.timeFromBytes(data); diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java index bd99042b4e..bc1c285cd1 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java @@ -210,8 +210,8 @@ public int getKeyAlgorithm() } /** - * Return true, if the signature is contains any signatures that follow. - * An bracketing OPS is followed by additional OPS packets and is calculated over all the data between itself + * Return true, if the signature contains any signatures that follow. + * A bracketing OPS is followed by additional OPS packets and is calculated over all the data between itself * and its corresponding signature (it is an attestation for contained signatures). * * @return true if containing, false otherwise diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java index 360f2a25c1..ba3ec6847a 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java @@ -16,6 +16,7 @@ import org.bouncycastle.bcpg.MPInteger; import org.bouncycastle.bcpg.Packet; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.bcpg.SignatureSubpacket; import org.bouncycastle.bcpg.TrustPacket; @@ -310,6 +311,17 @@ public void init(PGPContentVerifierBuilderProvider verifierBuilderProvider, PGPP { throw new PGPException("Illegal signature type 0xFF provided."); } + + if (getVersion() == SignaturePacket.VERSION_6 && pubKey.getVersion() != PublicKeyPacket.VERSION_6) + { + throw new PGPException("MUST NOT verify v6 signature with non-v6 key."); + } + + if (getVersion() == SignaturePacket.VERSION_4 && pubKey.getVersion() != PublicKeyPacket.VERSION_4) + { + throw new PGPException("MUST NOT verify v4 signature with non-v4 key."); + } + PGPContentVerifierBuilder verifierBuilder = createVerifierProvider(verifierBuilderProvider); init(verifierBuilder.build(pubKey)); diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java index 6700c89886..4cabe079c0 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java @@ -5,14 +5,17 @@ import java.math.BigInteger; import java.util.Date; +import org.bouncycastle.bcpg.HashUtils; import org.bouncycastle.bcpg.MPInteger; import org.bouncycastle.bcpg.OnePassSignaturePacket; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.bcpg.SignatureSubpacket; import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.SignatureCreationTime; +import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.bouncycastle.openpgp.operator.PGPContentSigner; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.util.Arrays; @@ -31,6 +34,7 @@ public class PGPSignatureGenerator //private int providedKeyAlgorithm = -1; private int providedKeyAlgorithm = -1; private PGPPublicKey signingPubKey; + private byte[] salt; /** * Create a version 4 signature generator built on the passed in contentSignerBuilder. @@ -88,8 +92,8 @@ public PGPSignatureGenerator( /** * Initialise the generator for signing. * - * @param signatureType - * @param key + * @param signatureType type of signature + * @param key private signing key * @throws PGPException */ public void init( @@ -106,12 +110,37 @@ public void init( sigType = contentSigner.getType(); lastb = 0; -// if (providedKeyAlgorithm >= 0 && providedKeyAlgorithm != contentSigner.getKeyAlgorithm()) -// { -// throw new PGPException("key algorithm mismatch"); -// } + if (providedKeyAlgorithm >= 0 && providedKeyAlgorithm != contentSigner.getKeyAlgorithm()) + { + throw new PGPException("key algorithm mismatch"); + } + + if (key.getPublicKeyPacket().getVersion() != version) + { + throw new PGPException("Key version mismatch."); + } + + if (version == SignaturePacket.VERSION_6) + { + int saltSize = HashUtils.getV6SignatureSaltSizeInBytes(contentSigner.getHashAlgorithm()); + salt = new byte[saltSize]; + CryptoServicesRegistrar.getSecureRandom().nextBytes(salt); + try + { + sigOut.write(salt); + } + catch (IOException e) + { + throw new PGPException("Cannot update signature with salt."); + } + } } + /** + * Set the hashed signature subpackets. + * Hashed signature subpackets are covered by the signature. + * @param hashedPcks hashed signature subpackets + */ public void setHashedSubpackets( PGPSignatureSubpacketVector hashedPcks) { @@ -124,6 +153,11 @@ public void setHashedSubpackets( hashed = hashedPcks.toSubpacketArray(); } + /** + * Set the unhashed signature subpackets. + * Unhashed signature subpackets are not covered by the signature. + * @param unhashedPcks unhashed signature subpackets + */ public void setUnhashedSubpackets( PGPSignatureSubpacketVector unhashedPcks) { @@ -147,7 +181,26 @@ public PGPOnePassSignature generateOnePassVersion( boolean isNested) throws PGPException { - return new PGPOnePassSignature(new OnePassSignaturePacket(sigType, contentSigner.getHashAlgorithm(), contentSigner.getKeyAlgorithm(), contentSigner.getKeyID(), isNested)); + if (version == SignaturePacket.VERSION_6) + { + return new PGPOnePassSignature(v6OPSPacket(isNested)); + } + else + { + return new PGPOnePassSignature(v3OPSPacket(isNested)); + } + } + + private OnePassSignaturePacket v3OPSPacket(boolean isNested) + { + return new OnePassSignaturePacket(sigType, contentSigner.getHashAlgorithm(), contentSigner.getKeyAlgorithm(), + contentSigner.getKeyID(), isNested); + } + + private OnePassSignaturePacket v6OPSPacket(boolean isNested) + { + return new OnePassSignaturePacket(sigType, contentSigner.getHashAlgorithm(), contentSigner.getKeyAlgorithm(), + salt, signingPubKey.getFingerprint(), isNested); } /** @@ -159,66 +212,51 @@ public PGPOnePassSignature generateOnePassVersion( public PGPSignature generate() throws PGPException { - MPInteger[] sigValues; - int version = 4; - ByteArrayOutputStream sOut = new ByteArrayOutputStream(); - SignatureSubpacket[] hPkts, unhPkts; - - if (packetNotPresent(hashed, SignatureSubpacketTags.CREATION_TIME)) - { - hPkts = insertSubpacket(hashed, new SignatureCreationTime(false, new Date())); - } - else - { - hPkts = hashed; - } - - if (packetNotPresent(hashed, SignatureSubpacketTags.ISSUER_KEY_ID) && packetNotPresent(unhashed, SignatureSubpacketTags.ISSUER_KEY_ID)) - { - unhPkts = insertSubpacket(unhashed, new IssuerKeyID(false, contentSigner.getKeyID())); - } - else - { - unhPkts = unhashed; - } + prepareSignatureSubpackets(); + ByteArrayOutputStream sOut = new ByteArrayOutputStream(); try { + // hash the "header" sOut.write((byte)version); sOut.write((byte)sigType); sOut.write((byte)contentSigner.getKeyAlgorithm()); sOut.write((byte)contentSigner.getHashAlgorithm()); + // hash signature subpackets ByteArrayOutputStream hOut = new ByteArrayOutputStream(); - - for (int i = 0; i != hPkts.length; i++) + for (int i = 0; i != hashed.length; i++) { - hPkts[i].encode(hOut); + hashed[i].encode(hOut); } - byte[] data = hOut.toByteArray(); + if (version == SignaturePacket.VERSION_6) + { + sOut.write((byte) (data.length >> 24)); + sOut.write((byte) (data.length >> 16)); + } sOut.write((byte)(data.length >> 8)); sOut.write((byte)data.length); sOut.write(data); - byte[] hData = sOut.toByteArray(); + // hash the "footer" + int dataLen = sOut.toByteArray().length; sOut.write((byte)version); sOut.write((byte)0xff); - sOut.write((byte)(hData.length >> 24)); - sOut.write((byte)(hData.length >> 16)); - sOut.write((byte)(hData.length >> 8)); - sOut.write((byte)(hData.length)); + sOut.write((byte)(dataLen >> 24)); + sOut.write((byte)(dataLen >> 16)); + sOut.write((byte)(dataLen >> 8)); + sOut.write((byte)(dataLen)); } catch (IOException e) { throw new PGPException("exception encoding hashed data.", e); } - byte[] trailer = sOut.toByteArray(); - blockUpdate(trailer, 0, trailer.length); + MPInteger[] sigValues; switch (contentSigner.getKeyAlgorithm()) { case PublicKeyAlgorithmTags.RSA_SIGN: @@ -253,16 +291,63 @@ public PGPSignature generate() fingerPrint[0] = digest[0]; fingerPrint[1] = digest[1]; - if (sigValues != null) + SignaturePacket sigPckt; + if (sigValues != null) // MPI encoding { - return new PGPSignature(new SignaturePacket(sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), - contentSigner.getHashAlgorithm(), hPkts, unhPkts, fingerPrint, sigValues)); + sigPckt = new SignaturePacket(version, sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), + contentSigner.getHashAlgorithm(), hashed, unhashed, fingerPrint, sigValues, salt); } - else + else // native encoding { // Ed25519, Ed448 use raw encoding instead of MPI - return new PGPSignature(new SignaturePacket(4, sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), - contentSigner.getHashAlgorithm(), hPkts, unhPkts, fingerPrint, contentSigner.getSignature(), null)); + + sigPckt = new SignaturePacket(version, sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), + contentSigner.getHashAlgorithm(), hashed, unhashed, fingerPrint, contentSigner.getSignature(), salt); + } + return new PGPSignature(sigPckt); + } + + protected void prepareSignatureSubpackets() + throws PGPException + { + switch (version) + { + case SignaturePacket.VERSION_4: + case SignaturePacket.VERSION_5: + { + // Insert hashed signature creation time if missing + if (packetNotPresent(hashed, SignatureSubpacketTags.CREATION_TIME)) + { + hashed = insertSubpacket(hashed, new SignatureCreationTime(true, new Date())); + } + + // Insert unhashed issuer key-ID if missing + if (packetNotPresent(hashed, SignatureSubpacketTags.ISSUER_KEY_ID) && packetNotPresent(unhashed, SignatureSubpacketTags.ISSUER_KEY_ID)) + { + unhashed = insertSubpacket(unhashed, new IssuerKeyID(false, contentSigner.getKeyID())); + } + + break; + } + + case SignaturePacket.VERSION_6: + { + // Insert hashed signature creation time if missing + if (packetNotPresent(hashed, SignatureSubpacketTags.CREATION_TIME)) + { + hashed = insertSubpacket(hashed, new SignatureCreationTime(true, new Date())); + } + + // Insert hashed issuer fingerprint subpacket if missing + if (packetNotPresent(hashed, SignatureSubpacketTags.ISSUER_FINGERPRINT) && + packetNotPresent(unhashed, SignatureSubpacketTags.ISSUER_FINGERPRINT) && + signingPubKey != null) + { + hashed = insertSubpacket(hashed, new IssuerFingerprint(true, version, signingPubKey.getFingerprint())); + } + + break; + } } } diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPSignatureTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPSignatureTest.java index 4b6687927b..40885397b0 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPSignatureTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPSignatureTest.java @@ -551,12 +551,13 @@ public void performTest() int[] criticalHashed = hashedPcks.getCriticalTags(); - if (criticalHashed.length != 1) + // SignerUserID and SignatureCreationTime are critical. + if (criticalHashed.length != 2) { fail("wrong number of critical packets found."); } - if (criticalHashed[0] != SignatureSubpacketTags.SIGNER_USER_ID) + if (criticalHashed[1] != SignatureSubpacketTags.SIGNER_USER_ID) { fail("wrong critical packet found in tag list."); } diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPv6SignatureTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPv6SignatureTest.java index 43c6c96f54..a2309a2dbd 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPv6SignatureTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPv6SignatureTest.java @@ -1,7 +1,11 @@ package org.bouncycastle.openpgp.test; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PacketFormat; import org.bouncycastle.bcpg.SignatureSubpacket; import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.IssuerFingerprint; @@ -9,15 +13,20 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; @@ -26,13 +35,15 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Iterator; public class PGPv6SignatureTest extends AbstractPacketTest { - + // https://www.rfc-editor.org/rfc/rfc9580.html#name-sample-version-6-certificat private static final String ARMORED_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + "xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf\n" + @@ -45,6 +56,7 @@ public class PGPv6SignatureTest "j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805\n" + "I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==\n" + "-----END PGP PUBLIC KEY BLOCK-----"; + // https://www.rfc-editor.org/rfc/rfc9580.html#name-sample-version-6-secret-key private static final String ARMORED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "\n" + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + @@ -63,27 +75,38 @@ public class PGPv6SignatureTest @Override public String getName() { - return "PGPV6SignatureTest"; + return "PGPv6SignatureTest"; } @Override public void performTest() throws Exception { - verifyV6DirectKeySignatureTestVector(); + verifySignatureOnTestKey(); + verifyKnownGoodCleartextSignedMessage(); - verifyV6BinarySignature(); + verifyV6DetachedSignature(); verifyV6InlineSignature(); verifyV6CleartextSignature(); + generateAndVerifyV6DetachedSignature(); + generateAndVerifyV6InlineSignature(); + generateAndVerifyV6CleartextSignature(); + verifyingSignatureWithMismatchedSaltSizeFails(); verifyingOPSWithMismatchedSaltSizeFails(); verifyingInlineSignatureWithSignatureSaltValueMismatchFails(); verifySignaturesOnEd448X448Key(); + generateAndVerifyInlineSignatureUsingRSAKey(); + + testVerificationOfV4SigWithV6KeyFails(); } - private void verifyV6DirectKeySignatureTestVector() + /** + * Verify that the known-good key signatures on the minimal test key verify properly. + */ + private void verifySignatureOnTestKey() throws IOException, PGPException { ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_CERT.getBytes(StandardCharsets.UTF_8)); @@ -107,7 +130,54 @@ private void verifyV6DirectKeySignatureTestVector() subkeyBinding.verifyCertification(primaryKey, subkey)); } - private void verifyV6BinarySignature() + private void verifyKnownGoodCleartextSignedMessage() throws IOException, PGPException { + // https://www.rfc-editor.org/rfc/rfc9580.html#name-sample-cleartext-signed-mes + String MSG = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "\n" + + "What we need from the grocery store:\n" + + "\n" + + "- - tofu\n" + + "- - vegetables\n" + + "- - noodles\n" + + "\n" + + "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo\n" + + "/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr\n" + + "NK2ay45cX1IVAQ==\n" + + "-----END PGP SIGNATURE-----"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_CERT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPPublicKeyRing cert = (PGPPublicKeyRing) objFac.nextObject(); + + bIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + aIn = new ArmoredInputStream(bIn); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + while (aIn.isClearText()) + { + int c = aIn.read(); + if (aIn.isClearText()) + { + bOut.write(c); + } + } + byte[] plaintext = Arrays.copyOf(bOut.toByteArray(), bOut.size()- 1); + objFac = new BcPGPObjectFactory(aIn); + PGPSignatureList sigs = (PGPSignatureList) objFac.nextObject(); + PGPSignature sig = sigs.get(0); + sig.init(new BcPGPContentVerifierBuilderProvider(), cert.getPublicKey(sig.getKeyID())); + sig.update(plaintext); + isTrue("Known good cleartext signature MUST verify successful", sig.verify()); + } + + /** + * Verify that a good v6 detached signature is verified properly. + */ + private void verifyV6DetachedSignature() throws IOException, PGPException { String msg = "Hello, World!\n"; @@ -139,6 +209,9 @@ private void verifyV6BinarySignature() binarySig.verify()); } + /** + * Verify that a good v6 inline signature is verified properly. + */ private void verifyV6InlineSignature() throws IOException, PGPException { @@ -181,6 +254,9 @@ private void verifyV6InlineSignature() isTrue("Verifying OPS signature MUST succeed", ops.verify(sig)); } + /** + * Verify that a good v6 cleartext signature is verified properly. + */ private void verifyV6CleartextSignature() throws IOException, PGPException { @@ -213,7 +289,8 @@ private void verifyV6CleartextSignature() plainOut.write(c); } } - isEncodingEqual("Plaintext MUST match", "Hello, World!\n".getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); + isEncodingEqual("Plaintext MUST match", + "Hello, World!\n".getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); pIn = new BCPGInputStream(aIn); objFac = new BcPGPObjectFactory(pIn); PGPSignatureList sigList = (PGPSignatureList) objFac.nextObject(); @@ -221,9 +298,13 @@ private void verifyV6CleartextSignature() PGPSignature sig = sigList.get(0); sig.init(new BcPGPContentVerifierBuilderProvider(), signingPubKey); sig.update("Hello, World!".getBytes(StandardCharsets.UTF_8)); - isTrue("Signature MUST verify successfully", sig.verify()); + isTrue("Cleartext Signature MUST verify successfully", sig.verify()); } + /** + * A v6 signature with too few salt bytes. + * This test verifies that the signature is properly rejected. + */ private void verifyingSignatureWithMismatchedSaltSizeFails() throws IOException { @@ -262,6 +343,10 @@ private void verifyingSignatureWithMismatchedSaltSizeFails() } } + /** + * Verify that a OPS signature where the length of the salt array does not match the expectations + * is rejected properly. + */ private void verifyingOPSWithMismatchedSaltSizeFails() throws IOException { @@ -302,6 +387,10 @@ private void verifyingOPSWithMismatchedSaltSizeFails() } } + /** + * Test verifying that an inline signature where the salt of the OPS packet mismatches that of the signature + * is rejected properly. + */ private void verifyingInlineSignatureWithSignatureSaltValueMismatchFails() throws IOException, PGPException { @@ -355,6 +444,9 @@ private void verifyingInlineSignatureWithSignatureSaltValueMismatchFails() } } + /** + * Verify self signatures on a v6 Ed448/X448 key. + */ private void verifySignaturesOnEd448X448Key() throws PGPException, IOException { @@ -495,6 +587,324 @@ private PGPPublicKey getSigningKeyFor(PGPKeyRing keys, PGPSignature sig) return null; } + /** + * Generate and verify a detached v6 signature using the v6 test key. + */ + private void generateAndVerifyV6DetachedSignature() + throws IOException, PGPException + { + String msg = "Hello, World!\n"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + + PGPSecretKey signingSecKey = secretKeys.getSecretKey(); // primary key + PGPPrivateKey signingPrivKey = signingSecKey.extractPrivateKey(null); + PGPPublicKey signingPubKey = signingSecKey.getPublicKey(); + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder( + signingPubKey.getAlgorithm(), + HashAlgorithmTags.SHA512), + signingPubKey); + sigGen.init(PGPSignature.BINARY_DOCUMENT, signingPrivKey); + sigGen.update(msg.getBytes(StandardCharsets.UTF_8)); + PGPSignature binarySig = sigGen.generate(); + + binarySig.init(new BcPGPContentVerifierBuilderProvider(), signingPubKey); + binarySig.update(msg.getBytes(StandardCharsets.UTF_8)); + isTrue("Detached binary signature MUST verify successful.", + binarySig.verify()); + } + + /** + * Generate and verify a v6 inline signature using the v6 test key. + */ + private void generateAndVerifyV6InlineSignature() + throws IOException, PGPException + { + String msg = "Hello, World!\n"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + + PGPSecretKey signingSecKey = secretKeys.getSecretKey(); // primary key + PGPPrivateKey signingPrivKey = signingSecKey.extractPrivateKey(null); + PGPPublicKey signingPubKey = signingSecKey.getPublicKey(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream aOut = ArmoredOutputStream.builder() + .clearHeaders() + .enableCRC(false) + .build(bOut); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(signingPubKey.getAlgorithm(), HashAlgorithmTags.SHA512), signingPubKey); + sigGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signingPrivKey); + sigGen.generateOnePassVersion(true).encode(pOut); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(pOut, PGPLiteralDataGenerator.UTF8, "", PGPLiteralDataGenerator.NOW, new byte[512]); + + litOut.write(msg.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + sigGen.update(msg.getBytes(StandardCharsets.UTF_8)); + sigGen.generate().encode(pOut); + + pOut.close(); + aOut.close(); + + bIn = new ByteArrayInputStream(bOut.toByteArray()); + aIn = new ArmoredInputStream(bIn); + pIn = new BCPGInputStream(aIn); + objFac = new BcPGPObjectFactory(pIn); + + PGPOnePassSignatureList opsList = (PGPOnePassSignatureList) objFac.nextObject(); + isEquals("There MUST be exactly 1 OPS", 1, opsList.size()); + PGPOnePassSignature ops = opsList.get(0); + + ops.init(new BcPGPContentVerifierBuilderProvider(), signingPubKey); + + PGPLiteralData lit = (PGPLiteralData) objFac.nextObject(); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + Streams.pipeAll(lit.getDataStream(), plainOut); + isEncodingEqual("Content of LiteralData packet MUST match plaintext", + msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); + + ops.update(plainOut.toByteArray()); + PGPSignatureList sigList = (PGPSignatureList) objFac.nextObject(); + isEquals("There MUST be exactly one signature", 1, sigList.size()); + PGPSignature sig = sigList.get(0); + isTrue("Generated Inline OPS signature MUST verify successful", ops.verify(sig)); + } + + /** + * Generate and verify a v6 signature using the cleartext signature framework and the v6 test key. + */ + private void generateAndVerifyV6CleartextSignature() + throws IOException, PGPException + { + String msg = "Hello, World!\n"; + String msgS = "Hello, World!"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + + PGPSecretKey signingSecKey = secretKeys.getSecretKey(); // primary key + PGPPrivateKey signingPrivKey = signingSecKey.extractPrivateKey(null); + PGPPublicKey signingPubKey = signingSecKey.getPublicKey(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream aOut = ArmoredOutputStream.builder() + .clearHeaders() + .enableCRC(false) + .build(bOut); + + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(signingPubKey.getAlgorithm(), HashAlgorithmTags.SHA512), + signingPubKey); + sigGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signingPrivKey); + + aOut.beginClearText(); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + + sigGen.update(msgS.getBytes(StandardCharsets.UTF_8)); + aOut.write(msg.getBytes(StandardCharsets.UTF_8)); + + aOut.endClearText(); + sigGen.generate().encode(pOut); + pOut.close(); + aOut.close(); + + // Verify + bIn = new ByteArrayInputStream(bOut.toByteArray()); + aIn = new ArmoredInputStream(bIn); + ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); + while (aIn.isClearText()) + { + int c = aIn.read(); + if (aIn.isClearText()) + { + plainOut.write(c); + } + } + isEncodingEqual("Plaintext MUST match", msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); + pIn = new BCPGInputStream(aIn); + objFac = new BcPGPObjectFactory(pIn); + PGPSignatureList sigList = (PGPSignatureList) objFac.nextObject(); + isEquals("There MUST be exactly 1 signature.", 1, sigList.size()); + PGPSignature sig = sigList.get(0); + sig.init(new BcPGPContentVerifierBuilderProvider(), signingPubKey); + sig.update(msgS.getBytes(StandardCharsets.UTF_8)); + boolean v = sig.verify(); + if (!v) + { + // -DM System.out.println + System.out.println(bOut); + } + isTrue("Generated Cleartext Signature MUST verify successfully", v); + } + + /** + * Generate and verify an inline text signature using a v6 RSA key. + */ + private void generateAndVerifyInlineSignatureUsingRSAKey() + throws PGPException, IOException + { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: B79E376A49446A250AB1738F657EAA7E8F91796B3CA950263C38FBBBEADC2352\n" + + "\n" + + "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\n" + + "4M2U\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + String MSG = "Hello, World!\n"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ArmoredOutputStream aOut = ArmoredOutputStream.builder() + .clearHeaders() + .enableCRC(false) + .build(bOut); + BCPGOutputStream pOut = new BCPGOutputStream(aOut, PacketFormat.CURRENT); + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder( + secretKeys.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA3_512), + secretKeys.getPublicKey()); + sigGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, secretKeys.getSecretKey().extractPrivateKey(null)); + PGPOnePassSignature ops = sigGen.generateOnePassVersion(false); + ops.encode(pOut); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(pOut, PGPLiteralDataGenerator.UTF8, "", + PGPLiteralDataGenerator.NOW, new byte[512]); + byte[] plaintext = MSG.getBytes(StandardCharsets.UTF_8); + litOut.write(plaintext); + litOut.close(); + sigGen.update(plaintext); + PGPSignature sig = sigGen.generate(); + sig.encode(pOut); + pOut.close(); + aOut.close(); + + bIn = new ByteArrayInputStream(bOut.toByteArray()); + aIn = new ArmoredInputStream(bIn); + pIn = new BCPGInputStream(aIn); + objFac = new BcPGPObjectFactory(pIn); + + PGPOnePassSignatureList opsList = (PGPOnePassSignatureList) objFac.nextObject(); + ops = opsList.get(0); + ops.init(new BcPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); + PGPLiteralData lit = (PGPLiteralData) objFac.nextObject(); + InputStream litIn = lit.getDataStream(); + plaintext = Streams.readAll(litIn); + ops.update(plaintext); + PGPSignatureList sigList = (PGPSignatureList) objFac.nextObject(); + sig = sigList.get(0); + isTrue("V6 inline sig made using RSA key MUST verify", ops.verify(sig)); + } + + /** + * A version 4 signature generated using the v6 key. + * This test verifies that the signature is properly rejected. + */ + private void testVerificationOfV4SigWithV6KeyFails() + throws IOException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(ARMORED_KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + BCPGInputStream pIn = new BCPGInputStream(aIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + + // v4 timestamp signature containing an IssuerKeyId subpacket + String V4_SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wloEQBsKABAJEMsYbE8GCaaXBQJmzHd2AAA5wlKWl7C0Dp6dVGDrCFCiISbyL4UE\n" + + "eYFLRZRnfn25OQmobhAHm2WgY/YOH5bTRLLBSIJiJlstQXMwGQvNNtheQAA=\n" + + "-----END PGP SIGNATURE-----"; + + bIn = new ByteArrayInputStream(V4_SIG.getBytes(StandardCharsets.UTF_8)); + aIn = new ArmoredInputStream(bIn); + pIn = new BCPGInputStream(aIn); + objFac = new BcPGPObjectFactory(pIn); + PGPSignatureList sigs = (PGPSignatureList) objFac.nextObject(); + PGPSignature sig = sigs.get(0); + + isNotNull(testException("MUST NOT verify v4 signature with non-v4 key.", "PGPException", + new TestExceptionOperation() { + @Override + public void operation() throws Exception { + sig.init(new BcPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); + sig.verify(); + } + })); + } + public static void main(String[] args) { runTest(new PGPv6SignatureTest()); diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java index 6c5ea8dd24..3a3acfea97 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/RegressionTest.java @@ -76,7 +76,8 @@ public class RegressionTest new Curve25519PrivateKeyEncodingTest(), new EdDSAKeyConversionWithLeadingZeroTest(), - new ECDSAKeyPairTest() + new ECDSAKeyPairTest(), + new PGPv6SignatureTest() }; public static void main(String[] args)