+100
-1
src/pds.js
+100
-1
src/pds.js
···
138
138
return tid
139
139
}
140
140
141
+
// === P-256 SIGNING ===
142
+
// Web Crypto ECDSA with P-256 curve
143
+
144
+
async function importPrivateKey(privateKeyBytes) {
145
+
// PKCS#8 wrapper for raw P-256 private key
146
+
const pkcs8Prefix = new Uint8Array([
147
+
0x30, 0x41, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48,
148
+
0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03,
149
+
0x01, 0x07, 0x04, 0x27, 0x30, 0x25, 0x02, 0x01, 0x01, 0x04, 0x20
150
+
])
151
+
152
+
const pkcs8 = new Uint8Array(pkcs8Prefix.length + 32)
153
+
pkcs8.set(pkcs8Prefix)
154
+
pkcs8.set(privateKeyBytes, pkcs8Prefix.length)
155
+
156
+
return crypto.subtle.importKey(
157
+
'pkcs8',
158
+
pkcs8,
159
+
{ name: 'ECDSA', namedCurve: 'P-256' },
160
+
false,
161
+
['sign']
162
+
)
163
+
}
164
+
165
+
async function sign(privateKey, data) {
166
+
const signature = await crypto.subtle.sign(
167
+
{ name: 'ECDSA', hash: 'SHA-256' },
168
+
privateKey,
169
+
data
170
+
)
171
+
return new Uint8Array(signature)
172
+
}
173
+
174
+
async function generateKeyPair() {
175
+
const keyPair = await crypto.subtle.generateKey(
176
+
{ name: 'ECDSA', namedCurve: 'P-256' },
177
+
true,
178
+
['sign', 'verify']
179
+
)
180
+
181
+
// Export private key as raw bytes
182
+
const privateJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
183
+
const privateBytes = base64UrlDecode(privateJwk.d)
184
+
185
+
// Export public key as compressed point
186
+
const publicRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey)
187
+
const publicBytes = new Uint8Array(publicRaw)
188
+
const compressed = compressPublicKey(publicBytes)
189
+
190
+
return { privateKey: privateBytes, publicKey: compressed }
191
+
}
192
+
193
+
function compressPublicKey(uncompressed) {
194
+
// uncompressed is 65 bytes: 0x04 + x(32) + y(32)
195
+
// compressed is 33 bytes: prefix(02 or 03) + x(32)
196
+
const x = uncompressed.slice(1, 33)
197
+
const y = uncompressed.slice(33, 65)
198
+
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03
199
+
const compressed = new Uint8Array(33)
200
+
compressed[0] = prefix
201
+
compressed.set(x, 1)
202
+
return compressed
203
+
}
204
+
205
+
function base64UrlDecode(str) {
206
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
207
+
const binary = atob(base64)
208
+
const bytes = new Uint8Array(binary.length)
209
+
for (let i = 0; i < binary.length; i++) {
210
+
bytes[i] = binary.charCodeAt(i)
211
+
}
212
+
return bytes
213
+
}
214
+
215
+
function bytesToHex(bytes) {
216
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
217
+
}
218
+
219
+
function hexToBytes(hex) {
220
+
const bytes = new Uint8Array(hex.length / 2)
221
+
for (let i = 0; i < hex.length; i += 2) {
222
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
223
+
}
224
+
return bytes
225
+
}
226
+
141
227
export class PersonalDataServer {
142
228
constructor(state, env) {
143
229
this.state = state
···
200
286
`).toArray()
201
287
return Response.json({ tables: tables.map(t => t.name) })
202
288
}
289
+
if (url.pathname === '/test/sign') {
290
+
const kp = await generateKeyPair()
291
+
const data = new TextEncoder().encode('test message')
292
+
const key = await importPrivateKey(kp.privateKey)
293
+
const sig = await sign(key, data)
294
+
return Response.json({
295
+
publicKey: bytesToHex(kp.publicKey),
296
+
signature: bytesToHex(sig)
297
+
})
298
+
}
203
299
return new Response('pds running', { status: 200 })
204
300
}
205
301
}
···
220
316
}
221
317
222
318
// Export utilities for testing
223
-
export { cborEncode, createCid, cidToString, base32Encode, createTid }
319
+
export {
320
+
cborEncode, createCid, cidToString, base32Encode, createTid,
321
+
generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes
322
+
}
+42
-1
test/pds.test.js
+42
-1
test/pds.test.js
···
1
1
import { test, describe } from 'node:test'
2
2
import assert from 'node:assert'
3
-
import { cborEncode, createCid, cidToString, base32Encode, createTid } from '../src/pds.js'
3
+
import {
4
+
cborEncode, createCid, cidToString, base32Encode, createTid,
5
+
generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes
6
+
} from '../src/pds.js'
4
7
5
8
describe('CBOR Encoding', () => {
6
9
test('encodes simple map', () => {
···
142
145
assert.strictEqual(tids.size, 100)
143
146
})
144
147
})
148
+
149
+
describe('P-256 Signing', () => {
150
+
test('generates key pair with correct sizes', async () => {
151
+
const kp = await generateKeyPair()
152
+
153
+
assert.strictEqual(kp.privateKey.length, 32)
154
+
assert.strictEqual(kp.publicKey.length, 33) // compressed
155
+
assert.ok(kp.publicKey[0] === 0x02 || kp.publicKey[0] === 0x03)
156
+
})
157
+
158
+
test('can sign data with generated key', async () => {
159
+
const kp = await generateKeyPair()
160
+
const key = await importPrivateKey(kp.privateKey)
161
+
const data = new TextEncoder().encode('test message')
162
+
const sig = await sign(key, data)
163
+
164
+
assert.strictEqual(sig.length, 64) // r (32) + s (32)
165
+
})
166
+
167
+
test('different messages produce different signatures', async () => {
168
+
const kp = await generateKeyPair()
169
+
const key = await importPrivateKey(kp.privateKey)
170
+
171
+
const sig1 = await sign(key, new TextEncoder().encode('message 1'))
172
+
const sig2 = await sign(key, new TextEncoder().encode('message 2'))
173
+
174
+
assert.notDeepStrictEqual(sig1, sig2)
175
+
})
176
+
177
+
test('bytesToHex and hexToBytes roundtrip', () => {
178
+
const original = new Uint8Array([0x00, 0x0f, 0xf0, 0xff, 0xab, 0xcd])
179
+
const hex = bytesToHex(original)
180
+
const back = hexToBytes(hex)
181
+
182
+
assert.strictEqual(hex, '000ff0ffabcd')
183
+
assert.deepStrictEqual(back, original)
184
+
})
185
+
})