atproto_identity 0.6.0
atproto_identity: ^0.6.0 copied to clipboard
AT Protocol identity resolution (handle/DID) and inbound service-auth JWT verification for Dart/Flutter.
Release Note #
0.6.0 #
- feat!:
resolvenow verifies the handle when resolution starts from a DID. It previously sethandleto null and skipped bidirectional verification entirely, so a DID document'salsoKnownAsclaim was read by nobody: a caller who passed a DID got no handle even when the document claimed one, and no way to learn whether that claim was real. The claimed handle is now resolved back and must return the DID that was asked about. - feat!:
ResolvedIdentity.handleis no longer nullable. It carries the newhandleInvalidconstant (handle.invalid) when no handle verifies, which is the shape the protocol already defines —com.atproto.identity.defs#identityInforequires the field and documents the same sentinel, and the generatedIdentityInfomirrors it. Only this hand-written type disagreed. - feat: verification never throws on the DID path. A handle that stopped resolving is an operational state — most often a verified domain whose DNS record was removed or misconfigured — and such an account is still valid, so
handle.invalidis reported instead. Transport failures are caught alongsideIdentityException, since the handle lookup wraps only timeouts;Errors are not caught, so a bug still surfaces. Resolution from a handle is unchanged and still throws when the document does not claim it back. - feat: a DID document claiming
handle.invalidverbatim is treated as claiming nothing. The string is a syntactically valid handle, so honouring such a claim would make a verified handle indistinguishable from one that failed to verify. - chore: a document that claims no handle costs no extra request, so this adds a round trip only when there is a claim to check.
Breaking: replace identity.handle == null with identity.handle == handleInvalid, and pass handle: when constructing a ResolvedIdentity yourself — it is now a required argument, because an identity always reports either a verified handle or the sentinel.
0.5.0 #
- feat: added
HttpIdentityResolver.resolveDidDocument, which fetches a DID document and returns it verbatim as aMap<String, dynamic>.resolveinterprets a document as an atproto identity and therefore rejects anything without an#atproto_pdsservice, so a document that is not an account's — a feed generator'sdid:webdocument, which declares only a#bsky_fgservice — was unreachable through this package even though the resolver already fetched, size-capped, redirect-checked, andid-bound it. The new method reuses that same fetch; only the atproto-specific interpretation is skipped. - feat: added
serviceEndpointOf, which reads a service entry out of a raw DID document (matching both the#bsky_fgand<did>#bsky_fgspellings, optionally ontype) and returns itsserviceEndpointheld to the same bar the resolver applies to a PDS endpoint: https-only, no credentials/query/fragment, and nolocalhostor reserved IP literal. Without it, reading a raw document would hand every caller an unvalidated attacker-controlled URL — the gapensureNonReservedHostwas added in v0.3.0 to close. Unlike the PDS endpoint the result keeps its path, since DID Core allows one. - security: the strict DID grammar that
verifyServiceAuthapplies to a JWT'sissnow also guards every DID document fetch, including one whose DID came back from handle resolution — which validated only thedid:prefix before the DID was interpolated into the PLC directory URL. A hostile or compromised handle resolver could returndid:plc:x/../../adminand change the URL the request reached.resolveis correspondingly stricter: a DID carrying a path, query, fragment, or whitespace now fails before any request is issued.
0.4.0 #
- fix:
resolvenow performs bidirectional handle verification the way the atproto DID spec defines it.alsoKnownAsis an ordered list in which only the first syntactically valid handle is the claimed handle, and every later handle URI is ignored; the check instead asked whether the supplied handle appeared anywhere in the list. A DID document listing["at://alice.example", "at://bob.example"]therefore verifiedbob.exampleas well asalice.example, so a single account could present several handles as bidirectionally verified when the protocol says exactly one of them is canonical — and a caller using this to answer "is this handle really this account's" got a wrong yes. Entries that are notat://URIs, andat://entries that are not syntactically valid handles, are now skipped rather than ending the scan, since neither can be the claimed handle. - chore: depends on
at_primitivesfor the handle grammar, rather than restating it, so the syntax rule this fix turns on cannot drift from the one the rest of the workspace enforces.
0.3.0 #
- feat: added
ensureNonReservedHost, which applies the same SSRF host policyHttpIdentityResolveruses for a PDS endpoint — rejectlocalhostand IP literals in loopback, private, link-local, CGNAT, unique-local, multicast, unspecified, or reserved ranges (unlessallowPrivateNetwork), with the same IP-literal-only limitation. Exposed so a caller that derives a further network target from resolver output can hold it to the same bar;atproto_oauthuses it to vet the authorization server taken from PDS metadata.
0.2.0 #
- security: bounded the accepted
publicKeyMultibaselength, closing an unauthenticated quadratic-CPU denial of service.verifyServiceAuthdecodes the issuer's signing key before it verifies the signature, and base58btc decoding is O(n^2), so a DID document declaring a ~512,000-character key (small enough to fit under the resolver's 512 KiB response cap) froze the isolate for minutes on a single unauthenticated request.signingKeyOfnow throws anIdentityExceptionabove the newmaxPublicKeyMultibaseLength(256 characters; a real secp256k1 / P-256 Multikey is 49), andverifyServiceAuthre-checks the bound on whatever theIdentityResolverreturns, since that interface is caller-implementable.
0.1.1 #
- docs: documented the
HttpIdentityResolverSSRF/DoS hardening parameters in the README (allowedHosts,allowPrivateNetwork,timeout,maxResponseBytes), explaining whydid:webresolution needs them. - docs: documented the exported
signingKeyOf(didDocument, did)helper and its exact-id matching. - docs: showed
verifyServiceAuth'smaxTokenLifetimein the code sample. - chore: bump
did_plcto^1.1.2.
0.1.0 #
- Initial release.
- feat:
IdentityResolver/HttpIdentityResolverresolve a handle (alice.example, optionally@/at://prefixed) or a DID (did:plc/did:web) to aResolvedIdentity(DID, PDS origin, handle,#atprotosigning key). Handle resolution goes throughcom.atproto.identity.resolveHandleand is verified bidirectionally against the DID document'salsoKnownAs. - feat:
verifyServiceAuthverifies an inbound AppView service-auth JWT from anAuthorization: Bearerheader and returns the issuer (viewer) DID. It validatesaud/exp/lxm/iss, resolves the issuer's#atprotosigning key, and checks the ES256K / P-256 signature viadid_plc. Fails closed: the JWTalgis not trusted, the signature must be a 64-byte compact ECDSA signature, and an out-of-rangeexpis rejected without overflow. - feat:
signingKeyOf(didDocument, did)returns thepublicKeyMultibaseof the#atprotoverification method, ornullwhen absent. - feat:
IdentityExceptionis thrown on any resolution or verification failure. - security:
HttpIdentityResolverhardensdid:webresolution against SSRF and DoS. Private, loopback, link-local, unique-local, multicast, unspecified, and otherwise reserved IP literals (andlocalhost) are rejected by default before any request is issued (allowPrivateNetwork: trueopts out; only IP literals are checked — no DNS resolution is performed, so pair with operator-level egress controls). An optionalallowedHostsallowlist restricts whichdid:webhosts may be contacted at all. Every fetch applies atimeout(default 10s) and amaxResponseBytesbody cap (default 512 KiB), anddid:webredirects are followed manually (max 5), each target re-validated against the host policy and required to behttps. - security:
verifyServiceAuthrejects bearer tokens larger than 8 KiB before any decoding and validates theissclaim against a strict DID grammar (no fragments, queries, paths, or whitespace) before handing it to the resolver. - security:
verifyServiceAuthnow validates the JWT JOSE header before any resolution work: thetyp, when present, must be a JWT media type and thealgmust be one ofES256K/ES256, failing closed onnone, HMAC (HS*), and RSA algorithms (defense in depth — the signing curve is still pinned from the DID document). - security:
verifyServiceAuthbounds the accepted token lifetime. A newmaxTokenLifetimeparameter (default 60 minutes; passnullto opt out) rejects any token whoseexpis implausibly far in the future, and theiat(not in the future) andnbf(not-before) claims are now enforced when present. A 30-second clock-skew allowance is applied consistently toexp/iat/nbf. - security:
signingKeyOfnow matches the#atprotoverification method by exact id (#atprotoor<did>#atproto) instead of aendsWith('#atproto')suffix match, preventing a crafted DID document from smuggling in a key under an id such asdid:plc:x#foo#atproto. - security:
HttpIdentityResolverbinds a fetcheddid:webdocument to the requested DID by asserting itsidequals the DID, rejecting a document that claims to describe a different DID (did:plc remains content-addressed via the trusted PLC directory).