Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions docs/DETACHED_VERIFIER.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ command is in the current preview checkout, not the pinned `v1.3.0` stable
package.

After the signature and hashes pass, witness replay also checks the claimed
timeline: the attestation issue time and bundle creation time must use exact
UTC milliseconds; the attestation cannot predate the challenge issue, and
bundle creation must follow the attestation, precede challenge expiry, and
have occurred by the verifier's replay clock. These fields are supplied
claims, not trusted timestamps. A resealed bundle with an impossible
creation time is rejected even when its signature remains valid.
timeline: evidence generation, attestation issue, and bundle creation must
use exact UTC milliseconds. A receipt's generation time must also use that
spelling when present. The receipt cannot postdate its evidence; evidence
cannot postdate its signed attestation; the attestation cannot predate the
challenge issue; and bundle creation must follow the attestation, precede
challenge expiry, and have occurred by the verifier's replay clock. These
fields are supplied claims, not trusted timestamps. A resealed bundle with
an impossible timeline is rejected even when its signature remains valid.

## Claim boundary

Expand Down
8 changes: 5 additions & 3 deletions docs/VERIFIER_PLURALITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,11 @@ challenge freshness and binding, and session signature. An explicit
challenge's inclusive issue-to-expiry interval, and cannot precede the
bundle's or attestation's canonical creation time or exceed the single local
replay instant. The underlying bundle replay also rejects malformed UTC
millisecond creation times, an attestation before challenge issue, a bundle
before attestation or after challenge expiry, and a bundle created after the
replay instant. A malformed, premature, or future claimed check time is a
millisecond evidence, attestation, or bundle times; an optional receipt time
with malformed spelling; a receipt after its evidence; evidence after
attestation; an attestation before challenge issue; a bundle before
attestation or after challenge expiry; and a bundle created after the replay
instant. A malformed, premature, or future claimed check time is a
reported conflict. These times are artifact and signer assertions. The
comparison cannot prove the signer actually performed the check at that time;
a trusted time source would require separate evidence. The result includes
Expand Down
9 changes: 8 additions & 1 deletion docs/aml-witness-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ function canonicalTime(value) {
function bundleTimeFailure(bundle, now) {
const attested = canonicalTime(bundle.attestation.issued_at);
const created = canonicalTime(bundle.created_at);
if (attested === null || created === null) return 'AML_WITNESS_BUNDLE_TIME_INVALID';
const evidenced = canonicalTime(bundle.evidence.generated_at);
const hasReceiptTime = Object.hasOwn(bundle.evidence.receipt, 'generated_at');
const receipted = hasReceiptTime ? canonicalTime(bundle.evidence.receipt.generated_at) : null;
if (attested === null || created === null || evidenced === null ||
(hasReceiptTime && receipted === null)) return 'AML_WITNESS_BUNDLE_TIME_INVALID';
if ((hasReceiptTime && receipted > evidenced) || evidenced > attested) {
return 'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID';
}
if (attested < Date.parse(bundle.challenge.issued_at) || created < attested ||
created > Date.parse(bundle.challenge.expires_at)) {
return 'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID';
Expand Down
5 changes: 3 additions & 2 deletions independent/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ test fixtures only; a historical override is not a live freshness check.
- challenge schema, nonce, issue time and expiry;
- equality with a separately retained, fresh challenge when supplied;
- challenge/evidence equality inside the session attestation;
- canonical UTC millisecond attestation/bundle times and their claimed order
from challenge issue through local replay and challenge expiry;
- canonical UTC millisecond evidence, attestation, and bundle times (and a
receipt time if present), with claimed order from receipt through evidence,
attestation, bundle creation, and the local replay clock;
- P-256 public key is on curve;
- public-JWK SHA-256 fingerprint;
- raw 64-byte P-256 ECDSA signature over the canonical attestation payload.
Expand Down
5 changes: 4 additions & 1 deletion independent/python/verify_witness.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ def canonical_utc_millis(value):

attested = canonical_utc_millis(attestation.get("issued_at"))
created = canonical_utc_millis(bundle.get("created_at"))
if attested < parse_time(challenge["issued_at"]) or created < attested or \
evidenced = canonical_utc_millis(evidence.get("generated_at"))
receipted = canonical_utc_millis(receipt["generated_at"]) if "generated_at" in receipt else None
if (receipted is not None and receipted > evidenced) or evidenced > attested or \
attested < parse_time(challenge["issued_at"]) or created < attested or \
created > parse_time(challenge["expires_at"]):
return False, "AML_PY_BUNDLE_TIME_ORDER_INVALID"
if created > now:
Expand Down
51 changes: 50 additions & 1 deletion scripts/check-signed-quorum-python.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { webcrypto } from 'node:crypto';
import { canonicalJSONStringifyBrowser, sealBrowserReceipt, sha256Browser } from '../docs/aml-browser-integrity.js';
import { createBrowserEvidence } from '../docs/aml-browser-evidence.js';
import { browserEvidencePayload, createBrowserEvidence } from '../docs/aml-browser-evidence.js';
import { createVerificationChallenge } from '../docs/aml-verification-challenge.js';
import { createSessionAttestation } from '../docs/aml-session-attestation.js';
import { createWitnessBundle, verifyWitnessBundle, witnessBundlePayload } from '../docs/aml-witness-bundle.js';
Expand Down Expand Up @@ -638,6 +638,53 @@ try {
assert.equal(jsResult.reason, jsReason, name);
}

// Sign altered evidence afresh so both runtimes reach the chronology check
// after all hashes and the session signature pass.
for (const [name, receiptTime, evidenceTime, jsReason, pyReason] of [
['receipt-after-evidence', 'future', 'current',
'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID', 'AML_PY_BUNDLE_TIME_ORDER_INVALID'],
['evidence-after-attestation', 'past', 'future',
'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID', 'AML_PY_BUNDLE_TIME_ORDER_INVALID'],
['malformed-receipt-time', 'malformed', 'current',
'AML_WITNESS_BUNDLE_TIME_INVALID', 'AML_PY_BUNDLE_TIME_INVALID'],
['missing-evidence-time', 'past', 'missing',
'AML_WITNESS_BUNDLE_TIME_INVALID', 'AML_PY_BUNDLE_TIME_INVALID']
]) {
const receipt = await sealBrowserReceipt({ schema: 'aml-dom-receipt/1', decisions: [],
generated_at: receiptTime === 'future' ? new Date(Date.now() + 30000).toISOString() :
receiptTime === 'malformed' ? '2030-02-30T00:00:00.000Z' :
new Date(Date.now() - 1000).toISOString() });
const candidateEvidence = await createBrowserEvidence(receipt, { url: 'https://example.test/' });
if (evidenceTime === 'future') {
candidateEvidence.generated_at = new Date(Date.now() + 30000).toISOString();
} else if (evidenceTime === 'missing') {
delete candidateEvidence.generated_at;
}
candidateEvidence.integrity.value = await sha256Browser(browserEvidencePayload(candidateEvidence));
const candidateChallenge = createVerificationChallenge({ ttl_ms: 120000 });
const candidateAttestation = await createSessionAttestation({
evidence: candidateEvidence, challenge: candidateChallenge
});
const candidateBundle = {
schema: 'aml-witness-bundle/1', prototype: true, created_at: new Date().toISOString(),
evidence: candidateEvidence, challenge: candidateChallenge, attestation: candidateAttestation
};
candidateBundle.integrity = { schema: 'aml-integrity/1', algorithm: 'SHA-256',
canonicalization: 'sorted-json-v1',
value: await sha256Browser(witnessBundlePayload(candidateBundle)) };
const jsResult = await verifyWitnessBundle(candidateBundle, {
expectedChallenge: candidateChallenge
});
assert.equal(jsResult.reason, jsReason, name);
assert.equal(jsResult.checks.session_signature, true, name);
const pyRun = spawnSync('python3', [witnessPython,
save(`${name}-bundle.json`, candidateBundle), '--challenge',
save(`${name}-challenge.json`, candidateChallenge)],
{ encoding: 'utf8', env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
assert.equal(pyRun.status, 1, `${name}: ${pyRun.stderr}`);
assert.equal(JSON.parse(pyRun.stdout).reason, pyReason, name);
}

console.log(JSON.stringify({
protocol: 'aml-signed-quorum-python-cross-runtime/1',
passed: true, cases: checks,
Expand All @@ -652,6 +699,8 @@ try {
'fixed_vector_bundle_calendar', 'fixed_vector_bundle_before_attestation',
'fixed_vector_bundle_after_expiry', 'fixed_vector_bundle_after_replay',
'fixed_vector_attestation_before_challenge',
'signed_receipt_after_evidence', 'signed_evidence_after_attestation',
'signed_malformed_receipt_time', 'signed_missing_evidence_time',
'null_input_expectations_rejected', 'null_held_policy_rejected'],
claim_boundary: 'Project-authored Python and JavaScript implementations agree on these saved quorum, held-policy, and live witness-replay cases. This is not an external witness, signer identity, or independent certification.'
}, null, 2));
Expand Down
40 changes: 39 additions & 1 deletion test/detached-witness.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'node:assert/strict';

import { canonicalJSONStringifyBrowser, sealBrowserReceipt,
sha256Browser } from '../docs/aml-browser-integrity.js';
import { createBrowserEvidence } from '../docs/aml-browser-evidence.js';
import { browserEvidencePayload, createBrowserEvidence } from '../docs/aml-browser-evidence.js';
import { createVerificationChallenge, verifyVerificationChallenge } from '../docs/aml-verification-challenge.js';
import {
createSessionAttestation,
Expand Down Expand Up @@ -184,6 +184,44 @@ test('bundle creation refuses an attestation dated before challenge issue', asyn
/AML_WITNESS_BUNDLE_TIME_ORDER_INVALID/);
});

test('receipt and evidence time claims cannot postdate their signed attestation', async () => {
for (const [name, mutateReceipt, mutateEvidence, reason] of [
['receipt-after-evidence', receipt => {
receipt.generated_at = new Date(Date.now() + 30000).toISOString();
}, () => {}, 'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID'],
['evidence-after-attestation', () => {}, evidence => {
evidence.generated_at = new Date(Date.now() + 30000).toISOString();
}, 'AML_WITNESS_BUNDLE_TIME_ORDER_INVALID'],
['invalid-receipt-date', receipt => {
receipt.generated_at = '2030-02-30T00:00:00.000Z';
}, () => {}, 'AML_WITNESS_BUNDLE_TIME_INVALID'],
['missing-evidence-time', () => {}, evidence => {
delete evidence.generated_at;
}, 'AML_WITNESS_BUNDLE_TIME_INVALID']
]) {
const source = await createFixture();
const receipt = structuredClone(source.receipt);
mutateReceipt(receipt);
await sealBrowserReceipt(receipt);
const evidence = await createBrowserEvidence(receipt, { url: 'https://example.test/' });
mutateEvidence(evidence);
evidence.integrity.value = await sha256Browser(browserEvidencePayload(evidence));
const challenge = createVerificationChallenge({ ttl_ms: 120000 });
const attestation = await createSessionAttestation({ evidence, challenge });
await assert.rejects(createWitnessBundle({ evidence, challenge, attestation }),
new RegExp(reason), name);
const bundle = {
schema: 'aml-witness-bundle/1', prototype: true, created_at: new Date().toISOString(),
evidence, challenge, attestation
};
bundle.integrity = { schema: 'aml-integrity/1', algorithm: 'SHA-256',
canonicalization: 'sorted-json-v1', value: await sha256Browser(witnessBundlePayload(bundle)) };
const result = await verifyWitnessBundle(bundle, { expectedChallenge: challenge });
assert.equal(result.reason, reason, name);
assert.equal(result.checks.session_signature, true, name);
}
});

test('signed witness cannot pass before its challenge issue time', async () => {
const { bundle, challenge } = await createFixture();
const beforeIssue = Date.parse(challenge.issued_at) - 1;
Expand Down
Loading