Status: NORMATIVE
Version: 0.1
Wire Format: peac-issuer/0.1
This document defines the normative specification for PEAC Issuer Configuration, served at /.well-known/peac-issuer.json. Issuer configuration enables verifiers to discover cryptographic keys and verification endpoints for validating PEAC receipts.
Key words: The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174 (when, and only when, they appear in all capitals).
Scope: This specification covers issuer configuration only. For policy documents (access terms, purposes, receipts), see PEAC-TXT.md.
https://{issuer}/.well-known/peac-issuer.json
Where {issuer} is the issuer URL from the receipt's iss claim.
Input: issuer URL (from receipt iss claim)
Output: issuer configuration URL
1. Canonicalize issuer to origin: origin = new URL(issuer).origin
2. Validate origin uses HTTPS scheme
3. issuer_config_url = origin + "/.well-known/peac-issuer.json"
4. RETURN issuer_config_url
Canonicalization via URL.origin handles trailing slashes, paths, and default port elision. This ensures https://api.example.com/, https://api.example.com/v1, and https://api.example.com:443 all resolve to the same configuration URL.
Example:
- Receipt
iss:https://api.example.com - Config URL:
https://api.example.com/.well-known/peac-issuer.json
- Implementations MUST require HTTPS
- HTTP MUST be rejected (no development exceptions for issuer config)
- Other schemes MUST be rejected
A PEAC Issuer Configuration is a JSON object with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
version |
string | Yes | Configuration format version |
issuer |
string | Yes | Issuer identifier URL (MUST match receipt iss) |
jwks_uri |
string | Yes | JWKS endpoint URL |
verify_endpoint |
string | No | Verification endpoint URL |
receipt_versions |
string[] | No | Supported receipt versions |
algorithms |
string[] | No | Supported signing algorithms |
payment_rails |
string[] | No | Supported payment rails |
security_contact |
string | No | Security contact email/URL |
revoked_keys |
array | No | Revoked signing keys (DD-148, v0.11.3+) |
The version field indicates the configuration format version:
peac-issuer/0.1- Initial version
Implementations MUST reject configurations with unrecognized major versions.
The issuer field MUST:
- Be an HTTPS URL
- Match the
issclaim in receipts issued by this issuer - Not include a trailing slash
- Be case-sensitive
The jwks_uri field provides the location of the issuer's JSON Web Key Set:
- MUST be a valid URL
- MUST use the HTTPS scheme; verifiers MUST reject non-HTTPS
jwks_urivalues with error codeE_VERIFY_JWKS_URI_INVALID - MUST return a valid JWKS document (RFC 7517)
- SHOULD be cacheable with appropriate headers
Normative key discovery chain: Verifiers MUST resolve keys ONLY via the canonical discovery chain: iss claim -> peac-issuer.json -> jwks_uri -> JWKS. This is the single normative mechanism for key discovery in PEAC. JWKS endpoints are implementation artifacts of the issuer, not protocol-level discovery surfaces. Implementations MUST NOT:
- Embed keys directly in
peac-issuer.json(no inlinekeysarray) - Fall back to direct
/.well-known/jwks.jsonwithout first resolvingpeac-issuer.json - Use
peac.txtfor key discovery (peac.txtis for policy only; see PEAC-TXT.md) - Construct JWKS URLs by convention (e.g., appending
/.well-known/jwks.jsonto the issuer origin)
Default if not specified: ["interaction-record+jwt"]
Well-known receipt versions:
| Version | Description |
|---|---|
interaction-record+jwt |
Current stable wire format (Wire 0.2) |
peac-receipt/0.1 |
Frozen legacy wire format (Wire 0.1) |
Default if not specified: ["EdDSA"]
Supported algorithms:
| Algorithm | Description |
|---|---|
EdDSA |
Ed25519 (RFC 8032) |
Issuer configuration MUST be JSON. YAML is not supported.
{
"version": "peac-issuer/0.1",
"issuer": "https://api.example.com",
"jwks_uri": "https://api.example.com/.well-known/jwks.json",
"verify_endpoint": "https://api.example.com/verify",
"receipt_versions": ["interaction-record+jwt"],
"algorithms": ["EdDSA"],
"payment_rails": ["x402", "stripe"],
"security_contact": "security@example.com"
}Servers MUST set:
Content-Type: application/json; charset=utf-8Implementations MUST:
- Use strict JSON parsing (RFC 8259)
- Reject trailing commas
- Reject comments
- Reject duplicate keys
- Accept only UTF-8 encoding
Implementations MUST ignore unknown fields for forward compatibility.
| Limit | Value | Reason |
|---|---|---|
| Maximum bytes | 64 KiB | DoS protection |
| Maximum nesting depth | 4 levels | Stack safety |
Implementations MUST enforce fetch timeouts:
- Connection timeout: 5 seconds
- Total timeout: 10 seconds
Implementations MUST reject configurations that:
- Missing
versionfield - Missing
issuerfield - Missing
jwks_urifield - Have
issuernot matching expected issuer - Have
jwks_urinot HTTPS
Implementations MUST canonicalize issuer URLs to their origin (scheme + host + port) using URL parsing (e.g., new URL(issuer).origin). Origin canonicalization is normative for three operations:
- Discovery URL derivation:
origin + "/.well-known/peac-issuer.json" - Issuer equality comparison:
canonical(receipt.iss) == canonical(config.issuer) - Cache key derivation: cache entries MUST be keyed by the canonical origin
This ensures that https://api.example.com/, https://api.example.com/v1, and https://api.example.com:443 all resolve to the same issuer identity https://api.example.com.
When verifying a receipt:
Input: receipt.iss, config.issuer
Output: boolean
1. Canonicalize both URLs to their origin (scheme + host + port)
using URL parsing (handles trailing slashes, paths, default port elision)
2. Compare case-sensitively
3. RETURN (canonical_iss == canonical_issuer)
Mismatch MUST result in verification failure with error code E_VERIFY_ISSUER_MISMATCH.
Implementations MUST compare canonical origins, not raw strings. Two issuer URLs that differ only in path, trailing slash, or default port MUST be treated as the same issuer.
Issuer configuration is verification-critical and frequently accessed. Implementations MUST:
- Cache successful responses keyed by canonical origin (Section 7.2)
- Honor
Cache-Controlheaders - Implement conditional requests (
If-None-Match,If-Modified-Since) - Use a minimum cache TTL of 5 minutes
- Use a maximum cache TTL of 24 hours
- Enforce bounded cache size (LRU eviction RECOMMENDED)
Servers SHOULD set:
Cache-Control: public, max-age=3600
ETag: "v1-abc123"On verification failure due to unknown key ID:
- Attempt cache refresh (conditional GET)
- If new config, retry verification
- If same config, fail verification
Implementations MUST block:
| Range | Reason |
|---|---|
| 10.0.0.0/8 | Private (RFC 1918) |
| 172.16.0.0/12 | Private (RFC 1918) |
| 192.168.0.0/16 | Private (RFC 1918) |
| 127.0.0.0/8 | Loopback |
| ::1 | IPv6 loopback |
| 169.254.0.0/16 | Link-local |
| 169.254.169.254 | Cloud metadata |
| fe80::/10 | IPv6 link-local |
Implementations MUST:
- Follow redirects (max 3 hops)
- Validate each redirect target against SSRF rules
- Reject cross-scheme redirects (HTTPS -> HTTP)
| Condition | Behavior |
|---|---|
| Network error | Retry with backoff, then fail |
| 404 Not Found | Fail (issuer not configured) |
| 5xx Server Error | Retry with backoff |
| Timeout | Fail with timeout error |
| Invalid JSON | Fail with parse error |
| Code | HTTP | Description |
|---|---|---|
E_VERIFY_ISSUER_CONFIG_MISSING |
502 | peac-issuer.json not found or not fetchable |
E_VERIFY_ISSUER_CONFIG_INVALID |
502 | peac-issuer.json not valid JSON or fails schema validation |
E_VERIFY_ISSUER_MISMATCH |
403 | issuer field does not match expected issuer origin |
E_VERIFY_JWKS_URI_INVALID |
502 | jwks_uri is not a valid HTTPS URL |
E_VERIFY_INSECURE_SCHEME_BLOCKED |
403 | Non-HTTPS URL in issuer discovery |
E_VERIFY_JWKS_INVALID |
502 | JWKS response not valid JSON or missing keys array |
E_VERIFY_KEY_FETCH_BLOCKED |
403 | SSRF protection blocked the fetch |
E_VERIFY_KEY_FETCH_FAILED |
502 | Network error during key fetch |
E_VERIFY_KEY_FETCH_TIMEOUT |
504 | Key fetch timed out |
Issuer configuration is trusted for key discovery only. It does not grant authorization or validate receipt claims beyond signature verification.
For the full normative key rotation lifecycle specification, see KEY-ROTATION.md (DD-148).
Issuers MUST:
- Maintain a minimum 30-day overlap between key deprecation and retirement
- Publish compromised keys in the
revoked_keys[]array (see Section 11.2.1) - Not reuse a
kidfor a different key
Issuers SHOULD:
- Include multiple keys in JWKS for rotation
- Use key IDs (
kid) to identify keys - Deprecate old keys gradually (>= 30 days overlap)
Verifiers MUST:
- Check
revoked_keys[]before accepting a receipt signature - Stateful resolvers: detect and reject kid reuse (
E_KID_REUSE_DETECTED)
Verifiers SHOULD:
- Refresh JWKS on unknown
kid - Cache keys by
kidfor efficiency
The revoked_keys field is an optional array of objects with the following shape:
| Field | Type | Required | Description |
|---|---|---|---|
kid |
string | Yes | Key ID that was revoked |
revoked_at |
string | Yes | ISO 8601 revocation timestamp |
reason |
string | No | RFC 5280 CRLReason: key_compromise, superseded, cessation_of_operation, privilege_withdrawn |
Maximum 100 entries. See KEY-ROTATION.md Section 6 for emergency revocation procedures.
- TLS 1.2 or higher REQUIRED
- Certificate validation REQUIRED
- HSTS RECOMMENDED
When an origin is both a publisher (issues receipts) and a content provider (has access terms), it MUST host the canonical trio of discovery surfaces:
| Surface | Purpose | Specification |
|---|---|---|
/.well-known/peac.txt |
Policy: access terms and purposes | PEAC-TXT.md |
/.well-known/peac-issuer.json |
Issuer: config and key discovery | This document |
{jwks_uri} (from issuer config) |
Keys: JWKS for signature verify | RFC 7517 |
These are independent documents serving different purposes. peac.txt is for policy only; peac-issuer.json is for verification key discovery only. Implementations MUST NOT conflate the two.
An issuer implementation MUST:
- Serve configuration at canonical location
- Return valid JSON with required fields
- Ensure
issuermatches receiptissclaims - Serve JWKS at
jwks_uri - Set appropriate cache headers
A verifier implementation MUST:
- Resolve configuration from issuer URL via
/.well-known/peac-issuer.json - Validate configuration format
- Validate issuer field matches expected
- Fetch JWKS from
jwks_uri(MUST NOT assume JWKS location without resolving issuer config) - Cache JWKS with appropriate TTL
- Verify receipts using discovered keys
- Handle errors appropriately
{
"version": "peac-issuer/0.1",
"issuer": "https://api.example.com",
"jwks_uri": "https://api.example.com/.well-known/jwks.json"
}{
"version": "peac-issuer/0.1",
"issuer": "https://api.example.com",
"jwks_uri": "https://api.example.com/.well-known/jwks.json",
"verify_endpoint": "https://api.example.com/peac/verify",
"receipt_versions": ["interaction-record+jwt"],
"algorithms": ["EdDSA"],
"payment_rails": ["x402", "stripe", "razorpay"],
"security_contact": "https://api.example.com/.well-known/security.txt"
}| Version | Date | Changes |
|---|---|---|
| 0.1 | 2026-01-14 | Initial specification |
- RFC 2119 - Key words for use in RFCs
- RFC 7517 - JSON Web Key (JWK)
- RFC 7518 - JSON Web Algorithms (JWA)
- RFC 8174 - Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words
- RFC 8259 - The JavaScript Object Notation (JSON) Data Interchange Format
- RFC 8615 - Well-Known Uniform Resource Identifiers (URIs)
- PEAC-TXT.md - Policy Document Specification
- PROTOCOL-BEHAVIOR.md - Receipt Verification