Aaron Parecki recently published a clear account of the missing trust anchor in Dynamic Client Registration and how Client ID Metadata Documents address it. He’s right about the core problem: Dynamic Client Registration lets a client walk up and assert an identity without demonstrating any authority over it. CIMD changes the shape of the problem by making the client_id an HTTPS URL, with the document at that URL as the source of the client’s metadata.

That gives an authorization server something outside the client’s request to check. It can fetch the URL, require the document’s client_id to match exactly, apply policy to the origin, and lean on ordinary Web security to establish who controls the metadata location.

This is the anchor DCR never had. It isn’t the only one an open ecosystem needs, though, and knowing who controls the metadata still leaves the server to decide what that control is worth.

What I want to add to Aaron’s analysis is a distinction between four questions that are easy to blur: control, review, runtime proof, and authorization.

QuestionMechanismWhat it establishesWhat it does not establish
Who controls this client identifier and its metadata source?CIMDcontrol of the HTTPS URL and an authentic source for the metadata currently served therethat the software is benign, reviewed, or the binary now running
Who reviewed and vouches for this metadata?a trusted issuer, via a software statementa named issuer’s signed decision about a specific metadata snapshot, for named audiencesthat every claim is independently true or that access should be granted
What software instance is presenting now?client authentication or instance attestationkey possession always; instance identity or platform provenance only with attestation-class mechanismspublisher reputation or approval of the client as a whole
What may this client do?the OAuth grant and local authorization policythe access a resource owner or policy actually granted, at this serverthat the software was reviewed, or that the binary is genuine

The first two are establishment anchors; the last two are runtime questions, answered when a presenter shows up and when access is decided. Blur them and CIMD starts to sound like a code review, or a statement like an access grant. And scale is what makes the review row matter: a review that can’t travel has to be repeated at every server the client meets. Every provider an enterprise adopts asks it to rebuild the same application allowlist, one console at a time. That approval should travel, and the second half of this post is about making it travel.

The Gap CIMD Actually Closes

RFC 7591 Dynamic Client Registration lets a client send metadata to an authorization server and get back a local client_id. Unless the server requires an initial access token, a software statement, or some other policy, it’s evaluating values supplied in the same request by the party asking to be registered. A malicious client can claim another product’s name, logo, software_id, or support URL, and nothing in the protocol pushes back.

With CIMD, the client brings its own identifier instead: a URL it controls, like https://client.example/oauth/client.json. The server retrieves it and expects a document like this:

1
2
3
4
5
6
7
{
  "client_id": "https://client.example/oauth/client.json",
  "client_name": "Example Client",
  "redirect_uris": ["https://client.example/callback"],
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks_uri": "https://client.example/jwks.json"
}

Now the client_id, the metadata source, the redirect URI, and the key location can tell one coherent, domain-based story. A server can pre-enroll the URL, tier domains, or add friction for an unfamiliar origin. And the same client presents the same identifier everywhere; each server decides what that identifier is allowed to do.

But HTTPS proves control of an origin, nothing more. acme-login.example can authentically publish metadata from acme-login.example. A compromised publisher domain can authentically publish malicious metadata. A desktop binary can truthfully point at its publisher’s URL without being the publisher’s binary.

So the claim CIMD supports is:

This metadata came from the party controlling this client identifier URL.

That’s much stronger than an ungrounded POST body. It’s not the same as “this software is trustworthy.”

The Second Anchor Carries a Review Decision

Aaron notes that RFC 7591 already defines software_statement, while “the DCR spec itself says nothing about how to create one, what it should contain, or how keys should be managed.” The RFC tells a registration endpoint how to consume a statement, a signed JWT of claims about client software whose trusted claims take precedence over plain JSON, and leaves to deployments the two questions that matter most: how a client gets one, and why a server should trust whoever signed it.

OAuth 2.0 Software Statement Issuance is my attempt at the first question, plus a stricter statement format so that independent issuers and consumers interoperate. Its central design choice is to build on CIMD rather than invent a competing client identity.

