| Internet-Draft | HVP | October 2026 |
| Goto | Expires 29 April 2027 | [Page] |
This document defines the Handle Verification Protocol (HVP), the HTTP-level protocol by which a browser obtains a signed Handle Verification Token (HVT) from an issuer attesting that the user controls a handle, and presents it to a verifier. HVP uses the same three-party model as the Email Verification Protocol ([EVP]), in which the browser intermediates between the verifier and the issuer and hides the verifier's identity from the issuer.¶
This document defines handles, handle types, handle normalization and resolution, issuers, the HVT request, the HVT, its key binding (HVT+KB), and the verification of the HVT+KB, which are the same for every handle type. It also defines one handle type: domain handles, such as @alice.example, which are used by the AT Protocol, and which are resolved through a DID document in a way that is backwards compatible with the AT Protocol. The browser API, which is defined separately (for example, by the W3C IdP Registration API ([IDP-REGISTRATION])), defines which handle types are supported, how the user selects a handle, and how the HVT+KB is delivered to the verifier.¶
Note: This section is to be removed before publishing as an RFC.¶
Source for this draft and an issue tracker can be found at https://github.com/samuelgoto/handle-verification.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 29 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Social networks built on open protocols let any server host a user's account, and let users bring their own domain name as their identity. The AT Protocol ([ATPROTO]) identifies every user with a handle that is a DNS name, such as @alice.example, which resolves to a stable decentralized identifier (DID). ActivityPub ([ACTIVITYPUB]) implementations identify users with handles of the form @alice@social.example, and the W3C Social Web Incubator Community Group is considering adding domain handles, such as @alice.example, as an alternative ([HANDLES-TF]). The result is an open federation: a website that wants to accept these users cannot know their providers in advance.¶
Email shows that an open federation can still be verified. A verifier accepts an email address at any mail domain without a prior relationship with the domain, and the Email Verification Protocol ([EVP]) lets the verifier check such an address with a signed token. HVP applies that model to handles, in a way that does not depend on which ecosystem the user's account belongs to.¶
A website that wants to let users sign in with a handle today has to integrate with each ecosystem separately. For the AT Protocol, it uses AT Protocol OAuth ([ATPROTO-OAUTH]): it presents a text field for the user to type their handle, resolves the handle to a DID and PDS, discovers the PDS's authorization server, and redirects the user to it. ActivityPub has no common sign-in protocol, so websites typically use the OAuth API of each server implementation. In both cases:¶
HVP separates the problem into three parts:¶
This document defines one handle type, domain handles (Section 6), which are resolved to a DID in a way that is backwards compatible with the AT Protocol (Section 6.4.1). Nothing in issuance or verification depends on the handle type, or on the ecosystem that the account belongs to.¶
HVP keeps the privacy properties of EVP: the browser intermediates between the verifier and the issuer, so the issuer does not learn which verifier the user is signing in to.¶
This document defines the protocol, which is the same for every handle type, and one handle type, domain handles, which consist of a DNS name alone. Other handle types are defined by other documents, which plug into the protocol: [URL-HANDLES] defines URL handles, that is, users identified by an https URL, such as a Solid WebID, an ActivityPub actor, or an IndieAuth personal website, and [USERNAME-HANDLES] builds on it to define username handles, which combine a user name and a domain, such as @alice@social.example, by mapping them to the URL of their account with WebFinger. More handle types can be added in the same way, without changing issuance or verification. Email addresses are out of scope: they are verified with [EVP].¶
HVP provides verified identification, for example to sign a user in: it tells the verifier that the user controls a handle, and which unique user identifier that handle resolves to. It does not provide authorization: an HVT grants no access to the user's account, and cannot be used to read or publish content or to act on the user's behalf. A verifier that needs such access continues to use the ecosystem's authorization protocol, such as AT Protocol OAuth ([ATPROTO-OAUTH]), and can use HVP and that protocol together, for example by signing the user in with HVP and requesting authorization later, only when it needs it. See Section 14.4.¶
HVP reuses the mechanics of [EVP] (the token request, key binding, and token format) with the substitutions described in this document. A browser API can offer both, using EVP for email addresses and HVP for handles.¶
This document specifies the IETF protocol aspects of handle verification: the HTTP-level interactions between the browser, the issuer, and the verifier, and the lookups needed to resolve a handle. How the browser learns about the user's handles, how the user selects one, and how the browser communicates with the verifier are defined separately. The W3C IdP Registration API ([IDP-REGISTRATION]), an extension of FedCM ([FEDCM]), is one such API. See Appendix B.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
This document uses the following terms:¶
@alice.example, of one of several handle types. A handle is not necessarily permanent: a user can change it. See Section 4.¶
alice.example, displayed as @alice.example. See Section 6.¶
sub claim. Each handle type defines it: for a domain handle, it is the DID that the handle resolves to. Unlike a handle, which can change hands and point to different accounts over time, it identifies one account.¶
did:plc:bftbdgcbe4zclvpkboe4hleq or did:web:alice.example. A handle resolves to a DID.¶
id, and lists the subject's other identifiers, verification methods, and services ([CID-1.0]). See Section 5.1.¶
id is the DID. It lists the handle that the account claims, the account's services, and, for HVP, its authorized issuer.¶
HandleVerification service (Section 5.2): for a domain handle, its DID document. Only the authorized issuer can issue an HVT that a verifier accepts for that handle. A handle has at most one authorized issuer.¶
The protocol involves three parties: the verifier, the browser, and the issuer. The following diagram shows the protocol flow:¶
Step Verifier Browser Issuer
| | |
3.1 Handle Discovery | [obtain handle from user] |
| | |
3.2 Session Binding |--- nonce ->| |
| | |
3.3 Handle Resolution | [resolve handle: subject |
| identifier, issuer] |
| | |
3.4 HVT Request | |-- POST /issuance ->|
| | (handle, sub) |
| | |
3.5 HVT Issuance | |<------- HVT -------|
| | |
3.6 KB Creation | [create KB-JWT] |
| | |
3.7 HVT+KB Presentation |<-- HVT+KB -| |
| | |
3.8 HVT+KB Verification [resolve handle; | |
verify HVT+KB] | |
¶
The flow is the same as the flow of [EVP], with two differences: the browser and the verifier resolve the handle (Section 5) where EVP looks up a DNS delegation record, and the token is an HVT rather than an EVT.¶
How the browser assembles candidate handles, from its own store, from accounts that an issuer pushes or the browser pulls, or from the user typing one, is defined by the browser API ([IDP-REGISTRATION]) and is out of scope for this document. When the browser starts from text that the user typed, it parses it as described in Section 4.2.¶
As in [EVP]: the verifier generates a cryptographically random nonce with at least 128 bits of entropy and binds it to a session it has with the browser. How the verifier provides the nonce to the browser is defined by the browser API.¶
The browser resolves the handle as described in Section 5. Resolution produces the unique user identifier and the authorized issuer. The browser fetches the issuer's metadata (Section 7.2). From this point onward, the handle and the unique user identifier are never altered: they are sent byte-for-byte in the HVT request, and returned byte-for-byte in the handle and sub claims.¶
The browser sends an HVT request to the issuer's issuance_endpoint. The request is the token request of [EVP], with the substitutions listed in Section 7.3.¶
The issuer verifies the request, checks that the browser's cookies represent a signed-in user who controls the handle, and returns an HVT (Section 8) as the value of issuance_token in an application/json response, as in [EVP].¶
The browser verifies the HVT (Section 8.3), additionally confirming that the handle and sub claims are byte-for-byte identical to the handle and unique user identifier it requested and that the cnf.jwk claim matches the public key it generated. It then creates a KB-JWT and combines it with the HVT as described in Section 9.¶
The browser provides the HVT+KB to the verifier. This mechanism is defined by the browser API.¶
The verifier verifies the HVT+KB as described in Section 10. The verifier resolves the handle itself, and does not rely on the browser's resolution. If verification succeeds, the verifier has verified that the user controls the handle in the handle claim, and has learned the user's unique user identifier in the sub claim.¶
A handle is a human-readable identifier of a user. Handles come in several handle types, whose syntaxes are mutually exclusive, so that any string is a handle of at most one type. Each handle type defines:¶
Everything else, that is, the issuers, the HVT request, the HVT, its key binding, and the verification of the HVT+KB, is the same for every handle type. This document defines one handle type, domain handles (Section 6).¶
Note: Other documents define other handle types: URL handles ([URL-HANDLES]), such as
https://social.example/users/alice, and username handles ([USERNAME-HANDLES]), such as@alice@social.example.¶
A browser, an issuer, or a verifier supports some set of handle types. A handle of a type that it does not support is not a handle for it.¶
Each handle type defines the normalized form of its handles. Every handle in this protocol (in the HVT request, in the handle claim, and in comparisons) is in normalized form, and every comparison of handles is a byte-for-byte comparison of normalized handles.¶
To parse a handle that a user typed, parse it as a handle of each supported handle type. Because their syntaxes are mutually exclusive, at most one succeeds, and its result is the handle. If none succeeds, the input is not a handle.¶
| Input | Handle |
|---|---|
@Alice.Example, alice.example
|
alice.example (domain handle, Section 6.3) |
https://Social.Example/users/alice
|
https://social.example/users/alice (URL handle, [URL-HANDLES]) |
@Alice@Social.Example
|
alice@social.example (username handle, [USERNAME-HANDLES]) |
alice@mail.example
|
not a handle |
An email address, such as alice@mail.example, is not a handle of any type: a browser API can verify it with [EVP].¶
Resolution takes a normalized handle and produces:¶
Each handle type defines its resolution: domain handles are resolved through a DID (Section 6.4). Resolution checks that the handle and the unique user identifier refer to each other, so that a handle cannot claim an account that does not agree to it, and finds the issuer that the user has explicitly authorized.¶
Both the browser and the verifier perform resolution. The browser does it to find an issuer to ask for an HVT; the verifier does it to check that the HVT was issued by an issuer that is authorized for the handle (Section 10).¶
The following rules apply to every resolution step, of every handle type:¶
https scheme, MUST NOT include credentials (cookies or HTTP authentication), and SHOULD NOT include a Referer header. When the browser makes them, they SHOULD be made from an opaque origin, so that the servers involved do not learn which verifier the user is visiting.¶
https URLs, and at most 5 times for a single request.¶
Handle types can resolve a handle to a controlled identifier document ([CID-1.0]), which describes the account: a DID document for a domain handle (Section 6.4), or a document fetched from a URL for other handle types. A controlled identifier document names the account's authorized issuer with the HandleVerification service (Section 5.2), and can claim the handle back with its alsoKnownAs member.¶
Resolving parties read a controlled identifier document as JSON ([RFC8259]), without JSON-LD processing ([JSON-LD11]), as follows:¶
id member is a string. Otherwise, it is not a controlled identifier document.¶
id, alsoKnownAs, service, and, within each service, id, type, and serviceEndpoint. A JSON-LD @context, if present, is ignored, so a document that spells these members differently, for example in expanded JSON-LD form, has none of them.¶
id of a service, such as #handle_verification, are resolved against the id of the document.¶
A handle type that resolves a handle to a controlled identifier document defines how the document is obtained, and how its id is checked: for example, a DID document's id is the DID, and a document fetched from a URL has that URL as its id ([CID-1.0]).¶
A controlled identifier document names the issuer authorized to vouch for its handles with a service ([CID-1.0]) whose type is HandleVerification, whose serviceEndpoint is an issuer identifier (Section 7.1), and whose id is #handle_verification, that is, the document's id followed by #handle_verification:¶
{ "id": "#handle_verification", "type": "HandleVerification",
"serviceEndpoint": "https://issuer.example" }
¶
HandleVerification is a service type of [CID-1.0], in the same way that, for example, the AT Protocol identifies the account's PDS with a service whose id is #atproto_pds ([ATPROTO-DID]). Service ids are unique within a controlled identifier document ([CID-1.0]), and in did:plc services are stored as a map keyed by id ([DID-PLC]), so a document names at most one issuer.¶
If the document has no service whose id is #handle_verification, or that service's type is not HandleVerification, or its serviceEndpoint is not an issuer identifier, that is, not an https origin with no path, query, or fragment, the handle has no authorized issuer, and HVP cannot be used with it.¶
Because a handle has a single authorized issuer, a change of issuer takes effect at once: as soon as the document names a new issuer, HVTs from the previous one no longer verify. This is the same as when a user moves their account to another server. A user who moves their account to another server, and whose issuer is that server, updates their HandleVerification service as part of the move.¶
Authorization is always explicit: resolution does not infer an issuer from the account's other services or properties, such as its AT Protocol PDS (the #atproto_pds service), the PDS's authorization server, or the server that hosts the document. An issuer has to deploy HVP anyway, and requiring the document to name it means that a server does not become able to vouch for a handle in a new way, to verifiers it may never have dealt with, without the account's owner opting in. See Section 14.7.¶
Like other services of controlled identifier documents, the HandleVerification service names a party to interact with, and does not involve the document's verification methods: the issuer signs HVTs with the keys of its own metadata (Section 7.2).¶
Open Issue: Whether the service type should be identified by a full IRI, for example a
w3id.orgURL, with a published JSON-LD context, so that JSON-LD consumers of DID documents and controlled identifier documents can process it, and its registration in the W3C DID Extensions registry, which also covers the properties of controlled identifier documents.¶
A resolving party MAY cache the results of resolution, subject to DNS time-to-live values and HTTP caching rules. A verifier SHOULD NOT cache resolution results for longer than a few minutes when verifying an HVT+KB, because a user who moves their handle to another issuer, or who loses control of it, expects the change to take effect promptly.¶
A domain handle is a DNS name, such as alice.example. Domain handles are the handles of the AT Protocol ([ATPROTO-HANDLE]), and the domain-based handles proposed for ActivityPub ([HANDLES-TF]). In DID documents, a domain handle is written as an at URI ([ATPROTO-URI]), at:// followed by the handle, such as at://alice.example, as in the AT Protocol. This document uses that form for domain handles of every ecosystem.¶
A domain handle is displayed with a leading @, as in @alice.example, and is carried in the protocol without it.¶
A domain handle MUST satisfy the following rules, which are a synthesis of the hostname rules of [RFC1035], [RFC3696] and [RFC3986]:¶
.). It has no leading or trailing period.¶
-). A label does not start or end with a hyphen.¶
Internationalized domain names are written in their A-label form ([RFC5890]), for example xn--ls8h.example.¶
In ABNF ([RFC5234]), before normalization:¶
handle = 1*( label "." ) tld-label label = alnum [ *61( alnum / "-" ) alnum ] tld-label = ALPHA [ *61( alnum / "-" ) alnum ] alnum = ALPHA / DIGIT¶
with the additional constraint that the whole handle is at most 253 characters long. A reference regular expression is:¶
^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+
[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$
¶
(The regular expression is shown on two lines for display; it is a single expression.)¶
The following top-level domains are syntactically valid in a handle, but a handle whose domain ends in one of them MUST fail resolution: .alt, .arpa, .example, .internal, .invalid, .local, .localhost, and .onion. The .test top-level domain MAY be used in development, and MUST fail resolution in production deployments. (The examples in this document use .example and so would fail resolution.)¶
Open Issue: How internationalized handles are displayed, and how homoglyph attacks are mitigated, in coordination with the internationalization work of [HANDLES-TF].¶
Domain handles are case-insensitive. The normalized form of a domain handle is the handle converted to ASCII lowercase.¶
To parse a domain handle that a user typed:¶
at://, remove it, or else if it starts with @, remove it.¶
/, remove it.¶
If any step fails, the input is not a domain handle.¶
| Input | Domain handle |
|---|---|
@Alice.Example, alice.example, at://alice.example
|
alice.example
|
@192.0.2.1, @com
|
not a domain handle |
A domain handle resolves to a DID ([DID-CORE]), which is its unique user identifier, and whose DID document claims the handle back and names the authorized issuer. This generalizes the handle resolution of the AT Protocol ([ATPROTO-HANDLE], [ATPROTO-DID]): it uses ecosystem-neutral names for the records that map a handle to its DID, also accepts the names of the AT Protocol (Section 6.4.1), and adds the extraction of the authorized issuer.¶
Given a normalized domain handle handle:¶
Handle to DID. Perform the following lookups. A DID MUST use the plc method ([DID-PLC]) or the web method ([DID-WEB]); otherwise fail. If both lookups yield a DID, and the DIDs differ, fail. If either lookup yields a DID, it is the handle's DID. If neither yields anything, the resolving party MUST fall back to the lookups of existing AT Protocol handles (Section 6.4.1), but only if both lookups established that the handle names nothing: that is, if the DNS query returned no TXT record whose value starts with a recognized key, or no record at all, and the well-known request returned a 404 (Not Found) or 410 (Gone) response, or a document with no recognized member. If either lookup failed in any other way, for example because of a DNS server failure, a timeout, or a server error, fail. A resolving party MAY perform the lookups of Section 6.4.1 in parallel with these, to avoid adding a round trip, and use their results only when it falls back. A handle near the 253-character limit cannot use the DNS lookups, because the prefix would push the query name over the limit; the well-known lookups work for such handles.¶
TXT record. Query TXT records at the name _handle. handle. A record whose value starts with did= names a DID, in the remainder of the value. Records whose value starts with anything else are ignored. This method aligns with [RFC1464].¶
GET https:// handle /.well-known/handle. A 2xx response with the application/json media type is a JSON object whose did member, if present, names a DID. Other members are ignored.¶
DID to DID document. Resolve the DID to its DID document ([DID-CORE]), a controlled identifier document that is read as described in Section 5.1, and whose id member MUST be identical to the DID:¶
did:plc, GET the DID from a PLC directory, by default https://plc.directory/ followed by the DID.¶
did:web, GET https:// domain /.well-known/did.json, where domain is the method-specific identifier ([DID-WEB]), such as alice.example for did:web:alice.example. A did:web DID with a path is not supported, as in the AT Protocol ([ATPROTO-DID]).¶
Back-link. Find the first element of the DID document's alsoKnownAs array that is at:// followed by a string that satisfies Section 6.1. After normalization, that string MUST be identical to handle. Otherwise fail. This check ensures that a DID document cannot be claimed by a handle that it did not agree to.¶
Issuer. The authorized issuer is the serviceEndpoint of the DID document's HandleVerification service (Section 5.2). If there is none, fail.¶
If any step fails, resolution fails. The unique user identifier is the DID.¶
For example, the handle @samuelgoto.bsky.social resolves as follows at the time of writing, through the AT Protocol names (Section 6.4.1), since it publishes none of the new ones:¶
GET https://samuelgoto.bsky.social/.well-known/atproto-did -> did:plc:bftbdgcbe4zclvpkboe4hleq GET https://plc.directory/did:plc:bftbdgcbe4zclvpkboe4hleq -> alsoKnownAs: ["at://samuelgoto.bsky.social"]¶
so its unique user identifier is the DID did:plc:bftbdgcbe4zclvpkboe4hleq. Its DID document has no HandleVerification service yet, so it has no authorized issuer. See Appendix A.¶
Because records and members with other keys are ignored, other kinds of records can be added later without affecting resolving parties that do not support them. For example, [USERNAME-HANDLES] defines the me= key and the me member, which let a domain handle name a username handle or a URL handle ([URL-HANDLES]) instead of a DID.¶
Open Issue: Other kinds of records could be added later with keys of their own: for example,
domain=for another domain handle, resolved recursively and failing when a loop is detected, oremail=for an email address verified with [EVP]. A handle could also be allowed to name several identifiers, any one of which proves it; this would require a rule for which unique user identifier an HVT for the handle carries, because each resolves to a different one, and a verifier identifies accounts by it.¶Open Issue: The SWICG Handles Task Force proposes
/.well-known/activitypub-actor, a JSON document that refers to an ActivityPub actor, to resolve domain handles in ActivityPub ([HANDLES-TF]). This document proposes/.well-known/handleand_handle, which refer to a DID instead, so that the same names serve every ecosystem. The two need to be reconciled.¶Open Issue: Whether a server that the DID document already names, such as the PDS named by the
#atproto_pdsservice, should be an authorized issuer by default, which would let HVP work with existing accounts without changing their DID documents, at the cost of the explicit opt-in.¶
The AT Protocol maps a handle to a DID with a TXT record at _atproto. followed by the handle, and with the well-known URL /.well-known/atproto-did ([ATPROTO-HANDLE]). These names are specific to the AT Protocol. This document defines ecosystem-neutral names, _handle and /.well-known/handle, which take precedence, and falls back to the AT Protocol names when a handle does not publish the new ones, so that every existing AT Protocol handle keeps working.¶
When step 1 of resolution (Section 6.4) falls back, the resolving party performs the following lookups, each of which yields the handle's DID:¶
TXT record. Query TXT records at the name _atproto. handle. Ignore records whose value does not start with did=. The DID is the remainder of the value.¶
GET https:// handle /.well-known/atproto-did. A 2xx response with the text/plain media type contains the DID, with leading and trailing whitespace removed.¶
If both lookups yield a DID, and the DIDs differ, resolution fails, in the same way that an AT Protocol handle with conflicting _atproto records fails today. If neither yields a DID, resolution fails.¶
As a consequence:¶
Open Issue: Whether, and when, the AT Protocol adopts the new names, and whether resolving parties can eventually stop querying the AT Protocol names. This needs coordination with the AT Protocol community and the IETF ATP working group.¶
An issuer identifier is an HTTPS origin: the string https:// followed by a host, with no port, path, or trailing slash, for example https://issuer.example. This is the same form as the issuer identifier of [EVP], and for the same reasons: it names the same thing as the iss claim of OpenID Connect and the issuer of [RFC8414], and it can be compared byte-for-byte.¶
Every use of the issuer identity in this document (the authorized issuer that resolution yields, the iss claim of the HVT, the issuer member of the metadata, and the base of the well-known URL) is this origin. Comparisons are byte-for-byte; no scheme defaulting, port normalization, or trailing-slash tolerance is applied.¶
An issuer serves its metadata at $ISSUER/.well-known/handle-verification, where $ISSUER is its issuer identifier. For https://issuer.example, this is https://issuer.example/.well-known/handle-verification.¶
The metadata document is a JSON object with the following members:¶
issuer (REQUIRED): The issuer identifier. It MUST be identical to the issuer identifier that the document was fetched under, and a party that fetches the document MUST reject it if it is not (as in Section 3.3 of [RFC8414]).¶
issuance_endpoint (REQUIRED): The URL that the browser sends HVT requests to. It MUST be an https URL that is same site ([WHATWG.HTML]) with the issuer identifier, so that the HVT request carries the cookies of the issuer's site; a party that fetches the metadata MUST reject it otherwise.¶
jwks_uri (REQUIRED): The URL of the issuer's JSON Web Key Set ([RFC7517]), used to verify HVTs.¶
signing_alg_values_supported (OPTIONAL): As defined by [EVP].¶
federations (OPTIONAL): An array of https URLs, each of which identifies a federation that the accounts of the issuer belong to, such as https://atproto.com. HVP does not interpret these URLs, and verification does not depend on them. A browser API uses them to offer a verifier only the handles whose issuers belong to the federations that the verifier asks for (Appendix B).¶
Example:¶
{
"issuer": "https://issuer.example",
"issuance_endpoint": "https://issuer.example/hvp/issuance",
"jwks_uri": "https://issuer.example/hvp/jwks",
"signing_alg_values_supported": ["Ed25519", "ES256"],
"federations": ["https://atproto.com"]
}
¶
An issuer that also acts as an EVP issuer serves its EVP metadata and its HVP metadata at their respective well-known paths. It MAY use the same keys for both.¶
Open Issue: Whether requiring the
issuance_endpointto be same site with the issuer identifier is appropriate, and whether [EVP] has, or should have, the same requirement, so that HVP does not diverge from it.¶
The HVT request is the token request of [EVP], including its use of HTTP Message Signatures ([RFC9421]) and the hwk key scheme, with the following substitutions:¶
issuance_endpoint of the HVP metadata.¶
The request body is a JSON object with the following members:¶
handle (REQUIRED): The handle that the user selected, in normalized form (Section 4.1).¶
sub (REQUIRED): The unique user identifier that the handle resolves to, byte-for-byte as resolution produced it: for a domain handle, its DID.¶
Sec-Fetch-Dest header has the value handle-verification.¶
Example:¶
POST /hvp/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: session=...
Content-Type: application/json
Sec-Fetch-Dest: handle-verification
Content-Digest: \
sha-256=:...:
Signature-Input: sig=("@method" "@authority" "@path" \
"content-digest" "signature-key");created=1792620000
Signature: sig=:MEQCIHd8Y8qYKm5e3dV8y....:
Signature-Key: sig=hwk;kty="OKP";crv="Ed25519"; \
x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";alg="Ed25519"
{"handle":"samuelgoto.bsky.social",
"sub":"did:plc:bftbdgcbe4zclvpkboe4hleq"}
¶
On receipt of an HVT request, the issuer:¶
Content-Digest, as in the request verification of [EVP], with handle-verification in place of email-verification as the expected Sec-Fetch-Dest.¶
handle is a handle of a handle type that the issuer supports (Section 4), in normalized form, and that sub is a valid unique user identifier for that handle type: for a domain handle, a syntactically valid did:plc or did:web DID. If it does not, the issuer returns an invalid_request error (Section 7.3.1).¶
handle, and whose account is sub. If they do not, the issuer returns an authentication_required error.¶
handle (Section 5) and check that it is the authorized issuer and that resolution yields sub. An HVT from an issuer that is not authorized is rejected by the verifier anyway, so this check is not needed for security, but it avoids issuing HVTs that will fail.¶
issuance_token in an application/json response, as in [EVP].¶
The issuer returns the error responses of [EVP] (invalid_request, invalid_signature, authentication_required, and server errors), with the handle member in place of the email member.¶
The Handle Verification Token (HVT) is a JWT ([RFC7519]) issued by an issuer. It contains a handle, the unique user identifier that the handle resolves to, and the browser's public key.¶
iss (REQUIRED): The issuer identifier (Section 7.1).¶
iat (REQUIRED): The time the HVT was issued.¶
cnf (REQUIRED): A confirmation claim ([RFC7800]) containing the browser's public key as a jwk, reproduced from the Signature-Key header of the HVT request with the same members, including its alg.¶
handle (REQUIRED): The handle that the user controls, byte-for-byte as it appeared in the HVT request.¶
sub (REQUIRED): The unique user identifier that the handle resolves to (Section 5), byte-for-byte as it appeared in the sub member of the HVT request.¶
Example:¶
{
"iss": "https://bsky.social",
"iat": 1792620000,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
"alg": "Ed25519"
}
},
"handle": "samuelgoto.bsky.social",
"sub": "did:plc:bftbdgcbe4zclvpkboe4hleq"
}
¶
As with the EVT, the HVT has a ~ appended to it for compatibility with SD-JWT with Key Binding ([I-D.ietf-oauth-selective-disclosure-jwt]).¶
After verifying the request and authenticating the user, the issuer:¶
Both the browser and the verifier verify the HVT:¶
typ is hvt+jwt, and validate alg and kid as for the EVT in [EVP].¶
iss, iat, cnf, handle, and sub claims. Verify that handle is a handle of a handle type that the verifier supports (Section 4), in normalized form.¶
iss (Section 7.2).¶
jwks_uri, and verify the HVT's signature with the key identified by kid.¶
iat is within an acceptable time window.¶
The browser additionally verifies that handle and sub are byte-for-byte identical to the handle and unique user identifier it requested, and that cnf.jwk matches the public key it generated. The browser MAY reuse the results of the resolution it performed before the HVT request for step 4.¶
Key binding is the key binding of [EVP], with the HVT in place of the EVT. The browser creates a KB-JWT whose aud is the verifier's origin, whose nonce is the verifier's session nonce, and whose sd_hash is the hash of the HVT (including its trailing ~), and signs it with its private key. The HVT+KB is:¶
<HVT>~<KB-JWT>¶
The HVT+KB is compatible with SD-JWT with Key Binding ([I-D.ietf-oauth-selective-disclosure-jwt]), in the same way as the EVT+KB.¶
The verifier receives the HVT+KB, and verifies it by:¶
alg matches the alg of the HVT's cnf.jwk, its aud is the verifier's origin, its nonce is the session's nonce, its iat is recent, and its sd_hash is the hash of the HVT.¶
cnf.jwk.¶
If all verification steps pass, the verifier has verified that the user controls the handle in the handle claim. The verifier SHOULD identify the user's account by the unique user identifier in the sub claim, and treat the handle as a mutable attribute of the account (Section 12.2).¶
The verifier MUST perform its own resolution in step 2, and MUST NOT rely on resolution results provided by the browser. The verifier's resolution is what establishes that the issuer is authorized for the handle.¶
As in [EVP], the browser intermediates between the verifier and the issuer. The issuer learns which handle the user is verifying, but not which verifier the user is signing in to, and the verifier learns the handle only after the user has chosen to share it.¶
Handles and unique user identifiers are public by design: they are meant to be shared, are the same at every site, and are linked to the user's public activity in the AT Protocol or ActivityPub. A verifier that receives a handle can correlate the user with other verifiers that receive the same handle, and with the user's public activity under that handle. HVP does not change this, and offers no equivalent of the private email addresses of [EVP]. Browser APIs that use HVP should make clear to the user that the verifier will learn their handle and unique user identifier.¶
Resolution consists of DNS queries and unauthenticated HTTPS requests. Unlike the lookup of [EVP], which queries only the email domain and is shared by every user of that domain, every resolution step of HVP is specific to one user: for a domain handle, for example, the DNS name queried is the handle itself, and the DID document is the user's. Any party that observes a resolution therefore learns which user is being resolved, and when.¶
When the browser resolves a handle, the DNS resolver and the servers involved (for a domain handle, the handle's host, the host of a did:web DID document, and the PLC directory for did:plc) can observe that the user's device is resolving that handle, but not which verifier is involved, because the requests carry no credentials or referrer.¶
When the verifier resolves a handle to verify an HVT+KB, the requests carry nothing that names the verifier either, but they are sent by the verifier itself. The servers involved see the verifier's network address, which they can often attribute to the verifier, for example through reverse DNS or the verifier's hosting provider, or by signing in to a suspected verifier themselves and observing which address resolves their handle. Combined with the per-user nature of resolution, this can let them learn that a particular user signed in to the verifier at that time. When a server involved in resolution, such as the host of a domain handle or of its DID document, is also the user's issuer, as is common for handles under a server's domain, the issuer can correlate the HVTs it issues with the resolutions that follow, which weakens the property, described above, that the issuer does not learn which verifier the user signs in to. A verifier that goes on to interact with the user's account in the AT Protocol or ActivityPub reveals this anyway.¶
Verifiers can reduce this exposure by:¶
did:plc DIDs, maintaining a local copy of the PLC directory, as described below.¶
A browser or verifier can remove the PLC directory from per-sign-in traffic entirely by maintaining a local copy of the directory, ingested through its bulk export endpoint and kept current through its streaming endpoint ([DID-PLC]), and verifying each operation's signatures as it is ingested. Resolving a did:plc DID then consults the local copy and sends no request that reveals which DID is being resolved. This has limits:¶
Resolution establishes which issuer is authorized for a handle. It does not establish that the user controls the handle: that is established by the issuer's signature on an HVT, which the issuer produces only after authenticating the user. An attacker who manipulates the browser's resolution can cause the browser to send the user's handle to an issuer of the attacker's choosing, but cannot produce an HVT that the verifier accepts, because the verifier resolves the handle itself.¶
Handles can be reassigned: a domain can expire and be re-registered, and a hosting provider can let a new account take a handle that a deleted account used. Verifiers SHOULD identify accounts by the unique user identifier in the sub claim rather than by the handle claim. For example, the DID of a domain handle survives changes of handle, so a verifier that keys accounts on it is not affected when a handle moves to another account.¶
Resolution makes a verifier fetch URLs derived from user-controlled input. Verifiers MUST resolve only https URLs, MUST NOT fetch URLs whose host resolves to a loopback, link-local, or private IP address, and SHOULD apply limits on redirects, response size, and time.¶
The typ of the HVT (hvt+jwt) is distinct from that of the EVT (evt+jwt), so an HVT cannot be presented as an EVT or the other way around.¶
The following considerations are specific to domain handles.¶
Resolution of domain handles relies on DNS TXT records, which are not authenticated unless DNSSEC is used. As in [EVP], spoofing the browser's DNS resolution discloses the handle to the attacker but yields no HVT that the verifier will accept, while spoofing the verifier's DNS resolution lets the attacker choose the DID, and therefore the authorized issuer. Verifiers SHOULD use a DNSSEC-validating resolver, and operators of handle domains SHOULD sign their zones. The other resolution steps rely on HTTPS and the Web PKI.¶
Every domain handle rests on control of a DNS name: whoever controls the handle's domain controls its _handle and _atproto records and its well-known URLs, and therefore which DID the handle resolves to. For a did:web DID on the handle's domain, the same party also controls the DID document, and therefore the authorized issuer. This is by design: it is what lets users bring their own domain. It also means that when a domain changes hands, for example when it expires and is registered by someone else, its handle changes hands too. The back-link check means the new owner cannot claim the previous owner's DID, but can point the handle at a DID of their own.¶
A domain handle can be mapped to a DID by up to four records (Section 6.4): two with the new names, and two with the AT Protocol names, which are used only as a fallback (Section 6.4.1). Resolution is designed so that the browser and the verifier cannot reach different results for the same handle, and so that an attacker who can block or delay some lookups cannot choose between them:¶
An attacker who can block lookups can still cause resolution to fail, but cannot cause it to succeed with a different result. Only the owner of the handle's domain can create records that disagree.¶
For did:plc DIDs, resolution trusts the PLC directory to return the current DID document, and therefore the current authorized issuer.¶
Operations in the PLC directory are signed by the account's rotation keys, and the DID is derived from the hash of the signed genesis operation, so the operation log is self-certifying: a resolving party can fetch the log and verify it instead of trusting the directory's rendering of the DID document. Doing so prevents the directory from forging operations, and therefore from naming an issuer that the account's rotation keys did not authorize.¶
Verifying the log does not remove all trust in the directory. As the "PLC Server Trust Model" section of [DID-PLC] states, some trust in the directory is required, and its attacks are limited to:¶
HandleVerification service, so that a resolving party keeps accepting HVTs from an issuer that the user no longer authorizes, or withholding an operation that adds one, so that a newly authorized issuer is rejected.¶
HandleVerification service is in effect.¶
Neither the browser nor the verifier can detect these attacks. Operations are submitted only to the PLC directory, and are not published anywhere else that a resolving party could observe independently: mirrors and replicas obtain their operations from the directory, and the timestamps that the directory assigns to operations, on which the resolution of forks depends, are not signed. Both the browser and the verifier therefore place this trust in the PLC directory, and HVP inherits it for every handle that resolves to a did:plc DID. Handles that resolve to a did:web DID instead rely on the security of the DID's domain, as described in Section 12.5.1.¶
This document registers the following well-known URIs in the "Well-Known URIs" registry ([RFC8615]):¶
URI suffix: handle¶
URI suffix: handle-verification¶
URI suffix: atproto-did¶
This document registers the following entries in the "Underscored and Globally Scoped DNS Node Names" registry ([RFC8552]):¶
RR Type: TXT¶
_NODE NAME: _handle¶
RR Type: TXT¶
_NODE NAME: _atproto¶
Open Issue: The
atproto-didwell-known URI and the_atprotoDNS node name are defined by the AT Protocol. Whether this document or a document of the IETF ATP working group should register them needs coordination with that working group.¶
This document registers the following claim in the "JSON Web Token Claims" registry ([RFC7519]):¶
This document registers the application/hvt+jwt media type in the "Media Types" registry ([RFC6838]), for the typ header of the HVT.¶
Open Issue: Complete the media type registration template.¶
Users are identified in different ways in different ecosystems: by a domain name in the AT Protocol, by a URL in Solid and ActivityPub, and by a user name and a domain in ActivityPub implementations. Each of these already has a resolution mechanism that is deployed, and that the others do not use. Rather than choosing one, HVP lets each be defined as a handle type, with its own syntax and resolution, and shares everything that does not depend on how a handle is resolved: the issuers, the tokens, and their verification. Requiring the syntaxes to be mutually exclusive means that a browser, an issuer, and a verifier always agree on how a handle is resolved, from the handle alone.¶
HVP needs three things from the target of a domain handle: a stable identifier for the account, a document that claims the handle back, and a place to name the issuer. A DID provides all three, through its DID document, in a form that is not specific to any ecosystem, and the AT Protocol already resolves domain handles to DIDs.¶
The record names the kind of value in its key, did= as in the AT Protocol, so that resolution does not have to infer it, and holds a single value rather than a list. Values of other kinds, such as username handles ([USERNAME-HANDLES]), domain handles, or email addresses, can be added later with keys of their own, which resolving parties that do not support them ignore.¶
Only did:plc and did:web are supported, because they are the methods that the AT Protocol uses. Other methods can be added if an ecosystem needs them. A did:key DID is not sufficient on its own: its DID document is derived from the key, and so has no alsoKnownAs to claim the handle back and no services to name the issuer.¶
The AT Protocol's _atproto record and /.well-known/atproto-did URL are specific to one ecosystem, and an ActivityPub account that published them would appear to be an AT Protocol account. Neutral names let any ecosystem use the same records. The names do not mention DIDs, so that they can refer to other kinds of identifiers later, and the well-known document is JSON, so that it can carry more information later. Resolving parties fall back to the AT Protocol names when the new ones are absent (Section 6.4.1), so that every existing AT Protocol handle keeps working, and the new names take precedence when present, so that a handle can move to the new names at its own pace.¶
AT Protocol OAuth ([ATPROTO-OAUTH]) and the OAuth APIs of ActivityPub servers let a verifier sign a user in, and grant it access to the user's account. But they ask the verifier to present a text field for the user to type a handle, and they reveal the verifier to the user's authorization server. HVP lets the browser offer the user's handles without the user typing them, and keeps the verifier hidden from the issuer. A verifier that needs access to the user's account, and not only proof that the user controls a handle, still uses OAuth.¶
HVP also changes how interoperability is achieved. In OAuth, the authorization server's response goes to the client, and nothing in between checks it: whether an authorization server interoperates with a given client is discovered by testing that pair, and ecosystems with many of each rely on conformance test suites and certification programs. In HVP, the browser verifies every HVT (Section 8.3) with the same checks that a verifier performs, before any verifier receives it, and does not deliver an HVT that fails. An issuer that develops against a browser therefore has an independent verifier in its development loop, and an HVT that the browser accepts is accepted by any verifier that implements Section 10, unless the handle's resolution changes in between. Each issuer and each verifier needs to interoperate only with the browser's verification, rather than each issuer with each verifier.¶
EVP already solves the same problem for email addresses, with a three-party model, proof of possession of a browser key, and a token format compatible with SD-JWT. Reusing it lets verifiers and issuers share code and keys between the two, and keeps the privacy properties. An alternative would be to profile OpenID Connect, but OpenID Connect's redirect-based flow reveals the verifier to the provider.¶
The verifier cannot trust the browser's resolution, which could be manipulated, and it cannot trust the issuer to say which handles it is authorized for. Resolving the handle itself is how the verifier checks that the issuer is authorized, in the same way that the verifier looks up the DNS delegation in [EVP].¶
An account already says who hosts it: an AT Protocol account names its PDS, whose authorization server authenticates it, and an ActivityPub account is hosted by the server of its actor. HVP could infer its issuer from them. It does not, for two reasons. First, there is little to gain: an issuer has to deploy HVP's issuance endpoint and metadata anyway, and a server can add a HandleVerification service to the DID documents of its accounts at the same time. Second, an HVT is a new kind of statement, presented to verifiers through the browser, and a user who authorized a server to host their account, or to sign them in with OAuth, has not necessarily authorized it to make that statement. An explicit service makes the user's opt-in visible in their DID document, or other controlled identifier document.¶
An explicit service also lets a user choose an issuer that is not the server that hosts their account, and gives both ecosystems the same way to name it.¶
Handle resolution is useful outside any one ecosystem. Defining it here, in a form that is shared by the AT Protocol and ActivityPub, makes HVP implementable from this document alone, by verifiers that implement neither protocol.¶
Email addresses already have a protocol of their own, [EVP], which is further along, has implementations, and has requirements specific to email, such as private email addresses. A browser API can use EVP for email addresses and HVP for handles side by side; an email address is never parsed as a domain handle (Section 4.2).¶
Note: This section is to be removed before publishing as an RFC.¶
There are no implementations of this document yet. Prototypes of the related IdP Registration API ([IDP-REGISTRATION]) exist in Chrome behind a flag.¶
Note: This section is to be removed before publishing as an RFC.¶
draft-goto-handle-verification-00¶
This appendix shows the resolution of handles. The examples are real handles at the time of writing, which publish only the AT Protocol names, and so resolve through the fallback of Section 6.4.1. Neither publishes a HandleVerification service yet, so each shows what would be added to opt in. The values will change over time.¶
Handle: @sgo.to
DNS TXT _atproto.sgo.to -> "did=did:plc:thaoj4hhvraos7y6v3yog7re"
DID document (plc.directory):
alsoKnownAs: ["at://sgo.to"] (back-link ok)
DID: did:plc:thaoj4hhvraos7y6v3yog7re
To opt in, add to the DID document's services:
{ "id": "#handle_verification", "type": "HandleVerification",
"serviceEndpoint": "https://pds.sgo.to" }
Issuer: https://pds.sgo.to
¶
Handle: @samuelgoto.bsky.social
GET https://samuelgoto.bsky.social/.well-known/atproto-did
-> did:plc:bftbdgcbe4zclvpkboe4hleq
DID document (plc.directory):
alsoKnownAs: ["at://samuelgoto.bsky.social"] (back-link ok)
DID: did:plc:bftbdgcbe4zclvpkboe4hleq
To opt in, add to the DID document's services:
{ "id": "#handle_verification", "type": "HandleVerification",
"serviceEndpoint": "https://bsky.social" }
Issuer: https://bsky.social
¶
This section is non-normative.¶
The W3C IdP Registration API ([IDP-REGISTRATION]) extends FedCM ([FEDCM]) so that a verifier can request any issuer in an open federation, and the browser keeps a registry of the user's handles. It relates to HVP the way the Email Verification API ([EMAIL-VERIFICATION-API]) relates to EVP: the browser API decides which handles to offer, lets the user register and select them, and delivers the HVT+KB to the verifier, while HVP defines what a handle is, how it is resolved, and how the HVT is issued and the HVT+KB is verified. In particular:¶
accounts_endpoint and login_url that the browser needs to show the user's accounts and let the user sign in from FedCM's well-known file, /.well-known/web-identity, on the issuer's site.¶
https://atproto.com. The browser offers the handles whose issuers list that federation in the federations member of their issuer metadata.¶
This document builds on the Email Verification Protocol by Dick Hardt and Sam Goto, on the AT Protocol handle, DID, and AT URI specifications by Bluesky, and on the Handles Task Force proposal by Anuj Ahooja and Emelia Smith.¶