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:
- Subject binding. Confirm
subject.domainis the domain serving the profile, andsubject.key_thumbprint(when present) matches one of the profile'ssigning_keys. - Content binding. If
claim.targetis set, dereference the JSON Pointer in the document, canonicalize, and confirm it matchesclaim.value/claim.value_digest. - Authenticity. Resolve
issuer.domain's/.well-known/ucp, fetch the JWK byissuer.kid, and verifyproof.jwsover the JCS canonicalization of the attestation withoutproof. - Freshness. Confirm
issued_atis not in the future and, if present,expires_athas not passed. - Revocation. For high-stakes decisions (e.g. moving money), fetch
status.urland confirm the attestation is not revoked. Results MAY be cached for a short TTL. - Trust policy. Decide whether
issuer.domainis acceptable for thisclaim_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.registrycould 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
subjectis 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.urland advertises itsstatus_endpointin thedev.ugp.verificationcapability. expires_atforces 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.