~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
204 lines · 15.0 KB Raw History

identity: signatures, key claims, associations, sign-in

  • who a repo DID is, which key signs for it, and which operator it belongs to
  • lexicons: lexicons/network/attested/, lexicons/com/publicdomainrelay/temp/badgeBlueKeys.json; association rows in lexicons-market.md
  • rule (plan 0036): the authority is always the repo in the record's at-uri, never a record field or an issuer claim
  • index: README.md. sequence steps: S0, S1, S5, S9

Lexicons And NSIDs In This File

NSIDtyperkeyused
network.attested.signatureobject (and #signatures array)-inline on every market and compute event record
network.attested.proofrecord, key tid-defined, not written
network.attested.verifyquery-defined, not served or called
com.publicdomainrelay.temp.badgeBlueKeysrecord, key anythree schemes, see belowsigner key binding, operator claim, association mirror, ssh key claim
com.publicdomainrelay.temp.market.associationrecordlexicons-market.mdflow below
com.publicdomainrelay.temp.market.associationRequestrecordlexicons-market.mdflow below
com.publicdomainrelay.temp.requester.associateConfirmXRPC (no lexicon file)-OAuth scope only, see below
com.publicdomainrelay.temp.market.bidderAssociationcollection (no lexicon file)-OAuth scope only, see below

network.attested

Signing

flowchart LR
  REC["record (rfp, bid, accept, receipt, event, compute.events.*)"] --> CID["attestation CID: DAG-CBOR CIDv1 sha2-256 over the record without signatures, plus $sig metadata with repository DID"]
  CID --> SIG["secp256k1 low-S ECDSA over the CID bytes"]
  SIG --> ENT["network.attested.signature {key: did:key, cid, signature, issuer}"]
  ENT --> ARR["appended to the record's signatures array"]
stepcode
attestation CIDinternal/atproto/market/attest.go CreateAttestationCID, DagCborCID
sign inlineattest.go SignInline, SignRecord, CreateSignedRecord, CreateSignedRecordAt
market recordsinternal/atproto/market/records.go createSigned, PutEvent
bidderinternal/market/bidder/bids.go, internal/market/bidder/adapters.go
requesterinternal/market/requester/contractevents.go, internal/market/requester/runner.go (vm.delete)
receiptattest.go CreateReceiptRecordAt: cid = the attestation CID of the accept, plus an inline signature
  • records with a signatures field: market.rfp, market.bid, bids.free, bids.x402, market.accept, accepts.free, accepts.x402, market.receipt, receipts.free, receipts.x402, market.event, every compute.events.vm.* and compute.events.guest.*
  • signing keys: secp256k1 JWK files, mode 0600 (internal/atproto/market/keys.go LoadOrCreateSignerFile)
    • bidder: <stateDir>/attestation.jwk (internal/services/bidder/wiring.go)
    • requester: attestation-key.json, else repo-key.json (internal/market/requester/requester.go); compose seeds both with the same key (internal/app/compose/pds.go)
  • the public did:key (Signer.PublicDID) is the keyId of the signer key binding below

Verifying

functionchecks
VerifyInlineAttestation (attest.go)recompute the CID, ECDSA against key
VerifyRecordSignatures (attest.go)the key is in the issuer's DID document, or bound by a badgeBlueKeys signer key binding
VerifyRemoteProof (attest.go)used only by VerifyReceipt (internal/atproto/market/contract.go)
  • callers: internal/atproto/market/contract.go, internal/market/participant/comms.go, internal/market/requester/inbox.go, internal/market/requester/events.go, internal/market/requester/runner.go
  • network.attested.proof (CreateRemoteProofRecord) has no caller outside tests. network.attested.verify is neither served nor called; only network.attested.signature is in the lexgen include list (internal/atproto/market/nsid.go)

badgeBlueKeys

fieldvalue
NSIDcom.publicdomainrelay.temp.badgeBlueKeys
typerecord, key any
key fieldskeyId (the bound key or subject DID), challenge (the repo DID it binds to, or the operator), service (the role), nonce, proof (strongRef, on a mirror), name, createdAt
Gointernal/atproto/market/badgeblue.go, internal/policyengine/trust/trust.go, internal/app/compose/operator.go, internal/app/compose/records.go

One collection, four uses. Each has its own rkey and service.

usein repo ofrkeykeyIdchallengeservicewritten byread bystep
signer key bindingthe signer (bidder or requester)BadgeBlueKeysRkey(did, keyId): base58 of sha256(did:keyId)[:24], max 32 charssigner did:keythe signer's repo DIDbidder_attestation (bidder), market_requester (requester)bidder internal/market/bidder/offering.go; requester internal/market/requester/identity.goKeyBoundByBadgeBlueKey from participant/comms.go, requester/events.goS0, before the first signed record
operator claimthe operatorClaimRKey: <service>-<subject DID with : as ->the subject DIDthe operator DIDbidder_associate, requester_associateinternal/app/compose/operator.go, scripts/associate-bidder.tstrust.claimHeld, requester discovery.go operatedByS1
association mirrorthe subjectassociation (trust.MirrorRKey)the subject DID (status.did)the operator (status.associations[0])same as the claimcompose ApplyAssociationRecord (an AtprotoRecord fed from MarketIdentity status)trust.mirrorOf, bidder association.go, requester discovery.goS1
ssh key claimthe userstable keyRkeythe ssh keythe user DIDsshssh.socialweb.computer /keysssh authorizer, one getRecord per offered key (internal/sshtransport)ssh sign-in
  • proof on the mirror: the strongRef of the authority record that associates the subject (status.associationRef)
  • an association counts only when both repos agree: the operator's claim (or association record) AND the subject's mirror. a mirror alone never associates
  • a deleted or edited authority record revokes on the next trust refresh (trust cache TTL 5 min, defaultTrustCacheTTL)
  • PDS outage: fail closed, logged
  • OAuth scope: create, update, delete on requester and bidder; create only on desktop and compute SPA (outside.md)

Associations (plan 0036)

  • plan: docs/plans/0036-record-association.yaml; runbook: docs/acceptance/0005-guest-requester-association.md
  • a subject is a requester or bidder repo DID; the authority (operator) declares it as its own
  • verifiers: MarketBidder checks requesters, MarketRequester checks bidders. both in internal/policyengine/trust
kindrecordmatcher (internal/policyengine/trust/association.go)default
claimbadgeBlueKeys operator claimkeyId == subject, challenge == authority, service == roleon
associationmarket.association in the authority reposubject, service, contract, expiryon
bskyPostapp.bsky.feed.posttext == template (default associate {did}) and one app.bsky.richtext.facet#mention with did == subjectopt-in
delvePosttown.delve.feed.postsame as bskyPostopt-in

How an operator is resolved

flowchart TD
  S["counterparty DID + role"] --> M{"mirror badgeBlueKeys/association in subject repo"}
  M -->|none| NO["no operator"]
  M -->|has proof| P["getRecord proof in authority repo: cid equal, kind matcher, expiry, contract held"]
  M -->|no proof| A["standing market.association at ClaimRKey in operator repo"]
  A -->|missing| C["operator claim badgeBlueKeys at ClaimRKey"]
  P --> OK["operator = repo of the proof at-uri"]
  A --> OK
  C --> OK
  • trust.Resolver.ResolveOperatorDidFor / operatorOf (internal/policyengine/trust/trust.go); results cached 5 min, errors 5 s
  • scope lane and full lane of the policy both use it: policy.md

Standing association

  • no contract, no expiresAt: valid until deleted or edited (cid pinned)
  • rkey <service>-<subject with : as -> (same as the claim)
  • a standing badgeBlueKeys claim is an independent standing grant: it associates the subject even when a contract-scoped association expired. an operator that wants scope-only association must not also hold a standing claim

Contract-scoped association

The guest requester (R2) case: a requester rents a VM that runs another requester, and vouches for it only for the life of the contract.

recordin repo ofrkey (internal/atproto/market/guest.go ContractScopedRKey)written by
market.associationRequestguest requester (subject)<service>-<sha256(authority)[:32 hex]>-<accept rkey>compose ApplyAssociationRequestRecord (internal/app/compose/records.go), from spec.identity.associationRequest {authority, contract, nonce}
market.associationauthority requester<service>-<sha256(subject)[:32 hex]>-<accept rkey>internal/market/requester/association.go ensureAssociation
badgeBlueKeys/association (mirror, proof = the association)guest requesterassociationcompose ApplyAssociationRecord
  • market.association fields: subject, service (requester_associate), contract (strongRef of the authority's market.accept), expiresAt (always, the contract deadline), createdAt
  • valid only while: not past expiresAt, the contract ref resolves, the contract is still Held (canonical receipt, no vm.delete or vm.terminated) (trust/association.go scopeHeld, contractHeld)
  • the bidder caps the guest lifetime at the association expiresAt (internal/market/bidder/association.go associationExpiry)
sequenceDiagram
  autonumber
  participant R1 as authority requester
  participant P1 as authority repo
  participant G as guest VM
  participant R2 as guest requester (did:plc)
  participant P2 as guest repo

  R1->>P1: S5 market.accept
  Note over R1,G: Infra contract association.fromGuest (or subject)
  R2->>P2: market.associationRequest {authority, contract = accept, nonce}
  G->>R1: guest.requesterDid event (R2 DID)
  R1->>P2: getRecord associationRequest
  R1->>R1: authority == self, service, contract == accept, nonce (constant time)
  R1->>P1: putRecord market.association {subject, contract, expiresAt}
  R1->>G: secrets bundle: refFile holds the association uri and cid
  R2->>P2: badgeBlueKeys/association mirror, proof = association
  Note over R2: run request --association-ref (pinned getRecord bootstrap)
  R1->>P1: S9 release: deleteRecord market.association
  • spec.association takes subject or fromGuest, not both; fromGuest needs a secretsRef (internal/market/requester/secrets.go)
  • guest DID must be a did:plc and not the authority's own DID (internal/atproto/market/guest.go)
  • refFile default /etc/socialweb-computer/association-ref (internal/infra/declaration.go, internal/atproto/cloudinit/agent.go)
  • the guest requester bootstraps from the pinned ref (MarketIdentity spec.associationRef {uri, cid}, run request --association-ref), not from the record watch
  • release deletes the association: the only record deletion of the flow, a contract-scoped record the app wrote itself
  • guest events of this flow (requesterDid, r2Start, r2Release, r2Released): guest-events.md
  • atirc side: the mint carries the association strongRef in its lease; the token exp is clamped to expiresAt: outside.md

requester.associateConfirm

  • NSID com.publicdomainrelay.temp.requester.associateConfirm, constant AssociateConfirmNSID (internal/app/compose/identity.go)
  • only an OAuth rpc scope: RPCAssociateConfirm, in DIDKeyAssociatorOAuthScope (internal/atproto/oauth/scope.go)
  • meant as the fast path for an allow-listed operator (docs/ARCHITECTURE.md); no Go server or caller

market.bidderAssociation

  • NSID com.publicdomainrelay.temp.market.bidderAssociation, constant CollectionBidderAssociation (internal/atproto/oauth/scope.go)
  • create scope on the bidder and desktop sets only. no lexicon file, no writer, no reader in Go

Sign-In

  • a MarketIdentity is a repo DID plus how the app reaches it (api/v1alpha1/types_marketidentity.go, internal/marketidentity/atproto, internal/controllers/marketidentity)
modereposign-inatproto calls
localRepothe app's own Pds componentnonein-process PDS (internal/pds/local)
pdsAccounta PDS account, password in the vaultapp passwordcom.atproto.server.createSession, refreshSession
oauthQruser's PDSQR session transfer at qr.fedfork.com (5 min)OAuth (DPoP), scope DIDKeyAssociatorOAuthScope
oauthBrowseruser's PDSauthorize URL in status.pendingActionOAuth
oauthDelegateduser's PDSsession handed in by another appOAuth
oauthSessionProxyuser's PDSa session another component stored in OpenBao (auth/atproto session/<did>)OAuth
  • OAuth sessions live only in the app's own OpenBao; the atproto auth plugin refreshes them
  • granted scopes are read from the token response, never the access token's scope claim
  • scopes per role and optional scopes: outside.md
  • scripted sign-in: scripts/oauth-signin.ts, ../oauth.md
  • the QR transfer is signed with the CLI's did:key (internal/marketidentity/atproto/oauth.go, internal/atproto/oauthclient/qr.go)

did:key Ingress

  • apps without a public host (CLI requester, page, iOS, guest requester) still receive submitBid, submitAccept, submitEvent callbacks
  • IngressProxy (internal/services/ingressproxy) registers with xrpc.fedproxy.com (the Go ingress relay, internal/atproto/ingress) as a did:key
  • registration: {key, nonce, signatures} (internal/atproto/ingress/protocol.go); a service-auth did:key issuer is verified without a DID document (internal/atproto/ingress/auth.go); encoding in internal/didkey
  • the callback is named in submitBid / submitAccept / submitEvent (a did#serviceId or an HTTP URL); for a stream listener with no reachable HTTP service the ingress strongRefs point at com.publicdomainrelay.temp.ingress.webRTC listen records: lexicons-market.md
  • a guest requester gets public ingress by default through the same relay (plan 0036)

See Also