diff --git a/src/core/verification.ts b/src/core/verification.ts index 415cce5..1697eaf 100644 --- a/src/core/verification.ts +++ b/src/core/verification.ts @@ -1455,7 +1455,7 @@ export async function verifySignature( options.verifyTimestamps === false || (timestampResult?.isValid ?? true); - const isValid = + let isValid = certResult.isValid && checksumResult.isValid && signatureResult.isValid && timestampValid; // Determine validation status and limitations @@ -1530,6 +1530,30 @@ export async function verifySignature( } } + // If everything else is valid but the issuer is not on the trusted list, + // downgrade from VALID to INDETERMINATE — crypto is sound but trust is unconfirmed + if (status === "VALID" && options.trustListProvider) { + if (trustListMatch && (!trustListMatch.found || trustListMatch.trustedAtTime === false)) { + status = "INDETERMINATE"; + isValid = false; + statusMessage = trustListMatch.found + ? "Signer issuer found in trusted list but was not trusted at signing time" + : "Signer issuer not found in trusted list"; + limitations.push({ + code: "ISSUER_NOT_TRUSTED", + description: statusMessage, + }); + } else if (!trustListMatch && trustListError) { + status = "INDETERMINATE"; + isValid = false; + statusMessage = "Trusted list check failed"; + limitations.push({ + code: "TRUST_LIST_CHECK_FAILED", + description: trustListError, + }); + } + } + const checklist = options.includeChecklist ? buildVerificationChecklist({ options, diff --git a/tests/fixtures/invalid_samples/test-environment-sig-sample.edoc b/tests/fixtures/invalid_samples/test-environment-sig-sample.edoc new file mode 100644 index 0000000..ed07da1 Binary files /dev/null and b/tests/fixtures/invalid_samples/test-environment-sig-sample.edoc differ diff --git a/tests/integration/batch_edoc.test.ts b/tests/integration/batch_edoc.test.ts index f89b330..d3be402 100644 --- a/tests/integration/batch_edoc.test.ts +++ b/tests/integration/batch_edoc.test.ts @@ -527,6 +527,89 @@ describeSensitive("Sensitive Samples Trusted List Checklist", () => { } }); +// Test that a document signed with a test/demo certificate (not in trusted list) +// gets INDETERMINATE status when trust list checking is enabled +const invalidSamplesDir = join(__dirname, "../fixtures/invalid_samples"); +const sensitiveInvalidSamplesDir = join(__dirname, "../fixtures/sensitive/invalid_samples"); +const invalidSamplesDirExists = existsSync(invalidSamplesDir); +const sensitiveInvalidSamplesDirExists = existsSync(sensitiveInvalidSamplesDir); + +const verifyUntrustedIssuer = (dir: string) => { + const testEnvFile = join(dir, "test-environment-sig-sample.edoc"); + const testEnvFileExists = existsSync(testEnvFile); + + (testEnvFileExists ? it : it.skip)( + "should return INDETERMINATE when issuer is not in trusted list", + async () => { + const edocBuffer = readFileSync(testEnvFile); + const container = parseEdoc(edocBuffer); + expect(container.signatures.length).toBeGreaterThan(0); + + const signature = container.signatures[0]; + const result = await verifySignature(signature, container.files, { + verifyTime: signature.signingTime, + checkRevocation: false, + includeChecklist: true, + trustListProvider, + }); + + // Crypto should be sound + expect(result.checksums.isValid).toBe(true); + expect(result.signature?.isValid).toBe(true); + + // But overall should be INDETERMINATE because issuer is not in trusted list + expect(result.isValid).toBe(false); + expect(result.status).toBe("INDETERMINATE"); + expect(result.trustListMatch?.found).toBe(false); + expect(result.limitations).toEqual( + expect.arrayContaining([expect.objectContaining({ code: "ISSUER_NOT_TRUSTED" })]), + ); + + // Verify checklist statuses + expect(getChecklistStatusMap(result)).toEqual({ + document_integrity: "pass", + signature_valid: "pass", + certificate_valid_at_signing_time: "pass", + timestamp_present: "pass", + timestamp_valid: "pass", + timestamp_authority_trusted_at_signing_time: "indeterminate", + certificate_not_revoked_at_signing_time: "skipped", + issuer_trusted_at_signing_time: "fail", + }); + }, + ); + + (testEnvFileExists ? it : it.skip)( + "should return VALID without trust list provider (crypto-only check)", + async () => { + const edocBuffer = readFileSync(testEnvFile); + const container = parseEdoc(edocBuffer); + const signature = container.signatures[0]; + + const result = await verifySignature(signature, container.files, { + verifyTime: signature.signingTime, + checkRevocation: false, + }); + + // Without trust list, crypto-valid signatures should be VALID + expect(result.isValid).toBe(true); + expect(result.status).toBe("VALID"); + }, + ); +}; + +const describeInvalid = invalidSamplesDirExists ? describe : describe.skip; +describeInvalid("Untrusted issuer verification (public samples)", () => { + jest.setTimeout(30000); + verifyUntrustedIssuer(invalidSamplesDir); +}); + +const describeSensitiveInvalid = sensitiveInvalidSamplesDirExists ? describe : describe.skip; +describeSensitiveInvalid("Untrusted issuer verification (sensitive samples)", () => { + jest.setTimeout(30000); + verifyUntrustedIssuer(sensitiveInvalidSamplesDir); +}); + describePublic("Public Samples Trusted List Checklist", () => { const publicFiles = collectFiles(validSamplesDir, [".edoc", ".asice"]);