@meith/accounts 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +165 -0
- package/package.json +25 -0
- package/src/ban-filter.ts +127 -0
- package/src/ban-service.ts +71 -0
- package/src/case-fold.ts +3 -0
- package/src/credential-proof.ts +17 -0
- package/src/crypto/base64url.ts +53 -0
- package/src/crypto/legacy.ts +127 -0
- package/src/crypto/password.ts +92 -0
- package/src/crypto/tokens.ts +28 -0
- package/src/federation/catalog.ts +115 -0
- package/src/federation/github.ts +117 -0
- package/src/federation/http.ts +48 -0
- package/src/federation/jwt.ts +198 -0
- package/src/federation/oidc.ts +175 -0
- package/src/federation/pkce.ts +20 -0
- package/src/federation/service.ts +198 -0
- package/src/federation/types.ts +50 -0
- package/src/index.ts +223 -0
- package/src/member-settings.ts +390 -0
- package/src/memory-bans.ts +88 -0
- package/src/memory-repos.ts +665 -0
- package/src/policy.ts +104 -0
- package/src/ports.ts +415 -0
- package/src/register-fields.ts +17 -0
- package/src/service.ts +684 -0
- package/src/session-service.ts +134 -0
- package/src/test-support.fixture.ts +8 -0
- package/src/totp/base32.ts +52 -0
- package/src/totp/secret-box.ts +83 -0
- package/src/totp/service.ts +252 -0
- package/src/totp/totp.ts +120 -0
- package/src/webauthn/authenticator.fixture.ts +268 -0
- package/src/webauthn/cbor.ts +110 -0
- package/src/webauthn/cose.ts +130 -0
- package/src/webauthn/service.ts +266 -0
- package/src/webauthn/verify.ts +231 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { decodeBase64Url, encodeBase64Url } from '../crypto/base64url'
|
|
2
|
+
|
|
3
|
+
export type FixtureAlgorithm = 'ES256' | 'RS256' | 'EdDSA'
|
|
4
|
+
|
|
5
|
+
export function encodeCbor(value: unknown): Uint8Array {
|
|
6
|
+
if (typeof value === 'number') {
|
|
7
|
+
return value < 0 ? head(1, -1 - value) : head(0, value)
|
|
8
|
+
}
|
|
9
|
+
if (value instanceof Uint8Array) {
|
|
10
|
+
return concat(head(2, value.length), value)
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === 'string') {
|
|
13
|
+
const bytes = new TextEncoder().encode(value)
|
|
14
|
+
return concat(head(3, bytes.length), bytes)
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return concat(head(4, value.length), ...value.map(encodeCbor))
|
|
18
|
+
}
|
|
19
|
+
if (value instanceof Map) {
|
|
20
|
+
const parts: Uint8Array[] = [head(5, value.size)]
|
|
21
|
+
for (const [key, entry] of value) {
|
|
22
|
+
parts.push(encodeCbor(key), encodeCbor(entry))
|
|
23
|
+
}
|
|
24
|
+
return concat(...parts)
|
|
25
|
+
}
|
|
26
|
+
throw new Error('the fixture encoder does not know that shape')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function head(major: number, argument: number): Uint8Array {
|
|
30
|
+
if (argument < 24) return Uint8Array.from([(major << 5) | argument])
|
|
31
|
+
if (argument < 0x100) return Uint8Array.from([(major << 5) | 24, argument])
|
|
32
|
+
if (argument < 0x10000) {
|
|
33
|
+
return Uint8Array.from([(major << 5) | 25, argument >> 8, argument & 0xff])
|
|
34
|
+
}
|
|
35
|
+
return Uint8Array.from([
|
|
36
|
+
(major << 5) | 26,
|
|
37
|
+
(argument >>> 24) & 0xff,
|
|
38
|
+
(argument >>> 16) & 0xff,
|
|
39
|
+
(argument >>> 8) & 0xff,
|
|
40
|
+
argument & 0xff,
|
|
41
|
+
])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
|
|
45
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0))
|
|
46
|
+
let offset = 0
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
out.set(part, offset)
|
|
49
|
+
offset += part.length
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FixtureResponse {
|
|
55
|
+
readonly clientDataJSON: string
|
|
56
|
+
readonly attestationObject: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FixtureAssertion {
|
|
60
|
+
readonly clientDataJSON: string
|
|
61
|
+
readonly authenticatorData: string
|
|
62
|
+
readonly signature: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FixtureAuthenticator {
|
|
66
|
+
readonly credentialId: string
|
|
67
|
+
readonly publicKey: string
|
|
68
|
+
register(input: {
|
|
69
|
+
readonly challenge: string
|
|
70
|
+
readonly origin: string
|
|
71
|
+
readonly type?: string
|
|
72
|
+
readonly rpId?: string
|
|
73
|
+
readonly flags?: number
|
|
74
|
+
}): Promise<FixtureResponse>
|
|
75
|
+
assert(input: {
|
|
76
|
+
readonly challenge: string
|
|
77
|
+
readonly origin: string
|
|
78
|
+
readonly type?: string
|
|
79
|
+
readonly rpId?: string
|
|
80
|
+
readonly signCount?: number
|
|
81
|
+
readonly flags?: number
|
|
82
|
+
readonly tamper?: boolean
|
|
83
|
+
}): Promise<FixtureAssertion>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createAuthenticator(input: {
|
|
87
|
+
readonly rpId: string
|
|
88
|
+
readonly algorithm?: FixtureAlgorithm
|
|
89
|
+
readonly credentialId?: Uint8Array
|
|
90
|
+
}): Promise<FixtureAuthenticator> {
|
|
91
|
+
const algorithm = input.algorithm ?? 'ES256'
|
|
92
|
+
const credentialId = input.credentialId ?? Uint8Array.from({ length: 16 }, (_, i) => i + 1)
|
|
93
|
+
|
|
94
|
+
const pair = (await crypto.subtle.generateKey(generateParams(algorithm), true, [
|
|
95
|
+
'sign',
|
|
96
|
+
'verify',
|
|
97
|
+
])) as CryptoKeyPair
|
|
98
|
+
|
|
99
|
+
const jwk = await crypto.subtle.exportKey('jwk', pair.publicKey)
|
|
100
|
+
|
|
101
|
+
const coseKey = encodeCbor(coseEntries(algorithm, jwk))
|
|
102
|
+
|
|
103
|
+
const clientData = (type: string, challenge: string, origin: string): string =>
|
|
104
|
+
encodeBase64Url(
|
|
105
|
+
new TextEncoder().encode(JSON.stringify({ type, challenge, origin, crossOrigin: false })),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const authenticatorData = async (
|
|
109
|
+
rpId: string,
|
|
110
|
+
flags: number,
|
|
111
|
+
signCount: number,
|
|
112
|
+
attested: Uint8Array | null,
|
|
113
|
+
): Promise<Uint8Array> => {
|
|
114
|
+
const rpIdHash = new Uint8Array(
|
|
115
|
+
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpId)),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const counter = new Uint8Array(4)
|
|
119
|
+
new DataView(counter.buffer).setUint32(0, signCount)
|
|
120
|
+
|
|
121
|
+
if (attested === null) {
|
|
122
|
+
return concat(rpIdHash, Uint8Array.from([flags]), counter)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const idLength = new Uint8Array(2)
|
|
126
|
+
new DataView(idLength.buffer).setUint16(0, credentialId.length)
|
|
127
|
+
|
|
128
|
+
return concat(
|
|
129
|
+
rpIdHash,
|
|
130
|
+
Uint8Array.from([flags]),
|
|
131
|
+
counter,
|
|
132
|
+
new Uint8Array(16),
|
|
133
|
+
idLength,
|
|
134
|
+
credentialId,
|
|
135
|
+
attested,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
credentialId: encodeBase64Url(credentialId),
|
|
141
|
+
publicKey: encodeBase64Url(coseKey),
|
|
142
|
+
|
|
143
|
+
async register(request): Promise<FixtureResponse> {
|
|
144
|
+
const authData = await authenticatorData(
|
|
145
|
+
request.rpId ?? input.rpId,
|
|
146
|
+
request.flags ?? 0b0100_0101,
|
|
147
|
+
0,
|
|
148
|
+
coseKey,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
clientDataJSON: clientData(
|
|
153
|
+
request.type ?? 'webauthn.create',
|
|
154
|
+
request.challenge,
|
|
155
|
+
request.origin,
|
|
156
|
+
),
|
|
157
|
+
attestationObject: encodeBase64Url(
|
|
158
|
+
encodeCbor(
|
|
159
|
+
new Map<string, unknown>([
|
|
160
|
+
['fmt', 'none'],
|
|
161
|
+
['attStmt', new Map()],
|
|
162
|
+
['authData', authData],
|
|
163
|
+
]),
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async assert(request): Promise<FixtureAssertion> {
|
|
170
|
+
const authData = await authenticatorData(
|
|
171
|
+
request.rpId ?? input.rpId,
|
|
172
|
+
request.flags ?? 0b0000_0101,
|
|
173
|
+
request.signCount ?? 1,
|
|
174
|
+
null,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const json = clientData(request.type ?? 'webauthn.get', request.challenge, request.origin)
|
|
178
|
+
|
|
179
|
+
const clientDataHash = new Uint8Array(
|
|
180
|
+
await crypto.subtle.digest('SHA-256', decodeBase64Url(json)),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const raw = new Uint8Array(
|
|
184
|
+
await crypto.subtle.sign(
|
|
185
|
+
signParams(algorithm),
|
|
186
|
+
pair.privateKey,
|
|
187
|
+
concat(authData, clientDataHash),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const signature = algorithm === 'ES256' ? derFromRaw(raw) : raw
|
|
192
|
+
if (request.tamper === true) {
|
|
193
|
+
signature[signature.length - 1] = (signature[signature.length - 1] ?? 0) ^ 0xff
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
clientDataJSON: json,
|
|
198
|
+
authenticatorData: encodeBase64Url(authData),
|
|
199
|
+
signature: encodeBase64Url(signature),
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function generateParams(
|
|
206
|
+
algorithm: FixtureAlgorithm,
|
|
207
|
+
): EcKeyGenParams | RsaHashedKeyGenParams | Algorithm {
|
|
208
|
+
if (algorithm === 'ES256') return { name: 'ECDSA', namedCurve: 'P-256' }
|
|
209
|
+
if (algorithm === 'EdDSA') return { name: 'Ed25519' }
|
|
210
|
+
return {
|
|
211
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
212
|
+
modulusLength: 2048,
|
|
213
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
214
|
+
hash: 'SHA-256',
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function signParams(algorithm: FixtureAlgorithm): EcdsaParams | Algorithm {
|
|
219
|
+
if (algorithm === 'ES256') return { name: 'ECDSA', hash: 'SHA-256' }
|
|
220
|
+
if (algorithm === 'EdDSA') return { name: 'Ed25519' }
|
|
221
|
+
return { name: 'RSASSA-PKCS1-v1_5' }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function coseEntries(algorithm: FixtureAlgorithm, jwk: JsonWebKey): Map<number, unknown> {
|
|
225
|
+
if (algorithm === 'ES256') {
|
|
226
|
+
return new Map<number, unknown>([
|
|
227
|
+
[1, 2],
|
|
228
|
+
[3, -7],
|
|
229
|
+
[-1, 1],
|
|
230
|
+
[-2, decodeBase64Url(jwk.x!)],
|
|
231
|
+
[-3, decodeBase64Url(jwk.y!)],
|
|
232
|
+
])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (algorithm === 'EdDSA') {
|
|
236
|
+
return new Map<number, unknown>([
|
|
237
|
+
[1, 1],
|
|
238
|
+
[3, -8],
|
|
239
|
+
[-1, 6],
|
|
240
|
+
[-2, decodeBase64Url(jwk.x!)],
|
|
241
|
+
])
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new Map<number, unknown>([
|
|
245
|
+
[1, 3],
|
|
246
|
+
[3, -257],
|
|
247
|
+
[-1, decodeBase64Url(jwk.n!)],
|
|
248
|
+
[-2, decodeBase64Url(jwk.e!)],
|
|
249
|
+
])
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function derFromRaw(raw: Uint8Array): Uint8Array {
|
|
253
|
+
const half = raw.length / 2
|
|
254
|
+
const r = trim(raw.slice(0, half))
|
|
255
|
+
const s = trim(raw.slice(half))
|
|
256
|
+
|
|
257
|
+
const body = concat(Uint8Array.from([0x02, r.length]), r, Uint8Array.from([0x02, s.length]), s)
|
|
258
|
+
|
|
259
|
+
return concat(Uint8Array.from([0x30, body.length]), body)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function trim(value: Uint8Array): Uint8Array {
|
|
263
|
+
let start = 0
|
|
264
|
+
while (start < value.length - 1 && value[start] === 0) start += 1
|
|
265
|
+
|
|
266
|
+
const trimmed = value.slice(start)
|
|
267
|
+
return trimmed[0]! > 0x7f ? concat(Uint8Array.from([0]), trimmed) : trimmed
|
|
268
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export type CborValue =
|
|
2
|
+
| number
|
|
3
|
+
| bigint
|
|
4
|
+
| string
|
|
5
|
+
| boolean
|
|
6
|
+
| null
|
|
7
|
+
| Uint8Array
|
|
8
|
+
| readonly CborValue[]
|
|
9
|
+
| ReadonlyMap<CborValue, CborValue>
|
|
10
|
+
|
|
11
|
+
export interface CborRead {
|
|
12
|
+
readonly value: CborValue
|
|
13
|
+
readonly length: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function decodeCbor(bytes: Uint8Array, offset = 0): CborRead {
|
|
17
|
+
const start = offset
|
|
18
|
+
const initial = byteAt(bytes, offset)
|
|
19
|
+
offset += 1
|
|
20
|
+
|
|
21
|
+
const major = initial >> 5
|
|
22
|
+
const minor = initial & 0b11111
|
|
23
|
+
|
|
24
|
+
if (major === 7) {
|
|
25
|
+
if (minor === 20) return { value: false, length: offset - start }
|
|
26
|
+
if (minor === 21) return { value: true, length: offset - start }
|
|
27
|
+
if (minor === 22) return { value: null, length: offset - start }
|
|
28
|
+
if (minor === 23) return { value: null, length: offset - start }
|
|
29
|
+
throw new Error(`unsupported CBOR simple value ${minor}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const head = readArgument(bytes, offset, minor)
|
|
33
|
+
offset = head.offset
|
|
34
|
+
const argument = head.value
|
|
35
|
+
|
|
36
|
+
if (major === 0) return { value: argument, length: offset - start }
|
|
37
|
+
if (major === 1) return { value: -1 - argument, length: offset - start }
|
|
38
|
+
|
|
39
|
+
if (major === 2) {
|
|
40
|
+
const end = offset + argument
|
|
41
|
+
if (end > bytes.length) throw new Error('truncated CBOR byte string')
|
|
42
|
+
return { value: bytes.slice(offset, end), length: end - start }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (major === 3) {
|
|
46
|
+
const end = offset + argument
|
|
47
|
+
if (end > bytes.length) throw new Error('truncated CBOR text string')
|
|
48
|
+
return {
|
|
49
|
+
value: new TextDecoder().decode(bytes.subarray(offset, end)),
|
|
50
|
+
length: end - start,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (major === 4) {
|
|
55
|
+
const items: CborValue[] = []
|
|
56
|
+
for (let index = 0; index < argument; index += 1) {
|
|
57
|
+
const item = decodeCbor(bytes, offset)
|
|
58
|
+
items.push(item.value)
|
|
59
|
+
offset += item.length
|
|
60
|
+
}
|
|
61
|
+
return { value: items, length: offset - start }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (major === 5) {
|
|
65
|
+
const entries = new Map<CborValue, CborValue>()
|
|
66
|
+
for (let index = 0; index < argument; index += 1) {
|
|
67
|
+
const key = decodeCbor(bytes, offset)
|
|
68
|
+
offset += key.length
|
|
69
|
+
const value = decodeCbor(bytes, offset)
|
|
70
|
+
offset += value.length
|
|
71
|
+
entries.set(key.value, value.value)
|
|
72
|
+
}
|
|
73
|
+
return { value: entries, length: offset - start }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`unsupported CBOR major type ${major}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readArgument(
|
|
80
|
+
bytes: Uint8Array,
|
|
81
|
+
offset: number,
|
|
82
|
+
minor: number,
|
|
83
|
+
): { value: number; offset: number } {
|
|
84
|
+
if (minor < 24) return { value: minor, offset }
|
|
85
|
+
if (minor === 24) return { value: byteAt(bytes, offset), offset: offset + 1 }
|
|
86
|
+
if (minor === 25) {
|
|
87
|
+
return { value: (byteAt(bytes, offset) << 8) | byteAt(bytes, offset + 1), offset: offset + 2 }
|
|
88
|
+
}
|
|
89
|
+
if (minor === 26) {
|
|
90
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 4)
|
|
91
|
+
return { value: view.getUint32(0), offset: offset + 4 }
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`unsupported CBOR argument width ${minor}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function byteAt(bytes: Uint8Array, offset: number): number {
|
|
97
|
+
const value = bytes[offset]
|
|
98
|
+
if (value === undefined) throw new Error('truncated CBOR value')
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function cborMap(value: CborValue): ReadonlyMap<CborValue, CborValue> {
|
|
103
|
+
if (!(value instanceof Map)) throw new Error('expected a CBOR map')
|
|
104
|
+
return value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function cborBytes(value: CborValue | undefined): Uint8Array {
|
|
108
|
+
if (!(value instanceof Uint8Array)) throw new Error('expected a CBOR byte string')
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { encodeBase64Url } from '../crypto/base64url'
|
|
2
|
+
import { type CborValue, cborMap, decodeCbor } from './cbor'
|
|
3
|
+
|
|
4
|
+
export type CoseAlgorithm = -7 | -8 | -257
|
|
5
|
+
|
|
6
|
+
export const SUPPORTED_ALGORITHMS: readonly CoseAlgorithm[] = [-7, -257, -8]
|
|
7
|
+
|
|
8
|
+
const KTY = 1
|
|
9
|
+
const ALG = 3
|
|
10
|
+
const CRV = -1
|
|
11
|
+
const X = -2
|
|
12
|
+
const Y = -3
|
|
13
|
+
const RSA_MODULUS = -1
|
|
14
|
+
const RSA_EXPONENT = -2
|
|
15
|
+
|
|
16
|
+
interface ImportedKey {
|
|
17
|
+
readonly jwk: JsonWebKey
|
|
18
|
+
readonly importParams: RsaHashedImportParams | EcKeyImportParams | AlgorithmIdentifier
|
|
19
|
+
readonly verifyParams: AlgorithmIdentifier | EcdsaParams
|
|
20
|
+
readonly derSignature: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function algorithmOf(publicKey: Uint8Array): CoseAlgorithm {
|
|
24
|
+
const key = cborMap(decodeCbor(publicKey).value)
|
|
25
|
+
const alg = key.get(ALG)
|
|
26
|
+
if (alg === -7 || alg === -8 || alg === -257) return alg
|
|
27
|
+
throw new Error('unsupported COSE algorithm')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function importCoseKey(publicKey: Uint8Array): Promise<{
|
|
31
|
+
readonly key: CryptoKey
|
|
32
|
+
readonly verifyParams: AlgorithmIdentifier | EcdsaParams
|
|
33
|
+
readonly derSignature: boolean
|
|
34
|
+
}> {
|
|
35
|
+
const described = describe(cborMap(decodeCbor(publicKey).value))
|
|
36
|
+
|
|
37
|
+
const key = await crypto.subtle.importKey('jwk', described.jwk, described.importParams, false, [
|
|
38
|
+
'verify',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
key,
|
|
43
|
+
verifyParams: described.verifyParams,
|
|
44
|
+
derSignature: described.derSignature,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function describe(key: ReadonlyMap<CborValue, CborValue>): ImportedKey {
|
|
49
|
+
const kty = key.get(KTY)
|
|
50
|
+
const alg = key.get(ALG)
|
|
51
|
+
|
|
52
|
+
if (kty === 2 && alg === -7) {
|
|
53
|
+
if (key.get(CRV) !== 1) throw new Error('unsupported elliptic curve')
|
|
54
|
+
return {
|
|
55
|
+
jwk: {
|
|
56
|
+
kty: 'EC',
|
|
57
|
+
crv: 'P-256',
|
|
58
|
+
x: coordinate(key.get(X)),
|
|
59
|
+
y: coordinate(key.get(Y)),
|
|
60
|
+
},
|
|
61
|
+
importParams: { name: 'ECDSA', namedCurve: 'P-256' },
|
|
62
|
+
verifyParams: { name: 'ECDSA', hash: 'SHA-256' },
|
|
63
|
+
derSignature: true,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (kty === 3 && alg === -257) {
|
|
68
|
+
return {
|
|
69
|
+
jwk: {
|
|
70
|
+
kty: 'RSA',
|
|
71
|
+
n: coordinate(key.get(RSA_MODULUS)),
|
|
72
|
+
e: coordinate(key.get(RSA_EXPONENT)),
|
|
73
|
+
alg: 'RS256',
|
|
74
|
+
},
|
|
75
|
+
importParams: { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
|
|
76
|
+
verifyParams: { name: 'RSASSA-PKCS1-v1_5' },
|
|
77
|
+
derSignature: false,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (kty === 1 && alg === -8) {
|
|
82
|
+
if (key.get(CRV) !== 6) throw new Error('unsupported edwards curve')
|
|
83
|
+
return {
|
|
84
|
+
jwk: { kty: 'OKP', crv: 'Ed25519', x: coordinate(key.get(X)) } as JsonWebKey,
|
|
85
|
+
importParams: { name: 'Ed25519' },
|
|
86
|
+
verifyParams: { name: 'Ed25519' },
|
|
87
|
+
derSignature: false,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error('unsupported COSE key type')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function coordinate(value: CborValue | undefined): string {
|
|
95
|
+
if (!(value instanceof Uint8Array)) throw new Error('malformed COSE key')
|
|
96
|
+
return encodeBase64Url(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function rawSignatureFromDer(signature: Uint8Array, coordinateBytes = 32): Uint8Array {
|
|
100
|
+
if (signature[0] !== 0x30) throw new Error('malformed ECDSA signature')
|
|
101
|
+
|
|
102
|
+
let offset = 2
|
|
103
|
+
if (signature[1] !== undefined && signature[1] > 0x80) offset += signature[1] - 0x80
|
|
104
|
+
|
|
105
|
+
const readInteger = (): Uint8Array => {
|
|
106
|
+
if (signature[offset] !== 0x02) throw new Error('malformed ECDSA signature')
|
|
107
|
+
const length = signature[offset + 1]
|
|
108
|
+
if (length === undefined) throw new Error('malformed ECDSA signature')
|
|
109
|
+
const start = offset + 2
|
|
110
|
+
offset = start + length
|
|
111
|
+
return signature.slice(start, offset)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const r = readInteger()
|
|
115
|
+
const s = readInteger()
|
|
116
|
+
|
|
117
|
+
const out = new Uint8Array(coordinateBytes * 2)
|
|
118
|
+
out.set(pad(r, coordinateBytes), 0)
|
|
119
|
+
out.set(pad(s, coordinateBytes), coordinateBytes)
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pad(value: Uint8Array, size: number): Uint8Array {
|
|
124
|
+
const trimmed = value[0] === 0 ? value.slice(1) : value
|
|
125
|
+
if (trimmed.length > size) throw new Error('malformed ECDSA signature')
|
|
126
|
+
|
|
127
|
+
const out = new Uint8Array(size)
|
|
128
|
+
out.set(trimmed, size - trimmed.length)
|
|
129
|
+
return out
|
|
130
|
+
}
|