Skip to content

Commit af82318

Browse files
committed
feat: varsig initial impl
1 parent 3821092 commit af82318

File tree

6 files changed

+247
-7
lines changed

6 files changed

+247
-7
lines changed

packages/ucan/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@
5858
"test:browser": "playwright-test 'test/**/!(*.node).test.js'"
5959
},
6060
"dependencies": {
61+
"@ipld/dag-cbor": "^9.2.0",
6162
"@noble/ed25519": "^2.0.0",
6263
"@scure/bip39": "^1.2.2",
63-
"iso-base": "^3.0.0",
64+
"iso-base": "^4.0.0",
6465
"iso-did": "^1.6.0",
6566
"iso-kv": "^3.0.1",
6667
"iso-signatures": "^0.3.2",

packages/ucan/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { code as RAW_CODE } from 'multiformats/codecs/raw'
66
import type { sha256 } from 'multiformats/hashes/sha2'
77
import type { Block } from 'multiformats'
88
import type { Driver, IKV } from 'iso-kv'
9+
import type { ENCODING } from './varsig'
910

1011
export { CID } from 'multiformats/cid'
1112

@@ -203,3 +204,12 @@ export interface AgentCreateOptions {
203204
exported: string | CryptoKeyPair | undefined
204205
) => Promise<ISigner<string | CryptoKeyPair>>
205206
}
207+
208+
/**
209+
* Varsig types
210+
*/
211+
212+
export interface VarsigOptions {
213+
encoding: keyof typeof ENCODING
214+
alg: SignatureAlgorithm
215+
}

packages/ucan/src/varsig.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { varint } from 'iso-base/varint'
2+
import { equals } from 'iso-base/utils'
3+
import { hex } from 'iso-base/rfc4648'
4+
5+
export const VARSIG = 0x34
6+
7+
export const ENCODING = /** @types {const} */ {
8+
RAW: 0x5f,
9+
'DAG-PB': 0x70,
10+
'DAG-CBOR': 0x71,
11+
'DAG-JSON': 0x1_29,
12+
JWT: 0x6a_77,
13+
}
14+
15+
/**
16+
* @type {Record<number, string>}
17+
*/
18+
export const CODE_ENCODING = /** @types {const} */ {
19+
0x5f: 'RAW',
20+
0x70: 'DAG-PB',
21+
0x71: 'DAG-CBOR',
22+
0x1_29: 'DAG-JSON',
23+
0x6a_77: 'JWT',
24+
}
25+
26+
export const ALG = /** @type {const} */ ({
27+
EdDSA: 0xed,
28+
RS256: 0x12_05,
29+
ES256: 0x12_00,
30+
ES384: 0x12_01,
31+
ES512: 0x12_02,
32+
ES256K: 0xe7,
33+
})
34+
35+
/**
36+
* @type {Record<number, string>}
37+
*/
38+
export const CODE_ALG = /** @type {const} */ ({
39+
0xed: 'EdDSA',
40+
0x12_05: 'RS256',
41+
0x12_00: 'ES256',
42+
0x12_01: 'ES384',
43+
0x12_02: 'ES512',
44+
0xe7: 'ES256K',
45+
})
46+
47+
/**
48+
* @param {keyof typeof ENCODING} type
49+
*/
50+
export function encCode(type) {
51+
const encCode = ENCODING[type]
52+
if (!encCode) {
53+
throw new TypeError(`Unsupported encoding ${type}`)
54+
}
55+
return encCode
56+
}
57+
58+
/**
59+
* Varint encoding for signature algorithms and encodings.
60+
*
61+
* @type {Record<keyof typeof ENCODING | 'VARSIG' | import('iso-did/types').SignatureAlgorithm, number[] >}
62+
*/
63+
const VARINT = {
64+
VARSIG: [52],
65+
66+
RAW: [95],
67+
'DAG-CBOR': [113],
68+
'DAG-JSON': [169, 2],
69+
'DAG-PB': [112],
70+
JWT: [247, 212, 1],
71+
72+
EdDSA: [237, 1],
73+
// rsa + SHA2-256 + 256
74+
RS256: [133, 36, 18, 128, 2],
75+
// secp256k1 + SHA2-256
76+
ES256K: [231, 1, 18],
77+
// p256 + SHA2-256
78+
ES256: [128, 36, 18],
79+
// p384 + SHA2-384
80+
ES384: [129, 36, 32],
81+
// p512 + SHA2-512
82+
ES512: [130, 36, 19],
83+
}
84+
85+
/**
86+
* @param {import('./types.js').VarsigOptions} options
87+
*/
88+
export function encode(options) {
89+
const enc = VARINT[options.encoding]
90+
if (!enc) {
91+
throw new TypeError(`Unsupported encoding ${options.encoding}`)
92+
}
93+
94+
const alg = VARINT[options.alg]
95+
if (!alg) {
96+
throw new TypeError(`Unsupported algorithm ${options.alg}`)
97+
}
98+
99+
return Uint8Array.from([...VARINT.VARSIG, ...alg, ...enc])
100+
}
101+
102+
/**
103+
* Match the algorithm and encoding.
104+
*
105+
* @param {keyof typeof ALG} alg
106+
* @param {Uint8Array} buf
107+
*/
108+
function matchAlg(alg, buf) {
109+
const expected = Uint8Array.from([...VARINT.VARSIG, ...VARINT[alg]])
110+
const actual = buf.slice(0, VARINT[alg].length + 1)
111+
const match = equals(actual, expected)
112+
if (!match)
113+
throw new TypeError(
114+
`Header 0x${hex.encode(actual)} does not match expected 0x${hex.encode(expected)} for ${alg}`
115+
)
116+
const enc = varint.decode(buf, expected.length)
117+
const encoding = CODE_ENCODING[enc[0]]
118+
if (!encoding) {
119+
throw new TypeError(`Unsupported encoding 0x${enc[0]}`)
120+
}
121+
122+
return encoding
123+
}
124+
125+
/**
126+
* Decode varsig header
127+
*
128+
* @param {Uint8Array} buf
129+
*/
130+
export function decode(buf) {
131+
const alg = varint.decode(buf, varint.encodingLength(VARSIG))
132+
133+
switch (CODE_ALG[alg[0]]) {
134+
case 'RS256': {
135+
return {
136+
alg: 'RS256',
137+
encoding: matchAlg('RS256', buf),
138+
}
139+
}
140+
141+
case 'ES256': {
142+
return {
143+
alg: 'ES256',
144+
encoding: matchAlg('ES256', buf),
145+
}
146+
}
147+
148+
case 'ES384': {
149+
return {
150+
alg: 'ES384',
151+
encoding: matchAlg('ES384', buf),
152+
}
153+
}
154+
155+
case 'ES512': {
156+
return {
157+
alg: 'ES512',
158+
encoding: matchAlg('ES512', buf),
159+
}
160+
}
161+
162+
case 'ES256K': {
163+
return {
164+
alg: 'ES256K',
165+
encoding: matchAlg('ES256K', buf),
166+
}
167+
}
168+
169+
case 'EdDSA': {
170+
return {
171+
alg: 'EdDSA',
172+
encoding: matchAlg('EdDSA', buf),
173+
}
174+
}
175+
176+
default: {
177+
throw new TypeError(`Unsupported algorithm with code ${alg[0]}`)
178+
}
179+
}
180+
}

