/* 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>; let bobKeyPair: Awaited>; let aliceWithSubkeyKeyPair: Awaited>; 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; 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."); // }); // }); }); */