Skip to content

Verification — Verifiable Claims

UGP profiles are self-hosted, so by default every field is self-asserted. A nonprofit can publish eins: ["12-3456789"] whether or not it owns that EIN. Verification adds a way for independent third parties to vouch for specific claims, so a reader can tell a checked fact from an unchecked one — and see who checked it.

The mechanism is an attestation: a small, independently signed statement by a verifier that a specific claim about a nonprofit was checked and found true. The nonprofit carries the attestation inside its own profile, but cannot forge or alter it. Trust flows from the verifier's signature, not from the fact that the nonprofit is hosting it.

This is the issuer / holder / verifier shape of verifiable credentials, expressed in the primitives UGP already uses: public JWKs, ES256 signatures, and /.well-known/ucp discovery. There is no JSON-LD and no DID resolution.

Roles

Role Who What they do
Issuer (verifier) e.g. "Trusted EIN Check" Performs the check (KYB/KYC, document review, …) and signs an attestation with its own key.
Holder the nonprofit Receives the attestation and republishes it in its /.well-known/ucp. Cannot tamper with it.
Relying party a giving platform or donor Reads the profile, verifies the attestation against the issuer's key, and applies its own trust policy.

The relying party resolves the issuer's public key from the issuer's own /.well-known/ucp — never from the nonprofit. The nonprofit only transports the signed blob.

Where attestations live

A nonprofit carries attestations in a top-level attestations array in its discovery document, a sibling of signing_keys. This sits outside the ucp object because an attestation can cover any capability's config (or an external document), not just the giving profile. The base profile document permits it (additionalProperties: true); each item is validated by source/schemas/verification/attestation.json ($id: https://ugp.dev/schemas/verification/attestation.json).

jsonc { "ucp": { /* services, capabilities, … */ }, "signing_keys": [ /* the nonprofit's own keys */ ], "attestations": [ { "id": "urn:uuid:8c1d4f2a-…", "claim_type": "dev.ugp.verification.claim.ein_ownership", "issuer": { "domain": "trustedeincheck.com", "name": "Trusted EIN Check", "kid": "tec-2026-05" }, "subject": { "domain": "eastsideshelter.org", "key_thumbprint": "NzbLsXh8…", "eins": ["12-3456789"] }, "claim": { "target": "/ucp/capabilities/dev.ugp.giving.profile/0/config/eins/0", "value": "12-3456789", "value_digest": "sha-256:9b2f…" }, "method": { "type": "kyb", "statement": "Verified IRS determination letter and bank-account control.", "evidence_ref": "https://trustedeincheck.com/methodology" }, "issued_at": "2026-05-14T09:30:00Z", "expires_at": "2027-05-14T09:30:00Z", "status": { "url": "https://trustedeincheck.com/.well-known/ugp/status/8c1d4f2a" }, "proof": { "type": "dev.ugp.verification.proof.jws-jcs", "jws": "eyJhbG…..MEUCIQ…" } } ] }

See example-profile.json for a complete document with two attestations.

Attestation field reference

Field Type Required Notes
id string yes Globally unique id (e.g. urn:uuid:…). Revocation-status lookup key.
claim_type reverse-domain string yes The kind of claim (e.g. dev.ugp.verification.claim.ein_ownership). Fixes the meaning and shape of claim.
issuer.domain string yes Authority domain of the verifier. Resolve https://<domain>/.well-known/ucp for its identity and keys.
issuer.kid string yes Which key in the issuer's signing_keys produced proof.jws.
issuer.name string no Display name. Advisory; domain is authoritative.
subject.domain string yes Domain of the nonprofit. MUST match the domain serving the embedding profile.
subject.key_thumbprint string no (recommended) RFC 7638 JWK thumbprint of one of the subject's signing_keys. Pins the attestation to this holder.
subject.eins array of string no EINs concerned, for indexing/display.
claim.target JSON Pointer no RFC 6901 pointer into the embedding document the claim covers.
claim.value any no The literal value verified (e.g. the EIN, or a document URL).
claim.value_digest string no (recommended for documents) <alg>:<base64url-digest> over the canonicalized content.
method.type enum no kyb, kyc, document_review, irs_pub78_match, registry_match, attestation_chain, manual, other.
method.statement string no Human summary of what was checked. MUST NOT contain PII.
method.evidence_ref string (uri) no Link to methodology / non-sensitive evidence. MUST NOT expose private documents.
issued_at string (date-time) yes When issued (RFC 3339). Reject if in the future.
expires_at string (date-time) no When it must be re-verified.
status.url string (uri) no Issuer-hosted live revocation status (e.g. { "revoked": false }).
proof.type const yes dev.ugp.verification.proof.jws-jcs.
proof.jws string yes Detached JWS over the JCS canonicalization of the attestation without proof.

