OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / test / labels.test.mjs

56 lines · javascript ref:
// com.atproto.label sign/verify conformance — build + sign + verify + tamper/neg/determinism.
// Run: node test/labels.test.mjs
import { keyFromPrivate } from '../lib/atproto-identity.js'
import { buildLabel, signLabel, verifyLabel, labelSigningBytes } from '../lib/labels.js'

const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m) }

const labeler = keyFromPrivate(new Uint8Array(32).fill(3)) // { privateKey, publicKey, didKey }
const cts = '2026-07-01T00:00:00.000Z'
const uri = 'at://did:plc:alice/app.bsky.feed.post/1'

const label = buildLabel({ src: 'did:plc:labeler', uri, val: 'spam', cts })
const signed = signLabel(label, labeler.privateKey)
assert(signed.sig && signed.sig.length === 64, 'signLabel produces a 64-byte sig')
assert(verifyLabel(signed, labeler.didKey) === true, 'verifyLabel accepts a good signature')
assert(verifyLabel({ ...signed, val: 'nice' }, labeler.didKey) === false, 'tampered val fails verification')
assert(verifyLabel({ ...signed, uri: 'at://did:plc:bob/app.bsky.feed.post/2' }, labeler.didKey) === false, 'tampered uri fails verification')

const other = keyFromPrivate(new Uint8Array(32).fill(5))
assert(verifyLabel(signed, other.didKey) === false, 'a different labeler key fails verification')
assert(verifyLabel(label, labeler.didKey) === false, 'an unsigned label fails verification')

// negation (retraction) label
const neg = signLabel(buildLabel({ src: 'did:plc:labeler', uri, val: 'spam', cts: '2026-07-02T00:00:00.000Z', neg: true }), labeler.privateKey)
assert(verifyLabel(neg, labeler.didKey) === true && neg.neg === true, 'negation label signs+verifies + carries neg:true')

// determinism: signing bytes are canonical regardless of field insertion order
const a = labelSigningBytes({ uri: 'x', src: 'y', val: 'z', cts: 't' })
const b = labelSigningBytes({ cts: 't', val: 'z', src: 'y', uri: 'x' })
assert(Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0, 'signing bytes are canonical (field-order independent)')

// buildLabel requires the mandatory fields
let threw = false
try { buildLabel({ src: 'x', uri: 'y', val: 'z' }) } catch { threw = true }
assert(threw, 'buildLabel throws without cts (required field)')

// ⛔ `ver` IS PART OF THE SIGNED OBJECT. The label spec (atproto.com/specs/label): "ver (integer, required):
// label schema version. Current version is always 1" — required whenever labels move between services — and the
// signature covers "a complete version of the label … This means including the `ver` field". Measured
// 2026-10-14: loser-club's member labels were served without `ver`, and bsky's AppView never showed them while
// 7 of its subscribeLabels connections received both frames.
assert(label.ver === 1, 'buildLabel sets ver: 1 (the spec\'s only version) when the caller passes none')
assert(buildLabel({ src: 'x', uri: 'y', val: 'z', cts, ver: 2 }).ver === 2, 'an explicit ver is kept')

// A REAL label from bsky's own labeler (mod.bsky.app subscribeLabels, cursor=0, captured 2026-10-14) with the
// issuer's #atproto_label did:key. Our signing bytes must match theirs byte for byte: it verifies as served, and
// stripping `ver` — the shape we used to emit — breaks it.
const { readFileSync } = await import('node:fs')
const fx = JSON.parse(readFileSync(new URL('./fixtures/bsky-mod-label.json', import.meta.url), 'utf8'))
const real = { ...fx.label, sig: Uint8Array.from(Buffer.from(fx.label.sig, 'hex')) }
assert(verifyLabel(real, fx.didKey) === true, 'a real bsky label verifies with our codec (signing bytes match bsky\'s)')
const { ver: _ver, ...withoutVer } = real
assert(verifyLabel(withoutVer, fx.didKey) === false, 'the same label without `ver` does NOT verify — ver is in the signed bytes')

console.log('\nLABELS OK: build + sign + verify + tamper/neg/determinism + ver conformance (com.atproto.label)')