Protocol Specification

The Veil Protocol

A Veil Certificate is a signed, timestamped, externally anchored attestation that a single AI inference request kept identity data and AI processing isolated throughout the pipeline. This page describes the protocol: what the certificate contains, how it is produced, the five consistency checks the Witness runs, and how an obtained certificate is verified end-to-end against public standards without trusting our code.

Last Updated: April 2026

The Veil Protocol covers isolation attestation— per-request proof that the split-knowledge boundary held. The evidence layer covers decision provenance— per-decision records of what the AI output and why. Both surfaces use the same cryptographic primitives (Ed25519, SHA-256 hash chains, RFC 3161, Sigstore Rekor), but they attest to different things. This page is the protocol reference; the evidence layer has its own page.

What a Veil Certificate Is

For every inference request that passes through the Gateway, the protocol produces a signed envelope containing: a fail-closed Gateway claim, best-effort claims from the four downstream pipeline services (dsa-bridge, dsa-sanitizer, dsa-ai, dsa-audit), the Witness verdict over five consistency checks, an RFC 3161 timestamp token, and — on a best-effort async retry path — a Sigstore Rekor entry reference.

The certificate is retrievable at three gateway endpoints, one per view: /api/v1/veil/certificate/{request_id} (full envelope, technical-proof view), /api/v1/veil/certificate/{request_id}/summary (DPO summary), and /api/v1/veil/certificate/{request_id}/regulatory (regulatory-mapping view). It is not a credential and not a license. It is an attestation about one request, independently verifiable against external trust anchors.

Attestation scope

Attests:

Does not attest:

The Five Consistency Checks

A Veil Certificate carries two label axes the Witness computes independently:

The five checks the Witness runs, in code order:

  1. 1

    Signature validity

    Every claim’s Ed25519 signature must verify against that service’s published public key. In addition, the service’s typed proto fields (IsolationProbe, QiScore, ModelUsed, etc.) are cross-checked against the signed canonical payload — a mismatch means the outer fields were tampered with after signing. Any mismatch is fatal → FAILED.

  2. 2

    Claim completeness

    All four expected downstream services must be present to earn completeness = FULL; otherwise PARTIAL, with the missing services listed. Incomplete on its own only degrades overall_verdict to PARTIAL — unless the missing service is dsa-ai, in which case the isolation-probe check has no claim to consume and overall_verdict falls to FAILED.

  3. 3

    Temporal consistency

    Claim timestamps must fall within a 120-second skew window and arrive in the expected pipeline order (dsa-bridgedsa-sanitizer dsa-aidsa-audit). Either sub-check failing degrades overall_verdict to PARTIAL.

  4. 4

    Data visibility

    Sandbox B (dsa-ai) must not claim to have seen any PII field name matching the pattern list (customer_id, email, name, first_name, last_name, phone, address, ssn, dob, date_of_birth, identity_id). A violation is fatal → FAILED.

  5. 5

    Isolation probe

    Sandbox B’s claim carries an IsolationProbe status. Only VERIFIED passes; BREACHED, LOCKED, and UNKNOWN all fail this check → FAILED.

The Signing Process

