Working draft for public review. Not a stable standard, IETF adoption, certification or production recommendation. Experimental profiles require explicit opt-in.
XCIM Encodings and Recipient Binding
Contents
WD-02 · Normative encoding rules and explicit limits of public projections
Erratum E1 is applied to this consolidated reading; the original WD-02 text remains available unchanged.
1. HTTPS identity issuer normalization
xcim-https-issuer-0.1 is the deliberately narrow URI profile for EVP-01's identity_issuer. It uses URI components from RFC 3986, not browser repair rules. Unsupported forms MUST be rejected, not guessed.
- Require printable ASCII, HTTPS and a DNS A-label hostname. Reject credentials, whitespace/control characters, backslashes, percent escapes, query/fragment delimiters, IP literals, a trailing hostname dot and invalid DNS labels. Internationalized hosts MUST be converted to validated A-labels before this procedure.
- Lowercase the scheme and hostname. Validate a decimal port in 1–65535; remove port 443 and serialize another port without leading zeros. An empty port is invalid.
- Preserve path case and characters exactly. Only ASCII letters/digits, unreserved characters, sub-delimiters,
:,@and/are allowed. Reject path segments exactly.or..; do not collapse repeated slashes or decode/re-encode a path. - Serialize an absent path as
/. A present trailing slash is preserved;/issuerand/issuer/are distinct issuer identifiers.
Thus https://ISSUER.EXAMPLE:443 and https://issuer.example/ normalize to https://issuer.example/. The public EVP field MUST already equal the normalized result. Its commitment is exactly:
base64url(SHA-256(UTF8("XCIM-IDENTITY-ISSUER-0.1\0") || UTF8(normalized_identity_issuer)))
EVP verification_origin follows the same authority validation but contains no path, query or fragment. Serialize it as https://lowercase-host[:nondefault-port], with no trailing slash, following the HTTPS origin model in RFC 6454. Validators MUST compare this exact serialized origin to the session-frozen origin. These EVP rules MUST NOT replace OIDC Core's exact issuer matching or rewrite an already signed object.
2. Commitment scope
| Field | Meaning and verification |
|---|---|
| Receipt, batch, event and state hashes | Publicly reproducible formulas in the core and selected publication profile |
presentation.manifest_hash |
SHA-256 of JCS of the complete inline-signed manifest, including vendor_signature |
EVP identity_issuer_commitment |
Exact normalized-URI recipe above |
| OIDC client and subject commitments; both profiles' nonce commitments | 32-byte issuer-local attestations under an explicitly versioned adapter recipe; not universally reproducible public hashes |
EVP evidence_fingerprint |
Adapter/revision-specific validated representation; cross-adapter recipe remains an experimental review gate |
recipient.pairwise_id |
Application-scoped private pseudonym, not a public email-to-ID lookup |
For an issuer-local commitment, the producer MUST document the source representation, byte encoding, domain separation, scoping and salt/key lifecycle in its versioned adapter contract. Receivers MUST enforce byte length, signed profile/context and required client-commitment equality; they MUST NOT invent a common preimage recipe or compare opaque commitments across issuers/adapters. A new documented recipe does not retroactively reinterpret historical values. Fixtures using repeated marker bytes are synthetic projection tests, not derivation vectors.
New issuance claiming WD-02's commitment baseline MUST select xcim-identity-commitments-wd02 in the versioned adapter contract identified by its policy and adapter_version. This is a producer recipe, not a new receipt member. Its closed private input objects are:
| Commitment | Exact private JCS input members | UTF-8 domain including terminal NUL |
|---|---|---|
| OIDC application/evidence client commitment | issuer, client_id, app_id, app_epoch |
XCIM-OIDC-CLIENT-WD02\0 |
| OIDC subject commitment | issuer, subject, app_id, app_epoch, salt |
XCIM-OIDC-SUBJECT-WD02\0 |
| OIDC or EVP nonce commitment | app_id, app_epoch, nonce |
XCIM-NONCE-WD02\0 |
Each result is base64url(SHA-256(domain || J(private_input))). Strings are exact validated values; the OIDC issuer is matched exactly under OIDC Core, not normalized by the EVP URI algorithm. app_epoch is a positive safe integer. salt and nonce encode exactly 32 independently random bytes as canonical unpadded base64url; the subject salt is retained privately for audit and the nonce is the session-frozen nonce used for identity validation. No private input object is added to the public receipt or log. The same client recipe/input MUST produce both client commitment fields. Separate application epochs or scopes change these commitments.
Historical receipts and other explicitly accepted adapter recipes remain issuer-local attestations, not claims to this new derivation recipe. A receiver MUST use the recorded adapter contract rather than recomputing historical fields with WD-02's domains. The bundled wd02-commitment-recipes.json contains synthetic private inputs and exact digests reproduced in Python and Node. It does not supply secrets or validate an actual identity assertion.
The baseline pairwise derivation is HMAC-SHA-256(issuer_app_pairwise_key, UTF8(canonical_email)), encoded as unpadded base64url. The key MUST be secret and independently scoped to each issuer/application or explicitly declared privacy domain. The canonical email is the exact authorized delivery address, not a provider's comparison alias. Key changes require authenticated mapping/lifecycle handling; a receiver MUST NOT infer that a changed ID represents the same recipient. The bundled helper and fixed test-only keys illustrate these bytes, not production key distribution.
3. Email canonicalization
The xcim-email-0.1 profile accepts one addr-spec without display names, comments or angle brackets. Trim only leading/trailing SPACE/HTAB; reject CR/LF, other controls and internal whitespace. Accept an ASCII dot-atom local part of at most 64 characters, without leading/trailing or consecutive dots; preserve its case, dots and plus tags. Quoted local parts and SMTPUTF8 are not supported by this profile.
Process the domain using UTS #46 non-transitional mapping with STD3 rules, serialize lowercase ASCII A-labels, and reject literals, underscores, trailing dots, invalid labels, domains longer than 253 ASCII characters and addresses longer than 254 characters. The reference runner pins Python idna==3.10, which fixes its IDNA/Unicode tables; another implementation MUST reproduce the published positive and negative vectors. Provider-specific equivalence rules remain independently versioned and MUST NOT change this canonical delivery address.
E1 requires these domain checks after UTS #46/IDNA conversion as well as the original-input checks. In particular, U+3002, U+FF0E and U+FF61 can map to an ASCII dot, and ignored code points can reveal a trailing dot. A mapped trailing dot, empty label or invalid DNS A-label MUST be rejected before the address can be returned or hashed. An accepted canonical address MUST itself pass the same canonicalizer unchanged. Internal Unicode separators may still map to valid internal ASCII dots; the correction does not reject internationalized domains generally.
4. Recipient verification boundary
A receiver knows the intended SMTP envelope recipient or the authenticated list-evaluation target. A signed pairwise ID is not enough to establish that match because the receiver does not hold the issuer's pairwise secret. A receiver MUST obtain an authenticated mapping bound to the issuer, application/epoch, exact canonical target and pairwise ID through its explicitly declared integration contract. A sender assertion, arbitrary lookup response or copied header is insufficient. Missing, untrusted or unavailable mapping MUST withhold PASS.
This draft does not standardize a portable recipient-mapping attestation or distribute pairwise keys to receivers. Header integrations therefore remain shadow-mode until their authenticated mapping and message-binding contracts are reviewed and exercised. No implementation can claim end-to-end receiver conformance solely from the public fixture verifiers.
The historical message-presentation digest vector is exactly:
SHA-256(UTF8("XCIM-MESSAGE-RECIPIENT-0.1\0") || UTF8(canonical_envelope_recipient) || salt_16_bytes)
Its unpadded base64url digest and independently random 16-byte salt belong only in the private message presentation, not public logs. The digest detects changes to that presentation's recipient bytes; it is not an issuer signature or an authenticated pairwise mapping. A malicious sender can recompute a digest and therefore cannot be constrained by this digest alone. check_message_recipient_vector.py tests bytes and mutations, not recipient authorization.
5. Standalone checks
From the extracted bundle root, install tools/requirements-ci.txt, make Node.js available, then run:
python tools/xcim_check.py
python tools/check_email_normalization_vector.py
python tools/check_message_recipient_vector.py
python tools/shadow_mode_adapter.py examples/xcim-shadow-mode.eml
All fixtures are synthetic, use explicitly public test-only key material and run without a private product workspace or live issuer. A successful fixture run is not current permission, chain verification, accreditation or independently operated interoperability.