Admission Profile: principal trust and strict-v1¶
Status: Draft, v1 Scope: Whether a verifier admits a V2 signed bundle as evidence from a principal it already trusts, under an operator-fixed profile
1. Purpose and layering¶
VERIFICATION.md (V2-1 to V2-14) decides whether a bundle is verified: its artifacts are intact, consistent, and signed by the keys the bundle itself lists. Anyone can create a responsible principal record (RPR) with any human_id and keys of their own, so a verified bundle proves integrity and signature, not who the principal is.
Admission adds two optional layers on top of verification, without changing it:
- Principal trust (§3): the keys that signed for the principal belong to a principal the operator already trusts, according to a trust set the operator supplies.
- The
strict-v1profile (§4): fixed, operator-chosen requirements (hybrid signatures, audience, a short signed session window, fresh revocation status) that a request cannot weaken.
A verifier that enables neither behaves exactly as before. Admission never re-implements a verification step: it reads what the verifier reports (§2.3).
Admission decides whether evidence is admitted. It does not authorize an action: this version never binds an external authorization to a tool, its arguments or its execution (§7).
2. Inputs¶
2.1 Clock¶
Every time comparison in verification and admission uses one instant now, in milliseconds since the Unix epoch. Implementations take it as a parameter, defaulting to the system clock. Timestamps are RFC 3339 with a Z or ±hh:mm offset, compared at millisecond precision, as in V2-4. The clock skew allowance is 30 seconds (SKEW), the same as V2-4 and DCP-04.
2.2 Trust set¶
Supplied only by the operator: local configuration or a source the operator chose. It MUST NOT be derived from the bundle (its keys, legal_name, jurisdiction or any other self-declared field), from a request or tool parameter, or from a URL the RPR names. Schema: principal_trust_set.schema.json.
{
"trust_set_version": "2026-11-01.1",
"principals": [
{
"human_id": "human:acme-ops",
"keys": [
{ "kid": "…", "alg": "ed25519", "public_key_b64": "…", "status": "active",
"not_before": null, "expires_at": "2027-06-01T00:00:00Z" },
{ "kid": "…", "alg": "ml-dsa-65", "public_key_b64": "…", "status": "active" }
]
}
]
}
A trust set is validated when it is loaded. It is rejected, with the configuration error code in brackets, when:
| Condition | Code |
|---|---|
| A field outside the schema at any level | CONFIG_UNKNOWN_FIELD |
trust_set_version missing or not a non-empty string; principals not an array; a principal's keys not a non-empty array; status not active, revoked or expired; not_before or expires_at neither null nor RFC 3339 |
CONFIG_INVALID_VALUE |
human_id missing, not a string, or empty |
CONFIG_TRUST_EMPTY_ID |
alg not ed25519, ml-dsa-65 or slh-dsa-192f; public_key_b64 not canonical base64 (V2-5) or not 32, 1952 or 48 bytes for that algorithm |
CONFIG_TRUST_MALFORMED_KEY |
kid not the kid derived from alg and the key bytes (V2-13) |
CONFIG_TRUST_KID_NOT_DERIVED |
A human_id listed twice, or a kid listed twice in the whole set (under one principal or two) |
CONFIG_TRUST_DUPLICATE |
Any duplicate is rejected, identical or not: an operator merging sources resolves them before loading. A trust set with no principals is valid and trusts no one.
When a trust set has several of these problems, the code of any one of the problems it has may be reported. The trust set is rejected whichever code is reported, and no order of precedence among the codes is defined.
The authoritative status of a key is the trust set's. A bundle that lists a key as active cannot reinstate one the trust set marks revoked or expired, or whose expires_at has passed.
Rotation is an explicit update of the trust set (a new trust_set_version): the operator adds the new key, keeps the old one active for an overlap if needed, then retires it (revoked, expired, an expires_at, or removal). A bundle cannot authorize its own rotation, because its keys are never a source of trust.
Agent keys are not registered in the trust set. The agent is tied to the principal by the principal's signature over the manifest, which covers passport_hash (V2-6, V2-7).
2.3 Signer keys reported by the verifier¶
For every composite signature it checks (the bundle signature over the manifest, the four artifacts, each PQ checkpoint), the verifier reports, per component, the key entry it resolved and whether that component verified:
{ "signature": "bundle", "component": "classical", "kid": "…", "alg": "ed25519",
"public_key_b64": "…", "verified": true }
signature is bundle, responsible_principal_record, agent_passport, intent, policy_decision or pq_checkpoints[<checkpoint_id>]; component is classical or pq. A component whose key the verifier did not resolve (a classical key absent under pq_only) is not reported. These are the keys that actually verified the signatures, resolved exactly as the verifier resolved them; admission never resolves keys a second way. The list is defined for a verified bundle; for one that did not verify, implementations may stop at the first failure or go on, and admission does not use it.
The principal signatures are bundle and responsible_principal_record.
2.4 Revocation snapshot¶
Required by strict-v1. The operator's adapter obtains it from a local or remote source the operator configured (no central service is required) for the revocation subjects of the bundle: the passport agent_id, and the kid of every verified signer key (§2.3), all signatures included. Schema: revocation_snapshot.schema.json.
{ "checked_at": "2026-11-01T00:00:20Z",
"agents": { "agent:acme-bot": "not_revoked" },
"keys": { "<kid>": "not_revoked", "<kid>": "not_revoked" } }
checked_at is when the source established these statuses, not when the adapter read them. Each status is revoked, not_revoked or unknown. A source that cannot answer (timeout, error, unreachable) produces no snapshot; an adapter MUST NOT turn a failure into not_revoked.
3. Principal trust¶
Evaluated only when a trust set is supplied and the bundle verified (V2-1 to V2-14); otherwise the result is not_evaluated. With human_id the RPR payload's human_id:
- T1.
human_idis not a principal of the trust set:TRUST_PRINCIPAL_UNKNOWN. Stop. - T2. For each verified component of each principal signature (§2.3), the trust set entry of that principal with the same
kid, with the first of these that applies: - none, or a different
algor different key bytes:TRUST_KEY_NOT_ANCHORED; statusrevoked:TRUST_KEY_REVOKED;statusexpired, orexpires_atbeforenow:TRUST_KEY_EXPIRED;not_beforeafternow:TRUST_KEY_NOT_YET_VALID.
A verified bundle with no verified principal component at all also gets TRUST_KEY_NOT_ANCHORED. The principal is trusted when T1 and T2 add no code, untrusted otherwise. Every verified component counts: trusting only a principal's Ed25519 key does not make its PQ key, or any other key in the RPR, trusted.
4. The strict-v1 profile¶
4.1 Configuration¶
Schema: admission_profile.schema.json.
{ "profile": "strict-v1", "verifier_id": "api.acme.example",
"max_session_duration_seconds": 300, "max_revocation_age_seconds": 300,
"verifier_policy": { "advisory_rejected_algs": ["slh-dsa-192f"] } }
| Field | Rule |
|---|---|
profile |
Required, "strict-v1" |
verifier_id |
Required, a non-empty string; the bundle's signed intended_verifier must equal it |
max_session_duration_seconds |
Integer from 1 to 300, default 300 |
max_revocation_age_seconds |
Integer from 1 to 300, default 300 |
verifier_policy |
Optional VerifierPolicy fields; each may only keep or tighten the profile's policy (§4.2) |
The profile sets requirements, not defaults. Its verifier policy is:
| Field | Value | What verifier_policy may set |
|---|---|---|
default_mode, every risk_overrides tier |
hybrid_required |
Only hybrid_required. pq_only drops the classical signature, so it does not satisfy the profile |
require_session_binding, require_composite_binding, require_session_expiry, require_audience_binding |
true |
Only true |
allow_v1_bundles, allow_classical_fallback_disable |
false |
Only false |
verifier_id |
the profile's | Only the same value |
max_session_duration_seconds |
the profile's | From 1 to the profile's value; the smaller applies |
min_classical, min_pq |
1 |
Only 1: a composite signature has one classical and one PQ component |
accepted_hash_algs |
["sha256", "sha384"] |
Only these two, in any order: verifiers check the bundle hash_alg by V2-4 |
max_key_age_days |
365 |
Only 365: verifiers only warn on key age |
accepted_classical_algs |
["ed25519"] |
A non-empty subset |
accepted_pq_algs |
["ml-dsa-65", "slh-dsa-192f"] |
A non-empty subset |
advisory_rejected_algs |
[] |
Any list of strings |
warn_classical_only_deprecated |
false |
Any boolean (warnings only) |
Every field verifier_policy may change is one every verifier enforces the same way, so a profile never loads with a requirement some SDK would silently ignore or refuse.
Configuration errors, raised when the profile is loaded:
| Condition | Code |
|---|---|
A field outside the schema, at the top level or in verifier_policy |
CONFIG_UNKNOWN_FIELD |
A value of the wrong type, or a mode (default_mode, a risk_overrides value) outside the enumeration classical_only, hybrid_preferred, hybrid_required, pq_only; profile not "strict-v1" |
CONFIG_INVALID_VALUE |
verifier_id missing or empty; a maximum below 1 or above 300; another verifier_policy value the table above does not allow, such as a mode of that enumeration other than hybrid_required, or an algorithm outside the allowed subset, known or not |
CONFIG_PROFILE_CONTRADICTION |
When a configuration has several problems, the code of any one of them may be reported.
An adapter that enables the profile also rejects, with CONFIG_PROFILE_CONTRADICTION, a missing trust set or revocation source, and any of its own options that would weaken the profile (accepting requests without a bundle, its own verifier policy, a fail-open revocation check).
4.2 Profile checks¶
With max the effective maximum session duration and m the manifest of the bundle:
- P1.
bundle.dcp_bundle_versionis not"2.0":PROFILE_V2_REQUIRED. Stop: nothing else is evaluated. - P2.
m.intended_verifierabsent, null, not a string or empty:PROFILE_AUDIENCE_MISSING; any other value different fromverifier_id:PROFILE_AUDIENCE_MISMATCH. - P3.
m.session_issued_atabsent or null:PROFILE_SESSION_START_MISSING.m.session_expires_atabsent or null:PROFILE_SESSION_EXPIRY_MISSING. Either present but not RFC 3339, or the start after the expiry:PROFILE_SESSION_TIME_INVALID. Otherwise: - the start after
now + SKEW:PROFILE_SESSION_NOT_STARTED; - the expiry before
now:PROFILE_SESSION_EXPIRED; - the expiry minus the start greater than
maxseconds:PROFILE_SESSION_TOO_LONG. - P4. The
composite_sigof the bundle signature or of the RPR is notpq_over_classicalwith apqcomponent:PROFILE_HYBRID_REQUIRED. After verification, also when either principal signature lacks a verified classical or PQ component, or the verification mode is nothybrid_required.
P2 and P3 repeat, with codes, what the profile's policy makes V2-4 enforce, so a rejected bundle says which requirement failed. They read the manifest before it is authenticated; only a verified bundle is admitted.
4.3 Revocation checks¶
Evaluated when the bundle verified, with the subjects of §2.4:
- R1. No snapshot:
REVOCATION_UNAVAILABLE. Stop. - R2. A snapshot outside the schema (a missing or extra field,
checked_atnot RFC 3339, a status other than the three), orchecked_atafternow + SKEW:REVOCATION_INVALID. Stop. - R3.
now - checked_atgreater thanmax_revocation_age_seconds:REVOCATION_STALE. - R4. A subject
revoked:REVOCATION_REVOKED. - R5. A subject absent from the snapshot or
unknown:REVOCATION_UNKNOWN.
A stale snapshot that marks a subject revoked reports both codes. None of these outcomes counts as not_revoked.
5. Admission¶
5.1 Procedure¶
Given the signed bundle, an optional profile, an optional trust set (required by the profile), a revocation snapshot (evaluated only under the profile) and now:
- Under the profile, P1: when it applies, nothing else is evaluated and the bundle is reported as not verified. Otherwise P2, P3 and the structural part of P4.
- Verify the bundle as a V2 signed bundle (V2-1 to V2-14) at
now, under the profile's policy, or the caller's policy without a profile. Not verified:VERIFICATION_FAILED. A bundle that is not V2 is not verified here, whatever the policy'sallow_v1_bundlessays (DCP-AI-v2.0, note on §17). - If verified: principal trust (§3) when a trust set is supplied; under the profile, the verified part of P4 and the revocation checks (§4.3).
An implementation may expose step 3 on its own (evaluateAdmissionV2 and its counterparts) so a caller can reuse a verification it already has. That function trusts the verification result it is given and does not re-verify: it must be the result of step 2 for the same bundle, at the same now, under the profile's policy when there is a profile. A result made up, from another bundle or under a weaker policy gives a wrong answer; the combined function (admitSignedBundleV2) does both steps.
A profile requires a trust set. A call with a profile and no trust set is refused with CONFIG_PROFILE_CONTRADICTION, by the combined function and by step 3 exposed on its own, as adapters refuse it (§4.1). The combined function also accepts the caller's verifier policy, for use without a profile; with a profile, it refuses such a policy with the same code, because the profile sets its own. This text does not fix whether these refusals come before or after verification.
5.2 Result¶
| Field | Value |
|---|---|
verified |
The verifier's verdict, with its meaning unchanged |
verification |
The verifier's full result (errors, warnings, details, signer keys) |
principal_trust |
trusted, untrusted, or not_evaluated (no trust set, or not verified) |
profile |
"strict-v1" or null |
profile_compliance |
compliant, non_compliant, or not_evaluated (no profile) |
admitted |
verified, and principal_trust is trusted when a trust set is supplied, and profile_compliance is compliant under the profile |
action_authorized |
Always false in this version (§7) |
recorded_decision |
The policy decision's decision (approve, escalate, block) of a verified bundle, else null |
trust_set_version |
The trust set's version, or null |
errors |
{ code, message } for every code added, in the order of §5.1: P1; P2; P3; the structural part of P4; VERIFICATION_FAILED; principal trust; the verified part of P4; revocation. A check that applies to several components or subjects may add its code once for each. The order is between these phases only: within one phase, such as principal trust or revocation, no order is fixed |
profile_compliance is compliant when the bundle verified and no PROFILE_* or REVOCATION_* code was added. A bundle that records a block or escalate decision can be admitted as evidence of that decision; action_authorized stays false.
A self-consistent bundle from an unknown principal stays verified, and is not admitted when a trust set is supplied.
5.3 Codes¶
| Code | Step |
|---|---|
VERIFICATION_FAILED |
§5.1 step 2 |
TRUST_PRINCIPAL_UNKNOWN, TRUST_KEY_NOT_ANCHORED, TRUST_KEY_REVOKED, TRUST_KEY_EXPIRED, TRUST_KEY_NOT_YET_VALID |
§3 |
PROFILE_V2_REQUIRED, PROFILE_AUDIENCE_MISSING, PROFILE_AUDIENCE_MISMATCH, PROFILE_SESSION_START_MISSING, PROFILE_SESSION_EXPIRY_MISSING, PROFILE_SESSION_TIME_INVALID, PROFILE_SESSION_NOT_STARTED, PROFILE_SESSION_EXPIRED, PROFILE_SESSION_TOO_LONG, PROFILE_HYBRID_REQUIRED |
§4.2 |
REVOCATION_UNAVAILABLE, REVOCATION_INVALID, REVOCATION_STALE, REVOCATION_REVOKED, REVOCATION_UNKNOWN |
§4.3 |
CONFIG_UNKNOWN_FIELD, CONFIG_INVALID_VALUE, CONFIG_PROFILE_CONTRADICTION, CONFIG_TRUST_EMPTY_ID, CONFIG_TRUST_MALFORMED_KEY, CONFIG_TRUST_KID_NOT_DERIVED, CONFIG_TRUST_DUPLICATE |
Raised when a trust set or profile is loaded |
Messages are bounded and never contain private keys, tokens or the operator's configuration beyond the identifiers being compared.
6. Adapters and caching¶
The core functions are deterministic and make no network call. Adapters (middleware, tools, servers) resolve the operator's configuration, call the trust and revocation sources, and enforce the decision:
- A1. A request reaches the protected handler only when
admittedis true. An adapter never proceeds onverifiedalone once trust or a profile is configured, and refuses at configuration (CONFIG_PROFILE_CONTRADICTION) an option that would let a request without a bundle reach the handler. When the operator configured a trust source, every answer is loaded as a trust set (§2.2): a source that fails, or answers nothing (undefined,null) or something that is not a valid trust set, refuses the request. It never stands for "no trust set", which the evaluation of §5.1 reads as admission without principal trust. - A2. The profile, the trust set or its source, the revocation source, its URL and credentials, and the tenant come from the operator's code and configuration. No request field, header or tool parameter selects or changes them. A remote source is fetched over TLS (plain HTTP only to a loopback host), following no redirect, with a bounded size and time.
- A3. A cache MAY keep the verifier's result (step 2 of §5.1), keyed by the SHA-256 of the canonical signed bundle, the SHA-256 of the canonical effective verifier policy and the operator-derived tenant, and kept no later than the earliest of its TTL,
session_expires_at, the RPRexpires_atand every key'sexpires_at. Principal trust, the profile checks and revocation are evaluated on every admission, at the currentnow, with the current trust set and a fresh revocation snapshot. A cached acceptance therefore never survives a trust set change, a revocation, a different policy or a different tenant.
7. Guarantees and limits¶
Under strict-v1, an admitted bundle:
- verified under V2-1 to V2-14 with both signature components valid for every signature;
- was signed for the principal (RPR and manifest, both components) with keys the operator's trust set lists for that
human_id, active and in their validity window atnow; - names this verifier in its signed
intended_verifier; - has a signed session window of at most
max_session_duration_seconds, started (withinSKEW) and not expired atnow; - has an agent and verified signer keys that a revocation snapshot at most
max_revocation_age_secondsold reports as not revoked.
It does not guarantee:
- Single use. A shared session nonce, the audience and the expiry bound where and how long a bundle can be replayed; they do not make it single-use. A verifier that needs single use keeps its own record of consumed nonces or bundle hashes; verification and admission do not consume them.
- Authorization of an action.
action_authorizedis alwaysfalse. Admission says the evidence is authentic and comes from a trusted principal; whether a tool may run with given arguments is a later phase. - Identity beyond the trust set. Trust is exactly as good as the operator's trust set and its source.
- Revocation fresher than the snapshot. A revocation made after
checked_atis not seen until the next snapshot.
8. API correspondence¶
| Concept | TypeScript (@dcp-ai/sdk) |
Python (dcp_ai.v2) |
Go (dcp/v2) |
Rust (dcp_ai::v2) |
|---|---|---|---|---|
Verify at now |
verifySignedBundleV2(b, registry, policy, { now }) |
verify_signed_bundle_v2(b, registry, policy, now_ms=…) |
VerifySignedBundleV2At(registry, json, policy, now) |
verify_signed_bundle_v2_at(&b, &policy, now_ms) |
| Signer keys (§2.3) | details.signer_keys |
details["signer_keys"] |
SignerKeys |
V2VerificationDetails.signer_keys |
| Load a trust set | createPrincipalTrustStore(json) |
create_principal_trust_store(json) |
NewPrincipalTrustStore(json) |
PrincipalTrustStore::from_json(&json) |
| Load a profile | resolveStrictV1Profile(json) |
resolve_strict_v1_profile(json) |
ResolveStrictV1Profile(json) |
resolve_strict_v1_profile(&json) |
| Revocation subjects | revocationSubjects(b, verification) |
revocation_subjects(b, verification) |
RevocationSubjects(b, verification) |
revocation_subjects(&b, &verification) |
| Evaluate (no crypto) | evaluateAdmissionV2(input) |
evaluate_admission_v2(…) |
EvaluateAdmissionV2(…) |
evaluate_admission_v2(…) |
| Verify and evaluate | admitSignedBundleV2(b, registry, options) |
admit_signed_bundle_v2(b, registry, …) |
AdmitSignedBundleV2(registry, json, options) |
admit_signed_bundle_v2(&b, &options) |
| Configuration error | AdmissionConfigError (code) |
AdmissionConfigError (code) |
*AdmissionConfigError (Code) |
AdmissionConfigError (code) |
Result fields keep the names of §5.2 in every SDK's JSON form.
9. Test vectors¶
tests/interop/v2/admission_vectors.json holds the shared cases: signed bundles, trust sets, profiles, revocation snapshots, now, and for each case the expected verified, principal_trust, profile_compliance, admitted and set of codes, with the rule it checks. Configuration cases give the configuration and the expected error code. Every SDK runs them; agreement between SDKs is not the criterion, the expected values are.
Implementation notes (informative)¶
Added 25/10/2026. Not normative. These notes add no requirement and change nothing above: they describe how the reference code on
mainrelates to this text. Where they differ, the normative text above stands.
- The references to V2-1 to V2-14 point to the normative checklist of VERIFICATION.md. SDK packages earlier than
@dcp-ai/sdk2.2.1,@dcp-ai/wasm2.1.1,dcp-ai2.10.1 (PyPI), Go v2.11.1 and Rustdcp-ai2.11.1 do not check the secondary SHA3-256 fields of V2-11, so with them "verified under V2-1 to V2-14" (§7) holds with that limit; from those versions the SDK verifiers check the secondary fields that are present, and absent ones are not required. Every other verification check, principal trust and everystrict-v1check still apply; the SHA-256 chain alone never admits a bundle. - The JSON in §2.2, §2.3, §2.4 and §4.1 illustrates the shapes, with placeholders (
…,<kid>) for keys and identifiers; it is not a fixture and does not load as it is. The two<kid>entries of §2.4 stand for two different real kids: an adapted snapshot has no duplicate JSON keys. The shared, runnable cases are intests/interop/v2/admission_vectors.json(§9). - On 25/10/2026 the APIs of §8 were in the SDKs on
mainand not in a published release. Updated 29/10/2026, replacing that availability: they are published in@dcp-ai/sdkfrom 2.2.0,dcp-ai(PyPI) from 2.10.0, Go from v2.11.0 and Rustdcp-aifrom 2.11.0; see the release notes for npm, Python, Go and Rust.@dcp-ai/wasmhas no admission API.