Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
dholms committed Nov 1, 2023
2 parents 693d66f + 2166add commit 09f8837
Show file tree
Hide file tree
Showing 16 changed files with 298 additions and 51 deletions.
12 changes: 8 additions & 4 deletions interop-test-files/crypto/signature-fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo",
"publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh",
"signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg",
"validSignature": true
"validSignature": true,
"tags": []
},
{
"comment": "valid K-256 key and signature, with low-S signature",
Expand All @@ -17,7 +18,8 @@
"publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc",
"publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t",
"signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA",
"validSignature": true
"validSignature": true,
"tags": []
},
{
"comment": "P-256 key and signature, with non-low-S signature which is invalid in atproto",
Expand All @@ -27,7 +29,8 @@
"publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo",
"publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh",
"signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw",
"validSignature": false
"validSignature": false,
"tags": ["high-s"]
},
{
"comment": "K-256 key and signature, with non-low-S signature which is invalid in atproto",
Expand All @@ -37,6 +40,7 @@
"publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc",
"publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t",
"signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ",
"validSignature": false
"validSignature": false,
"tags": ["high-s"]
}
]
15 changes: 11 additions & 4 deletions packages/bsky/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@ export const authVerifier =
if (!jwtStr) {
throw new AuthRequiredError('missing jwt', 'MissingJwt')
}
const payload = await verifyJwt(jwtStr, opts.aud, async (did: string) => {
const atprotoData = await idResolver.did.resolveAtprotoData(did)
return atprotoData.signingKey
})
const payload = await verifyJwt(
jwtStr,
opts.aud,
async (did, forceRefresh) => {
const atprotoData = await idResolver.did.resolveAtprotoData(
did,
forceRefresh,
)
return atprotoData.signingKey
},
)
return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } }
}

Expand Down
64 changes: 64 additions & 0 deletions packages/bsky/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import AtpAgent from '@atproto/api'
import { SeedClient, TestNetwork } from '@atproto/dev-env'
import usersSeed from './seeds/users'
import { createServiceJwt } from '@atproto/xrpc-server'
import { Keypair, Secp256k1Keypair } from '@atproto/crypto'

describe('auth', () => {
let network: TestNetwork
let agent: AtpAgent
let sc: SeedClient

beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_auth',
})
agent = network.bsky.getClient()
sc = network.getSeedClient()
await usersSeed(sc)
await network.processAll()
})

afterAll(async () => {
await network.close()
})

it('handles signing key change for service auth.', async () => {
const issuer = sc.dids.alice
const attemptWithKey = async (keypair: Keypair) => {
const jwt = await createServiceJwt({
iss: issuer,
aud: network.bsky.ctx.cfg.serverDid,
keypair,
})
return agent.api.app.bsky.actor.getProfile(
{ actor: sc.dids.carol },
{ headers: { authorization: `Bearer ${jwt}` } },
)
}
const origSigningKey = await network.pds.ctx.actorStore.keypair(issuer)
const newSigningKey = await Secp256k1Keypair.create({ exportable: true })
// confirm original signing key works
await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()
// confirm next signing key doesn't work yet
await expect(attemptWithKey(newSigningKey)).rejects.toThrow(
'jwt signature does not match jwt issuer',
)
// update to new signing key
await network.plc
.getClient()
.updateAtprotoKey(
issuer,
network.pds.ctx.plcRotationKey,
newSigningKey.did(),
)
// old signing key still works due to did doc cache
await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()
// new signing key works
await expect(attemptWithKey(newSigningKey)).resolves.toBeDefined()
// old signing key no longer works after cache is updated
await expect(attemptWithKey(origSigningKey)).rejects.toThrow(
'jwt signature does not match jwt issuer',
)
})
})
9 changes: 7 additions & 2 deletions packages/crypto/src/p256/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import { p256 } from '@noble/curves/p256'
import { sha256 } from '@noble/hashes/sha256'
import { P256_JWT_ALG } from '../const'
import { parseDidKey } from '../did'
import { VerifyOptions } from '../types'

