Key Resolution
Part of the Machine Payment Control Protocol (MPCP).
Overview
MPCP artifacts carry two fields that identify the signing authority:
issuer— identifies the authority (domain, HTTPS URL, or DID)issuerKeyId— selects the specific key within that authority's key set
Verifiers use these two fields to retrieve the public key required to validate the artifact signature.
MPCP defines HTTPS well-known as the baseline key resolution mechanism. All conforming implementations MUST support it. DID (W3C DID Core) resolution and Verifiable Credentials (W3C VC Data Model) are optional higher-level mechanisms that deployments MAY use in addition.
Canonical Key Format: JWK
MPCP uses JSON Web Key (JWK) format (RFC 7517) as the canonical representation for public keys.
All key set documents MUST express public keys as JWK objects. Verifiers MUST accept keys in JWK format.
Ed25519 (recommended)
{
"kid": "policy-auth-key-1",
"use": "sig",
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"x": "base64url-encoded-32-byte-public-key",
"active": true
}
secp256k1
{
"kid": "payment-auth-key-1",
"use": "sig",
"kty": "EC",
"crv": "secp256k1",
"alg": "ES256K",
"x": "base64url-encoded-x-coordinate",
"y": "base64url-encoded-y-coordinate",
"active": true
}
Required JWK fields for MPCP keys:
| Field | Description |
|---|---|
kid |
Key identifier. MUST match issuerKeyId in the artifact. |
use |
MUST be "sig". |
kty |
Key type. "OKP" for Ed25519; "EC" for secp256k1. |
crv |
Curve. "Ed25519" or "secp256k1". |
x |
Public key material (base64url-encoded). |
y |
y-coordinate for EC keys (base64url-encoded). |
active |
Optional boolean (default true). When false, the key is revoked. Verifiers MUST reject signatures made with a key whose active field is false. See Key Revocation. |
Recommended JWK fields for MPCP keys:
| Field | Description |
|---|---|
alg |
RFC 7518 JWS algorithm for signatures produced with this key. RECOMMENDED. Use "EdDSA" for Ed25519 (kty OKP, crv Ed25519) and "ES256K" for secp256k1 (kty EC, crv secp256k1). If alg is omitted, verifiers MUST infer the algorithm from kty and crv as above. If alg is present and conflicts with kty/crv, verifiers MUST reject the key. |
Private key material (d) MUST NOT be present in key set documents.
Algorithm deprecation (version-gated)
MPCP v1.0 normative signing algorithms for artifact verification are Ed25519 (EdDSA) and secp256k1 (ES256K, low-S only — see Hashing).
Future minor specification revisions MAY:
- Register additional algorithms and JWK
algvalues. - Mark algorithms as deprecated in this document; conforming verifiers MUST NOT accept newly issued artifacts signed with a deprecated algorithm after the revision’s effective date.
- Require key set publishers to set
active: falseon JWKs for deprecated algorithms and to rotate to supported keys.
Implementations SHOULD surface the spec version they implement so operators can enforce alignment with PA key rotation.
HTTPS Well-Known Endpoint
Endpoint
GET https://{issuer-domain}/.well-known/mpcp-keys.json
The endpoint MUST be served over HTTPS. Verifiers MUST validate the TLS certificate. Plaintext HTTP MUST NOT be used.
TLS validation and issuer domain spoofing
Standard TLS validation (hostname match to certificate SAN/CN, chain to a trusted anchor) mitigates
most issuer domain spoofing attacks (e.g. homoglyph or look-alike domains serving a forged
mpcp-keys.json). Verifiers MUST NOT disable hostname verification or accept self-signed
certificates unless the deployment uses an explicit, documented trust override (e.g. private PKI).
High-assurance deployments: Operators SHOULD additionally use certificate pinning (or
equivalent application-layer trust anchors) for the issuer's HTTPS endpoint when fetching
/.well-known/mpcp-keys.json, so that a mis-issued or fraudulently obtained public CA certificate
cannot silently substitute keys. Pinning MUST be combined with a rotation strategy (backup pins,
versioned app updates, or operational procedures) to avoid bricking verifiers when the PA rotates
TLS certificates.
Deriving the URL from issuer
issuer format |
Derived URL |
|---|---|
Bare domain: operator.example.com |
https://operator.example.com/.well-known/mpcp-keys.json |
HTTPS URL: https://operator.example.com |
https://operator.example.com/.well-known/mpcp-keys.json |
did:web:operator.example.com |
https://operator.example.com/.well-known/mpcp-keys.json |
did:web:operator.example.com:path:to:key |
https://operator.example.com/path/to/key/.well-known/mpcp-keys.json |
Key Set Document Format
{
"version": "1.0",
"keys": [
{
"kid": "policy-auth-key-1",
"use": "sig",
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"x": "base64url...",
"active": true
},
{
"kid": "policy-auth-key-2",
"use": "sig",
"kty": "EC",
"crv": "secp256k1",
"alg": "ES256K",
"x": "base64url...",
"y": "base64url...",
"active": false
}
]
}
The document is a plain JSON object. The keys array contains one or more JWK entries. An authority MAY publish multiple keys to support key rotation. Keys with active: false are retained in the document for audit traceability but MUST NOT be used for signature verification. See Key Revocation.
HTTP Response Requirements
| Requirement | Value |
|---|---|
| Content-Type | application/json |
| Status | 200 OK for valid responses |
| Encoding | UTF-8 |
Verifiers SHOULD cache key set responses according to standard HTTP cache semantics (Cache-Control, ETag). Verifiers MUST revalidate cached responses before use when they have expired.
Resolution Algorithm
Verifiers MUST attempt resolution in the following priority order:
function resolvePublicKey(issuer, issuerKeyId):
# 1. Trust Bundle lookup (offline / pre-distributed)
jwk = resolveFromTrustBundle(issuer, issuerKeyId)
if jwk:
if jwk.active == false: raise KEY_REVOKED
return jwk
# 2. HTTPS well-known (online)
url = deriveKeySetUrl(issuer)
keySetDoc = httpGet(url) # HTTPS required; TLS validated
keys = parseKeySet(keySetDoc)
jwk = keys.find(k => k.kid == issuerKeyId)
if jwk:
if jwk.active == false: raise KEY_REVOKED
return jwk
# 3. XRPL Credential check (optional, online — see Key Revocation)
if xrplKeyCredentialCheckEnabled(issuer):
if not verifyKeyCredential(issuer, issuerKeyId): raise KEY_REVOKED
# 4. DID resolution (optional, online)
if issuer starts with "did:" and not "did:web:":
jwk = resolveDid(issuer, issuerKeyId) # method-specific; see DID section below
if jwk: return jwk
raise KEY_NOT_FOUND
If no resolution path succeeds, the verifier MUST fail the signature check and reject the artifact. If a key is found but marked as revoked (active: false) or lacks a valid on-chain credential, the verifier MUST reject with KEY_REVOKED.
Trust Bundle Resolution
Trust Bundles are the primary resolution mechanism for deployments that operate without network access at verification time.
A Trust Bundle is a signed, distributable document containing the public keys of approved issuers for a given scope. Verifiers load one or more Trust Bundles at startup (or after periodic refresh) and consult them before attempting any live network call.
function resolveFromTrustBundle(issuer, issuerKeyId):
for each bundle in loadedBundles (sorted by expiresAt desc):
if bundle is expired: continue
if issuer not in bundle.approvedIssuers: continue
entry = bundle.issuers.find(e => e.issuer == issuer)
if entry is null: continue # approved but no embedded keys — fall through
jwk = entry.keys.find(k => k.kid == issuerKeyId)
if jwk: return jwk
return null
Trust Bundles are optional. If no Trust Bundle is loaded, resolution proceeds directly to the HTTPS well-known step.
See Trust Bundles for the full specification including signing, lifecycle, and offline revocation considerations.
Pre-Configured Keys
Individual keys MAY be pre-configured directly on a verifier, identified by issuer + issuerKeyId and expressed as JWK objects. When a matching entry is found, the verifier uses it without fetching the well-known endpoint.
Trust Bundles are the recommended mechanism for distributing pre-configured keys at scale. Direct pre-configuration is appropriate for single-key pinning or testing environments.
This supports:
- offline and air-gapped machine wallets
- embedded deployments where network access is unavailable
- pinned deployments where key mutation must be prevented
Pre-configured keys MUST still be expressed in JWK format.
DID and Verifiable Credentials (Optional)
Deployments MAY use decentralized identifiers (DIDs) or Verifiable Credentials (VCs) as a higher-level key binding mechanism.
When the issuer field carries a did: URI other than did:web:, verifiers MAY use DID resolution to retrieve the key material. Resolved key material MUST still be expressed and validated as JWK.
VC-based key discovery is outside the core protocol.
DID resolution and VC verification are never required for MPCP compliance. Implementations that support only HTTPS well-known key resolution are fully conformant.
DID Resolution — Example: did:xrpl
The following shows how a W3C DID can be resolved to obtain signing keys. The did:xrpl method
is used as a concrete example; other DID methods (e.g. did:hedera, did:key, did:web)
follow the same general pattern with method-specific resolution logic as defined by the
W3C DID Core specification.
DID Format
did:xrpl:{network}:{rAddress}
Examples:
- did:xrpl:mainnet:rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh
- did:xrpl:testnet:rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe
Network RPC Defaults
| Network | Default RPC Endpoint |
|---|---|
mainnet |
https://xrplcluster.com |
testnet |
https://s.altnet.rippletest.net:51234 |
Resolution Algorithm
- Parse network and account from DID string
- Call XRPL
account_objectsJSON-RPC withtype: "DID"to retrieve the DID object - Hex-decode the
DIDDocumentfield of the returned DID ledger entry - Parse the decoded string as JSON to obtain the DID Document
- Extract
verificationMethod[0].publicKeyJwk - Return the JWK
function resolveXrplDid(did):
(network, account) = parseDid(did)
rpcUrl = networkRpc(network)
response = xrplRpc(rpcUrl, "account_objects", { account, type: "DID" })
didObject = response.result.account_objects[0]
didDocumentJson = hexDecode(didObject.DIDDocument)
didDocument = JSON.parse(didDocumentJson)
return didDocument.verificationMethod[0].publicKeyJwk
Reference Implementation
import { resolveXrplDid } from "mpcp-service/sdk";
const result = await resolveXrplDid(
"did:xrpl:mainnet:rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
{ rpcUrl: "https://xrplcluster.com", timeoutMs: 5000 },
);
if ("error" in result) {
// resolution failed
} else {
const jwk = result.jwk; // JsonWebKey
}
Error Codes
| Code | Condition |
|---|---|
invalid_did_format |
DID does not match did:xrpl:{network}:{rAddress} |
unknown_xrpl_network |
Network is not mainnet or testnet and no rpcUrl provided |
xrpl_rpc_fetch_failed |
Network error calling the XRPL JSON-RPC endpoint |
xrpl_rpc_http_error |
Non-200 HTTP response from the RPC endpoint |
xrpl_did_not_found |
No DID object found for the account |
xrpl_did_document_missing |
DIDDocument field absent or empty |
xrpl_did_document_invalid_hex |
DIDDocument field is not valid hex |
xrpl_did_document_invalid_json |
Decoded DIDDocument is not valid JSON |
xrpl_did_no_verification_method |
DID Document has no verificationMethod entries |
xrpl_did_no_public_key_jwk |
verificationMethod[0] has no publicKeyJwk |
Error Codes
| Code | Condition |
|---|---|
KEY_NOT_FOUND |
issuerKeyId not present in the resolved key set |
KEY_REVOKED |
Key found but active is false, or XRPL key credential does not exist or is expired |
KEY_SET_FETCH_FAILED |
Well-known endpoint unreachable or returned non-200 |
KEY_SET_INVALID |
Key set document could not be parsed |
KEY_FORMAT_INVALID |
Key entry is not a valid JWK |
Key Revocation
Overview
If a PA signing key is compromised, an attacker can forge PolicyGrants with unlimited budgets.
MPCP provides two complementary revocation mechanisms — one off-chain (JWKS active field) and
one on-chain (XRPL Credentials) — to ensure that revoked keys are rejected as quickly as
possible.
Mechanism 1: JWKS active Field
Every JWK entry in a key set document MAY include an active field (boolean, default true).
When a PA rotates or revokes a key, it sets active: false for that key in the JWKS endpoint
response.
Verifier behaviour (MUST):
- After resolving a key via HTTPS well-known or Trust Bundle, check the
activefield. - If
activeis explicitlyfalse, reject the artifact withKEY_REVOKED. - If
activeis absent ortrue, proceed with verification.
PA behaviour on compromise:
- Set
active: falseon the compromised key in the JWKS endpoint immediately. - Issue new Trust Bundles that either omit the compromised key or include it with
active: false. - Rotate to a new key (
kid) for all subsequent grant signing.
Limitations: Verifiers that cache the key set document will continue to trust the
compromised key until the cache expires. Implementations SHOULD use short Cache-Control
max-age values (minutes, not hours) for the JWKS endpoint. Trust Bundles that embedded the
key before revocation remain valid until their expiresAt — deployments SHOULD use short
Trust Bundle lifetimes (hours, not days) for high-assurance environments.
Mechanism 2: XRPL Credential Key Lifecycle
For deployments on XRPL, the PA can maintain an on-chain credential for each active signing key using XLS-70 Credentials. This provides a definitive, ledger-based revocation signal that does not depend on JWKS cache expiry.
Setup:
- For each active signing key, the PA issues a
CredentialCreatetransaction to its own XRPL account: Issuer= PA's XRPL addressSubject= PA's XRPL address (self-issued)CredentialType= hex-encoded string"mpcp:pa-signing-key:{kid}"(e.g.hex("mpcp:pa-signing-key:policy-auth-key-1"))- Optional
Expirationaligned with the key's intended lifetime - PA calls
CredentialAcceptto activate the credential on-ledger.
Revocation on compromise:
- PA submits
CredentialDeletefor the compromised key's credential. - The credential is removed from the ledger immediately.
- Any verifier or Trust Bundle builder that checks the ledger will see that the credential no longer exists.
Verifier behaviour (SHOULD for XRPL deployments):
After resolving a key via JWKS or Trust Bundle, and before accepting the key for verification, the verifier SHOULD check on-chain that the PA holds a valid, non-expired credential for the key:
function verifyKeyCredential(issuer, issuerKeyId):
paXrplAddress = resolveXrplAddress(issuer)
credentialType = hexEncode("mpcp:pa-signing-key:" + issuerKeyId)
credential = lookupCredential(
subject: paXrplAddress,
issuer: paXrplAddress,
type: credentialType
)
return credential exists and is not expired
If the credential does not exist or is expired, the verifier MUST reject the artifact with
KEY_REVOKED.
Trust Bundle builder behaviour (SHOULD):
When building or refreshing a Trust Bundle, the builder SHOULD check each included key's on-chain credential before embedding it. Keys whose credentials have been deleted MUST NOT be included in new Trust Bundles.
Combining Both Mechanisms
| Mechanism | Speed of revocation | Requires connectivity | Requires XRPL |
|---|---|---|---|
JWKS active: false |
Next JWKS fetch (cache-dependent) | Yes | No |
| XRPL Credential delete | Immediate (ledger finality ~4s) | Yes | Yes |
For maximum safety, PA operators SHOULD use both mechanisms simultaneously:
- Set
active: falsein the JWKS endpoint (covers non-XRPL verifiers). - Delete the on-chain credential (covers XRPL-aware verifiers with near-instant effect).
- Issue updated Trust Bundles omitting the revoked key (covers offline verifiers on next bundle refresh).
Trust Bundle Freshness
Trust Bundles that were signed before a key was revoked will continue to include the compromised key until they expire. To limit the exposure window:
- Deployments SHOULD set Trust Bundle
expiresAtto short intervals (hours, not days) in high-assurance environments. - Trust Bundle builders SHOULD check both the JWKS
activefield and the on-chain credential for each key before embedding it in a new bundle. - Verifiers with connectivity SHOULD supplement Trust Bundle lookups with an on-chain credential check when available.