Signed reports
Every scan produces a cryptographically signed report. Reports are dual-signed — HMAC-SHA256 for Cavexia's internal integrity audits, and Ed25519 for offline verification by anyone holding the published Cavexia public key.
What gets signed
The signature covers the full report payload returned by GET /api/report/{id}: findings, risk score, scan metadata, and the report version. The signatures array itself is excluded from the canonicalized input, so a report can be re-signed for verification.
- All findings (severity, title, description, remediation, excerpt, category)
- Per-server risk scores and the aggregate risk score
- Scan timestamp, scan ID
- The literal field
version: 2— verifiers that don't recognise the version must refuse to verify rather than guess.
Algorithms
Canonicalization sorts object keys alphabetically at every nesting level. Arrays preserve order. Two semantically-identical reports always produce the same signature regardless of how the JSON was serialized.
The HMAC signature lets Cavexia internally verify report integrity using a server-side secret. The Ed25519 signature lets anyone — your CI pipeline, an auditor, a customer — verify a report offline using the public key. Both signatures cover the same canonical payload; either one verifying is sufficient proof of authenticity.
Public key
The current Cavexia public key is published at a stable, cacheable URL:
GET https://cavexia.com/.well-known/cavexia-signing.pub -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA... -----END PUBLIC KEY-----
Historical public keys are addressable by keyId so older signed reports continue to verify after key rotation:
GET https://cavexia.com/api/signing-keys/cavexia-2026-05 -----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----
Response shape
Signatures are carried in the JSON body and the response header so both archived bodies and proxy-stripped HEAD responses can verify.
HTTP/1.1 200 OK
Content-Type: application/json
X-Cavexia-Signatures: [{"alg":"HMAC-SHA256","value":"..."},{"alg":"Ed25519","value":"...","keyId":"cavexia-2026-05"}]
Cache-Control: public, max-age=86400, immutable
{
"version": 2,
"id": "cf3777e4-2b6f-4aa8-92c9-a6d743e29eac",
"scannedAt": "2026-05-21T15:00:00.000Z",
"riskScore": 100,
"totalFindings": 6,
"findings": { /* ... */ },
"signatures": [
{ "alg": "HMAC-SHA256", "value": "<base64>" },
{ "alg": "Ed25519", "value": "<base64>", "keyId": "cavexia-2026-05" }
]
}How to verify
Three paths depending on your threat model and your access to the Cavexia API:
Run the CLI against any local or hosted report. Fetches the public key (cached after first use), verifies the Ed25519 signature offline, exits 0 on success.
$ cavexia verify ./cavexia.json ✓ Signature valid (Ed25519, keyId=cavexia-2026-05) report: cf3777e4-2b6f-4aa8-92c9-a6d743e29eac scanned: 2026-05-21T15:00:00.000Z
Drop the verify action into your workflow. Fails the PR check if cavexia.json is missing, unsigned, or tampered.
- uses: cavexia/verify-action@v1
with:
report: cavexia.jsonHit GET /api/report/{id} and compare the returned signature bytes against your archived copy. Simplest path; requires Cavexia's availability and the original scan ID.
Key rotation
Cavexia rotates the signing keypair quarterly. New reports are signed under the new key; reports signed under prior keys remain verifiable indefinitely via the keyed lookup endpoint. If a key is ever compromised, the keyId scopes the blast radius to a single rotation period.
Verifiers should always pin a specific keyId from the report's signatures array — not the current public key — so a verified report stays verified across rotations.
Audit use cases
- SOC 2 evidence: attach signed reports to control reviews as proof of scan history.
- Customer assurance: send a signed scan to a customer who asks about MCP supply-chain risk.
- EU AI Act (high-risk systems): archive periodic scans of agent tooling as part of a risk-management file.
- Incident response: a signed pre-incident scan lets you prove the configuration you were running before a compromise.
- Supply-chain provenance: include the report ID in your release notes so downstream consumers can verify the security posture you scanned at release time.