export const verifyDidSig = async (
did: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const { jwtAlg, keyBytes } = parseDidKey(did)
if (jwtAlg !== P256_JWT_ALG) {
throw new Error(`Not a P-256 did:key: ${did}`)
}
return verifySig(keyBytes, data, sig)
return verifySig(keyBytes, data, sig, opts)
}

export const verifySig = async (
publicKey: Uint8Array,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const msgHash = await sha256(data)
return p256.verify(sig, msgHash, publicKey, { lowS: true })
return p256.verify(sig, msgHash, publicKey, {
lowS: opts?.lowS ?? true,
})
}
9 changes: 7 additions & 2 deletions packages/crypto/src/secp256k1/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import { secp256k1 as k256 } from '@noble/curves/secp256k1'
import { sha256 } from '@noble/hashes/sha256'
import { SECP256K1_JWT_ALG } from '../const'
import { parseDidKey } from '../did'
import { VerifyOptions } from '../types'

export const verifyDidSig = async (
did: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const { jwtAlg, keyBytes } = parseDidKey(did)
if (jwtAlg !== SECP256K1_JWT_ALG) {
throw new Error(`Not a secp256k1 did:key: ${did}`)
}
return verifySig(keyBytes, data, sig)
return verifySig(keyBytes, data, sig, opts)
}

export const verifySig = async (
publicKey: Uint8Array,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const msgHash = await sha256(data)
return k256.verify(sig, msgHash, publicKey, { lowS: true })
return k256.verify(sig, msgHash, publicKey, {
lowS: opts?.lowS ?? true,
})
}
5 changes: 5 additions & 0 deletions packages/crypto/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ export type DidKeyPlugin = {
did: string,
msg: Uint8Array,
data: Uint8Array,
opts?: VerifyOptions,
) => Promise<boolean>
}

export type VerifyOptions = {
lowS?: boolean
}
9 changes: 6 additions & 3 deletions packages/crypto/src/verify.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import * as uint8arrays from 'uint8arrays'
import { parseDidKey } from './did'
import plugins from './plugins'
import { VerifyOptions } from './types'

export const verifySignature = (
didKey: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const parsed = parseDidKey(didKey)
const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg)
if (!plugin) {
throw new Error(`Unsupported signature alg: :${parsed.jwtAlg}`)
throw new Error(`Unsupported signature alg: ${parsed.jwtAlg}`)
}
return plugin.verifySignature(didKey, data, sig)
return plugin.verifySignature(didKey, data, sig, opts)
}

export const verifySignatureUtf8 = async (
didKey: string,
data: string,
sig: string,
opts?: VerifyOptions,
): Promise<boolean> => {
const dataBytes = uint8arrays.fromString(data, 'utf8')
const sigBytes = uint8arrays.fromString(sig, 'base64url')
return verifySignature(didKey, dataBytes, sigBytes)
return verifySignature(didKey, dataBytes, sigBytes, opts)
}
44 changes: 44 additions & 0 deletions packages/crypto/tests/signatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,45 @@ describe('signatures', () => {
}
}
})