The second anchor isn’t the JWT by itself. It’s the consumer’s scoped trust in the statement issuer; the statement carries that issuer’s decision, and useful issuers make the decision by review. The two anchors also root differently: domain control grounds in WebPKI and DNS, trust the whole web already shares, while issuer trust grounds in pairwise configuration, which is why it needs the scoping machinery the first anchor gets for free.

The nearest prior art is OpenID Federation’s trust marks: signed third-party vouching with scoped authority. The difference is what you have to join: a trust mark assumes both parties are enrolled in a federation, moving the closed-world assumption up a level while buying key discovery, policy, delegation, and trust-mark machinery. A software statement rides the RFC 7591 path on pairwise issuer configuration: simpler, closed-world at each edge. One scales through federation membership; the other through reusable signed decisions between parties that already trust the issuer.

The statement’s sub is the exact CIMD URL. Its cimd_digest is the SHA-256 of the document bytes the issuer evaluated. Its aud names the authorization servers permitted to accept it. The remaining claims are the subset of client metadata the issuer decided to attest.

A shortened statement’s claims look like this (on the wire it’s a signed JWS, and the header’s kid points into issuer keys the consumer configured locally):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "iss": "https://publisher-program.example",
  "sub": "https://client.example/oauth/client.json",
  "aud": ["https://as.customer-a.example", "https://as.customer-b.example"],
  "iat": 1785859200,
  "exp": 1785945600,
  "jti": "ss-7f35a9",
  "cimd_digest": "3W6cWfLXi0mZbUIhk8N4Zt2v9Qq7oT1xJdKe5RgYs0A",
  "client_name": "Example Client",
  "redirect_uris": ["https://client.example/callback"],
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks_uri": "https://client.example/jwks.json"
}

The two anchors compose:

  1. The client publishes its canonical metadata at a URL it controls.
  2. The statement issuer retrieves that URL, verifies the CIMD rules, snapshots the exact content, performs whatever review its policy requires, and signs selected metadata together with the URL and digest.
  3. The client presents the statement to a registration endpoint that already understands the RFC 7591 software_statement member.
  4. The consuming authorization server verifies the statement under a configured issuer policy and still applies its own registration policy.

The issuer isn’t replacing the domain anchor; it’s signing a decision about the domain-anchored identity. Theft is bounded when the statement binds its audience, expires quickly, and attests the security-sensitive members, keys and redirect URIs above all: the thief gains neither the attested private key nor control of the registered redirect endpoint, though replay can still poke at registration state. Skip the bindings and it gets worse: junk registrations, or impersonation where a public client has no key to prove.

Consumption is the awkward part: it rides RFC 7591, a registration request, while part of CIMD’s charm is resolving a client with no ceremony at all. A CIMD-native conveyance could fix that, and the proposal constrains its shape. CIMD itself permits an ordinary statement to sit inside the document; this digest-bound profile can’t allow that, since a statement carries the document’s digest and can’t live inside the bytes it hashes. What works instead is a stable pointer to wherever the client publishes its statements, fetched out of band: it doesn’t churn the digest, it needs audience-selection care, and it isn’t defined yet.

Hashing exact served bytes means there’s no JSON canonicalization to argue about; the price is that the served representation becomes the canonical one, down to byte order marks and trailing newlines. Serve unstable bytes and the digest breaks for honest reasons. Without the digest, sub points at a mutable URL and nobody can say whether today’s document is the one reviewed yesterday; with it, a mismatch means the document changed after review. In the proposal that’s a policy input, not an automatic failure, because a spelling fix and a new redirect URI both change the bytes and shouldn’t carry the same consequence. The rule separating them, attested members changing versus unattested ones, still sits in server policy; it belongs in the specification.

What the Statement Actually Says

It’s easy to describe attestation too generously. A valid statement doesn’t make the client’s claims true. The client wrote the source document, and the issuer may have done anything from an automated syntax check to a full security and publisher review.

What a statement actually means is narrower:

