Per-commit attribution
(docs/planning/specs/q-gov-attribution-audit_spec.md) records who
claimed to have produced an artifact, but the trailers are
unsigned — anyone with write access to the submodule can forge
Task-Id:, Skill:, Account: lines. For artifacts that other
agents read as authoritative inputs (curated datasets, validated
hypotheses, blessed analyses), we need a cryptographic attestation:
"contributor X (with key K) attests that artifact Y has content hash
H at version V". This task adds Ed25519 signing keys per contributor
account, a signature column on artifact_versions, and a verifier
that re-checks signatures on read.
Effort: thorough
migrations/20260428_artifact_attestations.sql:CREATE TABLE contributor_keys (
contributor_id TEXT NOT NULL,
key_id TEXT NOT NULL, -- short fingerprint
public_key BYTEA NOT NULL, -- 32-byte Ed25519 public key
algorithm TEXT NOT NULL DEFAULT 'ed25519',
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
revoke_reason TEXT,
PRIMARY KEY (contributor_id, key_id)
);
CREATE TABLE artifact_attestation (
id BIGSERIAL PRIMARY KEY,
artifact_id TEXT NOT NULL,
version_number INT NOT NULL,
content_hash TEXT NOT NULL,
contributor_id TEXT NOT NULL,
key_id TEXT NOT NULL,
signature BYTEA NOT NULL,
attested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
verified_at TIMESTAMPTZ,
verification_status TEXT
CHECK (verification_status IN ('valid','invalid','revoked','unknown')),
UNIQUE (artifact_id, version_number, contributor_id, key_id)
);
CREATE INDEX idx_aa_artifact ON artifact_attestation(artifact_id);scidex/atlas/artifact_attestation.py:register_key(contributor_id, public_key_b64) -> key_id.revoke_key(contributor_id, key_id, reason).attest(artifact_id, version_number, content_hash,
contributor_id, key_id, signature_b64) — verifies andverify(artifact_id, version_number) -> list[Attestation]verification_status."scidex-attestation-v1\n{artifact_id}\n{version_number}\n
{content_hash}\n{contributor_id}\n{key_id}" — pinned in acryptography>=41 Ed25519PublicKey forscripts/attest_artifact.py/api/atlas/attestations with the public-key fingerprintPOST /api/atlas/keys/register {contributor_id,
public_key_b64} → 201.POST /api/atlas/keys/revoke {contributor_id, key_id,
reason}.POST /api/atlas/attestations {artifact_id, version_number,
content_hash, contributor_id, key_id, signature_b64} → 201GET /api/atlas/attestations/{artifact_id} returns the listverified_at is null or older than the contributor'sverification_status.senate_alerts of kindattestation_invalid (severity high).
'global' arena. Implemented as a recurring sweep that idem-tests/test_artifact_attestation.py:verification_status becomes revoked for(artifact, version, hash) triple.
q-gov-attribution-audit — establishes the contributor identityscidex/atlas/artifact_registry.compute_content_hash — hashesq-trust-provenance-integrity-scanner — verifies attestationsmigrations/20260428_artifact_attestations.sql): Created contributor_keys and artifact_attestation tables with all specified columns, indexes, FK constraints, and CHECK constraints. Applied successfully.scidex/atlas/artifact_attestation.py): Implemented register_key(), revoke_key(), attest(), verify(), verify_all_pending(), and get_attestations_for_artifact(). Uses cryptography>=41 Ed25519PublicKey for signature verification. Canonical message format pinned in CANONICAL_MESSAGE_TEMPLATE constant.POST /api/atlas/keys/register — register Ed25519 public keyPOST /api/atlas/keys/revoke — revoke a keyPOST /api/atlas/attestations — create attestation (verifies signature)GET /api/atlas/attestations/{artifact_id} — get attestations with current verification statusscripts/attest_artifact.py): Signs canonical message with Ed25519 private key PEM and POSTs to API.scidex/senate/scheduled_tasks.py): Added attestation-verifier daily task that re-verifies all pending attestations and emits senate_alerts for invalid ones.tests/test_artifact_attestation.pyNote: Pre-existing syntax error in api.py at line 35657 (f-string unmatched paren) exists in origin/main and is unrelated to this implementation.