Each certificate is produced through a four-stage pipeline. The first two stages run synchronously on the inference path; the external-anchoring stage runs asynchronously after Witness signing. Every stage is independently verifiable.

  1. 1

    Per-service Ed25519 claim

    Each pipeline service holds a per-service Ed25519 keypair. When it processes a request, it emits a claim: its service identifier, the request identifier, a structured summary of what data it saw, and its timestamp. It signs the claim with its private key, producing a canonical payload binding to the typed proto fields. Keys are managed through the deployment’s secret manager (HashiCorp Vault, AWS KMS, or Azure Key Vault).

  2. 2

    Witness verification and envelope signature

    The Witness collects claims emitted during the request, runs the five consistency checks, and composes the certificate envelope: all claims, the verdict axes (completeness, overall_verdict), the missing-service list (if any), and the signing key identifier. The envelope is signed with the Witness’s Ed25519 key; this signature is the certificate’s primary trust anchor.

  3. 3

    External anchoring (RFC 3161 TSA + Sigstore Rekor, async best-effort)

    After the Witness signs the envelope, an attestor submits a hash of the certificate to two external services. The input artifact for both anchors is the same protobuf-binary serialization of the VeilCertificateproto as it exists at Witness signing time — before the attestor populates three specific fields: attestation.timestamp, attestation.transparency_log, and the top-level anchor_status. Note that attestation.notaryis different: the assembler populates it at signing time with the Witness’s canonical-JSON signature re-asserted as metadata, so it is part of the hashed bytes. A verifier reconstructing the hash must therefore clear exactly those three fields (attestation.timestamp,attestation.transparency_log, anchor_status) on the fetched certificate before proto.Marshal, and preserve attestation.notary. The attestor sends this hash to an external Timestamp Authority per RFC 3161 (producing a signed timestamp token proving the envelope existed no later than the token’s genTime) and to Sigstore Rekor as a hashedrekord entry. Both submissions run on the same async retry path; the result is surfaced as anchor_status: PENDING_ANCHOR while retries are in flight, ANCHORED when both the TSA token and a Rekor inclusion proof are populated on the persisted certificate, and ANCHOR_FAILED when the retry window is exhausted without full confirmation. A cert with ANCHOR_FAILED remains valid against its Witness Ed25519 signature; only the external public anchors did not confirm.

  4. 4

    Key lifecycle

    Per-service Ed25519 keys rotate on an operator-configured schedule. Claims do not record a key_id; the VeilClaimproto carries only the signing service’s service_id and its signature. A verifier looks up the service’s current public key by service_id from the key manifest described below. The certificate envelope separately records witness_key_id— that identifier applies to the Witness envelope signature only. Rotation is independent per service, so a key compromise is scoped. Because claims do not pin a specific key version, a verifier checking a historical certificate relies on the manifest to publish the public key that was current for that service at the claim’s timestamp (or on retained historical keys if the operator has rotated since). Operators who rotate keys must serve a manifest that exposes the historical public key alongside the current one; otherwise, certificates signed under the retired key become unverifiable. An operator who cannot retain historical keys should retire certificates signed under the old key from verification flows before rotation.

    The gateway publishes the key manifest at GET /.well-known/veil-keys.json— public, unauthenticated. The manifest returns { issuer, version, supported_protocol_versions, keys[], signed_at }, where each entry in keys[] is { service_id, key_id, public_key, purpose, algorithm: "Ed25519" }. When the gateway is configured with a manifest signing key (VEIL_MANIFEST_SIGNING_KEY env var), the manifest is Ed25519-signed over its canonical JSON form and ships with signature, signing_key_id, and signing_algorithmattached. When that env var is absent, the manifest ships unsigned — consumers who require manifest integrity should pin a signed mirror (typically in their DPA appendix) and compare before trusting it.

The Three Views

Every certificate is retrievable in three views. The underlying signed envelope is identical; the views differ only in what they render. Each view is a deterministic render of the same canonical JSON — the render itself is verifiable against the signed envelope.

ViewAudienceRenders
DPO summaryData Protection OfficerPlain-language: what was separated, what the AI saw, what it did not, and the regulatory mapping. No cryptographic detail.
Technical proofSecurity engineerFull envelope: every claim with its Ed25519 signature, the TSA token, the Rekor entry reference (when ANCHORED), key identifiers, and canonical hashes.
Regulatory mappingAuditorArticle-by-article: which claim supports GDPR Art. 25/32 and EU AI Act Art. 10/15. Evidence mapping, not a held certification.

How to Verify a Veil Certificate Independently

A Veil Certificate is designed so that verification of an obtained certificate does not require trusting our code. The certificate fetch does require an authenticated call against the tier-gated /api/v1/veil/certificate/{request_id} endpoint (x-api-key, Pro or Enterprise tier) — that’s our transport. Once fetched, the certificate can be verified end-to-end against public standards (Ed25519, RFC 3161, Sigstore Rekor) and against keys we publish at /.well-known/veil-keys.json (unauthenticated). Two paths exist: a convenience path via the TypeScript SDK, and a protocol-agnostic path using standard tooling. Honest picture: certificate fetch and Witness-signature verification work end-to-end today; full multi-claim verification via an independent Go oracle is on the roadmap. Where current tooling stops short, we say so.