At the recorded time, this issuer evaluated this exact metadata snapshot under its policy and chose to attest these claims to these audiences.

That creates accountability bounded by the issuer’s process: a server can conclude only what the issuer decided. The same limit applies to portability: nothing machine-readable travels about assurance or review policy, and the proposal doesn’t even identify who approved, so configuring an issuer means learning its process out of band; interpretable review semantics would need issuer policy metadata that doesn’t exist yet.

And some things you can’t conclude from the statement alone:

  • the publisher is the well-known organization a similar name suggests;
  • the software is free of vulnerabilities or malicious behavior;
  • the binary now running corresponds to the reviewed software;
  • the presenter is an authorized instance of that software; or
  • any user has granted the client access to a resource.

This is why “attestation” should mean an attributable assertion, not truth by signature.

Routine issuer key rotation has an ordinary answer: keep the old verification keys configured until the statements they signed expire. The genuinely open lifecycle questions are revocation before expiry and recovery from a compromised issuer key, which needs distrust, re-issuance, and a hard look at registrations already created under it. For those, short lifetimes and re-issuance are what the proposal offers; the registration cleanup is on the deployment.

Why an Issuance Protocol Is Worth Having

The objection to software statements has always been practical: RFC 7591 defined the artifact without defining how an ecosystem produces it. Every deployment had to invent its own portal, review workflow, statement profile, key distribution, renewal process, and failure model.

The issuance proposal fills that operational gap with two paths:

  • A browser-mediated flow returns a short-lived software_statement_code; the client redeems it at the token endpoint, so the statement never appears in a redirect URL.
  • A client that already holds an initial access token authorizing issuance can use OAuth Token Exchange without a browser.

If review takes hours or days, Deferred Token Response supplies polling, cancellation, and sender constraint. The issuance request can’t ask for resource scope or produce an access token: establishing software is deliberately separate from granting it access.

A software statement is portable establishment evidence, not a privileged OAuth grant disguised as metadata.

Aaron’s Three Hard Cases, Revisited

Aaron identifies several places where domain control isn’t enough. Portable review helps with each of them, though it solves none outright.

Lookalike Domains

CIMD can’t help here: a lookalike’s metadata is authentic too, authentic to the lookalike domain. A software statement adds a review point where publisher identity, branding, redirect domains, and other evidence can be checked before anyone vouches.

The structural control is namespace-scoped issuer trust, the same discipline I’ve argued for issuer trust generally. A consuming authorization server shouldn’t just record “trust issuer X.” It should record which client identifier namespaces that issuer may attest. A publisher program authorized only for URLs under client.example can’t mint an acceptable statement for https://acme-login.example/, even with a valid signature. Three issuer shapes fall out: a publisher program speaks for its own namespace, so it isn’t independent of the domain anchor; an ecosystem reviewer spans enrolled namespaces under its policy; an enterprise issuer spans namespaces inside its tenant.

That limits issuer blast radius. It doesn’t detect every lookalike, an issuer trusted broadly can still make a bad call, but what you get is an accountable policy authority whose claims can be constrained and whose trust can be removed, not a cryptographic trademark oracle.

Weak Authorization-Server Posture

If a server accepts every CIMD URL without policy, it has gained a stable identifier and better lifecycle mechanics, but not much protection from impersonating metadata. Requiring a statement raises the floor only if statement trust is itself constrained.

There’s more to validate than the signature. Three examples: signing keys come from the locally configured issuer, never from a key URL inside the statement; the issuer is authorized for the sub’s namespace, the lookalike rule above; and an attested claim counts only where the server treats that issuer as authoritative for it. The proposal carries the full checklist through audience, lifetime, and digest change policy.

The signature authenticates the issuer. Policy decides the issuer’s authority.

The Cost of Deciding

With CIMD alone, each of M authorization servers can fetch the same document and make an independent approval decision. A portable statement changes the cost from M reviews to one issuance review plus M local acceptance checks.