it('verifies high-s signatures with explicit option', async () => {
const highSVectors = vectors.filter((vec) => vec.tags.includes('high-s'))
expect(highSVectors.length).toBeGreaterThanOrEqual(2)
for (const vector of highSVectors) {
const messageBytes = uint8arrays.fromString(
vector.messageBase64,
'base64',
)
const signatureBytes = uint8arrays.fromString(
vector.signatureBase64,
'base64',
)
const keyBytes = multibaseToBytes(vector.publicKeyMultibase)
const didKey = parseDidKey(vector.publicKeyDid)
expect(uint8arrays.equals(keyBytes, didKey.keyBytes))
if (vector.algorithm === P256_JWT_ALG) {
const verified = await p256.verifySig(
keyBytes,
messageBytes,
signatureBytes,
{ lowS: false },
)
expect(verified).toEqual(true)
expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement
} else if (vector.algorithm === SECP256K1_JWT_ALG) {
const verified = await secp.verifySig(
keyBytes,
messageBytes,
signatureBytes,
{ lowS: false },
)
expect(verified).toEqual(true)
expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement
} else {
throw new Error('Unsupported test vector')
}
}
})
})

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -79,6 +118,7 @@ async function generateTestVectors(): Promise<TestVector[]> {
'base64',
),
validSignature: true,
tags: [],
},
{
messageBase64,
Expand All @@ -93,6 +133,7 @@ async function generateTestVectors(): Promise<TestVector[]> {
'base64',
),
validSignature: true,
tags: [],
},
// these vectors test to ensure we don't allow high-s signatures
{
Expand All @@ -109,6 +150,7 @@ async function generateTestVectors(): Promise<TestVector[]> {
P256_JWT_ALG,
),
validSignature: false,
tags: ['high-s'],
},
{
messageBase64,
Expand All @@ -124,6 +166,7 @@ async function generateTestVectors(): Promise<TestVector[]> {
SECP256K1_JWT_ALG,
),
validSignature: false,
tags: ['high-s'],
},
]
}
Expand Down Expand Up @@ -159,4 +202,5 @@ type TestVector = {
messageBase64: string
signatureBase64: string
validSignature: boolean
tags: string[]
}
8 changes: 0 additions & 8 deletions packages/pds/src/account-manager/db/migrations/001-init.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { Kysely, sql } from 'kysely'

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('app_migration')
.addColumn('id', 'varchar', (col) => col.primaryKey())
.addColumn('success', 'int2', (col) => col.notNull().defaultTo(0))
.addColumn('completedAt', 'varchar', (col) => col)
.execute()

await db.schema
.createTable('app_password')
.addColumn('did', 'varchar', (col) => col.notNull())
Expand Down Expand Up @@ -119,5 +112,4 @@ export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('invite_code_use').execute()
await db.schema.dropTable('invite_code').execute()
await db.schema.dropTable('app_password').execute()
await db.schema.dropTable('app_migration').execute()
}
9 changes: 0 additions & 9 deletions packages/pds/src/account-manager/db/schema/app-migration.ts

This file was deleted.

5 changes: 1 addition & 4 deletions packages/pds/src/account-manager/db/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import * as refreshToken from './refresh-token'
import * as appPassword from './app-password'
import * as inviteCode from './invite-code'
import * as emailToken from './email-token'
import * as appMigration from './app-migration'

export type DatabaseSchema = appMigration.PartialDB &
actor.PartialDB &
export type DatabaseSchema = actor.PartialDB &
account.PartialDB &
refreshToken.PartialDB &
appPassword.PartialDB &
Expand All @@ -23,4 +21,3 @@ export type { RefreshToken } from './refresh-token'
export type { AppPassword } from './app-password'
export type { InviteCode, InviteCodeUse } from './invite-code'
export type { EmailToken, EmailTokenPurpose } from './email-token'
export type { AppMigration } from './app-migration'
1 change: 1 addition & 0 deletions packages/pds/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { DiskBlobStore } from './disk-blobstore'
export { AppContext } from './context'
export { httpLogger } from './logger'

// @NOTE added to get a PR diff
export class PDS {
public ctx: AppContext
public app: express.Application
Expand Down
2 changes: 2 additions & 0 deletions packages/xrpc-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"@types/http-errors": "^2.0.1",
"@types/ws": "^8.5.4",
"get-port": "^6.1.2",
"jose": "^4.15.4",
"key-encoder": "^2.0.3",
"multiformats": "^9.9.0"
}
}
Loading

0 comments on commit 09f8837

Please sign in to comment.