packages/ucan/test/agent.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MemoryDriver } from 'iso-kv/drivers/memory.js'
55
import { RSASigner } from 'iso-signatures/signers/rsa.js'
66
import { Agent } from '../src/agent.js'
77

8-
const test = suite('agent').only
8+
const test = suite('agent')
99

1010
const resolverEdOrEC = (
1111
/** @type {string | CryptoKeyPair | undefined} */ exported

packages/ucan/test/bip39.test.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/ucan/test/varsig.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { assert, suite } from 'playwright-test/taps'
2+
import { varint } from 'iso-base/varint'
3+
import { ALG, ENCODING, VARSIG, decode, encode } from '../src/varsig.js'
4+
5+
const { test } = suite('varsig')
6+
7+
test('should encode es384 cbor', async function () {
8+
const out = encode({
9+
encoding: 'DAG-CBOR',
10+
alg: 'ES384',
11+
})
12+
13+
const varsig = varint.decode(out)
14+
const alg = varint.decode(out, varsig[1])
15+
const hash = varint.decode(out, varsig[1] + alg[1])
16+
const enc = varint.decode(out, varsig[1] + alg[1] + hash[1])
17+
18+
assert.equal(varsig[0], VARSIG)
19+
assert.equal(alg[0], 0x12_01)
20+
assert.equal(hash[0], 0x20)
21+
assert.equal(enc[0], ENCODING['DAG-CBOR'])
22+
})
23+
24+
test('should encode rs256 cbor', async function () {
25+
const out = encode({
26+
encoding: 'DAG-CBOR',
27+
alg: 'RS256',
28+
})
29+
30+
const varsig = varint.decode(out)
31+
const alg = varint.decode(out, varsig[1])
32+
const hash = varint.decode(out, varsig[1] + alg[1])
33+
const size = varint.decode(out, varsig[1] + alg[1] + hash[1])
34+
const enc = varint.decode(out, varsig[1] + alg[1] + hash[1] + size[1])
35+
36+
assert.equal(varsig[0], VARSIG)
37+
assert.equal(alg[0], ALG.RS256)
38+
assert.equal(hash[0], 0x12)
39+
assert.equal(size[0], 0x01_00)
40+
assert.equal(enc[0], ENCODING['DAG-CBOR'])
41+
42+
assert.equal(out.length, varsig[1] + alg[1] + hash[1] + size[1] + enc[1])
43+
})
44+
45+
test('should decode', async function () {
46+
const header = {
47+
encoding: 'JWT',
48+
alg: 'RS256',
49+
}
50+
// @ts-ignore
51+
const out = encode(header)
52+
53+
assert.deepEqual(decode(out), header)
54+
})

0 commit comments

Comments
 (0)