The two bindings

An attestation is only meaningful because of two bindings. Both MUST be enforced by a relying party, or the attestation is trivially forgeable.

Content binding — claim.value / claim.value_digest

The attestation commits to the actual value verified, not merely to a field path. Otherwise a nonprofit could get 12-3456789 attested, then edit eins[0] to a different number while the attestation still appears to cover it. When claim.target is present, the reader dereferences the pointer and confirms the live value matches value / value_digest.

Subject binding — subject.domain / subject.key_thumbprint

The attestation names which nonprofit it is about. Otherwise organization B could copy organization A's genuine attestation into B's profile and it would verify. The reader confirms subject.domain is the domain serving the profile and subject.key_thumbprint matches one of that profile's signing_keys.

The proof: detached JWS over JCS

proof.type is dev.ugp.verification.proof.jws-jcs. The signature is a JOSE signature over the RFC 8785 JSON Canonicalization Scheme (JCS) serialization of the attestation object with the proof member removed.

proof.jws is a detached compact JWS (RFC 7515 with RFC 7797 unencoded, detached payload), of the form BASE64URL(header)..BASE64URL(signature) — the middle (payload) segment is empty. The protected header carries:

jsonc { "alg": "ES256", "kid": "tec-2026-05", "b64": false, "crit": ["b64"] }

Signing (issuer): remove proof, JCS-canonicalize the object, sign the canonical bytes with the key named by kid, attach proof.

Verifying (relying party): remove proof, JCS-canonicalize, resolve the issuer's JWK by kid, and verify the detached JWS over the canonical bytes.

Using JCS rather than JSON-LD canonicalization keeps verification dependency-free — a verifier needs only a JOSE library and a JCS serializer, both of which a UGP implementation already needs for HTTP Message Signatures.

Verifier discovery — the dev.ugp.verification capability

