Commit 128746f
authored
* feat: attestation trust boundary for Verification Context v1.0 (#47)
Implement the #47 attestation trust boundary, mirroring
qwed-verification's architecture (ADR-005 self-attestation stage,
fail-closed per its Issue #188 contract):
- new qwed_infra/attestation.py: ES256 (ECDSA P-256) JWT attestation
service with ephemeral key lifecycle auditing, revocation registry,
8192/4096-byte boundary checks, generic rejection messages, and the
never-None AttestationResult fail-closed contract (CRYPTO_UNAVAILABLE ->
UNVERIFIABLE; VERIFIED-without-proof or signing failure -> BLOCKED)
- enforce_trust_decision() in diagnostics.py: single consumption-side
gate validating signature/issuer/expiry/revocation plus claim bindings
(status match, query_hash == sha256(formal_statement), proof_hash ==
diagnostic proof_ref). Any failure -> BLOCKED. Closes the Greptile P1
statement-overclaim vector: a token minted for one statement cannot
admit a broader one.
- bridge: presence-only attestation policy replaced by full enforcement;
VERIFIED + valid token -> ADMIT with Proof.configuration bound to the
attested identity (issuer/jti/query_hash), so distinct accepted
attestations yield distinct document proof_refs; VERIFIED without
token -> UNVERIFIABLE (unchanged); invalid token -> BLOCKED
- InfraDiagnosticResult retains proof_data (canonical evidence string)
on VERIFIED results so attestations can bind to the exact evidence
commitment; mint_diagnostic_attestation() helper issues tokens bound
to a diagnostic's own proof_ref
- pyproject: add pyjwt + cryptography dependencies
Tests: 15 new trust-boundary tests (forged/tampered/expired/revoked
tokens -> BLOCKED/DENY; claim binding status/query/proof enforced;
statement overclaim rejected; distinct jti -> distinct proof_refs;
end-to-end guard attest->enforce->ADMIT flows). Existing fake-token tests
updated to mint real bound attestations.
501 tests pass; ruff clean; boundary check passes.
* fix: attestation review round - query binding, singleton race, serialization, scanner findings
CodeRabbit Critical / CodeAnt+Sentry Critical: _apply_attestation_policy
did not forward formal_statement to enforce_trust_decision, so the
qwed.query_hash binding check was skipped - a token minted for a narrow
statement could admit a broader one through the bridge. Thread
formal_statement through as the enforcement query and add a bridge-level
regression test (narrow-minted token + broad submitted statement ->
BLOCKED/DENY via trust_gate.claims_query_mismatch).
QWED Security (codeguard): remove the pre-verification base64 payload
decode from verify_attestation - jwt.decode performs payload parsing
itself after signature verification, so this layer never decodes
attacker-controlled bytes. Size/segment boundary checks retained;
RecursionError handler added for deeply-nested payloads inside JWT JSON
parsing; explicit isinstance(str) guard rejects non-string tokens.
QWED Security (pattern_scan): trust_gate.attestation_verification_error
now records error_type (exception class name) instead of str(exc), so no
internal detail can leak into developer_fields.
CodeAnt Major (race): get_attestation_service() now uses double-checked
locking - concurrent first callers can no longer install different
ephemeral-key services, which made valid tokens fail verification
intermittently.
CodeAnt Major (serialization): proof_data is now round-tripped through
to_dict()/from_dict() with an integrity check - a serialized pair where
sha256(proof_data) != proof_ref raises ValueError, so persisted VERIFIED
diagnostics can still mint attestations after reload and tampered pairs
fail closed.
CodeRabbit nitpicks: removed unused conftest helper/fixture; documented
the pyjwt !=2.12.1 exclusion (GHSA-jq35-7prp-9v3f, PyJWK path; PEM keys
used here, excluded defensively).
502 tests pass; ruff clean on changed files; boundary check passes.
* fix: proof-pair integrity on empty string, from_dict complexity, rebuilt-object field
CodeRabbit Major: the from_dict integrity check skipped validation when
proof_data was an empty string (truthiness guard), letting a tampered
empty proof_data pass with the original proof_ref. The hash comparison
now runs whenever both fields are present.
SonarCloud: from_dict cognitive complexity 16 > 15 - extracted status
parsing (_parse_status) and proof-pair validation (_validated_proof_pair)
into module-level helpers; from_dict is now a flat sequence of validated
field reads.
Sentry LOW: _cast_status_preserving did not initialize the new proof_data
attribute when rebuilding malformed diagnostic objects via object.__new__,
so a subsequent to_dict() would raise AttributeError. Rebuilt objects now
carry getattr(result, 'proof_data', None).
502 tests pass; ruff clean; boundary check passes.
* fix: enforce proof commitment on every construction path + drop proof_data from document payloads
CodeRabbit Major: _validated_proof_pair only guarded from_dict, so a
directly-constructed VERIFIED result could carry altered proof_data while
attestation enforcement checked only proof_ref. __post_init__ now fails
closed for VERIFIED results unless proof_data is a non-empty string whose
sha256 commits to proof_ref (all paths: factory, from_dict, direct). The
dataclass is frozen, preventing later divergence. Regression tests cover
mismatched/empty/missing proof_data on direct construction.
Sentry MEDIUM: to_dict() now includes proof_data, which leaked into VC
evidence payloads - every VERIFIED document carried its evidence twice
(structured dict + raw canonical JSON string). _build_evidence_payload
drops proof_data like proof_ref; it remains an issuance aid on the
diagnostic object for mint_diagnostic_attestation.
506 tests pass; ruff clean; boundary check passes.
* fix: empty-string proof_data test precision + missing-proof-hash issuance guard and block reason
CodeRabbit Minor: test_direct_construction_empty_proof_data_rejected used
an all-zero proof_ref, so the pair-integrity mismatch fired before the
dedicated non-empty proof_data check. The fixture now uses sha256 of an
empty string as proof_ref so the test reaches (and proves) the VERIFIED
non-empty proof_data rejection itself.
Sentry LOW: a VERIFIED token minted without proof_data carried no
qwed.proof_hash claim and was blocked with a generic claims_proof_mismatch
reason. Two changes:
- create_attestation now raises ValueError at issuance when verified=True
without proof_data - unbindable tokens cannot exist.
- _validate_attestation_claims distinguishes trust_gate.claims_proof_missing
from claims_proof_mismatch for accurate diagnostics.
Regressions: missing-proof-hash enforcement yields claims_proof_missing;
issuance guard rejects VERIFIED-without-proof_data.
508 tests pass; ruff clean; boundary check passes.
* test: extract inputs out of pytest.raises in direct-construction proof_data tests
SonarCloud: only the InfraDiagnosticResult constructor invocation remains
inside the exception-testing block; status/fields/digest constants are
pre-built variables. Applied to all three direct-construction tests
(mismatched/empty/missing) since they share the same shape.
* fix: mandatory query binding + land missing trust-gate production code (Sentry/CodeAnt/SonarCloud)
Sentry HIGH: enforce_trust_decision treated query as fully optional, so an
external caller could omit it and skip the qwed.query_hash binding check -
reusing one statement's attestation for another whenever status and
proof_hash matched. A provided attestation_token now REQUIRES query:
omitting it raises ValueError (API misuse, fail-loud) instead of silently
downgrading to an unbound check. The bridge path always passes
formal_statement; regression test covers token-without-query misuse.
Also lands the production half of the CodeRabbit Major round that was
inadvertently left out of 49e3e23 (tests shipped, code did not - root
cause of the CI failures):
- create_attestation rejects status='VERIFIED' with verified=False before
signing, and VERIFIED without proof_data at all times.
- _validate_attestation_claims distinguishes trust_gate.claims_proof_missing
from claims_proof_mismatch, and enforces claims_verified_mismatch so a
hand-signed legacy token whose claims do not affirm verification is
rejected even with a matching proof_hash.
SonarCloud: pytest.raises blocks in the direct-construction and issuance-
guard tests now contain a single invocation - inputs are extracted into
pre-built variables.
510 tests pass; ruff clean on changed files.
* fix: proof_data lifecycle invariants + targeted legacy-payload error (Sentry)
Sentry MEDIUM (backward compatibility): deserializing a pre-#47 VERIFIED
payload (no proof_data key) now fails with a TARGETED message explaining
that pre-attestation payloads cannot be loaded under VC v1.0 attestation
rules and the verification must be re-run - instead of the generic
non-empty-proof_data error. Deliberately fail-closed: without its
canonical evidence the diagnostic can never be attested, so loading it as
VERIFIED would violate the trust boundary.
Sentry LOW (state consistency): dataclasses.replace() status demotions
retained a stale proof_data on non-VERIFIED results. Two changes:
- __post_init__ enforces proof_data is None for every non-VERIFIED status
(mirrors the existing proof_ref invariant).
- the bridge's decision_status override clears proof_data alongside
proof_ref when demoting.
Regressions: legacy payload load -> ValueError with legacy guidance;
BLOCKED constructed with proof_data -> rejected; bridge demotion path
produces BLOCKED/DENY cleanly under the new invariant.
513 tests pass; ruff clean; boundary check passes.
* refactor: extract __post_init__ invariants into helpers (SonarCloud cognitive complexity)
__post_init__ exceeded the allowed cognitive complexity (16 > 15) after
the #47 proof_data invariants were added. Split into two single-purpose
validators - _validate_verified_invariants (proof_ref presence,
audit_trace requirement, evidence commitment, non-empty proof_data) and
_validate_non_verified_invariants (null proof_ref/proof_data) -
__post_init__ is now a thin dispatcher. All error messages unchanged.
513 tests pass; ruff clean.
1 parent 9a09356 commit 128746f
9 files changed
Lines changed: 1713 additions & 80 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
| |||
0 commit comments