The consuming servers still decide which issuers to trust, for which namespaces, metadata, lifetimes, and audiences. What becomes reusable is the expensive evidence-gathering and review, not the final right to register.

The decision is reusable; the artifact may stay audience-specific. A statement names the servers permitted to accept it, an issuer can narrow a requested audience but never widen it, and a client that meets a new server goes back for re-issuance, which the deferred path keeps cheap. The audience list is also a disclosure: it says which servers a client plans to establish relationships with, a privacy cost the proposal’s own considerations acknowledge.

The recurring role already exists: the UK Open Banking Directory and Australia’s Consumer Data Right Register both issue central software statements consumed through RFC 7591-shaped registration, portable review in production, ecosystem-specific at the issuance interface. The proposal standardizes the missing acquisition, deferral, renewal, delivery, and error surface.

Bring Your Own App Approvals

Enterprises already live with a version of this problem. If you administer Google Workspace, there’s a console where you control which third-party and internal apps can access your organization’s data: app by app, identified by OAuth client ID, marked trusted, limited, or blocked. It works fine, for that one provider. Every other provider the enterprise adopts brings another console and another allowlist. The lists drift the moment they’re born.

Now run it through the two anchors. This is design, not a description of anything shipping today. The enterprise’s app-review function becomes a statement issuer. Approval is a software statement: iss is the enterprise, sub is the app’s CIMD URL, and the claims are what the review actually covered. The deployment conditions are real: every approved app needs a CIMD identity, and its vendor has to obtain, renew, and present each enterprise’s audience-scoped statement. None of that machinery exists off the shelf; standardizing it is what the issuance protocol is for. Bringing the app estate to a new provider stops being a re-keying exercise and becomes one trust decision plus per-app re-issuance for the new audience. Approved apps show up carrying their approval; an unapproved app has no approval to present, at a tenant that requires one. What travels is the review: one evaluation, imported by every provider, instead of a review re-run in every console.

Authorizing an issuer for the sub’s namespace is the publisher-program instance of a general rule: every issuer is scoped to some authority it legitimately holds. A publisher program’s authority is its own URL namespace. An enterprise issuer attests apps across many vendors’ namespaces, so its authority axis is the tenant: within your tenant, your issuer speaks for app approval across every vendor namespace, and the blast radius stays inside the tenant that chose it.

What about offboarding? A statement consumed at registration is an import: its expiry doesn’t undo what RFC 7591 already registered or granted, so the artifact bootstraps the allowlist while the provider keeps local policy and revocation. Expiry bites only if the tenant requires an unexpired statement at authorization or issuance; under that rule, and only under it, stopping renewal lapses the app everywhere, which is where short lifetimes finally pay off. That’s a product semantic this design asks for explicitly. Continuous enforcement also needs a renewal-delivery path the proposal doesn’t define, some way for a renewed statement to reach the provider at all.

The per-grant version is stronger: an ID-JAG is validated at every grant, so an assertion-carried digest gets checked every time. And the sharpest lever today is one the enterprise already holds: where access rides ID-JAG and tokens are short-lived, stopping assertion issuance stops new grants, and access winds down as existing tokens expire.

On the provider side, importing statements into tenant policy is a product feature over existing registration machinery, not a protocol change: let a tenant configure a statement issuer, exactly as it configures an IdP today. And there’s a natural issuer that makes the configuration smaller still: the enterprise IdP’s authorization server, the party whose word providers already accept for SSO and subject resolution, and the party ID-JAG turns into a carrier of user authority. ID-JAG makes the IdP’s word about people portable; a software statement makes its word about software portable. Same issuer infrastructure, distinct authority and payload. The usual caution applies: the relationship exists, but the authority is new, and a provider still decides explicitly to accept the IdP’s word for app approvals. Cheaper, because the wiring is there; still not free.

I’ve filed the lightweight end of this against ID-JAG itself: issue #121 proposes an optional cimd_digest in the assertion the IdP signs anyway, so a provider can verify a client’s metadata and provision it just in time. The statement is the full-review version of the same move, with its own lifecycle and audiences; the assertion-carried digest is the minimal per-grant one. They compose rather than compete.