A verifier is a first-class UGP entity: it hosts its own /.well-known/ucp with its own signing_keys, and declares a dev.ugp.verification capability advertising what it attests. Its config is validated by source/schemas/verification/verifier.json ($id: https://ugp.dev/schemas/verification/verifier.json).

jsonc "capabilities": { "dev.ugp.verification": [ { "version": "2026-06-25", "spec": "https://ugp.dev/specification/verification", "schema": "https://ugp.dev/schemas/verification/verifier.json", "config": { "name": "Trusted EIN Check", "claim_types": ["dev.ugp.verification.claim.ein_ownership"], "methodology": "https://trustedeincheck.com/methodology", "status_endpoint": "https://trustedeincheck.com/.well-known/ugp/status" } } ] }

Key distribution and verifier identity reuse the exact discovery mechanism nonprofits already use — there is no new PKI.

Standard claim types

Claim types are open reverse-domain identifiers; anyone may define new ones without a spec change. The 2026-06-25 release defines one standard type and documents one illustrative type.

dev.ugp.verification.claim.ein_ownership

The subject controls the named EIN.

Field Expectation
claim.target JSON Pointer to the attested entry in eins, e.g. /ucp/capabilities/dev.ugp.giving.profile/0/config/eins/0.
claim.value The EIN string, matching ^\d{2}-\d{7}$.
subject.eins SHOULD include the attested EIN.
method.type Typically kyb or irs_pub78_match.

dev.ugp.verification.claim.impact_report (illustrative)

A third party reviewed an impact report and found its figures supported. Shows the same envelope covering an external document rather than a profile field:

Field Expectation
claim.target Usually omitted (the report is not a profile field).
claim.value URL of the report.
claim.value_digest sha-256:… over the report bytes, so the reviewed version is pinned.
method.type document_review.

Verifying an attestation

A relying party MUST run these steps before trusting a claim:

  1. Subject binding. Confirm subject.domain is the domain serving the profile, and subject.key_thumbprint (when present) matches one of the profile's signing_keys.
  2. Content binding. If claim.target is set, dereference the JSON Pointer in the document, canonicalize, and confirm it matches claim.value / claim.value_digest.
  3. Authenticity. Resolve issuer.domain's /.well-known/ucp, fetch the JWK by issuer.kid, and verify proof.jws over the JCS canonicalization of the attestation without proof.
  4. Freshness. Confirm issued_at is not in the future and, if present, expires_at has not passed.
  5. Revocation. For high-stakes decisions (e.g. moving money), fetch status.url and confirm the attestation is not revoked. Results MAY be cached for a short TTL.
  6. Trust policy. Decide whether issuer.domain is acceptable for this claim_type (next section). Only then treat the claim as verified.

Steps 1–5 establish that the claim was provably asserted by issuer.domain and is still live. Step 6 is the relying party's own decision.

Trust policy: relying-party allowlists

The protocol verifies authenticity, not trustworthiness

UGP proves who asserted a claim and that it has not been tampered with or revoked. It deliberately does not decide which verifiers are legitimate — that would make the protocol a gatekeeper. Trust is layered on top, by the relying party.

For the 2026-06-25 release, the recommended model is a relying-party allowlist: each platform decides which issuer domains it accepts, per claim type. For example, a platform might accept EIN attestations only from trustedeincheck.com and irs.gov, and ignore any attestation whose issuer is not on its list — even if the signature is valid. A platform assesses an issuer (in part via its published methodology) before adding it.

The data model is designed so richer trust sources can layer on later without a breaking change:

  • Registry directory. dev.ugp.registry could maintain a curated list of known verifiers and expose, per nonprofit, the attested claims with their issuer domains — a more turnkey source than per-platform allowlists.
  • Accreditation. A verifier accredited by an accreditor is just another attestation whose subject is a verifier (method.type: attestation_chain). The model is recursive; this release does not standardize an accreditation claim type.

Revocation and expiry

Because the holder controls republishing, a relying party cannot assume a revoked attestation has been removed from the profile. Revocation is therefore pull-based from the issuer:

  • The issuer hosts a status resource per attestation at status.url and advertises its status_endpoint in the dev.ugp.verification capability.
  • expires_at forces periodic re-verification regardless of explicit revocation.
  • Relying parties SHOULD check status before high-stakes use and MAY cache for a short TTL.

Relationship to registry verification flags

The registry's advisory verification flags (see Registry), such as ein_verified, predate attestations and remain a coarse, registry- computed signal. Attestations supersede them with attributable, independently signed evidence: rather than a bare ein_verified: true, a registry entry can surface "EIN verified by Trusted EIN Check, expires 2027-05-14". A registry MAY derive its flags from indexed attestations, but the authoritative source is always the signed attestation in the nonprofit's /.well-known/ucp.

Issuance and delivery

How a verifier performs KYB/KYC is out of scope — that is the verifier's business. The protocol only concerns the signed artifact and its consumption. Delivery of a freshly minted attestation to the holder (or to the provider that hosts the holder's well-known on its behalf) is left to the implementation in this release; a future revision may standardize an endpoint such as POST /attestations on the giving service, where the host verifies the issuer signature and subject binding before storing the attestation for inclusion in the served profile.