Secure connections with AutoPass invites, HMAC capabilities, and viewer default.
Release rolling / release (push) Successful in 12m24s
Release rolling / release (push) Successful in 12m24s
Default peers are read-only; admin requires seed proof and operators redeem AutoPass packages with signed grants. ACL UI and docs match the new trust model.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import test from 'brittle'
|
||||
import crypto from 'crypto'
|
||||
import {
|
||||
deriveMacKey,
|
||||
signCapability,
|
||||
verifyCapability,
|
||||
createAdminProof,
|
||||
verifyAdminProof,
|
||||
classifyConnectionInput,
|
||||
safeEqual,
|
||||
} from '../shared/crypto-auth.js'
|
||||
import { Roles } from '../shared/protocol.js'
|
||||
|
||||
const SEED = 'ab'.repeat(32)
|
||||
const SEED2 = 'cd'.repeat(32)
|
||||
const PEER = '11'.repeat(32)
|
||||
const SERVER_PK = '22'.repeat(32)
|
||||
|
||||
test('deriveMacKey is deterministic and differs by seed', (t) => {
|
||||
const a = deriveMacKey(SEED)
|
||||
const b = deriveMacKey(SEED)
|
||||
const c = deriveMacKey(SEED2)
|
||||
t.is(a.length, 32)
|
||||
t.ok(safeEqual(a, b))
|
||||
t.absent(safeEqual(a, c))
|
||||
})
|
||||
|
||||
test('sign and verify capability', (t) => {
|
||||
const { token, payload } = signCapability(SEED, {
|
||||
role: Roles.operator,
|
||||
ttlMs: 3600_000,
|
||||
})
|
||||
t.ok(token.includes('.'))
|
||||
t.is(payload.role, Roles.operator)
|
||||
const res = verifyCapability(SEED, token, { peerId: PEER })
|
||||
t.ok(res.ok)
|
||||
t.is(res.payload.role, Roles.operator)
|
||||
t.ok(res.payload.jti)
|
||||
})
|
||||
|
||||
test('tampered capability fails', (t) => {
|
||||
const { token } = signCapability(SEED, { role: Roles.admin, ttlMs: 3600_000 })
|
||||
const [body, mac] = token.split('.')
|
||||
// Flip last char of body (base64url)
|
||||
const flipped =
|
||||
body.slice(0, -1) + (body.endsWith('A') ? 'B' : 'A') + '.' + mac
|
||||
const res = verifyCapability(SEED, flipped)
|
||||
t.absent(res.ok)
|
||||
t.is(res.code, 'CAPABILITY_INVALID')
|
||||
})
|
||||
|
||||
test('wrong seed fails verify', (t) => {
|
||||
const { token } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000 })
|
||||
const res = verifyCapability(SEED2, token)
|
||||
t.absent(res.ok)
|
||||
t.is(res.code, 'CAPABILITY_INVALID')
|
||||
})
|
||||
|
||||
test('expired capability fails', (t) => {
|
||||
const { token } = signCapability(SEED, { role: Roles.viewer, ttlMs: 60_000 })
|
||||
const res = verifyCapability(SEED, token, { now: Date.now() + 120_000 })
|
||||
t.absent(res.ok)
|
||||
t.is(res.code, 'CAPABILITY_EXPIRED')
|
||||
})
|
||||
|
||||
test('peer-bound capability mismatch', (t) => {
|
||||
const { token } = signCapability(SEED, {
|
||||
role: Roles.operator,
|
||||
ttlMs: 3600_000,
|
||||
peerId: PEER,
|
||||
})
|
||||
const bad = verifyCapability(SEED, token, { peerId: '33'.repeat(32) })
|
||||
t.absent(bad.ok)
|
||||
t.is(bad.code, 'CAPABILITY_PEER_MISMATCH')
|
||||
const good = verifyCapability(SEED, token, { peerId: PEER })
|
||||
t.ok(good.ok)
|
||||
})
|
||||
|
||||
test('spent jti check', (t) => {
|
||||
const { token, payload } = signCapability(SEED, { role: Roles.operator, ttlMs: 3600_000 })
|
||||
const spent = new Set([payload.jti])
|
||||
const res = verifyCapability(SEED, token, {
|
||||
allowSpentCheck: (jti) => !spent.has(jti),
|
||||
})
|
||||
t.absent(res.ok)
|
||||
t.is(res.code, 'CAPABILITY_SPENT')
|
||||
})
|
||||
|
||||
test('admin proof round-trip', (t) => {
|
||||
const proof = createAdminProof(SEED, {
|
||||
peerId: PEER,
|
||||
serverPublicKeyHex: SERVER_PK,
|
||||
})
|
||||
t.ok(proof.nonce)
|
||||
t.ok(proof.mac)
|
||||
const ok = verifyAdminProof(SEED, proof, {
|
||||
peerId: PEER,
|
||||
serverPublicKeyHex: SERVER_PK,
|
||||
})
|
||||
t.ok(ok.ok)
|
||||
const bad = verifyAdminProof(SEED2, proof, {
|
||||
peerId: PEER,
|
||||
serverPublicKeyHex: SERVER_PK,
|
||||
})
|
||||
t.absent(bad.ok)
|
||||
t.is(bad.code, 'ADMIN_PROOF_FAILED')
|
||||
})
|
||||
|
||||
test('classifyConnectionInput', (t) => {
|
||||
t.is(classifyConnectionInput(SERVER_PK), 'publicKey')
|
||||
t.is(classifyConnectionInput('aa'.repeat(24)), 'legacyInvite')
|
||||
t.is(classifyConnectionInput('ynr' + 'a'.repeat(100)), 'autopassInvite')
|
||||
t.is(classifyConnectionInput('not-valid'), 'unknown')
|
||||
t.is(classifyConnectionInput(''), 'unknown')
|
||||
})
|
||||
|
||||
test('safeEqual length mismatch', (t) => {
|
||||
t.absent(safeEqual(Buffer.from('ab'), Buffer.from('abc')))
|
||||
t.ok(safeEqual(crypto.randomBytes(0), Buffer.alloc(0)))
|
||||
})
|
||||
Reference in New Issue
Block a user