Convenience path (TypeScript SDK)

The @dsaveil/theveilSDK ships a certificate fetch and a Witness-signature verification helper. The verifier does not trust the certificate to self-identify its signer — the caller supplies the expected Witness key out-of-band.

import { TheVeil, TheVeilCertificateError, TheVeilHttpError } from "@dsaveil/theveil";

const client = new TheVeil({ apiKey: process.env.VEIL_API_KEY });

// 1) Fetch. A cert that is not yet assembled surfaces as a 202 HTTP error
//    so the happy-path return type stays a narrow VeilCertificate.
let cert;
try {
  cert = await client.getCertificate("req_4f3a1b2c8d9e");
} catch (err) {
  if (err instanceof TheVeilHttpError && err.status === 202) {
    // retry after err.body.retry_after_seconds
  }
  throw err;
}

// 2) Verify the Witness Ed25519 signature over the certificate's
//    canonical 7-field subset. Throws TheVeilCertificateError on
//    failure — you do not check a boolean return.
try {
  const result = await client.verifyCertificate(cert, {
    witnessKeyId:     "witness_v1",                     // expected label
    witnessPublicKey: process.env.VEIL_WITNESS_PUBKEY!, // base64 or Uint8Array (32 bytes)
  });
  // result.overallVerdict — VERDICT_UNSPECIFIED | VERDICT_VERIFIED | VERDICT_PARTIAL | VERDICT_FAILED
  // result.anchorStatus   — ANCHOR_STATUS_UNSPECIFIED | ANCHOR_STATUS_PENDING | ANCHOR_STATUS_ANCHORED | ANCHOR_STATUS_FAILED
  // result.witnessAssertedIssuedAt — Date (millisecond precision)
  if (result.overallVerdict === "VERDICT_VERIFIED") {
    // all five Witness checks passed, complete claim set
  }
} catch (err) {
  if (err instanceof TheVeilCertificateError) {
    // err.reason is one of:
    //   malformed | unsupported_protocol_version | witness_mismatch
    //   witness_signature_missing | invalid_signature
  }
  throw err;
}

verifyCertificate today performs four checks: protocol version (protocol_version === 2), witness identity (cert.witness_key_idmatches the caller-supplied expected label), signature presence, and Ed25519 verification over the canonical 7-field subset. External RFC 3161 timestamp verification and Rekor inclusion-proof verification are notperformed by the SDK today — anchorStatus and overallVerdict on the result are pass-through metadata. Full multi-claim verification (each per-service Ed25519 signature, the TSA token chain, the Rekor inclusion proof) is the roadmap step; when shipped, it will run inside the SDK without additional caller configuration.

A note on enum forms. Your code compares against the protojson full-name literals shown above: VERDICT_VERIFIED, VERDICT_PARTIAL, VERDICT_FAILED, ANCHOR_STATUS_ANCHORED, and so on. These are what the SDK returns, because the gateway serves the certificate as protojson with UseProtoNames: true. Elsewhere on this page we write the shorter forms — VERIFIED, PARTIAL, ANCHORED— when narrating protocol behaviour, because those are the strings the Go Witness uses internally and canonically signs. Same values, two encodings. In your code, always compare to the full-name form.

Protocol-agnostic path (standard tooling)

