Migration Guide: DCP-AI v1.0 to v2.0¶
Specification version: 2.0
Last updated: 09/11/2026
Applies to: the SDKs on main and published @dcp-ai/sdk 2.4.0. npm versions were verified on 09/11/2026; see NPM_RELEASES.md and SECURITY.md. The versions of the other SDKs are in Published versions.
This guide walks you through migrating an existing DCP-AI v1.0 integration to v2.0. The migration introduces post-quantum composite signatures, dual-hash chains, session binding, adaptive security tiers, and agent-to-agent communication — while valid v1.0 bundles keep verifying where the verifier's policy allows them (see 1.2).
1. What Changed in v2.0¶
1.1 Summary of Changes¶
| Feature | v1.0 | v2.0 |
|---|---|---|
| Signatures | Ed25519 only | Composite: Ed25519 + ML-DSA-65 (PQ) |
| Hash Chains | SHA-256 only | Dual: SHA-256 + SHA3-256 |
| Session Binding | None | session_nonce on every artifact |
| Security Tiers | None | Adaptive: routine / standard / elevated / maximum |
| Bundle Manifest | bundle_hash + merkle_root |
Full manifest with per-artifact hashes, secondary Merkle root, PQ checkpoints |
| Signature Format | { alg, public_key_b64, sig_b64 } |
{ composite_sig: { classical, pq, binding } } |
| A2A Communication | Not specified | DCP-04: discovery, handshake, encrypted sessions |
| Verification Policy | Agent-declared | Verifier-authoritative |
| Domain Separation | None | Context tags on all signatures |
| kid Derivation | Implementation-specific | Deterministic: hex(SHA-256(alg || 0x00 || pk))[0:32] |
| Numeric Fields | Mixed (int/float) | Integer-only (float prohibited) |
| Privacy | Full RPR only | Blinded RPR mode available |
| Key Recovery | Not specified | M-of-N social recovery (Shamir SSS) |
| Emergency Revocation | Not specified | Pre-registered revocation token |
| Algorithm Governance | Not specified | Signed advisories with auto-response |
| Wire Format | JSON only | JSON (default) + CBOR (optional) |
1.2 Breaking Changes¶
The v1.0 format stays valid, with these limits:
- A verifier that handles both versions accepts a valid v1.0 bundle when its policy has
allow_v1_bundles: true(the default) and the effective mode tolerates a classical-only signature (below).verifySignedBundleV2itself accepts only v2.0: route v1.0 bundles to the V1 verifier (see 3.2). - Acceptance is stricter than in earlier versions: for example, an Ed25519 signature whose
Sis not below the group orderLis now rejected (see the changelog's[Unreleased]), and from the versions of 09/11/2026, P-ED-1 also requires a canonical public key and R that are not points of small order (see SECURITY.md), so a stored v1.0 bundle that an earlier verifier accepted may no longer verify. - v1.0 verifiers reject v2.0 bundles (
"unsupported dcp_version 2.0").
A v1.0 bundle has one Ed25519 signature and no post-quantum one, so the verification middlewares (@dcp-ai/express, @dcp-ai/openclaw, dcp_ai.fastapi) accept it only where a classical-only v2.0 bundle would pass (VERIFICATION.md V2-10): the stricter of the policy mode for the V1 passport's risk_tier and the mode of the security tier computed from the V1 intent and policy decision (V1 risk_score, 0 to 1, scaled by 1000; a missing one counts as elevated) must be classical_only or hybrid_preferred. Under the default policy, a V1 bundle with a routine or standard intent passes, and one with a payment, sensitive data, a high estimated impact or a risk score of 0.5 or more is refused; Express requireHybrid refuses every V1 bundle.
The migration is additive: v2.0 introduces new fields and structures alongside the v1.0 format. You can migrate producers (agents) and consumers (verifiers) independently.
2. Step-by-Step Migration¶
Step 1: Update SDK¶
The examples work with published TypeScript SDK 2.4.0, Python dcp-ai 2.11.0, Go v2.14.0 and Rust dcp-ai 2.14.0. See the Python, Go and Rust release notes and each SDK README for its documented limits.
TypeScript (published: 2.4.0 on 09/11/2026):
Python:
pip install -e /path/to/dcp-ai/sdks/python # main
# or the published version, with optional framework extras: pip install --upgrade 'dcp-ai[fastapi]'
Go (note the v2 major suffix required by Go modules):
Rust:
WebAssembly: install the published @dcp-ai/wasm 2.2.0 (npm install @dcp-ai/wasm@^2.2.0), or build sdks/wasm from a checkout (npm run build), then npm install /path/to/dcp-ai/sdks/wasm.
Step 2: Generate Hybrid Key Pairs¶
This hybrid migration uses both classical (Ed25519) and post-quantum (ML-DSA-65) key pairs. Classical-only acceptance remains subject to the effective verification mode (1.2).
TypeScript:
import { getDefaultRegistry, registerDefaultProviders } from '@dcp-ai/sdk';
registerDefaultProviders(); // Ed25519, ML-DSA-65, SLH-DSA-192f into the default registry
const registry = getDefaultRegistry();
const classicalKp = await registry.getSigner('ed25519').generateKeypair();
const pqKp = await registry.getSigner('ml-dsa-65').generateKeypair();
// each: { kid, publicKeyB64, secretKeyB64 }; kid = deriveKid(alg, publicKey)
Python:
import asyncio
from dcp_ai.providers.ed25519_provider import Ed25519Provider
from dcp_ai.providers.ml_dsa_65_provider import MlDsa65Provider
async def generate_hybrid():
return await Ed25519Provider().generate_keypair(), await MlDsa65Provider().generate_keypair()
classical, pq = asyncio.run(generate_hybrid())
# each: {"kid", "public_key_b64", "secret_key_b64"}
The Python SDK builds, signs and verifies V2 bundles with dcp_ai.v2 (BundleBuilderV2, sign_bundle_v2,
verify_signed_bundle_v2; see 5.2).
CLI (bin/dcp.js in a checkout):
dcp keygen --hybrid ./keys/
# Creates: ed25519_{public_key,secret_key,kid}.txt and ml_dsa_65_{public_key,secret_key,kid}.txt
Step 3: Update Artifact Construction¶
Each of the V2 RPR, passport, intent and policy decision gains dcp_version: "2.0" and the bundle's session_nonce, and is signed on its own as a
SignedPayload (Step 4). Keys move into the artifacts: the RPR lists the principal's keys in binding_keys,
and the passport lists the agent's in keys, each with kid, alg, public_key_b64, created_at,
expires_at and status.
Before (v1.0 RPR): a detached Ed25519 signature over the object without it.
{
"dcp_version": "1.0",
"human_id": "human:example-001",
"legal_name": "Example Principal",
"entity_type": "natural_person",
"jurisdiction": "ES",
"liability_mode": "owner_responsible",
"override_rights": true,
"issued_at": "2026-10-25T00:00:00Z",
"expires_at": null,
"contact": null,
"signature": "<base64 Ed25519 signature>"
}
After (v2.0 RPR payload): no signature field; the composite signature sits beside the payload (Step 4).
{
"dcp_version": "2.0",
"human_id": "human:example-001",
"session_nonce": "<64 hex characters>",
"legal_name": "Example Principal",
"entity_type": "natural_person",
"jurisdiction": "ES",
"liability_mode": "owner_responsible",
"override_rights": true,
"issued_at": "2026-10-25T00:00:00Z",
"expires_at": null,
"contact": null,
"binding_keys": [
{ "kid": "<32 hex>", "alg": "ed25519", "public_key_b64": "...", "created_at": "2026-01-01T00:00:00Z", "expires_at": null, "status": "active" },
{ "kid": "<32 hex>", "alg": "ml-dsa-65", "public_key_b64": "...", "created_at": "2026-01-01T00:00:00Z", "expires_at": null, "status": "active" }
]
}
Key differences:
- dcp_version is "2.0", and session_nonce (64 hex characters, 256 random bits) is the same in every
artifact of the bundle.
- The passport replaces public_key with keys (Ed25519 and ML-DSA-65 entries), and the policy decision's
risk_score becomes an integer from 0 to 1000 (spec §14.2).
- Signatures leave the payloads.
Step 4: Switch to Composite Signatures¶
Each of these four V2 artifacts is a SignedPayload: the payload, its hash and a composite signature under the artifact's
domain-separation context (the RPR with the principal's keys, the passport, intent and policy decision with the
agent's).
{
"payload": { "dcp_version": "2.0", "...": "..." },
"payload_hash": "sha256:<hex of the canonical payload>",
"composite_sig": {
"classical": { "alg": "ed25519", "kid": "<32 hex>", "sig_b64": "..." },
"pq": { "alg": "ml-dsa-65", "kid": "<32 hex>", "sig_b64": "..." },
"binding": "pq_over_classical"
}
}
Key differences:
- Each component names its key by kid (deterministic, Step 2) instead of carrying the public key.
- binding: "pq_over_classical": the PQ signature covers the classical signature, so the classical signature cannot
be replaced without invalidating the PQ one. The PQ signature itself can still be removed and the binding relabelled
classical_only: verifiers accept that in the classical_only and hybrid_preferred modes (the latter with a
warning) and refuse it in hybrid_required and pq_only. A PQ signature that is present but invalid is refused in
every mode.
TypeScript SDK (a fragment; the TypeScript SDK's Quick Start (V2) is the complete, runnable flow):
import { compositeSign, preparePayload, DCP_CONTEXTS } from '@dcp-ai/sdk';
// payload: a V2 artifact payload; registry and agentKeys (a CompositeKeyPair) as in Step 2
const { canonicalBytes, payloadHash } = preparePayload(payload);
const signedIntent = {
payload,
payload_hash: payloadHash,
composite_sig: await compositeSign(registry, DCP_CONTEXTS.Intent, canonicalBytes, agentKeys),
};
Step 5: Add Domain Separation¶
Every V2 signature is taken over UTF8(context) || 0x00 || canonical_bytes, with the context of the artifact
(DCP-AI.v2.Intent, DCP-AI.v2.Bundle and so on, DCP_CONTEXTS). compositeSign, signBundleV2 and their
equivalents in the other SDKs do it for you. If you build the message yourself, use the V2 canonicalization
(dcp-jcs-v1):
import { canonicalizeV2, domainSeparatedMessage, DCP_CONTEXTS } from '@dcp-ai/sdk';
// manifest: the bundle manifest (Step 7)
const message = domainSeparatedMessage(DCP_CONTEXTS.Bundle, new TextEncoder().encode(canonicalizeV2(manifest)));
// classical signature over `message`; the PQ signature over `message || classical signature bytes`
Step 6: Update Audit Entries¶
Before (v1.0):
{
"dcp_version": "1.0",
"audit_id": "audit:example-001",
"prev_hash": "GENESIS",
"timestamp": "2026-10-25T00:01:00Z",
"agent_id": "agent:example-001",
"human_id": "human:example-001",
"intent_id": "intent:example-001",
"intent_hash": "<hex>",
"policy_decision": "approved",
"outcome": "call_completed",
"evidence": { "tool": "http", "result_ref": null }
}
After (v2.0):
{
"dcp_version": "2.0",
"audit_id": "audit:example-001",
"session_nonce": "<the bundle's nonce>",
"prev_hash": "GENESIS",
"hash_alg": "sha256",
"timestamp": "2026-10-25T00:01:00Z",
"agent_id": "agent:example-001",
"human_id": "human:example-001",
"intent_id": "intent:example-001",
"intent_hash": "sha256:<hex of the canonical intent payload>",
"policy_decision": "approved",
"outcome": "call_completed",
"evidence": { "tool": "http", "result_ref": null, "evidence_hash": null },
"pq_checkpoint_ref": null
}
New fields: session_nonce, hash_alg, evidence.evidence_hash and pq_checkpoint_ref; hashes carry the
sha256: prefix. A dual-hash chain adds prev_hash_secondary (SHA3-256) and hash_alg: "sha256+sha3-256".
The V2 verifiers from @dcp-ai/sdk 2.2.1, @dcp-ai/wasm 2.1.1, dcp-ai 2.10.1 (PyPI), Go v2.11.1 and Rust dcp-ai 2.11.1, the experimental candidate included, check the secondary links, intent hashes and audit_merkle_root_secondary that are present; absent ones are not required, and earlier versions do not check them, so producing these fields establishes the additional protection only where both the producer and the verifier include it
(STORAGE_AND_ANCHORING.md).
Audit entries are not signed one by one: the manifest commits to them through the audit Merkle root, and PQ
checkpoints sign ranges of them.
Step 7: Construct Bundle Manifest¶
V1 signs the whole bundle (signature.bundle_hash); V2 signs a manifest that commits to each artifact.
Before (v1.0 signed bundle):
{
"bundle": {
"responsible_principal_record": { "...": "..." },
"agent_passport": { "...": "..." },
"intent": { "...": "..." },
"policy_decision": { "...": "..." },
"audit_entries": [{ "...": "..." }]
},
"signature": {
"alg": "ed25519",
"created_at": "2026-10-25T00:02:00Z",
"signer": { "type": "human", "id": "human:example-001", "public_key_b64": "..." },
"bundle_hash": "sha256:<hex>",
"merkle_root": "sha256:<hex>",
"sig_b64": "..."
}
}
After (v2.0 signed bundle):
{
"bundle": {
"dcp_bundle_version": "2.0",
"manifest": {
"session_nonce": "<64 hex>",
"rpr_hash": "sha256:...",
"passport_hash": "sha256:...",
"intent_hash": "sha256:...",
"policy_hash": "sha256:...",
"audit_merkle_root": "sha256:...",
"audit_count": 1,
"canonicalization_profile": "dcp-jcs-v1"
},
"responsible_principal_record": { "payload": { "...": "..." }, "payload_hash": "sha256:...", "composite_sig": { "...": "..." } },
"agent_passport": { "payload": { "...": "..." }, "payload_hash": "sha256:...", "composite_sig": { "...": "..." } },
"intent": { "payload": { "...": "..." }, "payload_hash": "sha256:...", "composite_sig": { "...": "..." } },
"policy_decision": { "payload": { "...": "..." }, "payload_hash": "sha256:...", "composite_sig": { "...": "..." } },
"audit_entries": [{ "...": "..." }]
},
"signature": {
"hash_alg": "sha256",
"created_at": "2026-10-25T00:02:00Z",
"signer": { "type": "human", "id": "human:example-001", "kids": ["<classical kid>", "<pq kid>"] },
"manifest_hash": "sha256:<hex of the canonical manifest>",
"composite_sig": { "classical": { "...": "..." }, "pq": { "...": "..." }, "binding": "pq_over_classical" }
}
}
The manifest:
- has one hash per artifact (each artifact's payload_hash) and the audit Merkle root;
- may add audit_merkle_root_secondary (SHA3-256), pq_checkpoints, the signed session window
(session_issued_at, session_expires_at), intended_verifier and canonicalization_profile;
- is signed by the responsible principal with a composite signature under DCP-AI.v2.Bundle.
Step 8: Update Verifier Configuration¶
V2 verification takes a verifier policy that the verifier, not the bundle, decides. Start from
DEFAULT_VERIFIER_POLICY and change what you need: fields you leave out are not filled in from the defaults
(without max_key_age_days, for example, no key-age warning is produced; that check warns, it does not reject).
import { DEFAULT_VERIFIER_POLICY, verifySignedBundleV2 } from '@dcp-ai/sdk';
import type { VerifierPolicy } from '@dcp-ai/sdk';
const policy: VerifierPolicy = {
...DEFAULT_VERIFIER_POLICY,
default_mode: 'hybrid_required',
risk_overrides: { high: 'hybrid_required', medium: 'hybrid_required', low: 'hybrid_preferred' },
allow_v1_bundles: true,
};
// signedBundle and registry as in the TypeScript SDK's Quick Start (V2)
const result = await verifySignedBundleV2(signedBundle, registry, policy);
The fields are in the API reference. Keep allow_v1_bundles: true during the
transition period. V1 bundles still need a mode that tolerates a classical-only signature: with this policy
(hybrid_required for the medium and high tiers), the verification middlewares accept V1 bundles only from
low-risk passports with routine or standard intents.
Step 9: Add Security Tier Support (Optional)¶
Compute the security tier from the intent and the policy decision, which carries the risk_score:
// Fragment: the intent and the policy decision are V2 payloads, shortened here.
import { computeSecurityTier } from '@dcp-ai/sdk';
import type { IntentV2, PolicyDecisionV2 } from '@dcp-ai/sdk';
const intent = {
intent_id: 'intent:...',
action_type: 'api_call',
data_classes: ['pii'],
estimated_impact: 'medium',
// ...
} as IntentV2;
const policyDecision = { risk_score: 350 /* , ... */ } as PolicyDecisionV2;
const tier = computeSecurityTier(intent, policyDecision);
// tier = "elevated" (pii is sensitive data; risk_score 350 alone would give "standard")
Step 10: Run Conformance Tests¶
From the root of a checkout (each command in its own subshell):
# TypeScript
(cd sdks/typescript && npm test)
# Python
(cd sdks/python && pytest)
# NIST KAT compliance only
(cd sdks/typescript && npx vitest run src/__tests__/nist-kat.test.ts)
# Protocol conformance (V1 + V2)
npm run conformance
3. Backward Compatibility¶
3.1 Compatibility Matrix¶
V1 Verifier + V1 Bundle -> PASS (unchanged)
V1 Verifier + V2 Bundle -> REJECT "unsupported dcp_version 2.0"
V2 Verifier + V1 Bundle -> PASS (when allow_v1_bundles=true and the effective mode
for the bundle is classical_only or hybrid_preferred)
V2 Verifier + V2 Bundle -> PASS (full V2 verification)
3.2 Coexistence Strategy¶
During migration, your system can handle both v1.0 and v2.0 bundles by routing on the version:
// Fragment: signedBundle comes from the request; v1PublicKeyB64, registry and policy from configuration.
import { detectDcpVersion, verifySignedBundle, verifySignedBundleV2 } from '@dcp-ai/sdk';
const version = detectDcpVersion(signedBundle); // '2.0', '1.0', or null for anything else
let result;
if (version === '2.0') {
result = await verifySignedBundleV2(signedBundle, registry, policy); // composite verification, tier enforcement
} else if (version === '1.0' && policy.allow_v1_bundles) {
result = verifySignedBundle(signedBundle, v1PublicKeyB64); // the Ed25519 signature, hashes and chain
} else {
result = { verified: false, errors: ['unsupported or disallowed bundle version'] };
}
This only routes formats. The V1 branch does not apply the V2 policy modes or the security tier to the V1 bundle
(VERIFICATION.md V2-10), and neither branch decides trust: the verification middlewares do the former, and
admission the latter. verifySignedBundleV2 only accepts v2.0 bundles; the reference CLI
(dcp verify-bundle) and the middlewares route by version themselves.
3.3 Schema Routing¶
A verifier routes on the bundle's version before checking its schema:
- a V2 signed bundle has
bundle.dcp_bundle_version: "2.0"and abundle.manifest→schemas/v2/*.schema.json; - a V1 bundle's artifacts carry
dcp_version: "1.0"→schemas/v1/*.schema.json.
3.4 When to Drop V1 Support¶
V2 verifiers MUST support V1 bundles indefinitely per the specification. However, individual deployments may set a policy to reject V1 bundles after a transition period:
This should only be done after all agents in your ecosystem have migrated to v2.0.
4. Bundle Format Changes (Detailed)¶
4.1 Signed Bundle Envelope¶
The complete V1 and V2 envelopes are in Step 7. Key structural differences:
- Individual artifact signing: the RPR, passport, intent and policy decision are each a
SignedPayload(payload,payload_hash,composite_sig) signed under their own context. Audit entries stay unsigned and are committed through the audit Merkle root. - Manifest:
bundle.manifestcommits to each artifact's hash and the audit Merkle root; it replaces V1'ssignature.bundle_hash. - Session nonce: the same
session_noncein every artifact, audit entry and the manifest. - Bundle signature:
signature.composite_sigsigns the manifest (signature.manifest_hash), not the whole bundle;signature.signer.kidsnames the keys instead of carrying a public key. - Version marker:
bundle.dcp_bundle_version: "2.0".
4.2 Composite Signature Object¶
{
"composite_sig": {
"classical": {
"alg": "ed25519",
"kid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"sig_b64": "<base64-encoded Ed25519 signature>"
},
"pq": {
"alg": "ml-dsa-65",
"kid": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
"sig_b64": "<base64-encoded ML-DSA-65 signature>"
},
"binding": "pq_over_classical"
}
}
4.3 Audit Entry Changes¶
| Field | v1.0 | v2.0 |
|---|---|---|
dcp_version |
"1.0" |
"2.0" |
session_nonce |
Not present | Required |
prev_hash |
Hex SHA-256 of the previous entry, or "GENESIS" |
sha256:<hex> of the previous entry, or "GENESIS" |
hash_alg |
Not present | Required: sha256, or sha256+sha3-256 with a dual-hash chain |
prev_hash_secondary |
Not present | Optional: sha3-256:<hex> (dual-hash chain) |
intent_hash |
Hex SHA-256 | sha256:<hex> |
intent_hash_secondary |
Not present | Optional: sha3-256:<hex> |
evidence.evidence_hash |
Not present | Present, may be null |
pq_checkpoint_ref |
Not present | Required, may be null |
| Signature | None per entry; the bundle signature covers them | None per entry; the manifest's audit Merkle root commits to them, and PQ checkpoints sign ranges of them |
5. SDK API Changes¶
5.1 TypeScript SDK¶
| v1.0 API | v2.0 API | Notes |
|---|---|---|
signBundle(bundle, secretKey) |
signBundleV2(bundle, { registry, signerType, signerId, keys }) |
Composite signature over the manifest |
verifySignedBundle(sb, publicKey) |
verifySignedBundleV2(sb, registry, policy?) |
Policy-driven verification |
new BundleBuilder()…build() |
new BundleBuilderV2(sessionNonce)…build() |
Session nonce required; artifacts are SignedPayloads |
hashObject(obj) |
hashObject(obj) (unchanged) |
Still SHA-256 + JCS |
| N/A | dualHashCanonical(canonicalJson) |
SHA-256 + SHA3-256 |
| N/A | registerDefaultProviders() + getDefaultRegistry() |
Ed25519, ML-DSA-65, SLH-DSA-192f via the algorithm registry |
| N/A | computeSecurityTier(intent, policyDecision?) |
Adaptive tier calculation (spec §14.2) |
| N/A | deriveKid(alg, publicKeyBytes) |
Deterministic kid |
| N/A | compositeSign(registry, context, payloadBytes, keys) |
Manual composite signing |
| N/A | compositeVerify(registry, context, payloadBytes, sig, classicalPk, pqPk?) |
Manual composite verification |
5.2 Python SDK¶
| v1.0 API | v2.0 API | Notes |
|---|---|---|
sign_bundle(bundle, secret_key) |
prepare_payload + await composite_sign(registry, context, payload_bytes, classical_key, pq_key) per artifact, then BundleBuilderV2(session_nonce)…build() + await sign_bundle_v2(bundle, registry, signer_type, signer_id, classical_key, pq_key) (dcp_ai.v2) |
Composite signatures over each artifact and over the manifest |
verify_signed_bundle(sb, public_key) |
await verify_signed_bundle_v2(signed_bundle, registry, policy=None) (dcp_ai.v2) |
Policy-driven |
| N/A | Ed25519Provider / MlDsa65Provider (dcp_ai.providers), async generate_keypair() |
Hybrid keys |
| N/A | compute_security_tier(intent, policy_decision=None) (dcp_ai.v2) |
Adaptive tier (spec §14.2) |
| N/A | derive_kid(alg, public_key_bytes) (dcp_ai.v2) |
Deterministic kid |
5.3 Go SDK¶
| v1.0 API | v2.0 API | Notes |
|---|---|---|
SignObject(bundle, secretKeyB64) |
v2.SignPayload(registry, context, payload, classicalKey, pqKey) per artifact, then v2.BuildBundleV2(input) + v2.SignBundleV2(registry, bundle, classicalKey, pqKey) |
Composite signatures over each artifact and over the manifest |
VerifySignedBundle(sb, publicKeyB64) |
v2.VerifySignedBundleV2(registry, signedBundleJSON) |
Policy-driven |
| N/A | GenerateKeypair() on providers.Ed25519Provider / providers.MlDsa65Provider |
Hybrid keys |
| N/A | v2.ComputeSecurityTier(input), v2.SecurityTierInputFromPayloads(intent, policyDecision) |
Adaptive tier (spec §14.2) |
| N/A | v2.DeriveKid(alg, publicKey) |
Deterministic kid |
5.4 Rust SDK¶
| v1.0 API | v2.0 API | Notes |
|---|---|---|
sign_object(bundle, secret_key_b64) |
prepare_payload (dcp_ai::v2::signed_payload) + composite_sign(ed, pq, context, payload_bytes, classical_key, pq_key) (dcp_ai::v2::composite_ops) per artifact, then build_bundle_v2(&rpr, &passport, &intent, &policy, &audit_entries, session_nonce) + sign_bundle_v2(bundle, &classical_key, &pq_key) (dcp_ai::v2::bundle) |
Composite signatures over each artifact and over the manifest |
verify_signed_bundle(sb, public_key_b64) |
verify_signed_bundle_v2_with_policy(&signed_bundle, &policy) (dcp_ai::v2::verify_bundle) |
Policy-driven |
5.5 CLI Changes¶
| v1.0 Command | v2.0 Command | Notes |
|---|---|---|
dcp keygen [out_dir] |
dcp keygen --hybrid [out_dir] |
Generates both key types |
dcp sign-bundle |
dcp sign-bundle --composite <bundle_v2.json> <ed25519_sk> <ml_dsa_65_sk> [out] |
Composite signing via @dcp-ai/sdk |
dcp verify-bundle |
dcp verify-bundle [--policy <mode>] <signed.json> [public_key.txt] |
Full V2 verification; <mode> is classical_only, hybrid_preferred, hybrid_required or pq_only; the key pins the classical signer |
| N/A | dcp kid <public_key_file> --alg <algorithm> |
Compute kid |
| N/A | dcp recovery-setup --threshold <M> --shares <N> [out] |
M-of-N social recovery |
| N/A | dcp emergency-revoke --agent <id> --token <secret> |
Panic-button revocation |
| N/A | dcp rotate-key --old-kid <kid> --new-alg <alg> |
Key rotation with PoP |
| N/A | dcp capabilities <endpoint_url> |
Query endpoint capabilities |
| N/A | dcp advisory check [url] |
Check algorithm advisories |
6. Testing Your Migration¶
6.1 Verification Checklist¶
Run through this checklist to confirm your migration is complete:
- Key generation: Hybrid key pairs (Ed25519 + ML-DSA-65) generate successfully
- kid derivation:
deriveKid('ed25519', pk)matches expected value - kid derivation:
deriveKid('ml-dsa-65', pk)matches expected value - RPR construction: v2.0 RPR includes
dcp_version,session_nonce, both key types - Passport construction: v2.0 passport includes
dcp_version,session_nonce - Intent construction: v2.0 intent includes
dcp_version,session_nonce(and, optionally, asecurity_tierthat can only raise the computed tier) - Composite signing:
signBundleV2produces valid composite signatures - Domain separation: Signatures include context tags
- Composite binding: PQ signature covers classical signature bytes
- Dual-hash chain: Audit entries include
prev_hash_secondary(SHA3-256); verifiers from@dcp-ai/sdk2.2.1,@dcp-ai/wasm2.1.1,dcp-ai2.10.1 (PyPI), Go v2.11.1 and Rustdcp-ai2.11.1 check every link present (Step 6); earlier versions do not - Manifest: Bundle manifest contains all artifact hashes and, with a dual-hash chain, the secondary Merkle root, which verifiers from
@dcp-ai/sdk2.2.1,@dcp-ai/wasm2.1.1,dcp-ai2.10.1 (PyPI), Go v2.11.1 and Rustdcp-ai2.11.1 check when present - Session nonce: All artifacts in a bundle share the same nonce
- V2 verification:
verifySignedBundleV2(ordcp verify-bundle) passes for correctly signed v2.0 bundles - V1 compatibility: v1.0 bundles still verify with the V1 function
verifySignedBundle(which accepts only v1.0), withdcp verify-bundleor with a version router as in 3.2; the V2 functionverifySignedBundleV2accepts only v2.0 - Tamper detection: Modifying any artifact field causes verification to fail
- Stripping detection: Removing the PQ signature (binding relabelled
classical_only) fails underhybrid_requiredandpq_onlyand is accepted underclassical_onlyandhybrid_preferred; a present but invalid PQ signature fails in every mode - NIST KAT: All cryptographic operations pass KAT validation
- Cross-SDK: Bundles signed by one SDK verify in another
6.2 Common Migration Issues¶
Issue: "kid mismatch" during verification
The v2.0 kid derivation is deterministic: hex(SHA-256(UTF8(alg) || 0x00 || raw_pk))[0:32]. If your v1.0 implementation used a different kid scheme, regenerate kids using the v2.0 formula. The SDK's deriveKid function handles this.
Issue: "session_nonce mismatch" across artifacts
All artifacts in a single bundle MUST share the same session_nonce. Generate the nonce once per bundle creation and pass it to all artifact constructors.
Issue: "composite_sig.pq verification failed"
The PQ signature signs context || 0x00 || payload || classical_sig. Ensure the classical signature bytes (raw, not base64) are appended to the signed message before PQ signing.
Issue: "unsupported dcp_version 2.0" from a v1.0 verifier
This is expected. v1.0 verifiers cannot process v2.0 bundles. The upstream verifier needs to upgrade to a v2.0-capable SDK.
Issue: Float values in signed payloads
v2.0 prohibits floating-point numbers in all signed payloads (canonicalization ambiguity). Replace risk_score: 0.75 with an integer scale: risk_score: 750 (0-1000).
Issue: Large bundle sizes
v2.0 bundles are larger, mainly because of the ML-DSA-65 signatures (3309 B each). Measure your largest bundles in
the format your verifier actually reads and give the request body an explicit size budget (see
OPERATOR_GUIDE.md). The reference verification server reads JSON
bodies only, although its capability document lists application/cbor, and only the TypeScript SDK has a CBOR
encoder (cborEncode); this repository publishes no measured size reduction for it. presentCompact removes the
PQ signature (binding classical_only), so such a bundle passes only where the effective mode is classical_only or
hybrid_preferred. Transport size is not a reason to lower a security tier.
6.3 Validating Migrated Bundles¶
The CLI has no v1.0-to-v2.0 migration checker. Verify each migrated bundle with the V2 verifier instead:
It checks the manifest hashes, session binding, composite signatures and audit chain. risk_score belongs to the policy decision, as an integer from 0 to 1000; the V2 intent schema does not allow it (spec §14.2).
7. Recommended Migration Order¶
For teams migrating a production system, the recommended order is:
- Update SDK to the latest version supporting v2.0.
- Update verifiers first with
allow_v1_bundles: true. This ensures your verifiers can handle both v1.0 and v2.0 bundles during the transition. - Generate hybrid key pairs for all agents and humans.
- Update artifact construction to include
dcp_version: "2.0",session_nonce, and dual key entries. - Switch to composite signatures using the v2.0 signing APIs.
- Add dual-hash chains to audit entries.
- Implement bundle manifest structure.
- Sign sensitive intents with hybrid signatures. V2 verifiers enforce the security tier (spec §14.3): a
classical_onlybundle is rejected for a payment, sensitive data, high impact or a policy decisionrisk_scoreof 500 or more. - Run full conformance tests including NIST KAT validation.
- Disable v1.0 bundle acceptance when all agents in your ecosystem have migrated.
8. Timeline and Support¶
| Phase | Timeline | Actions |
|---|---|---|
| Preparation | Weeks 1-2 | Update SDK, generate hybrid keys, review this guide |
| Verifier upgrade | Weeks 2-3 | Deploy v2.0 verifiers with v1 compatibility |
| Agent migration | Weeks 3-6 | Update artifact construction, switch to composite signatures |
| Validation | Weeks 6-8 | Run conformance tests, cross-SDK interop testing |
| Cutover | Week 8+ | Set allow_v1_bundles: false when ready |
For migration support:
- GitHub Issues or Discussions: dcp-ai-protocol/dcp-ai
- Private vulnerability reports via GitHub's security advisory UI
- Email the maintainers per GOVERNANCE.md
There is no dedicated Slack, Discord, or office-hours call at this time. See EARLY_ADOPTERS.md for the current support model.