Canonicalization Version Migration Guide
Audience: Stella Ops platform engineers and operators responsible for attestations and content-addressed evidence. Purpose: Explain how Stella Ops content-addressed identifiers move from legacy (unversioned) canonicalization to versioned canonicalization (stella:canon:v1), and what the planned migration phases will require of operators.
Version: 1.0 Status: Active (Phase 1 only — see note below) Last Updated: 2025-12-24
Implementation status (reconciled against
src/). The versioned-canonicalization primitives are implemented and shipping:CanonVersion(V1 = "stella:canon:v1",VersionFieldName = "_canonVersion",Current = V1,IsVersioned,ExtractVersion) insrc/__Libraries/StellaOps.Canonical.Json/CanonVersion.cs, and the hashing/canonicalization helpersCanonJson.CanonicalizeVersioned<T>/CanonJson.HashVersioned<T>insrc/__Libraries/StellaOps.Canonical.Json/CanonJson.Versioned.csandCanonJson.Hashing.cs. Content-addressed ID generation (EvidenceId,ReasoningId,VexVerdictId,ProofBundleId) already emits versioned hashes viaContentAddressedIdGeneratorinsrc/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Identifiers/.The operator-facing tooling and lifecycle controls described below are not yet implemented and are retained as the planned migration design:
- There is no
stella rehash,stella inspect --show-versionCLI command, andstella verifydoes not expose a version flag (verified againstsrc/Cli/).- There is no
canon_legacy_hash_verified_totalmetric and no deprecation-warning logging for legacy hashes.- There is no dual-mode verifier that falls back from versioned to legacy hashing; generation is versioned-only and no consumer of
IsVersioned/ExtractVersionperforms the accept-both-formats flow shown here.Treat the phase timeline, CLI snippets, SQL, and metrics in this guide as roadmap/Draft until the corresponding code lands. Flagged items are marked NOT IMPLEMENTED inline.
Overview
This guide describes the migration path for content-addressed identifiers from legacy (unversioned) canonicalization to versioned canonicalization (stella:canon:v1). Versioned canonicalization embeds algorithm version markers in the canonical JSON before hashing, ensuring forward compatibility and verifier clarity.
Content-addressed identifiers covered: EvidenceId, ReasoningId, VexVerdictId, and ProofBundleId (defined under src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Identifiers/). All four are computed by ContentAddressedIdGenerator, and the three single-predicate IDs (EvidenceId, ReasoningId, VexVerdictId) already canonicalize with the stella:canon:v1 version marker. ProofBundleId is a Merkle root over the other IDs and SBOM entry ID rather than a direct versioned-canonicalization hash.
Why Versioning?
Problem
Legacy content-addressed IDs (EvidenceId, ReasoningId, VexVerdictId, ProofBundleId) were computed as:
hash = SHA256(canonicalize(payload))
If the canonicalization algorithm ever changed (bug fix, specification update, optimization), existing hashes would become invalid with no way to detect which algorithm produced them.
Solution
Versioned content-addressed IDs embed a version marker:
hash = SHA256(canonicalize_with_version(payload, "stella:canon:v1"))
The canonical JSON now includes _canonVersion as the first field:
{
"_canonVersion": "stella:canon:v1",
"sbomEntryId": "sha256:91f2ab3c:pkg:npm/lodash@4.17.21",
"vulnerabilityId": "CVE-2021-23337"
}
Migration Phases
Phase 1: Dual-Mode (Current — partially implemented)
Timeline: Now
Behavior:
- Generation: All new content-addressed IDs use versioned canonicalization (v1). Implemented —
ContentAddressedIdGenerator.ComputeEvidenceId/ComputeReasoningId/ComputeVexVerdictIdcallCanonicalizeVersionedwithCanonVersion.Current. - Verification: Accept both legacy and v1 hashes. NOT IMPLEMENTED — no shipping verifier falls back from versioned to legacy hashing today; the dual-mode flow shown under Verifying Both Formats is the planned design.
- Detection:
CanonVersion.IsVersioned()distinguishes format. Implemented as a primitive (src/__Libraries/StellaOps.Canonical.Json/CanonVersion.cs); not yet consumed by a verification path.
Impact:
- Zero downtime migration
- Existing attestations remain valid
- New attestations get version markers
Phase 2: Deprecation Warning (planned — NOT IMPLEMENTED)
Timeline: +6 months from Phase 1
Behavior:
- Log warnings when verifying legacy hashes
- Emit metrics for legacy hash encounters
- Continue accepting legacy hashes
Operator Action:
- Monitor
canon_legacy_hash_verified_totalmetric. NOT IMPLEMENTED — no such metric is emitted anywhere insrc/(verified). The metric name is a proposal. - Plan re-attestation of critical assets
Phase 3: Legacy Rejection (planned — NOT IMPLEMENTED)
Timeline: +12 months from Phase 1
Behavior:
- Reject legacy hashes during verification
- Only v1 (or newer) hashes accepted
Operator Action:
- Re-attest any remaining legacy attestations before cutoff
- Use
stella rehashCLI command for bulk re-attestation. NOT IMPLEMENTED — there is nostella rehashcommand insrc/Cli/(verified). Bulk re-attestation tooling is a roadmap item.
Detection and Verification
Detecting Versioned Hashes
Versioned canonical JSON always starts with {"_canonVersion": due to lexicographic sorting (underscore sorts before all ASCII letters).
using StellaOps.Canonical.Json;
// Check if canonical JSON is versioned
byte[] canonicalJson = GetCanonicalPayload();
bool isVersioned = CanonVersion.IsVersioned(canonicalJson);
// Extract version if present
string? version = CanonVersion.ExtractVersion(canonicalJson);
if (version == CanonVersion.V1)
{
// Use V1 verification algorithm
}
Verifying Both Formats
During Phase 1, verifiers should accept both formats. The pattern below is the planned dual-mode verifier; it is NOT IMPLEMENTED as a shipping component today.
API note.
CanonJson.Hash<T>(T obj)andCanonJson.HashVersioned<T>(T obj, string version = CanonVersion.Current)are generic helpers that canonicalize the object before hashing — they do not accept already-canonical bytes. To re-hash a payload that is already canonical UTF-8 JSON, hash the bytes directly withCanonJson.Sha256Hex(byte[]), or compare against the bytes recovered byIsVersioned/ExtractVersion. Signatures live insrc/__Libraries/StellaOps.Canonical.Json/CanonJson.Hashing.cs.Hash/HashVersionedreturn a 64-character lowercase hex string (nosha256:prefix); useHashPrefixed/HashVersionedPrefixedfor the prefixed form.
// Illustrative (proposed) dual-mode verifier — not a shipping API.
public bool VerifyContentAddressedId(byte[] canonicalPayload, string expectedHash)
{
// Both branches re-hash the already-canonical bytes directly.
// CanonVersion.IsVersioned only inspects the leading bytes for the
// {"_canonVersion":" prefix; it does not re-canonicalize.
var hash = CanonJson.Sha256Hex(canonicalPayload);
return hash == expectedHash;
}
Re-Attestation Procedure
When to Re-Attest
Re-attestation is required when:
- Moving from Phase 1 to Phase 3
- Migrating attestations between systems
- Archiving for long-term storage
CLI Re-Attestation (planned — NOT IMPLEMENTED)
NOT IMPLEMENTED. No
stella rehashcommand exists insrc/Cli/(verified). The snippet below is the proposed command surface for the bulk re-attestation tooling and will become accurate only once that tooling lands.
# Re-attest a single attestation bundle
stella rehash --input attestation.json --output attestation-v1.json
# Bulk re-attest all attestations in a directory
stella rehash --input-dir /var/stella/attestations \
--output-dir /var/stella/attestations-v1 \
--version stella:canon:v1
# Dry-run to preview changes
stella rehash --input attestation.json --dry-run
Database Migration
Illustrative — schema does not match the live Evidence Locker. The Evidence Locker has no
attestationstable and nocontentcolumn. Content-addressed records live in tables such asevidence_locker.decision_capsules,evidence_locker.verdict_attestations, andevidence_locker.regulatory_artifact_ledger, and the digest column is not uniformly named (verified against005_decision_capsules.sql,006_verdict_attestations.sql,007_regulatory_audit_retention_ledger.sql):
evidence_locker.decision_capsules— digest incontent_hash(^[0-9a-f]{64}$), and it does persist the canonical body inmanifest_json text(stored astext, notjsonb, for byte-identical replay). Amanifest_json LIKE '{"_canonVersion":%'scan is therefore structurally possible here, but it will not match: the stored body is the capsule manifest, not a versioned-canon predicate payload.evidence_locker.verdict_attestations— nocontent_hashcolumn; the digest ispredicate_digestand the signed body isenvelope jsonb(a DSSE envelope, not the bare canonical predicate).evidence_locker.regulatory_artifact_ledger— digest incontent_hash(sha256:-prefixed); the canonical body is not stored (onlysource_evidence_refs).The bare
content LIKE '{"_canonVersion":%'predicate below cannot be evaluated againstverdict_attestationsorregulatory_artifact_ledger(no body column), and is misleading againstdecision_capsules(the body column ismanifest_json, and capsule manifests are not the predicates whose IDs carry the version marker). Detecting legacy content-addressed records in PostgreSQL therefore means recomputing a versioned hash from the source predicate and comparing it against the stored digest (content_hash/predicate_digest), not aLIKEscan of stored JSON. Adapt the query below to your actual table once a real migration job is defined.
-- ILLUSTRATIVE ONLY: no `attestations` table exists in the Evidence Locker schema.
-- Find legacy attestations (those without version marker)
SELECT id, content_hash, created_at
FROM attestations
WHERE NOT content LIKE '{"_canonVersion":%'
ORDER BY created_at;
-- Export for re-processing
COPY (
SELECT id, content
FROM attestations
WHERE NOT content LIKE '{"_canonVersion":%'
) TO '/tmp/legacy_attestations.csv' WITH CSV HEADER;
Troubleshooting
Hash Mismatch Errors
Symptom: Verification fails with “hash mismatch” error.
Diagnosis:
- Check if the stored hash was computed with legacy or versioned canonicalization
- Check the current verification mode (Phase 1/2/3)
Resolution (planned — NOT IMPLEMENTED):
NOT IMPLEMENTED. There is no
stella inspect ... --show-versioncommand insrc/Cli/(verified). To check a payload’s format programmatically today, useCanonVersion.IsVersioned(bytes)/CanonVersion.ExtractVersion(bytes)against the canonical JSON.
# Inspect the attestation format
stella inspect attestation.json --show-version
# Output:
# Canonicalization Version: stella:canon:v1
# Hash Algorithm: SHA-256
# Computed Hash: sha256:7b8c9d0e...
Legacy Hash in Phase 3
Symptom: Verification rejected with “legacy hash not accepted” error.
Phase 3 rejection is NOT IMPLEMENTED (no verifier rejects legacy hashes today). This section applies only after the phase timeline lands.
Resolution:
- Re-attest the content with versioned canonicalization
- Update any references to the old hash
stella rehash --input old.json --output new.json # NOT IMPLEMENTED (no `stella rehash`)
stella verify new.json # Should pass
Performance Considerations
Versioned canonicalization adds ~35 bytes to each canonical payload (the prefix {"_canonVersion":"stella:canon:v1", is exactly 35 bytes — consistent with CanonVersion.IsVersioned, which requires a minimum length of 35 bytes). This has negligible impact on:
- Hash computation time (<1μs overhead)
- Storage size (<0.1% increase for typical payloads)
- Network transfer (compression eliminates overhead)
Version History
| Version | Identifier | Status | Notes |
|---|---|---|---|
| V1 | stella:canon:v1 | Current | RFC 8785 JSON canonicalization |
| Legacy | (none) | Deprecated | Pre-versioning; no version marker |
Related Documentation
- Proof Chain Specification — see its Canonicalization Versioning section, which mirrors the phase timeline here.
- Content-Addressed Identifier System
- CanonJson source of truth (no standalone API-reference doc exists yet):
src/__Libraries/StellaOps.Canonical.Json/CanonVersion.cs,CanonJson.Versioned.cs, andCanonJson.Hashing.cs.