Five explicit steps. No DSA-specific dependencies.

  1. 1

    Fetch the envelope

    curlthe certificate URL from the inference response’s veil.certificate_url field, or the /api/v1/veil/certificate/{request_id} endpoint directly (authenticated: x-api-key header, Pro or Enterprise tier). The response body is protojson output of the VeilCertificateproto — JSON, but with protojson’s convention that enum fields carry full-name string values (e.g. anchor_status.status = ANCHOR_STATUS_ANCHORED, verification.overall_verdict = VERDICT_VERIFIED) and unset fields are emitted with zero values. Parse as JSON for inspection, then — for signature and hash reconstruction — re-encode as protobuf binary using the proto schema at proto/veil/v1/veil.proto in the public DSA repo.

  2. 2

    Verify the Witness envelope signature

    Extract the envelope body and its Ed25519 signature. Obtain the Witness public key from the gateway’s key manifest: GET /.well-known/veil-keys.json (public, unauthenticated). Find the entry where service_id == "dsa-witness" and key_id matches the certificate’s witness_key_id (today the manifest exposes witness_v1for this role). If the gateway is configured with a manifest signing key, verify the manifest’s own Ed25519 signature first; otherwise pin a signed manifest mirror from the operator’s DPA appendix before trusting it. The signed payload is the certificate’s canonical 7-field subset (certificate_id, request_id, protocol_version, claim_ids in order, issued_at, overall_verdict in Witness short form (VERIFIED / PARTIAL / FAILED), and witness_key_id) — see the Witness assembler for the exact canonicalisation. Verify with any Ed25519 library (RFC 8032) openssl pkeyutl -verify, a PyNaCl call, or equivalent.

  3. 3

    Verify per-service claim signatures

    The envelope contains one claim per pipeline service that emitted one. For each, repeat step 2 against that service’s published key — looked up by the claim’s service_id from the same /.well-known/veil-keys.json manifest (today one current key_id per service_id). The VeilClaim proto does not carry a key_id field: claims expose only service_id and signature, so rotation handling lives in the manifest (see the Key Lifecycle subsection of Section 3). Missing best-effort claims among dsa-bridge/dsa-sanitizer/dsa-ai/dsa-audit are normal on PARTIAL certificates; a missing Gateway claim is not possible (the request would have been refused).

  4. 4

    Verify the RFC 3161 timestamp token (only when anchor_status == ANCHORED)

    When anchor_status == ANCHORED, the envelope’s attestation.timestamp field carries the TSA token (raw bytes at attestation.timestamp.timestamp_token). Verify the token’s signature against the TSA’s published CA chain, then verify that the token’s messageImprint.hashedMessage equals SHA-256(proto.Marshal(cert)), where cert is the fetched VeilCertificate with these three fields cleared to their unset state: attestation.timestamp, attestation.transparency_log, and the top-level anchor_status. Preserve attestation.notary— it was populated at signing time and is part of the hashed bytes (clearing it produces a hash mismatch on a legitimate certificate). The messageImprint.hashAlgorithm must be id-sha256 (OID 2.16.840.1.101.3.4.2.1). Tools: openssl ts -verify pointed at the reconstructed bytes via -data, or any TSP library. On PENDING_ANCHOR or ANCHOR_FAILED, skip this step — the attestation.timestamp field may be absent or incomplete; the certificate is still valid against the Witness Ed25519 signature, but external timestamp anchoring did not confirm.

  5. 5

    Verify the Rekor entry (only when anchor_status == ANCHORED)

    When anchor_status == ANCHORED, the envelope’s attestation.transparency_log field contains the Rekor entry: log_index (int64), inclusion_proof (bytes), signed_entry_timestamp (bytes), and log_url (the Rekor endpoint that issued the proof). Fetch the entry via the Rekor REST API or rekor-cli. The entry is a hashedrekord with data.hash.algorithm == "sha512" — confirm data.hash.value equals hex(SHA-512(proto.Marshal(cert))), where cert is the fetched VeilCertificate with these three fields cleared to their unset state: attestation.timestamp, attestation.transparency_log, and the top-level anchor_status. Preserve attestation.notary— it was populated at signing time and is part of the hashed bytes (clearing it produces a hash mismatch on a legitimate certificate). Note that the entry’s signature is Ed25519ph(pre-hash; RFC 8032 §5.1), notthe same construction as the Witness envelope signature — Rekor verifies it internally against the public key embedded in the entry’s spec.signature.publicKey.content (PEM-wrapped X.509 SPKI of the Witness public key). On PENDING_ANCHOR or ANCHOR_FAILED, skip this step — the transparency-log field may be absent or incomplete.

    # The log_index is a field of attestation.transparency_log in the fetched cert.
    rekor-cli get --log-index <attestation.transparency_log.log_index>

