diff options
author | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:08:41 -0300 |
---|---|---|
committer | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:50:43 -0300 |
commit | f9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch) | |
tree | d545e325ba1ae756fc2eac66fac1001b6753c40d /src/lib/pgp/verify.test.ts |
feat!: initial commit
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/lib/pgp/verify.test.ts')
-rw-r--r-- | src/lib/pgp/verify.test.ts | 619 |
1 files changed, 619 insertions, 0 deletions
diff --git a/src/lib/pgp/verify.test.ts b/src/lib/pgp/verify.test.ts new file mode 100644 index 0000000..9c8ae9c --- /dev/null +++ b/src/lib/pgp/verify.test.ts @@ -0,0 +1,619 @@ +/* +import { + afterEach, + beforeAll, + beforeEach, + describe, + it, +} from "@std/testing/bdd"; +import { type Stub, stub } from "@std/testing/mock"; +import { FakeTime } from "@std/testing/time"; +import { get } from "../../utils/anonymous.ts"; +import { SignatureVerifier } from "./verify.ts"; +import { assertEquals } from "@std/assert/equals"; +import { assert, assertExists, assertFalse, assertRejects } from "@std/assert"; +import { + corruptData, + corruptSignatureFormat, + createDetachedSignature, + createInMemoryFile, + generateKeyPair, + generateKeyPairWithSubkey, + startMockFs, +} from "../../../tests/fixtures/setup.ts"; +import { emptyCommandOutput } from "../../../tests/fixtures/test_data.ts"; + +startMockFs(); + +describe("SignatureVerifier", () => { + let verifier: SignatureVerifier; + let aliceKeyPair: Awaited<ReturnType<typeof generateKeyPair>>; + let bobKeyPair: Awaited<ReturnType<typeof generateKeyPair>>; + let aliceWithSubkeyKeyPair: Awaited<ReturnType<typeof generateKeyPair>>; + + beforeAll(async () => { + aliceKeyPair = await generateKeyPair("Alice"); + bobKeyPair = await generateKeyPair("Bob"); + aliceWithSubkeyKeyPair = await generateKeyPairWithSubkey("AliceWithSubkey"); + }); + + beforeEach(() => { + verifier = new SignatureVerifier(); + Deno.Command.prototype.output = stub( + Deno.Command.prototype, + "output", + () => emptyCommandOutput, + ); + }); + + afterEach(() => { + (Deno.Command.prototype.output as Stub).restore(); + }); + + describe("when verifying a file with a single signature", () => { + const originalData = new TextEncoder().encode( + "This is the original file content for single signature tests.", + ) as Uint8Array<ArrayBuffer>; + let originalDataUrl: URL; + + beforeEach(() => { + // Create the data file in memory for each single signature test + originalDataUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt"), + originalData, + ); + }); + + it("Scenario: No signature found", async () => { + const verification = await verifier.verify([originalDataUrl]); + + assertEquals(new Uint8Array(verification.data), originalData); + assertFalse( + await verification.dataCorrupted, + "Data is not corrupted in the absence of a signature to check against", + ); + assertEquals( + verification.verifications, + undefined, + "Should not find any signatures to verify", + ); + // commit is stubbed, so it will be undefined + }); + + it("Scenario: Signature cannot be checked (missing key - 'E')", async () => { + // Create a valid signature, but don't add the signing key to the verifier + const signature = await createDetachedSignature( + originalData, + aliceKeyPair.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt.sig"), + signature, + ); + + const verification = await verifier.verify([ + originalDataUrl, + signatureUrl, + ]); + + assertEquals(new Uint8Array(verification.data), originalData); + assertEquals(await verification.dataCorrupted, [false]); + + assertEquals(verification.signatureCorrupted, [false]); + + assertExists(verification.verifications, "Should find the signature"); + assertEquals(verification.verifications.length, 1); // One signature found + + const sigVerification = verification.verifications[0]; + assertExists(sigVerification.packet); + assertFalse(await sigVerification.signatureCorrupted.then(get(0))); + + assertRejects( + () => sigVerification.verified, + "Verification should fail due to missing key", + ); + // assertEquals(await sigVerification.status, "E", "Status should be 'E'"); + + // The keys promise might resolve with an empty array or throw depending on implementation + // assert(?) sigVerification.keys resolves as expected + }); + + it("Scenario: Signature cannot be checked (Signature corrupted/malformed - 'E')", async () => { + const signature = await createDetachedSignature( + originalData, + aliceKeyPair.privateKey, + ); + const corruptedSignature = corruptSignatureFormat(signature); + const corruptedSignatureUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt.sig"), + corruptedSignature, + ); + + verifier.addKey(aliceKeyPair.publicKey); + + const verification = await verifier.verify([ + originalDataUrl, + corruptedSignatureUrl, + ]); + + assertEquals(new Uint8Array(verification.data), originalData); + assertEquals(await verification.dataCorrupted, undefined); + + assertEquals(verification.verifications, undefined); + // assertEquals(await sigVerification.status, "E", "Status should be 'E'"); + }); + + it("Scenario: Bad signature ('B')", async () => { + // Create a valid signature for the original data + const signature = await createDetachedSignature( + originalData, + aliceKeyPair.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt.sig"), + signature, + ); + // Create corrupted data + const corruptedData = corruptData(originalData); + const corruptedDataUrl = createInMemoryFile( + new URL("file:///test/corrupted_single_sig_data.txt"), + corruptedData, + ); + + verifier.addKey(aliceKeyPair.publicKey); // Key is available + + // Verify the signature (of original data) against the corrupted data + const verification = await verifier.verify([ + corruptedDataUrl, + signatureUrl, + ]); + + assertEquals(new Uint8Array(verification.data), corruptedData); // The verifier processed the corrupted data + assert( + await verification.dataCorrupted, + "Data should be marked as corrupted because signature does not match", + ); // Assuming implementation detects this + + assertFalse(verification.signatureCorrupted?.[0]); + + assertExists(verification.verifications, "Should find the signature"); + assertEquals(verification.verifications.length, 1); // One signature found + + const sigVerification = verification.verifications[0]; + assertExists(sigVerification.key); // Key should be found + + assertExists(sigVerification.packet); + assertFalse(await sigVerification.signatureCorrupted.then(get(0))); // Signature data itself is not corrupted + + // Expect verification to fail and report 'B' + assertRejects( + () => sigVerification.verified, + "Verification should fail due to data mismatch", + ); + // assertEquals(await sigVerification.status, "B", "Status should be 'B'"); + }); + + it("Scenario: Good signature ('G')", async () => { + const signature = await createDetachedSignature( + originalData, + aliceKeyPair.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt.sig"), + signature, + ); + + // Add the key and assume it's ultimately trusted for this scenario + // In a real test, you might explicitly set trust levels if openpgp.js supports it easily + verifier.addKey(aliceKeyPair.publicKey); + + const verification = await verifier.verify([ + originalDataUrl, + signatureUrl, + ]); + + assertEquals(new Uint8Array(verification.data), originalData); + assertFalse( + await verification.dataCorrupted?.then((x) => x[0]), + "Data should not be marked corrupted for a good signature", + ); + + assertFalse(verification.signatureCorrupted?.[0]); + + assertExists(verification.verifications, "Should find the signature"); + assertEquals(verification.verifications.length, 1); + + const sigVerification = verification.verifications[0]; + assertExists(sigVerification.key, "Should find the signing key"); + const signingKey = await sigVerification.key; // Assuming one key found + assertExists(signingKey, "Should find the signing key"); + assertEquals(signingKey.getKeyID(), aliceKeyPair.publicKey.getKeyID()); + + assertExists(sigVerification.packet); + assertFalse(await sigVerification.signatureCorrupted.then((x) => x[0])); + + // Expect verification to succeed and report 'G' + assert( + await sigVerification.verified, + "Verification should succeed for a good signature", + ); + // assertEquals(await sigVerification.status, "G", "Status should be 'G'"); + }); + + it("Scenario: Good signature, unknown validity ('U')", async () => { + const signature = await createDetachedSignature( + originalData, + aliceKeyPair.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/single_sig_data.txt.sig"), + signature, + ); + + // Add the key but do *not* establish ultimate trust for this key in the verifier's context + // This scenario relies on your verifier or OpenPGP.js handling the 'unknown trust' case. + verifier.addKey(aliceKeyPair.publicKey); // Key is available, but trust level is not set + + const verification = await verifier.verify([ + originalDataUrl, + signatureUrl, + ]); + + assertEquals(new Uint8Array(verification.data), originalData); + assertFalse(await verification.dataCorrupted?.then((x) => x[0])); + + assertFalse(verification.signatureCorrupted?.[0]); + + assertExists(verification.verifications, "Should find the signature"); + assertEquals(verification.verifications.length, 1); + + const sigVerification = verification.verifications[0]; + assertExists(sigVerification.key); + + assertExists(sigVerification.packet); + assertFalse(await sigVerification.signatureCorrupted.then((x) => x[0])); + + // Expect cryptographic verification to succeed, but status to be 'U' + assert( + await sigVerification.verified, + "Cryptographic verification should succeed", + ); + // assertEquals( + // await sigVerification.status, + // "U", + // "Status should be 'U' due to unknown validity", + // ); + }); + + // TODO(#): Add tests for Scenarios involving Key Expiration ('X', 'Y') + // This requires creating keys with specific expiration dates and mocking the system clock + it("Scenario: Good signature, key expired *after* signature time ('X')", async () => { + // Use fake time to control the 'now' + const time = new FakeTime(); + + const keyExpirationTime = time.now + 30 * 1000; + const keyPairWithExpiry = await generateKeyPair("AliceWithExpiry", { + keyExpirationTime, + }); + + const signature = await createDetachedSignature( + originalData, + keyPairWithExpiry.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/sig_expired_after.sig"), + signature, + ); + + time.tick(60 * 1000); + + verifier.addKey(keyPairWithExpiry.publicKey); + + const verification = await verifier.verify([ + originalDataUrl, + signatureUrl, + ]); + + time.restore(); + + assertFalse(await verification.dataCorrupted?.then((x) => x[0])); + + assertFalse(verification.signatureCorrupted?.[0]); + + assertExists(verification.verifications); + // const expirationDate = await verification.verifications[0].keys[0].then(( + // x, + // ) => x.getExpirationTime()); + // assertEquals( + // expirationDate?.valueOf(), + // new Date(keyExpirationTime).valueOf(), + // ); + assertExists(await verification.verifications[0].packet); + assertFalse( + await verification.verifications[0].signatureCorrupted.then((x) => + x[0] + ), + ); + assert(await verification.verifications[0].verified); + + // assertEquals( + // await verification.verifications![0].status, + // "X", + // "Status should be 'X' due to key expired after signature", + // ); + }); + + it("Scenario: Good signature, key expired *before* signature time ('Y')", async () => { + // Use fake time to control the 'now' when creating the key (for expiration) + const time = new FakeTime(); + + const keyExpirationTime = time.now + 30 * 1000; + const keyPairExpiredBefore = await generateKeyPair("AliceExpiredBefore", { + keyExpirationTime, + }); + + time.tick(60 * 1000); + + const signature = await createDetachedSignature( + originalData, + keyPairExpiredBefore.privateKey, + ); + const signatureUrl = createInMemoryFile( + new URL("file:///test/sig_expired_before.sig"), + signature, + ); + + verifier.addKey(keyPairExpiredBefore.publicKey); + + time.tick(60 * 1000); + + const verification = await verifier.verify([ + originalDataUrl, + signatureUrl, + ]); + + time.restore(); + + assertFalse(await verification.dataCorrupted?.then((x) => x[0])); + + assertFalse(verification.signatureCorrupted?.[0]); + + assertExists(verification.verifications); + // const expirationDate = await verification.verifications[0].keys[0].then(( + // x, + // ) => x.getExpirationTime()); + // assertEquals( + // expirationDate?.valueOf(), + // new Date(keyExpirationTime).valueOf(), + // ); + assertExists(await verification.verifications[0].packet); + assertFalse( + await verification.verifications[0].signatureCorrupted.then((x) => + x[0] + ), + ); + assert(await verification.verifications[0].verified); + + //assertEquals( + // await verification.verifications![0].status, + // "Y", + // "Status should be 'Y' due to key expired before signature", + //); + }); + + // // TODO: Add tests for Scenarios involving Key Revocation ('R', 'Y') + // // This requires creating and distributing key revocation certificates. Simulating this is complex and might need mocking OpenPGP.js internal behavior or relying on its revocation handling. + + // it("Scenario: Good signature, key revoked *after* signature time ('R')", async () => { + // // This requires creating a revocation certificate for the key *after* signing. + // assert( + // false, + // "Test not implemented: Simulating key revocation requires revocation certs.", + // ); + // }); + + // it("Scenario: Good signature, key revoked *before* signature time ('Y')", async () => { + // // This requires creating a revocation certificate for the key *before* signing. + // assert( + // false, + // "Test not implemented: Simulating key revocation requires revocation certs.", + // ); + // }); + + // it("Scenario: Signature cannot be checked (Public key available but not signing)", async () => { + // // Generate a key with only encryption or certification usage flags + // const nonSigningKeyPair = await generateKeyPair("AliceNonSigning", { + // usage: ["encrypt"], + // }); // Or ["certify"] + + // const signature = await createDetachedSignature( + // originalData, + // aliceKeyPair.privateKey, + // ); // Signed with a signing key + // const signatureUrl = createInMemoryFile( + // new URL("file:///test/sig_non_signing_key.sig"), + // signature, + // ); + + // // Add the non-signing key to the verifier instead of the actual signing key + // await verifier.addKey(nonSigningKeyPair.publicKey); + + // const verification: Verification = await verifier.verify([ + // originalDataUrl, + // signatureUrl, + // ]); + + // assertExists(verification.verifications, "Should find the signature"); + // assertEquals(verification.verifications.length, 1); + + // const sigVerification = verification.verifications[0]; + // // Key is found, but it's the wrong type of key for verification + // assertExists(sigVerification.keys, "Should find a key"); + + // // Expect verification to fail and report 'E' or potentially 'B' depending on how openpgp.js handles this + // // OpenPGP.js often reports 'E' if the key's capabilities don't match the packet type. + // assertEquals( + // await sigVerification.verified, + // false, + // "Verification should fail with a non-signing key", + // ); + // // We expect 'E' as the most likely status + // assertEquals( + // await sigVerification.status, + // "E", + // "Status should be 'E' with a non-signing key", + // ); + // }); + + // TODO: Add scenarios involving signing subkeys if your verifier needs to distinguish them + // These would require more complex key generation and potentially inspecting the packet details. + }); + + // // --- Scenarios for multiple signatures --- + // describe("when verifying a file with multiple signatures", () => { + // const originalData = new TextEncoder().encode("This file has multiple signatures."); + // let originalDataUrl: URL; + // + // beforeEach(() => { + // originalDataUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt"), originalData); + // }); + // + // + // it("Scenario: All signatures are Good ('G')", async () => { + // // Create signatures by Alice and Bob + // const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey); + // const bobSignature = await createDetachedSignature(originalData, bobKeyPair.privateKey); + // + // const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature); + // const bobSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobSignature); + // + // // Add both signing keys (assume trusted for this scenario) + // await verifier.addKey(aliceKeyPair.publicKey); + // await verifier.addKey(bobKeyPair.publicKey); + // + // // Verify with multiple signature files + // const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobSignatureUrl]); + // + // assertEquals(new Uint8Array(verification.data), originalData); + // assertEquals(verification.dataCorrupted, false); + // assertExists(verification.verifications); + // assertEquals(verification.verifications.length, 2); // Two signatures found + // + // // Check the status of each verification result + // const statuses = await Promise.all(verification.verifications.map(v => v.status)); + // assertArrayIncludes(statuses, ['G', 'G'], "Both signatures should have 'G' status"); + // + // // Check the key IDs found for each verification + // const keyIDs = await Promise.all(verification.verifications.map(async v => (await v.keys)[0]?.getKeyID())); + // assertArrayIncludes(keyIDs.filter(defined), [aliceKeyPair.publicKey.getKeyID(), bobKeyPair.publicKey.getKeyID()]); + // }); + // + // it("Scenario: Some signatures are Good ('G'), others are Bad ('B')", async () => { + // // Create a good signature by Alice + // const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey); + // const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature); + // + // // Create a bad signature by attempting to sign corrupted data with Bob's key + // const corruptedDataForBadSig = corruptData(originalData); + // const bobBadSignature = await createDetachedSignature(corruptedDataForBadSig, bobKeyPair.privateKey); + // const bobBadSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobBadSignature); + // + // + // // Add both signing keys + // await verifier.addKey(aliceKeyPair.publicKey); + // await verifier.addKey(bobKeyPair.publicKey); + // + // // Verify against the original data, but provide one good and one bad signature file + // const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobBadSignatureUrl]); + // + // assertEquals(new Uint8Array(verification.data), originalData); // Verifier should use the original data if found and matching a good sig + // assertEquals(verification.dataCorrupted, false, "Data should not be marked corrupted if at least one good signature matches"); + // + // assertExists(verification.verifications); + // assertEquals(verification.verifications.length, 2); + // + // // Check the status of each verification result + // const statuses = await Promise.all(verification.verifications.map(v => v.status)); + // // Expect one 'G' and one 'B' status + // assertEquals(statuses.filter(s => s === 'G').length, 1); + // assertEquals(statuses.filter(s => s === 'B').length, 1); + // + // // You would also need to check which key corresponded to the 'G' and 'B' status + // // This requires correlating the verification result with the key ID/fingerprint. + // const verifications = await Promise.all(verification.verifications.map(async v => ({ status: await v.status, keyID: (await Promise.all(v.keys))[0]?.getKeyID() }))); + // + // assert(verifications.some(v => v.status === 'G' && v.keyID === aliceKeyPair.publicKey.getKeyID()), "Alice's signature should be Good"); + // assert(verifications.some(v => v.status === 'B' && v.keyID === bobKeyPair.publicKey.getKeyID()), "Bob's signature should be Bad"); + // }); + // + // it("Scenario: Some signatures cannot be checked ('E'), others are Good ('G')", async () => { + // // Create a good signature by Alice + // const aliceSignature = await createDetachedSignature(originalData, aliceKeyPair.privateKey); + // const aliceSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.alice.sig"), aliceSignature); + // + // // Create a signature by Bob but don't add Bob's key to the verifier (will result in 'E') + // const bobSignature = await createDetachedSignature(originalData, bobKeyPair.privateKey); + // const bobSignatureUrl = createInMemoryFile(new URL("file:///test/multi_sig_data.txt.bob.sig"), bobSignature); + // + // + // // Add only Alice's key + // await verifier.addKey(aliceKeyPair.publicKey); + // + // const verification: Verification = await verifier.verify([originalDataUrl, aliceSignatureUrl, bobSignatureUrl]); + // + // assertEquals(new Uint8Array(verification.data), originalData); + // assertEquals(verification.dataCorrupted, false); + // assertExists(verification.verifications); + // assertEquals(verification.verifications.length, 2); + // + // const statuses = await Promise.all(verification.verifications.map(v => v.status)); + // + // assertEquals(statuses.filter(s => s === 'G').length, 1, "One signature should be Good (Alice)"); + // assertEquals(statuses.filter(s => s === 'E').length, 1, "One signature should be 'E' (Bob - missing key)"); + // + // const verifications = await Promise.all(verification.verifications.map(async v => ({ status: await v.status, keyID: (await Promise.all(v.keys))[0]?.getKeyID() }))); + // + // assert(verifications.some(v => v.status === 'G' && v.keyID === aliceKeyPair.publicKey.getKeyID()), "Alice's signature should be Good"); + // // For the 'E' status (missing key), the keyID might be undefined or the partial KeyID from the packet. + // // We'll just check that one status is 'E'. + // assert(verifications.some(v => v.status === 'E'), "One signature should be 'E'"); + // }); + // + // + // // TODO: Continue adding tests for all combinations from the multiple signatures table + // // This requires combining different key states (expired, revoked, untrusted) for different signers + // // within the same verification process. This is the most complex part. + // + // it("Scenario: All signatures Unknown Validity ('U')", async () => { + // // Requires generating signatures with keys that are valid but not ultimately trusted for all signers. + // // Then verifying without establishing a trust path for any key. + // assert(false, "Test not implemented: Simulating unknown trust for all signatures."); + // }); + // + // it("Scenario: At least one Good signature, with others having Key Status issues (e.g., 'X', 'Y', 'R')", async () => { + // // Requires creating signatures with a mix of good keys and expired/revoked keys for different signers. + // assert(false, "Test not implemented: Combining different key states for multiple signers."); + // }); + // + // it("Scenario: All signatures have Key Status issues ('X', 'Y', 'R')", async () => { + // // Requires creating signatures with only expired or revoked keys for all signers. + // assert(false, "Test not implemented: Simulating all signatures with key status issues."); + // }); + // + // it("Scenario: Combination of Bad, Unknown, and Key Status issues", async () => { + // // This is a very complex scenario combining multiple failure types across different signatures. + // assert(false, "Test not implemented: Simulating a complex mix of failure types."); + // }); + // + // it("Scenario: At least one signature is valid, but some Public Keys not available", async () => { + // // Requires providing multiple signature files, but only providing some of the signing keys to the verifier. + // assert(false, "Test not implemented: Simulating missing keys for some signatures in a multi-signature scenario."); + // }); + // + // it("Scenario: At least one signature is valid, but some Public Keys available but not Signing Keys", async () => { + // // Requires providing multiple signature files, and providing a key that is NOT a signing key for one of them. + // assert(false, "Test not implemented: Simulating non-signing keys for some signatures in a multi-signature scenario."); + // }); + // }); +}); +*/ |