None of this works for a client that can’t host a metadata identity; there’s nothing for a statement to bind to. But if the question is whether portable review has a place outside regulated ecosystems, the enterprise case is my answer: the enterprise already makes the decision. It just has no portable way to express it.

The same story is queuing up again for agents, where every platform is busy building yet another tenant allowlist of its own. That one is a post of its own.

The Runtime Question Remains

The two establishment anchors describe a logical client and a review of its metadata. Neither proves which executable is making a particular request.

The difference is sharpest for desktop software. A publisher can host a CIMD document and obtain a statement for the product; a copied or malicious binary can still claim the same client_id, because desktop operating systems don’t offer a universal Web-verifiable binding between a process and a publisher-controlled domain.

OAuth Attestation-Based Client Authentication addresses the runtime layer by letting an instance present key-bound attestation. Ordinary client authentication, private_key_jwt or mutual TLS, identifies the credential holder; it identifies an instance only when credentials are provisioned per instance, which is where a client-instance assertion or workload identity comes in. DPoP can bind issued tokens to a key and reduce replay, but a client-asserted DPoP key alone doesn’t prove software identity.

These layers compose too: a CIMD document can name trusted instance issuers, a statement can attest that member, and the reviewer has then approved the delegation; at the token endpoint, a concrete instance still presents its own proof.

The sequence is:

  1. CIMD identifies the logical client.
  2. A software statement carries an issuer’s review of that client’s metadata.
  3. Instance authentication or attestation identifies the runtime presenter, as strongly as the mechanism allows.
  4. The ordinary OAuth grant determines what access, if any, the resource owner or policy grants.

Four orthogonal facts: control, review, runtime proof, authorization, each with its own mechanism and its own failure mode. Collapse any two of those steps and you either overstate trust or duplicate machinery.

When You Need the Second Anchor, and When You Don’t

Software-statement issuance has a real cost, so it should be used only where portability or independent review pays for it. Elsewhere, better fits exist:

  • a single server making a local onboarding decision wants CIMD pre-enrollment, an initial access token, or approval-based registration;
  • a resource-owner approver wants the ordinary OAuth grant, with CIMD supplying identity;
  • the which-instance-is-running question wants attestation, not a statement;
  • a client with no hostable metadata identity cannot use this digest-bound statement profile; and
  • issuer trust at multilateral scale wants a federation above pairwise configuration.

That leaves the cases this post is about: many servers trusting one publisher program or reviewer, and an enterprise wanting its approvals honored across its providers’ tenants. CIMD is sufficient when domain-based policy and local approval are sufficient.

Where This Came From

The design emerged from the OAuth working-group discussion around Philippe Dellaert’s Approval-Based Dynamic Client Registration. That thread surfaced three problems, and each shaped the proposal directly: an approver only ever sees client-authored input, so CIMD supplies the canonical input; a durable decision has to bind to what was actually reviewed, so cimd_digest binds the exact snapshot; and meaningful review can outlast a held request, so Deferred Token Response absorbs it. The thread also asked for durable denial records; refusals don’t yet leave an artifact of their own, and they probably should.

None of it replaces CIMD. It’s an attempt to finish the software-statement seam that CIMD and RFC 7591 leave exposed. Aaron’s formulation remains the right starting point: the domain gives the authorization server a real anchor where DCR had none. The addition is that open ecosystems may need two different parties to make two different claims.

CIMD says where the client identity speaks from. A software statement says who is willing to stand behind a reviewed version of what it said. Runtime attestation says what is speaking now. OAuth still decides what it may do.

The OAuth 2.0 Software Statement Issuance proposal is an early individual draft. The questions I most want pressure on: whether cimd_digest has the right change semantics; whether issuer scoping, by namespace for publisher programs and by tenant for enterprises, is manageable in practice; and whether providers would actually ship per-tenant issuer trust, which is where the enterprise case above lives or dies. Comments on the OAuth working-group list are especially welcome.