---
title: "CIMD Anchors the Client. A Software Statement Vouches for It."
date: "2026-08-06T09:00:00-07:00"
lastmod: "2026-08-06T09:00:00-07:00"
description: "Client ID Metadata Documents anchor a client identity in control of an HTTPS URL. Software statements add a trusted issuer's review of a specific metadata snapshot. The two mechanisms compose, but neither proves which software instance is running."
summary: "CIMD gives an unknown OAuth client a domain-anchored identity and an authentic source of metadata. A software statement adds a second, separately signed assertion: a trusted issuer evaluated a specific snapshot and is willing to vouch for selected metadata to named authorization servers. Binding the statement\u0026rsquo;s subject to the CIMD URL and its digest to the reviewed document makes the two anchors reinforce each other. It still leaves runtime instance identity, access grants, and local acceptance policy to their proper layers. The applied case: an enterprise reviews an app once and issues the statement itself, on the IdP wiring its providers already trust, with review authority those providers explicitly grant, so one review is imported everywhere instead of re-keyed in every console."
slug: "cimd-software-statements-two-anchors"
tags:
  - "OAuth"
  - "Standards"
  - "IAM"
  - "Internet-Draft"
---


Aaron Parecki recently published a clear account of [the missing trust anchor in Dynamic Client Registration and how Client ID Metadata Documents address it](https://aaronparecki.com/2026/07/29/19/solving-missing-trust-anchor-in-dynamic-client-registration-with-cimd). 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](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/) 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**.

| Question | Mechanism | What it establishes | What it does not establish |
| --- | --- | --- | --- |
| Who controls this client identifier and its metadata source? | CIMD | control of the HTTPS URL and an authentic source for the metadata currently served there | that the software is benign, reviewed, or the binary now running |
| Who reviewed and vouches for this metadata? | a trusted issuer, via a software statement | a named issuer's signed decision about a specific metadata snapshot, for named audiences | that every claim is independently true or that access should be granted |
| What software instance is presenting now? | client authentication or instance attestation | key possession always; instance identity or platform provenance only with attestation-class mechanisms | publisher reputation or approval of the client as a whole |
| What may this client do? | the OAuth grant and local authorization policy | the access a resource owner or policy actually granted, at this server | that 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](https://www.rfc-editor.org/rfc/rfc7591.html) 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:

```json
{
  "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](https://mcguinness.github.io/draft-mcguinness-oauth-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](https://openid.net/specs/openid-federation-1_0.html)'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):

```json
{
  "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](https://datatracker.ietf.org/doc/draft-gerber-oauth-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](/notes/trusting-issuers-in-open-world-oauth/). 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](https://www.openbanking.org.uk/) and [Australia's Consumer Data Right Register](https://www.cdr.gov.au/) 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](https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-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](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/) 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](https://github.com/oauth-wg/oauth-identity-assertion-authz-grant/issues/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](https://datatracker.ietf.org/doc/draft-ietf-oauth-attestation-based-client-auth/) 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](https://datatracker.ietf.org/doc/draft-mcguinness-oauth-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](https://datatracker.ietf.org/doc/draft-dellaert-oauth-approval-based-dcr/);
- 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](https://datatracker.ietf.org/doc/draft-dellaert-oauth-approval-based-dcr/). 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](https://mcguinness.github.io/draft-mcguinness-oauth-software-statement-issuance/) 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](https://datatracker.ietf.org/wg/oauth/about/) are especially welcome.

