# Contratos locales de P1 — schemas, bytes y validación

**Revisión de desarrollo:** P1-01, 2026-10-19. **Dirección:** D01–D03 adoptada
por el proyecto; [registro](https://xcim.org/standardization/source/ACCEPTED-D01-D03.md). Esta iteración concreta los
campos de [P1](https://xcim.org/standardization/source/SCOPE-AND-BINDING-PROPOSAL.md). Es trabajo local de draft con
[dependencias pendientes](https://xcim.org/standardization/p1/DEPENDENCIES.md), sin estado de estándar ni registro.

## 1. Schemas y límites de su autoridad

Todos usan JSON Schema 2020-12 y `additionalProperties: false` en sus objetos.
Los `$id` bajo `https://xcim.invalid/` son identificadores de desarrollo en
dominio reservado; se resuelven con un catálogo **local**, sin red. No afirman
publicación en xcim.org. Ningún schema establece confianza o frescura.

| Archivo | Contenido |
|---|---|
| [common](https://xcim.org/standardization/p1/schemas/common.schema.json) | Tokens de 32/64 bytes, enteros seguros, dominios, IDs, clave pública y límites compartidos. |
| [RBA](https://xcim.org/standardization/p1/schemas/rba.schema.json) | Payload de recipient binding: issuer, scope de permiso, receipt commitment, pairwise ID, perfil de dirección, dominio/salt/compromiso y vigencia. |
| [MSA](https://xcim.org/standardization/p1/schemas/msa.schema.json) | Payload de autorización de clave MP: app/epoch/authority_epoch, autorización, clave pública y thumbprint, rol y scopes finitos, vigencia. |
| [MP](https://xcim.org/standardization/p1/schemas/mp.schema.json) | Payload de presentación: scope, compromisos de RBA/MSA/contenido, nonce, dominios y vigencia. |
| [Protected](https://xcim.org/standardization/p1/schemas/protected.schema.json) | Header JWS cerrado con `alg`, `kid`, `typ`. |
| [JWS](https://xcim.org/standardization/p1/schemas/jws.schema.json) | Envelope Flattened cerrado: `protected`, `payload`, `signature`. |
| [Reference](https://xcim.org/standardization/p1/schemas/reference.schema.json) | Wrapper cerrado de RBA-JWS, MSA-JWS y MP-JWS, versión `p1`. |

IDs legacy `urn:xcim:*` se conservan como valores del baseline; G11 sigue
pendiente. P1 limita estos IDs a 256 caracteres, kid a 200 y propósito a token
ASCII de hasta 128. Esta restricción del perfil no cambia validez histórica de
un receipt fuera de P1. Scopes MSA tienen 1–64 elementos, sin duplicados,
wildcards ni subdelegación; rol único `message_presentation` y clase única
`explicit_consent`. `authorization_id`, `binding_id`, `presentation_id`, salt,
pairwise ID y hashes codifican exactamente 32 bytes. Firmas: 64 bytes.

`exp > iat`, coincidencias de scopes, validación de dominios A-label, key
thumbprint, autoridad, replay y relaciones de hashes son **semántica**, no algo
que la aceptación del schema demuestre. Ver §4 y la tabla de dependencias.

## 2. Parseo, JWS y compromisos

Aplicar la puerta numérica E1 al texto antes de conversión: tokens enteros
`-?(0|[1-9][0-9]*)`, rango seguro ±9007199254740991; sin fracciones, exponentes,
NaN/Infinity, duplicados o Unicode inválido. Enteros como bool no son epochs.
La validación matemática `integer` de JSON Schema no reemplaza esta puerta.
Profundidad máxima 32 por texto JSON; limitar antes de construir árboles.

Base64url es sin padding y canónico: decodificar y volver a codificar debe
reproducir el token. JWS protected y payload deben ser ya bytes JCS canónicos.
Un `-0` firmado no se reescribe a `0`; se rechaza si no coincide con JCS.
El wrapper Reference y el JSON transportado por Proof también requieren JCS.

| Objeto nuevo | `typ` exacto propuesto | Rol del firmante |
|---|---|---|
| RBA | `application/xcim-recipient-binding-p1+json` | `recipient_binding` del Consent Issuer aceptado. |
| MSA | `application/xcim-message-signer-authorization-p1+json` | Clave de control de aplicación aceptada; `application_control` es etiqueta local del fixture, no registro de roles. |
| MP | `application/xcim-message-presentation-p1+json` | `message_presentation` de la MSA vigente para app/epoch/delegado. |

Algoritmo de los tres objetos nuevos: `Ed25519`, `kid` de la autoridad
exactamente seleccionada; sin header no protegido, `crit`, `b64=false`, payload
detached ni sustitución de algoritmo/rol. Firmar directamente los bytes ASCII
`protected + '.' + payload`, sin SHA-256 adicional. JWK: exactamente
`kty: OKP`, `crv: Ed25519`, `x`; no miembro privado `d`. Thumbprint RFC 7638
con SHA-256 de esos tres miembros públicos canónicos, base64url sin padding.

Los `typ` son nombres candidatos, no asignaciones IANA ni Content-Type del
envelope. El fixture legacy de receipt conserva por separado `EdDSA` y su
`application/xcim-receipt+jcs`; eso no autoriza esos valores en RBA/MSA/MP.

Con `H=SHA-256`, `J=JCS`, `U=UTF8`, `\0` un byte NUL:

```text
receipt_commitment = b64u(H(U("XCIM-RECEIPT-0.1\0") || J(receipt_payload)))
binding_commitment = b64u(H(U("XCIM-BINDING-ENVELOPE-P1\0") || J(rba_jws)))
signer_authorization_commitment =
  b64u(H(U("XCIM-SIGNER-AUTHORIZATION-P1\0") || J(msa_jws)))
```

Los dos últimos hashes incluyen **todo** el envelope Flattened canónico, no
sólo su payload. El primero conserva la receta del receipt baseline.

La preimagen privada de `address_commitment` contiene exactamente los miembros
de RBA `issuer`, `app_id`, `app_epoch`, `receipt_id`, `receipt_commitment`,
`permission_id`, `purpose_id`, `authorization_kind`, `pairwise_id`,
`address_profile`, `recipient_domain`, `salt`, y además `canonical_recipient`.
Esa última dirección no se agrega al payload transportado.

```text
address_commitment = b64u(H(U("XCIM-RECIPIENT-BINDING-P1\0") || J(address_input)))
```

El issuer calcula desde su observación autenticada. El receiver usa RCPT TO
original local confiable, con perfil `xcim-email-wd02-e1-p1`: mismas reglas E1,
IDNA/UTS46 non-transitional/STD3, local part preservado y postvalidación DNS.
Herramienta de fixtures: `idna==3.10`. No inferir equivalencias de proveedor,
alias, forwarding ni dirección desde `To`; comparar dominio y compromiso.

## 3. Gramática y contenido del mensaje

[headers.abnf](https://xcim.org/standardization/p1/headers.abnf) define campos completos, incluido CRLF final:

```text
XCIM-Reference: v=p1; data=<base64url(J(reference_object))>
XCIM-Proof: v=p1; data=<base64url(J(selected_proof_object))>
```

Nombres de campo case-insensitive; `v=p1;` y `data=` case-sensitive y en ese
orden. FWS obligatorio después de colon y punto y coma; fragmentos de 1–76
caracteres separados por FWS no obsoleto. Sin parámetros extra, comentarios,
padding, whitespace final o fallback URL. Tras unfold, sólo quitar FWS entre
fragmentos; concatenar y aplicar base64url/JCS estrictos.

Máximos locales P1-01: Reference 16384 octetos raw, Proof 49152, suma 65536;
contar nombre, colon, folds y CRLF. Cada línea física de header tiene hasta
998 octetos sin CRLF. Una Reference, a lo sumo una Proof; Proof sola no sirve.
Estos límites requieren evaluación operacional G16; schema y ABNF no bastan
para verificarlos. El checker tiene además presupuesto propio de fixture de
16 MiB de mensaje y 256 KiB de cabeceras; no fija un máximo universal de email.

**Proof:** la gramática sólo transporta un objeto JSON. Su schema y verificación
dependen del perfil de publicación seleccionado. Los casos `FRAME_OK` con
`fixture` prueban framing y **no son proofs válidas**. El checker de binding
rechaza Proof presente con `PROOF_VALIDATOR_PENDING`; no tiene un verificador
de publicación capaz de otorgarle autoridad. Mantener esta dependencia es
preferible a aceptar un wrapper de prueba como evidencia.

Para `xcim-message-relaxed-p1`, incluir todos los campos, en orden de aparición,
excepto exactamente Received, Return-Path, Authentication-Results,
ARC-Authentication-Results, ARC-Message-Signature, ARC-Seal, DKIM-Signature,
XCIM-Reference y XCIM-Proof, sin distinción de mayúsculas. No excluir `x-*`.
Aplicar canonicalización de header relaxed de RFC 6376 §3.4.2, conservar CRLF
final, representar cada campo como string ASCII en un array ordenado. El cuerpo
completo usa §3.4.4 sobre octetos, sin truncar/decodificar MIME/renderizar.

```text
message_input = {"headers": ordered_relaxed_headers,
                 "body_sha256": b64u(H(relaxed_full_body))}
message_commitment = b64u(H(U("XCIM-DIRECT-MESSAGE-P1\0") || J(message_input)))
```

El cuerpo vacío relaxed tiene cero octetos. Se preserva igualdad bajo esas
equivalencias de canonicalización, no igualdad de bytes brutos/rendering.
JCS ordena miembros del objeto; **no** ordenar el array de headers.

Rechazar bare CR/LF, controles o cabeceras no ASCII. Exigir From de un mailbox,
Date con timezone y Message-ID único. P1-01 limita este último a `<local@domain>`
sin whitespace, angle brackets internos ni `@` adicionales. Singleton máximo
uno para From/Date/Message-ID/To/Cc/Bcc/Subject/Sender/Reply-To/MIME-Version/
Content-Type/Content-Transfer-Encoding/Reference/Proof; Bcc no vacío no soportado.
Headers repetibles restantes se incluyen todos. Validar en el ingreso antes
de agregar tags locales; transformaciones no excluidas pueden perder la señal.

Firmar MP antes de agregar Reference/Proof; finalmente DKIM. La firma DKIM
calificante debe ser criptográficamente válida, con `d=` exacto declarado,
autoridad actual y sin `l=`; cubrir Reference/Proof presentes y campos críticos.
P1-01 requiere oversigning de los singleton críticos, una mención `h=` más que
ocurrencias presentes; **Bcc queda exceptuado** conforme a la recomendación DKIM
de no firmarlo, manteniendo su valor/ausencia bajo MP y cardinalidad. Verificar
selección de ocurrencias real, no substrings de `h=`. Basta una firma DKIM que
cumpla todo; otra inválida no invalida esa firma. Algoritmo DKIM del fixture:
`ed25519-sha256` (RFC 8463), distinto del signing input Ed25519 directo de JWS.

## 4. Validación semántica necesaria

| Comprobación | Evidencia que requiere |
|---|---|
| Roles y claves RBA/control/MP | Autoridad autenticada actual, no key discovery ni un rol autoafirmado en la entrada. |
| Scope | Issuer/app/epoch/receipt/permiso/propósito/clase iguales entre RBA, MP y receipt; app/epoch y alcance MSA coincidentes. |
| MSA | ID, authority_epoch, clave/thumbprint, rol, dominios y propósitos; estado actual de MSA y clave, sin redelegación. |
| Tiempo | RBA/MP hasta 86400 s; MSA hasta 604800 s; `0 < exp-iat`; `iat <= now+60`; `now < exp` exclusivo; MP dentro de vigencia RBA/MSA y emitida después de ambas. |
| Ciclo de claves | Clave válida al emitir y al evaluar; vigencias y revocación limitan la señal aun con una firma correcta. |
| Destinatario | Dirección canónica exacta de origen SMTP confiable, dominio y compromiso contextualizado; no usar header visible como mapping. |
| Recibo/permiso | Firma/schema OIDC-01 y compromiso exacto, pairwise ID y scope, grant activo y tiempos; el estado actual se verifica por separado. |
| Contenido | Compromiso recalculado del mensaje/MIME completos bajo el perfil exacto y From correcto. |
| DKIM y dominios | Firma/cobertura calificantes y autoridad actual de roles app/dominio, sin heredar sólo del receipt histórico. |
| Estado público | Inclusión, estado, batch/anchor, confianza/finalidad y frescura; datos synthetic no sirven de sustituto. |
| Replay | Nonce de 32 bytes por copia; aceptación atómica y store coordinado entre MX; ningún dato inválido debe consumirlo. |

La intersección de todas las vigencias es efectiva. Los límites de piloto no
extienden frescura de estado ni transforman RBA/MP en send permits perpetuos.
Se mantiene el procedimiento SMTP/replay de P1 §5.3; no está implementado por
un lookup en una lista de fixtures.

## 5. Qué verifica el paquete local

`verify_fixture` requiere contexto etiquetado `fixture_only`, pins de prueba
con roles y estados sintéticos explícitos; nunca consulta red o DNS reales.
Comprueba schema/raw bytes, firmas, scope, hashes, dirección, tiempos, las
relaciones simuladas de autoridad y revocación, contenido, DKIM vía DNS local
de fixture y un caso de presentación ya aceptada.

Resultado positivo único: `P1_BINDING_FIXTURE_OK`, con
`can_assert_xcim_pass: false` y dependencias sin resolver. Contextos que faltan,
estado simulado revocado/stale o Proof sin verificador se rechazan. Estos
contextos no son contratos de confianza ni formatos de estado del protocolo.
Los códigos de error son etiquetas de test, sin registro de resultados XCIM.
La precedencia final de multifallos sigue en D05/D09.

El contraste Node reimplementa canonicalización/hash/firmas de los fixtures
positivos con crypto estándar de Node. No comparte helpers Python, no es un
segundo receiver completo ni evidencia de interoperabilidad independiente.

Fuentes: [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234.html),
[RFC 7405](https://www.rfc-editor.org/rfc/rfc7405.html),
[RFC 6376](https://www.rfc-editor.org/rfc/rfc6376.html),
[RFC 8463](https://www.rfc-editor.org/rfc/rfc8463.html),
[RFC 7515](https://www.rfc-editor.org/rfc/rfc7515.html),
[RFC 7638](https://www.rfc-editor.org/rfc/rfc7638.html),
[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html),
[RFC 9864](https://www.rfc-editor.org/rfc/rfc9864.html). Las construcciones
adicionales son diseño XCIM; las referencias no implican su validación externa.
