diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index f534e015ea5..1aa27267202 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - appview-v1-courier + - appview-v1-ozone-auth env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 5a2bf753072..9802c483d5a 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -2,9 +2,10 @@ import { AuthRequiredError, verifyJwt as verifyServiceJwt, } from '@atproto/xrpc-server' -import { IdResolver } from '@atproto/identity' +import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity' import * as ui8 from 'uint8arrays' import express from 'express' +import { getVerificationMaterial } from '@atproto/common' type ReqCtx = { req: express.Request @@ -151,7 +152,7 @@ export class AuthVerifier { adminService = async (reqCtx: ReqCtx): Promise => { const { iss, aud } = await this.verifyServiceJwt(reqCtx, { aud: this.ownDid, - iss: [this.adminDid], + iss: [this.adminDid, `${this.adminDid}#atproto_labeler`], }) return { credentials: { type: 'admin_service', aud, iss } } } @@ -190,13 +191,28 @@ export class AuthVerifier { opts: { aud: string | null; iss: string[] | null }, ) { const getSigningKey = async ( - did: string, + iss: string, forceRefresh: boolean, ): Promise => { - if (opts.iss !== null && !opts.iss.includes(did)) { + if (opts.iss !== null && !opts.iss.includes(iss)) { throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') } - return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) + const [did, serviceId] = iss.split('#') + const keyId = + serviceId === 'atproto_labeler' ? 'atproto_label' : 'atproto' + const didDoc = await this.idResolver.did.resolve(did, forceRefresh) + if (!didDoc) { + throw new AuthRequiredError('could not resolve iss did') + } + const parsedKey = getVerificationMaterial(didDoc, keyId) + if (!parsedKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + const didKey = getDidKeyFromMultibase(parsedKey) + if (!didKey) { + throw new AuthRequiredError('missing or bad key in did doc') + } + return didKey } const jwtStr = bearerTokenFromReq(reqCtx.req) diff --git a/packages/bsky/tests/admin/admin-auth.test.ts b/packages/bsky/tests/admin/admin-auth.test.ts index ff00d0906b0..5cd5a59b847 100644 --- a/packages/bsky/tests/admin/admin-auth.test.ts +++ b/packages/bsky/tests/admin/admin-auth.test.ts @@ -27,15 +27,31 @@ describe('admin auth', () => { bskyDid = network.bsky.ctx.cfg.serverDid modServiceKey = await Secp256k1Keypair.create() - const origResolve = network.bsky.ctx.idResolver.did.resolveAtprotoKey - network.bsky.ctx.idResolver.did.resolveAtprotoKey = async ( + + const origResolve = network.bsky.ctx.idResolver.did.resolve + network.bsky.ctx.idResolver.did.resolve = async function ( did: string, forceRefresh?: boolean, - ) => { + ) { if (did === modServiceDid || did === altModDid) { - return modServiceKey.did() + return { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/multikey/v1', + 'https://w3id.org/security/suites/secp256k1-2019/v1', + ], + id: did, + verificationMethod: [ + { + id: `${did}#atproto`, + type: 'Multikey', + controller: did, + publicKeyMultibase: modServiceKey.did().replace('did:key:', ''), + }, + ], + } } - return origResolve(did, forceRefresh) + return origResolve.call(this, did, forceRefresh) } agent = network.bsky.getClient() diff --git a/packages/common-web/src/did-doc.ts b/packages/common-web/src/did-doc.ts index 541e10d0937..f1bdd8b5330 100644 --- a/packages/common-web/src/did-doc.ts +++ b/packages/common-web/src/did-doc.ts @@ -44,6 +44,28 @@ export const getSigningKey = ( publicKeyMultibase: found.publicKeyMultibase, } } + +export const getVerificationMaterial = ( + doc: DidDocument, + keyId: string, +): { type: string; publicKeyMultibase: string } | undefined => { + const did = getDid(doc) + let keys = doc.verificationMethod + if (!keys) return undefined + if (typeof keys !== 'object') return undefined + if (!Array.isArray(keys)) { + keys = [keys] + } + const found = keys.find( + (key) => key.id === `#${keyId}` || key.id === `${did}#${keyId}`, + ) + if (!found?.publicKeyMultibase) return undefined + return { + type: found.type, + publicKeyMultibase: found.publicKeyMultibase, + } +} + export const getSigningDidKey = (doc: DidDocument): string | undefined => { const parsed = getSigningKey(doc) if (!parsed) return diff --git a/packages/identity/src/did/atproto-data.ts b/packages/identity/src/did/atproto-data.ts index c03f76ef598..cc1483b1d57 100644 --- a/packages/identity/src/did/atproto-data.ts +++ b/packages/identity/src/did/atproto-data.ts @@ -34,6 +34,23 @@ export const getKey = (doc: DidDocument): string | undefined => { return didKey } +export const getDidKeyFromMultibase = (key: { + type: string + publicKeyMultibase: string +}): string | undefined => { + const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase) + let didKey: string | undefined = undefined + if (key.type === 'EcdsaSecp256r1VerificationKey2019') { + didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes) + } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') { + didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes) + } else if (key.type === 'Multikey') { + const parsed = crypto.parseMultikey(key.publicKeyMultibase) + didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes) + } + return didKey +} + export const parseToAtprotoDocument = ( doc: DidDocument, ): Partial => {