The five steps above are a complete manual verification against the protocol. They do not require trusting our infrastructure or our code — every artifact is checked against public standards (Ed25519, RFC 3161, Sigstore Rekor) or against keys we publish at /.well-known/veil-keys.json (or pinned signed mirrors thereof).

Regulatory Mapping

The Veil Protocol is one mechanism a processor can use to meet specific articles of GDPR and the EU AI Act. These are evidence and architectural mappings — not a held certification under either regulation.

RegulationRequirementWhat the Veil Certificate contributes
GDPR Art. 25Data protection by design and by defaultPer-request evidence that pseudonymisation was the default processing mode and that identity fields did not cross into the AI environment.
GDPR Art. 32Security of processingCryptographic attestation signed at the infrastructure layer. The signature chain is tamper-evident; external TSA timestamps and Rekor anchoring make tampering detectable across custody boundaries. Independent verification of an obtained certificate does not require the processor's cooperation (verification uses only public standards and the unauthenticated /.well-known/veil-keys.json manifest). Certificate fetch requires the customer's own authenticated call to a tier-gated gateway endpoint.
EU AI Act Art. 10Data governance for high-risk AIThe isolation-probe claim demonstrates the AI system did not receive identity data for the specific request. The certificate is a per-request, inspectable governance artifact.
EU AI Act Art. 15Accuracy, robustness, and cybersecurityThe certificate is an externally-anchored robustness signal: any alteration of the envelope after signing is detectable. Independent verification of an obtained certificate uses only public standards and the unauthenticated /.well-known/veil-keys.json manifest; certificate fetch requires the customer's own authenticated call to the tier-gated gateway endpoint.

What the Protocol Does Not Do

The Veil Protocol proves isolation happened for a given request. It does not, by itself, prove that the AI’s output was correct, that your application interpreted the output safely, or that your own systems downstream of the Gateway are equally isolated. Infrastructure enforcement (Kubernetes NetworkPolicies, Docker network segmentation) is what enforces the isolation; the protocol is what proves it. Neither replaces the other.

FAQ

What happens if anchor_status is stuck on PENDING_ANCHOR?
PENDING_ANCHORmeans the async retry loop has not yet populated both external attestations (the RFC 3161 TSA timestamp and the Sigstore Rekor inclusion proof) on the persisted certificate. The Witness Ed25519 signature over the envelope is produced synchronously and is already valid; both external anchors are the async part and are what PENDING_ANCHOR is waiting on. If the status does not transition to ANCHORED within the retry window, it moves to ANCHOR_FAILED; an auditor who requires a transparency-log anchor for a specific certificate can retry the fetch or request a fresh run. Long-running PENDING_ANCHOR beyond the configured window is an operational signal that the attestor service cannot reach the external TSA or Rekor endpoints — check the deployment’s attestor logs and anchor retry metrics.
How do I verify a certificate after my API key has been rotated or revoked?
Your API key is unrelated to certificate verification. Certificates are signed with the Witness service’sEd25519 key and cross-anchored by the RFC 3161 TSA and Sigstore Rekor. The envelope records witness_key_id and is signed by the Witness Ed25519 key. Per-service claims carry only service_id and signature — there is no claim-level key_id field. The public key used to verify a given claim is looked up by service_id from the /.well-known/veil-keys.json manifest. Operators who rotate service keys must publish the retired public keys in the manifest alongside the current ones, otherwise claims signed before the rotation become unverifiable. API-key revocation prevents new inference requests under that API key but has no effect on certificates already produced.
Can I replay an old certificate to prove something today?
A Veil Certificate is bound to a specific request_id, claim chain, and TSA timestamp. Its Ed25519 signatures cover that exact envelope — presenting the same certificate as proof of a different request or a current decision fails on the request_id binding alone. What the certificate does continue to prove, indefinitely, is that the original request went through the isolated pipeline at the time recorded in the TSA token. Replay against the original request is a feature; replay against anything else is not a threat the protocol tries to prevent, because the binding makes it mechanically impossible.

References

Want to see this in action?

Book a working session — we'll walk through your use case together.