🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@atproto/crypto

Package Overview
Dependencies
Maintainers
5
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@atproto/crypto - npm Package Compare versions

Comparing version
0.5.2
to
0.5.3
+10
-0
CHANGELOG.md
# @atproto/crypto
## 0.5.3
### Patch Changes
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build with `noImplicitAny` enabled
## 0.5.2

@@ -4,0 +14,0 @@

+14
-9
{
"name": "@atproto/crypto",
"version": "0.5.2",
"version": "0.5.3",
"license": "MIT",

@@ -16,2 +16,14 @@ "description": "Library for cryptographic keys and signing in atproto",

},
"files": [
"./dist",
"./README.md",
"./CHANGELOG.md"
],
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"engines": {

@@ -27,11 +39,4 @@ "node": ">=22"

"jest": "^30.0.0",
"@atproto/common": "^0.6.4"
"@atproto/common": "^0.6.5"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {

@@ -38,0 +43,0 @@ "test": "NODE_OPTIONS=--experimental-vm-modules jest",

/** @type {import('jest').Config} */
module.exports = {
displayName: 'Crypto',
transform: {
'^.+\\.(t|j)s$': [
'@swc/jest',
{
jsc: {
parser: { syntax: 'typescript', importAttributes: true },
experimental: { keepImportAttributes: true },
transform: {},
},
module: { type: 'es6' },
},
],
},
extensionsToTreatAsEsm: ['.ts'],
transformIgnorePatterns: [],
setupFiles: ['<rootDir>/../../test.setup.ts'],
moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
}
export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24])
export const SECP256K1_DID_PREFIX = new Uint8Array([0xe7, 0x01])
export const BASE58_MULTIBASE_PREFIX = 'z'
export const DID_KEY_PREFIX = 'did:key:'
export const P256_JWT_ALG = 'ES256'
export const SECP256K1_JWT_ALG = 'ES256K'
import * as uint8arrays from 'uint8arrays'
import { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from './const.js'
import { plugins } from './plugins.js'
import { extractMultikey, extractPrefixedBytes, hasPrefix } from './utils.js'
export type ParsedMultikey = {
jwtAlg: string
keyBytes: Uint8Array
}
export const parseMultikey = (multikey: string): ParsedMultikey => {
const prefixedBytes = extractPrefixedBytes(multikey)
const plugin = plugins.find((p) => hasPrefix(prefixedBytes, p.prefix))
if (!plugin) {
throw new Error('Unsupported key type')
}
const keyBytes = plugin.decompressPubkey(
prefixedBytes.slice(plugin.prefix.length),
)
return {
jwtAlg: plugin.jwtAlg,
keyBytes,
}
}
export const formatMultikey = (
jwtAlg: string,
keyBytes: Uint8Array,
): string => {
const plugin = plugins.find((p) => p.jwtAlg === jwtAlg)
if (!plugin) {
throw new Error('Unsupported key type')
}
const prefixedBytes = uint8arrays.concat([
plugin.prefix,
plugin.compressPubkey(keyBytes),
])
return (
BASE58_MULTIBASE_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc')
)
}
export const parseDidKey = (did: string): ParsedMultikey => {
const multikey = extractMultikey(did)
return parseMultikey(multikey)
}
export function formatDidKey(
jwtAlg: string,
keyBytes: Uint8Array,
): `did:key:${string}` {
return `${DID_KEY_PREFIX}${formatMultikey(jwtAlg, keyBytes)}` as const
}
export * from './const.js'
export * from './did.js'
export * from './multibase.js'
export * from './random.js'
export * from './sha.js'
export * from './types.js'
export * from './verify.js'
export * from './utils.js'
export * from './p256/keypair.js'
export * from './p256/plugin.js'
export * from './secp256k1/keypair.js'
export * from './secp256k1/plugin.js'
import * as uint8arrays from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/to-string'
export const multibaseToBytes = (mb: string): Uint8Array => {
const base = mb[0]
const key = mb.slice(1)
switch (base) {
case 'f':
return uint8arrays.fromString(key, 'base16')
case 'F':
return uint8arrays.fromString(key, 'base16upper')
case 'b':
return uint8arrays.fromString(key, 'base32')
case 'B':
return uint8arrays.fromString(key, 'base32upper')
case 'z':
return uint8arrays.fromString(key, 'base58btc')
case 'm':
return uint8arrays.fromString(key, 'base64')
case 'u':
return uint8arrays.fromString(key, 'base64url')
case 'U':
return uint8arrays.fromString(key, 'base64urlpad')
default:
throw new Error(`Unsupported multibase: :${mb}`)
}
}
export const bytesToMultibase = (
mb: Uint8Array,
encoding: SupportedEncodings,
): string => {
switch (encoding) {
case 'base16':
return 'f' + uint8arrays.toString(mb, encoding)
case 'base16upper':
return 'F' + uint8arrays.toString(mb, encoding)
case 'base32':
return 'b' + uint8arrays.toString(mb, encoding)
case 'base32upper':
return 'B' + uint8arrays.toString(mb, encoding)
case 'base58btc':
return 'z' + uint8arrays.toString(mb, encoding)
case 'base64':
return 'm' + uint8arrays.toString(mb, encoding)
case 'base64url':
return 'u' + uint8arrays.toString(mb, encoding)
case 'base64urlpad':
return 'U' + uint8arrays.toString(mb, encoding)
default:
throw new Error(`Unsupported multibase: :${encoding}`)
}
}
import { p256 } from '@noble/curves/p256'
export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
const point = p256.ProjectivePoint.fromHex(pubkeyBytes)
return point.toRawBytes(true)
}
export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
if (compressed.length !== 33) {
throw new Error('Expected 33 byte compress pubkey')
}
const point = p256.ProjectivePoint.fromHex(compressed)
return point.toRawBytes(false)
}
import { p256 } from '@noble/curves/p256'
import { sha256 } from '@noble/hashes/sha256'
import {
fromString as ui8FromString,
toString as ui8ToString,
} from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/to-string'
import { P256_JWT_ALG } from '../const.js'
import * as did from '../did.js'
import { Keypair } from '../types.js'
export type P256KeypairOptions = {
exportable: boolean
}
export class P256Keypair implements Keypair {
jwtAlg = P256_JWT_ALG
private publicKey: Uint8Array
constructor(
private privateKey: Uint8Array,
private exportable: boolean,
) {
this.publicKey = p256.getPublicKey(privateKey)
}
static async create(
opts?: Partial<P256KeypairOptions>,
): Promise<P256Keypair> {
const { exportable = false } = opts || {}
const privKey = p256.utils.randomPrivateKey()
return new P256Keypair(privKey, exportable)
}
static async import(
privKey: Uint8Array | string,
opts?: Partial<P256KeypairOptions>,
): Promise<P256Keypair> {
const { exportable = false } = opts || {}
const privKeyBytes =
typeof privKey === 'string' ? ui8FromString(privKey, 'hex') : privKey
return new P256Keypair(privKeyBytes, exportable)
}
publicKeyBytes(): Uint8Array {
return this.publicKey
}
publicKeyStr(encoding: SupportedEncodings = 'base64pad'): string {
return ui8ToString(this.publicKey, encoding)
}
did(): string {
return did.formatDidKey(this.jwtAlg, this.publicKey)
}
async sign(msg: Uint8Array): Promise<Uint8Array> {
const msgHash = await sha256(msg)
// return raw 64 byte sig not DER-encoded
const sig = await p256.sign(msgHash, this.privateKey, { lowS: true })
return sig.toCompactRawBytes()
}
async export(): Promise<Uint8Array> {
if (!this.exportable) {
throw new Error('Private key is not exportable')
}
return this.privateKey
}
}
import { p256 } from '@noble/curves/p256'
import { sha256 } from '@noble/hashes/sha256'
import { equals as ui8equals } from 'uint8arrays'
import { P256_DID_PREFIX } from '../const.js'
import { VerifyOptions } from '../types.js'
import { extractMultikey, extractPrefixedBytes, hasPrefix } from '../utils.js'
export const verifyDidSig = async (
did: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const prefixedBytes = extractPrefixedBytes(extractMultikey(did))
if (!hasPrefix(prefixedBytes, P256_DID_PREFIX)) {
throw new Error(`Not a P-256 did:key: ${did}`)
}
const keyBytes = prefixedBytes.slice(P256_DID_PREFIX.length)
return verifySig(keyBytes, data, sig, opts)
}
export const verifySig = async (
publicKey: Uint8Array,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const allowMalleable = opts?.allowMalleableSig ?? false
const msgHash = await sha256(data)
return p256.verify(sig, msgHash, publicKey, {
format: allowMalleable ? undefined : 'compact', // prevent DER-encoded signatures
lowS: !allowMalleable,
})
}
export const isCompactFormat = (sig: Uint8Array) => {
try {
const parsed = p256.Signature.fromCompact(sig)
return ui8equals(parsed.toCompactRawBytes(), sig)
} catch {
return false
}
}
import { P256_DID_PREFIX, P256_JWT_ALG } from '../const.js'
import { DidKeyPlugin } from '../types.js'
import { compressPubkey, decompressPubkey } from './encoding.js'
import { verifyDidSig } from './operations.js'
export const p256Plugin: DidKeyPlugin = {
prefix: P256_DID_PREFIX,
jwtAlg: P256_JWT_ALG,
verifySignature: verifyDidSig,
compressPubkey,
decompressPubkey,
}
import { p256Plugin } from './p256/plugin.js'
import { secp256k1Plugin } from './secp256k1/plugin.js'
export const plugins = [p256Plugin, secp256k1Plugin]
import * as noble from '@noble/hashes/utils'
import * as uint8arrays from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/to-string'
import { sha256 } from './sha.js'
export const randomBytes = noble.randomBytes as (
bytesLength?: number,
) => Uint8Array<ArrayBuffer>
export const randomStr = (
byteLength: number,
encoding: SupportedEncodings,
): string => {
const bytes = randomBytes(byteLength)
return uint8arrays.toString(bytes, encoding)
}
export const randomIntFromSeed = async (
seed: string,
high: number,
low = 0,
): Promise<number> => {
const hash = await sha256(seed)
const number = Buffer.from(hash).readUintBE(0, 6)
const range = high - low
const normalized = number % range
return normalized + low
}
import { secp256k1 as k256 } from '@noble/curves/secp256k1'
export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
const point = k256.ProjectivePoint.fromHex(pubkeyBytes)
return point.toRawBytes(true)
}
export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
if (compressed.length !== 33) {
throw new Error('Expected 33 byte compress pubkey')
}
const point = k256.ProjectivePoint.fromHex(compressed)
return point.toRawBytes(false)
}
import { secp256k1 as k256 } from '@noble/curves/secp256k1'
import { sha256 } from '@noble/hashes/sha256'
import {
fromString as ui8FromString,
toString as ui8ToString,
} from 'uint8arrays'
import { SupportedEncodings } from 'uint8arrays/to-string'
import { SECP256K1_JWT_ALG } from '../const.js'
import * as did from '../did.js'
import { Keypair } from '../types.js'
export type Secp256k1KeypairOptions = {
exportable: boolean
}
export class Secp256k1Keypair implements Keypair {
jwtAlg = SECP256K1_JWT_ALG
private publicKey: Uint8Array
constructor(
private privateKey: Uint8Array,
private exportable: boolean,
) {
this.publicKey = k256.getPublicKey(privateKey)
}
static async create(
opts?: Partial<Secp256k1KeypairOptions>,
): Promise<Secp256k1Keypair> {
const { exportable = false } = opts || {}
const privKey = k256.utils.randomPrivateKey()
return new Secp256k1Keypair(privKey, exportable)
}
static async import(
privKey: Uint8Array | string,
opts?: Partial<Secp256k1KeypairOptions>,
): Promise<Secp256k1Keypair> {
const { exportable = false } = opts || {}
const privKeyBytes =
typeof privKey === 'string' ? ui8FromString(privKey, 'hex') : privKey
return new Secp256k1Keypair(privKeyBytes, exportable)
}
publicKeyBytes(): Uint8Array {
return this.publicKey
}
publicKeyStr(encoding: SupportedEncodings = 'base64pad'): string {
return ui8ToString(this.publicKey, encoding)
}
did(): string {
return did.formatDidKey(this.jwtAlg, this.publicKey)
}
async sign(msg: Uint8Array): Promise<Uint8Array> {
const msgHash = await sha256(msg)
// return raw 64 byte sig not DER-encoded
const sig = await k256.sign(msgHash, this.privateKey, { lowS: true })
return sig.toCompactRawBytes()
}
async export(): Promise<Uint8Array> {
if (!this.exportable) {
throw new Error('Private key is not exportable')
}
return this.privateKey
}
}
import { secp256k1 as k256 } from '@noble/curves/secp256k1'
import { sha256 } from '@noble/hashes/sha256'
import * as ui8 from 'uint8arrays'
import { SECP256K1_DID_PREFIX } from '../const.js'
import { VerifyOptions } from '../types.js'
import { extractMultikey, extractPrefixedBytes, hasPrefix } from '../utils.js'
export const verifyDidSig = async (
did: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const prefixedBytes = extractPrefixedBytes(extractMultikey(did))
if (!hasPrefix(prefixedBytes, SECP256K1_DID_PREFIX)) {
throw new Error(`Not a secp256k1 did:key: ${did}`)
}
const keyBytes = prefixedBytes.slice(SECP256K1_DID_PREFIX.length)
return verifySig(keyBytes, data, sig, opts)
}
export const verifySig = async (
publicKey: Uint8Array,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions,
): Promise<boolean> => {
const allowMalleable = opts?.allowMalleableSig ?? false
const msgHash = await sha256(data)
return k256.verify(sig, msgHash, publicKey, {
format: allowMalleable ? undefined : 'compact', // prevent DER-encoded signatures
lowS: !allowMalleable,
})
}
export const isCompactFormat = (sig: Uint8Array) => {
try {
const parsed = k256.Signature.fromCompact(sig)
return ui8.equals(parsed.toCompactRawBytes(), sig)
} catch {
return false
}
}
import { SECP256K1_DID_PREFIX, SECP256K1_JWT_ALG } from '../const.js'
import { DidKeyPlugin } from '../types.js'
import { compressPubkey, decompressPubkey } from './encoding.js'
import { verifyDidSig } from './operations.js'
export const secp256k1Plugin: DidKeyPlugin = {
prefix: SECP256K1_DID_PREFIX,
jwtAlg: SECP256K1_JWT_ALG,
verifySignature: verifyDidSig,
compressPubkey,
decompressPubkey,
}
import * as noble from '@noble/hashes/sha256'
import * as uint8arrays from 'uint8arrays'
// takes either bytes of utf8 input
// @TODO this can be sync
export const sha256 = async (
input: Uint8Array | string,
): Promise<Uint8Array> => {
const bytes =
typeof input === 'string' ? uint8arrays.fromString(input, 'utf8') : input
return noble.sha256(bytes)
}
// @TODO this can be sync
export const sha256Hex = async (
input: Uint8Array | string,
): Promise<string> => {
const hash = await sha256(input)
return uint8arrays.toString(hash, 'hex')
}
export interface Signer {
jwtAlg: string
sign(msg: Uint8Array): Promise<Uint8Array>
}
export interface Didable {
did(): string
}
export interface Keypair extends Signer, Didable {}
export interface ExportableKeypair extends Keypair {
export(): Promise<Uint8Array>
}
export type DidKeyPlugin = {
prefix: Uint8Array
jwtAlg: string
verifySignature: (
did: string,
msg: Uint8Array,
data: Uint8Array,
opts?: VerifyOptions,
) => Promise<boolean>
compressPubkey: (uncompressed: Uint8Array) => Uint8Array
decompressPubkey: (compressed: Uint8Array) => Uint8Array
}
export type VerifyOptions = {
allowMalleableSig?: boolean
}
import * as uint8arrays from 'uint8arrays'
import { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from './const.js'
export const extractMultikey = (did: string): string => {
if (!did.startsWith(DID_KEY_PREFIX)) {
throw new Error(`Incorrect prefix for did:key: ${did}`)
}
return did.slice(DID_KEY_PREFIX.length)
}
export const extractPrefixedBytes = (multikey: string): Uint8Array => {
if (!multikey.startsWith(BASE58_MULTIBASE_PREFIX)) {
throw new Error(`Incorrect prefix for multikey: ${multikey}`)
}
return uint8arrays.fromString(
multikey.slice(BASE58_MULTIBASE_PREFIX.length),
'base58btc',
)
}
export const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => {
return uint8arrays.equals(prefix, bytes.subarray(0, prefix.byteLength))
}
import * as uint8arrays from 'uint8arrays'
import { parseDidKey } from './did.js'
import { plugins } from './plugins.js'
import { VerifyOptions } from './types.js'
export const verifySignature = (
didKey: string,
data: Uint8Array,
sig: Uint8Array,
opts?: VerifyOptions & {
jwtAlg?: string
},
): Promise<boolean> => {
const parsed = parseDidKey(didKey)
if (opts?.jwtAlg && opts.jwtAlg !== parsed.jwtAlg) {
throw new Error(`Expected key alg ${opts.jwtAlg}, got ${parsed.jwtAlg}`)
}
const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg)
if (!plugin) {
throw new Error(`Unsupported signature alg: ${parsed.jwtAlg}`)
}
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, opts)
}
import * as uint8arrays from 'uint8arrays'
import * as did from '../src/did.js'
import { P256Keypair, Secp256k1Keypair } from '../src/index.js'
import { decompressPubkey as p256Decompress } from '../src/p256/encoding.js'
import { decompressPubkey as secp256k1Decompress } from '../src/secp256k1/encoding.js'
describe('secp256k1 did:key', () => {
it('derives the correct DID from the privatekey', async () => {
for (const vector of secpTestVectors) {
const keypair = await Secp256k1Keypair.import(vector.seed)
const did = keypair.did()
expect(did).toEqual(vector.id)
}
})
it('converts between bytes and did', async () => {
for (const vector of secpTestVectors) {
const keypair = await Secp256k1Keypair.import(vector.seed)
const didKey = did.formatDidKey('ES256K', keypair.publicKeyBytes())
expect(didKey).toEqual(vector.id)
const { jwtAlg, keyBytes } = did.parseDidKey(didKey)
expect(jwtAlg).toBe('ES256K')
expect(
uint8arrays.equals(
keyBytes,
secp256k1Decompress(keypair.publicKeyBytes()),
),
).toBeTruthy()
}
})
})
describe('P-256 did:key', () => {
it('derives the correct DID from the JWK', async () => {
for (const vector of p256TestVectors) {
const bytes = uint8arrays.fromString(vector.privateKeyBase58, 'base58btc')
const keypair = await P256Keypair.import(bytes)
const did = keypair.did()
expect(did).toEqual(vector.id)
}
})
it('converts between bytes and did', async () => {
for (const vector of p256TestVectors) {
const bytes = uint8arrays.fromString(vector.privateKeyBase58, 'base58btc')
const keypair = await P256Keypair.import(bytes)
const didKey = did.formatDidKey('ES256', keypair.publicKeyBytes())
expect(didKey).toEqual(vector.id)
const { jwtAlg, keyBytes } = did.parseDidKey(didKey)
expect(jwtAlg).toBe('ES256')
expect(
uint8arrays.equals(keyBytes, p256Decompress(keypair.publicKeyBytes())),
).toBeTruthy()
}
})
})
// did:key secp256k1 test vectors from W3C
// https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json
const secpTestVectors = [
{
seed: '9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c',
id: 'did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme',
},
{
seed: 'f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed',
id: 'did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2',
},
{
seed: '6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02',
id: 'did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N',
},
{
seed: 'c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15',
id: 'did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy',
},
{
seed: '175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133',
id: 'did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj',
},
]
// did:key p-256 test vectors from W3C
// https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/nist-curves.json
const p256TestVectors = [
{
privateKeyBase58: '9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp',
id: 'did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb',
},
]
import * as did from '../src/did.js'
import * as p256Encoding from '../src/p256/encoding.js'
import { P256Keypair } from '../src/p256/keypair.js'
import * as secpEncoding from '../src/secp256k1/encoding.js'
import { Secp256k1Keypair } from '../src/secp256k1/keypair.js'
describe('public key compression', () => {
describe('secp256k1', () => {
let keyBytes: Uint8Array
let compressed: Uint8Array
it('compresses a key to the correct length', async () => {
const keypair = await Secp256k1Keypair.create()
const parsed = did.parseDidKey(keypair.did())
keyBytes = parsed.keyBytes
compressed = secpEncoding.compressPubkey(keyBytes)
expect(compressed.length).toBe(33)
})
it('decompresses a key to the original', async () => {
const decompressed = secpEncoding.decompressPubkey(compressed)
expect(decompressed.length).toBe(65)
expect(decompressed).toEqual(keyBytes)
})
it('works consistently', async () => {
const pubkeys: Uint8Array[] = []
for (let i = 0; i < 100; i++) {
const key = await Secp256k1Keypair.create()
const parsed = did.parseDidKey(key.did())
pubkeys.push(parsed.keyBytes)
}
const compressed = pubkeys.map(secpEncoding.compressPubkey)
const decompressed = compressed.map(secpEncoding.decompressPubkey)
expect(pubkeys).toEqual(decompressed)
})
})
describe('P-256', () => {
let keyBytes: Uint8Array
let compressed: Uint8Array
it('compresses a key to the correct length', async () => {
const keypair = await P256Keypair.create()
const parsed = did.parseDidKey(keypair.did())
keyBytes = parsed.keyBytes
compressed = p256Encoding.compressPubkey(keyBytes)
expect(compressed.length).toBe(33)
})
it('decompresses a key to the original', async () => {
const decompressed = p256Encoding.decompressPubkey(compressed)
expect(decompressed.length).toBe(65)
expect(decompressed).toEqual(keyBytes)
})
it('works consistently', async () => {
const pubkeys: Uint8Array[] = []
for (let i = 0; i < 100; i++) {
const key = await P256Keypair.create()
const parsed = did.parseDidKey(key.did())
pubkeys.push(parsed.keyBytes)
}
const compressed = pubkeys.map(p256Encoding.compressPubkey)
const decompressed = compressed.map(p256Encoding.decompressPubkey)
expect(pubkeys).toEqual(decompressed)
})
})
})
import { randomBytes } from '../src/index.js'
import { P256Keypair } from '../src/p256/keypair.js'
import * as p256 from '../src/p256/operations.js'
import { Secp256k1Keypair } from '../src/secp256k1/keypair.js'
import * as secp from '../src/secp256k1/operations.js'
describe('keypairs', () => {
describe('secp256k1', () => {
let keypair: Secp256k1Keypair
let imported: Secp256k1Keypair
it('has the same DID on import', async () => {
keypair = await Secp256k1Keypair.create({ exportable: true })
const exported = await keypair.export()
imported = await Secp256k1Keypair.import(exported, { exportable: true })
expect(keypair.did()).toBe(imported.did())
})
it('produces a valid signature', async () => {
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
const sig = await imported.sign(data)
const validSig = await secp.verifyDidSig(keypair.did(), data, sig)
expect(validSig).toBeTruthy()
})
it('produces a valid signature on a typed array of a large arraybuffer', async () => {
const bytes = await randomBytes(8192)
const arrBuf = bytes.buffer
const sliceView = new Uint8Array(arrBuf, 1024, 1024)
expect(sliceView.buffer.byteLength).toBe(8192)
const sig = await imported.sign(sliceView)
const validSig = await secp.verifyDidSig(keypair.did(), sliceView, sig)
expect(validSig).toBeTruthy()
})
})
describe('P-256', () => {
let keypair: P256Keypair
let imported: P256Keypair
it('has the same DID on import', async () => {
keypair = await P256Keypair.create({ exportable: true })
const exported = await keypair.export()
imported = await P256Keypair.import(exported, { exportable: true })
expect(keypair.did()).toBe(imported.did())
})
it('produces a valid signature', async () => {
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])
const sig = await imported.sign(data)
const validSig = await p256.verifyDidSig(keypair.did(), data, sig)
expect(validSig).toBeTruthy()
})
it('produces a valid signature on a typed array of a large arraybuffer', async () => {
const bytes = await randomBytes(8192)
const arrBuf = bytes.buffer
const sliceView = new Uint8Array(arrBuf, 1024, 1024)
expect(sliceView.buffer.byteLength).toBe(8192)
const sig = await imported.sign(sliceView)
const validSig = await p256.verifyDidSig(keypair.did(), sliceView, sig)
expect(validSig).toBeTruthy()
})
})
})
import { randomIntFromSeed } from '../src/index.js'
describe('randomIntFromSeed()', () => {
it('has good distribution for low bucket count.', async () => {
const counts: [zero: number, one: number] = [0, 0]
const salt = Math.random()
for (let i = 0; i < 10000; ++i) {
const int = await randomIntFromSeed(`${i}${salt}`, 2)
counts[int]++
}
const [zero, one] = counts
expect(zero + one).toEqual(10000)
expect(Math.max(zero, one) / Math.min(zero, one)).toBeLessThan(1.1)
})
})
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { p256 as nobleP256 } from '@noble/curves/p256'
import { secp256k1 as nobleK256 } from '@noble/curves/secp256k1'
import * as uint8arrays from 'uint8arrays'
import { cborEncode } from '@atproto/common'
import {
P256_JWT_ALG,
SECP256K1_JWT_ALG,
bytesToMultibase,
multibaseToBytes,
parseDidKey,
sha256,
} from '../src/index.js'
import { P256Keypair } from '../src/p256/keypair.js'
import * as p256 from '../src/p256/operations.js'
import { Secp256k1Keypair } from '../src/secp256k1/keypair.js'
import * as secp from '../src/secp256k1/operations.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
describe('signatures', () => {
let vectors: TestVector[]
beforeAll(() => {
vectors = JSON.parse(
fs.readFileSync(`${__dirname}/signature-fixtures.json`).toString(),
)
})
it('verifies secp256k1 and P-256 test vectors', async () => {
for (const vector of vectors) {
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,
)
expect(verified).toEqual(vector.validSignature)
} else if (vector.algorithm === SECP256K1_JWT_ALG) {
const verified = await secp.verifySig(
keyBytes,
messageBytes,
signatureBytes,
)
expect(verified).toEqual(vector.validSignature)
} else {
throw new Error('Unsupported test vector')
}
}
})
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,
{ allowMalleableSig: true },
)
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,
{ allowMalleableSig: true },
)
expect(verified).toEqual(true)
expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement
} else {
throw new Error('Unsupported test vector')
}
}
})
it('verifies der-encoded signatures with explicit option', async () => {
const DERVectors = vectors.filter((vec) => vec.tags.includes('der-encoded'))
expect(DERVectors.length).toBeGreaterThanOrEqual(2)
for (const vector of DERVectors) {
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,
{ allowMalleableSig: true },
)
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,
{ allowMalleableSig: true },
)
expect(verified).toEqual(true)
expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement
} else {
throw new Error('Unsupported test vector')
}
}
})
})
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function generateTestVectors(): Promise<TestVector[]> {
const p256Key = await P256Keypair.create({ exportable: true })
const secpKey = await Secp256k1Keypair.create({ exportable: true })
const messageBytes = cborEncode({ hello: 'world' })
const messageBase64 = uint8arrays.toString(messageBytes, 'base64')
return [
{
messageBase64,
algorithm: P256_JWT_ALG, // "ES256" / ecdsa p-256
publicKeyDid: p256Key.did(),
publicKeyMultibase: bytesToMultibase(
p256Key.publicKeyBytes(),
'base58btc',
),
signatureBase64: uint8arrays.toString(
await p256Key.sign(messageBytes),
'base64',
),
validSignature: true,
tags: [],
},
{
messageBase64,
algorithm: SECP256K1_JWT_ALG, // "ES256K" / secp256k
publicKeyDid: secpKey.did(),
publicKeyMultibase: bytesToMultibase(
secpKey.publicKeyBytes(),
'base58btc',
),
signatureBase64: uint8arrays.toString(
await secpKey.sign(messageBytes),
'base64',
),
validSignature: true,
tags: [],
},
// these vectors test to ensure we don't allow high-s signatures
{
messageBase64,
algorithm: P256_JWT_ALG, // "ES256" / ecdsa p-256
publicKeyDid: p256Key.did(),
publicKeyMultibase: bytesToMultibase(
p256Key.publicKeyBytes(),
'base58btc',
),
signatureBase64: await makeHighSSig(
messageBytes,
await p256Key.export(),
P256_JWT_ALG,
),
validSignature: false,
tags: ['high-s'],
},
{
messageBase64,
algorithm: SECP256K1_JWT_ALG, // "ES256K" / secp256k
publicKeyDid: secpKey.did(),
publicKeyMultibase: bytesToMultibase(
secpKey.publicKeyBytes(),
'base58btc',
),
signatureBase64: await makeHighSSig(
messageBytes,
await secpKey.export(),
SECP256K1_JWT_ALG,
),
validSignature: false,
tags: ['high-s'],
},
// these vectors test to ensure we don't allow der-encoded signatures
{
messageBase64,
algorithm: P256_JWT_ALG, // "ES256" / ecdsa p-256
publicKeyDid: p256Key.did(),
publicKeyMultibase: bytesToMultibase(
p256Key.publicKeyBytes(),
'base58btc',
),
signatureBase64: await makeDerEncodedSig(
messageBytes,
await p256Key.export(),
P256_JWT_ALG,
),
validSignature: false,
tags: ['der-encoded'],
},
{
messageBase64,
algorithm: SECP256K1_JWT_ALG, // "ES256K" / secp256k
publicKeyDid: secpKey.did(),
publicKeyMultibase: bytesToMultibase(
secpKey.publicKeyBytes(),
'base58btc',
),
signatureBase64: await makeDerEncodedSig(
messageBytes,
await secpKey.export(),
SECP256K1_JWT_ALG,
),
validSignature: false,
tags: ['der-encoded'],
},
]
}
async function makeHighSSig(
msgBytes: Uint8Array,
keyBytes: Uint8Array,
alg: string,
): Promise<string> {
const hash = await sha256(msgBytes)
let sig: string | undefined
do {
if (alg === SECP256K1_JWT_ALG) {
const attempt = await nobleK256.sign(hash, keyBytes, { lowS: false })
if (attempt.hasHighS()) {
sig = uint8arrays.toString(attempt.toCompactRawBytes(), 'base64')
}
} else {
const attempt = await nobleP256.sign(hash, keyBytes, { lowS: false })
if (attempt.hasHighS()) {
sig = uint8arrays.toString(attempt.toCompactRawBytes(), 'base64')
}
}
} while (sig === undefined)
return sig
}
async function makeDerEncodedSig(
msgBytes: Uint8Array,
keyBytes: Uint8Array,
alg: string,
): Promise<string> {
const hash = await sha256(msgBytes)
let sig: string
if (alg === SECP256K1_JWT_ALG) {
const attempt = await nobleK256.sign(hash, keyBytes, { lowS: true })
sig = uint8arrays.toString(attempt.toDERRawBytes(), 'base64')
} else {
const attempt = await nobleP256.sign(hash, keyBytes, { lowS: true })
sig = uint8arrays.toString(attempt.toDERRawBytes(), 'base64')
}
return sig
}
type TestVector = {
algorithm: string
publicKeyDid: string
publicKeyMultibase: string
messageBase64: string
signatureBase64: string
validSignature: boolean
tags: string[]
}
{
"extends": "../../tsconfig/isomorphic.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"include": ["./src"],
}

Sorry, the diff of this file is not supported yet

{
"include": [],
"references": [
{ "path": "./tsconfig.build.json" },
{ "path": "./tsconfig.tests.json" },
],
}
{
"extends": "../../tsconfig/tests.json",
"compilerOptions": {
"rootDir": ".",
},
"include": ["./tests"],
}