# Attestor Verification Workflows > How StellaOps turns DSSE bundles into verifiable evidence, how the verification API reports outcomes, and how explainability signals surface in UI/CLI flows. > ⚠️ **2025-11-01 coordination note:** `StellaOps.Attestor.WebService` is failing to compile until downstream fixes land (`Contracts/AttestationBundleContracts.cs` null-coalescing update and scope/token variables restored in `Program.cs`). Verification flows ship in infrastructure/tests, but the WebService hand-off stays blocked — track via `ATTESTOR-73-002` (see Attestor task board). ## 1. Verification flow (API and service contract) - **Entry point.** `POST /api/v1/rekor/verify` deserialises to `AttestorVerificationRequest`. - **Resolution order.** The service tries `uuid`, then canonicalised `bundle`, then `artifactSha256`. At least one selector must be present (`invalid_query` otherwise). - **Optional proof refresh.** `refreshProof=true` forces a Rekor lookup before returning. Proofs are cached in Mongo. - **Signature replay.** Supplying `bundle` lets the service recompute the canonical hash and re-run signature checks; omitting the bundle skips those steps but still validates Merkle proofs and cached policy decisions. - **Auth scopes.** Endpoints demand `attestor.verify` (write scope is also accepted); read-only detail/list APIs require `attestor.read` at minimum. ### 1.1 Request properties | Field | Type | Required | Purpose | |-------|------|----------|---------| | `uuid` | string | optional | Rekor V2 UUID to verify and (optionally) refresh. | | `bundle` | object | optional | DSSE envelope (same shape as submission) for signature re-verification. | | `artifactSha256` | string | optional | Resolve the most recent entry for an attestable artefact digest. | | `subject` | string | optional | Logical subject identifier used for cache/telemetry tagging; defaults to the stored artifact digest. | | `envelopeId` | string | optional | Stable identifier for the DSSE bundle (typically the canonical hash); enables cache lookups. | | `policyVersion` | string | optional | Policy digest/version driving verification; feeds cache keys and observability dimensions. | | `refreshProof` | bool | optional (default `false`) | Pull the current inclusion proof and checkpoint from Rekor before evaluating. | All selectors are mutually compatible; if more than one is set the service uses the first match (`uuid` → `bundle` → `artifactSha256`). ### 1.2 Response schema (`AttestorVerificationResult`) | Field | Type | Description | |-------|------|-------------| | `ok` | bool | `true` when the entry status is `included` **and** no issues were recorded. | | `uuid` | string | Rekor UUID that satisfied the query. Useful for follow-up fetches. | | `index` | number (int64) | Rekor log index, when supplied by the backend. | | `logUrl` | string | Fully-qualified Rekor entry URL for operators and auditors. | | `status` | string | Transparency-log status seen in Mongo (`included`, `pending`, `failed`, …). | | `checkedAt` | string (ISO-8601 UTC) | Timestamp emitted when the response is created. | | `issues` | array[string] | Machine-readable explainability codes. Empty when `ok=true`. | > **Note:** `checkedAt` is recomputed each call; cache hits do not recycle previous timestamps. ### 1.3 Success criteria `ok=true` requires: 1. Entry exists and status equals `included`. 2. Canonical DSSE hash matches the stored bundle hash. 3. Signature re-verification (when a bundle is supplied) succeeds. 4. Inclusion proof validates against the cached or refreshed checkpoint. Any deviation records at least one issue and flips `ok` to `false`. Consumers **must** inspect `issues` rather than inferring from `status` alone. ## 2. Verification report schema `AttestorVerificationResult` carries the flattened summary shown above. When callers request the detailed report (`GET /api/v1/rekor/entries/{uuid}?refresh=true` or via SDK) they receive a `VerificationReport` shaped as follows: ```json { "overallStatus": "pass", "succeeded": true, "policy": { ... }, "issuer": { ... }, "freshness": { ... }, "signatures": { ... }, "transparency": { ... }, "issues": [ "bundle_hash_mismatch" ] } ``` | Field | Type | Description | |-------|------|-------------| | `overallStatus` | string (`pass`, `warn`, `fail`, `skipped`) | Aggregated verdict derived from the individual section statuses. | | `succeeded` | bool | Convenience flag; `true` when `overallStatus ∈ {pass, warn}`. | | `policy` | object | Results from policy evaluation (see below). | | `issuer` | object | Identity/result of the signing entity. | | `freshness` | object | Age analysis relative to policy settings. | | `signatures` | object | Signature validation summary. | | `transparency` | object | Inclusion proof / checkpoint evaluation summary. | | `issues` | array[string] | De-duplicated set drawn from the sections; order is deterministic and stable. | ### 2.1 `policy` | Field | Description | |-------|-------------| | `status` | Section verdict (`pass`, `warn`, `fail`, `skipped`). | | `policyId` / `policyVersion` | DSL identifier and revision used for evaluation. | | `verdict` | Policy outcome (`allow`, `challenge`, `deny`, etc.). | | `issues` | Policy-specific explainability codes (e.g., `policy_rule_blocked`). | | `attributes` | Key/value map emitted by the policy for downstream observability (e.g., applicable rules, matched waivers). | ### 2.2 `issuer` | Field | Description | |-------|-------------| | `status` | Result of issuer validation. | | `mode` | Signing mode detected (`keyless`, `kms`, `unknown`). | | `issuer` | Distinguished name / issuer URI recorded during signing. | | `subjectAlternativeName` | SAN pulled from the Fulcio certificate (keyless) or recorded KMS identity. | | `keyId` | Logical key identifier associated with the signature. | | `issues` | Issuer-specific issues (e.g., `issuer_trust_root_mismatch`, `signer_mode_unsupported:kid`). | ### 2.3 `freshness` | Field | Description | |-------|-------------| | `status` | `fail` when the attestation exceeds `verification.freshnessMaxAgeMinutes`; `warn` when only the warning threshold is hit. | | `createdAt` | Timestamp embedded in the attestation metadata. | | `evaluatedAt` | Server-side timestamp used for age calculations. | | `age` | ISO8601 duration of `evaluatedAt - createdAt`. | | `maxAge` | Policy-driven ceiling (null when unchecked). | | `issues` | `freshness_max_age_exceeded`, `freshness_warning`, etc. | ### 2.4 `signatures` | Field | Description | |-------|-------------| | `status` | Signature validation verdict. | | `bundleProvided` | `true` when canonical DSSE bytes were supplied. | | `totalSignatures` | Count observed in the DSSE envelope. | | `verifiedSignatures` | Number of signatures that validated against trusted keys. | | `requiredSignatures` | Policy / configuration minimum enforced. | | `issues` | Signature codes such as `bundle_payload_invalid_base64`, `signature_invalid`, `signer_mode_unknown`. | ### 2.5 `transparency` | Field | Description | |-------|-------------| | `status` | Inclusion proof / checkpoint verdict. | | `proofPresent` | Whether a proof document was available. | | `checkpointPresent` | Indicates the Rekor checkpoint existed and parsed. | | `inclusionPathPresent` | `true` when the Merkle path array contained nodes. | | `issues` | Merkle/rekor codes (`proof_missing`, `proof_leafhash_mismatch`, `checkpoint_missing`, `proof_root_mismatch`). | ### 2.6 Issue catalogue (non-exhaustive) | Code | Trigger | Notes | |------|---------|-------| | `bundle_hash_mismatch` | Canonical DSSE hash differs from stored value. | Often indicates tampering or inconsistent canonicalisation. | | `bundle_payload_invalid_base64` | DSSE payload cannot be base64-decoded. | Validate producer pipeline; the attestation is unusable. | | `signature_invalid` | At least one signature failed cryptographic verification. | Consider checking key rotation / revocation status. | | `signer_mode_unknown` / `signer_mode_unsupported:` | Signing mode not configured for this installation. | Update `attestorOptions.security.signerIdentity.mode`. | | `issuer_trust_root_mismatch` | Certificate chain does not terminate in configured Fulcio/KMS roots. | Check Fulcio bundle / KMS configuration. | | `freshness_max_age_exceeded` | Attestation older than permitted maximum. | Regenerate attestation or extend policy window. | | `proof_missing` | No inclusion proof stored or supplied. | When running offline, import bundles with proofs or allow warn-level policies. | | `proof_root_mismatch` | Rebuilt Merkle root differs from checkpoint. | Proof may be stale or log compromised; escalate. | | `checkpoint_missing` | No Rekor checkpoint available. | Configure `RequireCheckpoint=false` to downgrade severity. | Downstream consumers (UI, CLI, policy studio) should render human-readable messages but must retain the exact issue codes for automation and audit replay. ## 3. Explainability signals 1. **Canonicalisation.** The service replays DSSE canonicalisation to derive `bundleSha256`. Failures surface as `bundle_hash_mismatch` or decoding errors. 2. **Signature checks.** Mode-aware handling: - `kms` (HMAC) compares against configured shared secrets. - `keyless` rebuilds the certificate chain, enforces Fulcio roots, SAN allow-lists, and verifies with the leaf certificate. - Unknown modes emit `signer_mode_unknown` / `signer_mode_unsupported:`. 3. **Proof acquisition.** When `refreshProof` is requested the Rekor backend may contribute a textual issue (`Proof refresh failed: …`) without stopping evaluation. 4. **Merkle validation.** Structured helper ensures leaf hash, path orientation, and checkpoint root are consistent; each validation failure has a discrete issue code. 5. **Observability.** The meter `attestor.verify_total` increments with `result=ok|failed`; structured logs and traces carry the same `issues` vector for UI/CLI drill-down. All issues are appended in detection order to simplify chronological replay in the Console’s chain-of-custody view. ## 3. Issue catalogue | Code | Trigger | Operator guidance | |------|---------|-------------------| | `bundle_hash_mismatch` | Canonicalised DSSE hash differs from stored bundle hash. | Re-download artefact; investigate tampering or submission races. | | `bundle_payload_invalid_base64` | Payload could not be base64-decoded. | Ensure bundle transport preserved payload; capture original DSSE for forensics. | | `signature_invalid_kms` | HMAC verification failed for `mode=kms`. | Confirm shared secret alignment with Signer; rotate keys if drift detected. | | `signer_mode_unknown` | Entry lacks signer mode metadata and bundle omitted it. | Re-ingest bundle or inspect submission pipeline metadata. | | `signer_mode_unsupported:` | Signer mode is unsupported by the verifier. | Add support or block unsupported issuers in policy. | | `kms_key_missing` | No configured KMS secrets to verify `mode=kms`. | Populate `security:signerIdentity:kmsKeys` in Attestor config before retry. | | `signature_invalid_base64` | One or more signatures were not valid base64. | Bundle corruption; capture raw payload and re-submit. | | `certificate_chain_missing` | `mode=keyless` bundle lacked any certificates. | Ensure Signer attaches Fulcio chain; review submission pipeline. | | `certificate_chain_invalid` | Certificates could not be parsed. | Fetch original DSSE bundle for repair; confirm certificate encoding. | | `certificate_chain_untrusted[:detail]` | Chain failed custom-root validation. | Import correct Fulcio roots or investigate potential impersonation. | | `certificate_san_untrusted` | Leaf SAN not in configured allow-list. | Update allow-list or revoke offending issuer. | | `signature_invalid` | No signature validated with supplied public keys. | Treat as tampering; trigger incident response. | | `proof_missing` | No Merkle proof stored for the entry. | Re-run with `refreshProof=true`; check Rekor availability. | | `bundle_hash_decode_failed` | Stored bundle hash could not be decoded. | Verify Mongo record integrity; re-enqueue submission if necessary. | | `proof_inclusion_missing` | Inclusion section absent from proof. | Retry proof refresh; inspect Rekor health. | | `proof_leafhash_decode_failed` | Leaf hash malformed. | Replay submission; inspect Rekor data corruption. | | `proof_leafhash_mismatch` | Leaf hash differs from canonical bundle hash. | Raises tamper alert; reconcile Rekor entry vs stored bundle. | | `proof_path_decode_failed` | Inclusion path entry malformed. | Same action as above; likely Rekor data corruption. | | `proof_path_orientation_missing` | Inclusion path lacks left/right marker. | File Rekor bug; fallback to mirror log if configured. | | `checkpoint_missing` | Proof lacks checkpoint metadata. | Retry refresh; ensure Rekor is configured to return checkpoints. | | `checkpoint_root_decode_failed` | Checkpoint root hash malformed. | Investigate Rekor/mirror integrity before trusting log. | | `proof_root_mismatch` | Computed root hash != checkpoint root. | Critical alert; assume inclusion proof compromised. | | `Proof refresh failed: …` | Rekor fetch threw an exception. | Message includes upstream error; surface alongside telemetry for debugging. | Future explainability flags must follow the same pattern: short, lowercase codes with optional suffix payload (`code:detail`). ## 4. Worked examples ### 4.1 Successful verification ```json { "ok": true, "uuid": "0192fdb4-a82b-7f90-b894-6fd1dd918b85", "index": 73421, "logUrl": "https://rekor.stellaops.test/api/v2/log/entries/0192fdb4a82b7f90b8946fd1dd918b85", "status": "included", "checkedAt": "2025-11-01T17:06:52.182394Z", "issues": [] } ``` This mirrors the happy-path asserted in `AttestorVerificationServiceTests.VerifyAsync_ReturnsOk_ForExistingUuid`, which replays the entire submission→verification loop. ### 4.2 Tampered bundle ```json { "ok": false, "uuid": "0192fdb4-a82b-7f90-b894-6fd1dd918b85", "index": 73421, "logUrl": "https://rekor.stellaops.test/api/v2/log/entries/0192fdb4a82b7f90b8946fd1dd918b85", "status": "included", "checkedAt": "2025-11-01T17:09:05.443218Z", "issues": [ "bundle_hash_mismatch", "signature_invalid" ] } ``` Derived from `AttestorVerificationServiceTests.VerifyAsync_FlagsTamperedBundle`, which flips the DSSE payload and expects both issues to surface. CLI and Console consumers should display these codes verbatim and provide remediation tips from the table above. ## 5. Validating the documentation - Run `dotnet test src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests` to exercise the scenarios behind the examples. - API integrators can `curl` the verify endpoint and compare responses with the JSON above. - UI/CLI teams should ensure explainability tooltips and runbooks reference the same issue catalogue. Keeping the documentation aligned with the test suite guarantees explainability remains deterministic and audit-friendly. ## 6. Offline bundles & air-gapped verification Stella Ops Attestor now supports packaging attestations for sealed environments and rehydrating them without calling Rekor: - **Export bundles.** `POST /api/v1/attestations:export` accepts either a list of Rekor UUIDs or filter criteria (`subject`, `type`, `issuer`, `scope`, `createdAfter|Before`, `limit`, `continuationToken`) and returns an `attestor.bundle.v1` document. Each item contains the attestation entry, canonical DSSE payload (base64), optional proof payload, and metadata. Responses include a `continuationToken` so callers can page through large result sets (limits default to 100 and are capped at 200). JSON content is required and requests are gated by the `attestor.read` scope. - **Import bundles.** `POST /api/v1/attestations:import` ingests the bundle document, upserts attestation metadata, and restores the canonical DSSE/proof into the configured archive store. The S3 archive integration must be enabled; the response reports how many entries were imported versus updated, any skipped items, and issue codes (`bundle_payload_invalid_base64`, `bundle_hash_mismatch`, `archive_disabled`, …). - **Offline verification.** When replaying verification without log connectivity, submit the DSSE bundle and set `offline=true` on `POST /api/v1/rekor/verify`. The service reuses imported proofs when present and surfaces deterministic explainability codes (`proof_missing`, `proof_inclusion_missing`, …) instead of attempting Rekor fetches. Tests `AttestorBundleServiceTests.ExportAsync_AppliesFiltersAndContinuation`, `AttestationBundleEndpointsTests`, `AttestorVerificationServiceTests.VerifyAsync_OfflineSkipsProofRefreshWhenMissing`, and `AttestorVerificationServiceTests.VerifyAsync_OfflineUsesImportedProof` exercise the exporter/importer, API contracts, and the offline verification path with and without witness data.