+31
docker-compose.yml
+31
docker-compose.yml
···
1
+
services:
2
+
plc:
3
+
build:
4
+
context: https://github.com/did-method-plc/did-method-plc.git
5
+
dockerfile: packages/server/Dockerfile
6
+
ports:
7
+
- "2582:2582"
8
+
environment:
9
+
- DATABASE_URL=postgres://plc:plc@postgres:5432/plc
10
+
- PORT=2582
11
+
command: ["dumb-init", "node", "--enable-source-maps", "../dist/bin.js"]
12
+
depends_on:
13
+
postgres:
14
+
condition: service_healthy
15
+
16
+
postgres:
17
+
image: postgres:16-alpine
18
+
environment:
19
+
- POSTGRES_USER=plc
20
+
- POSTGRES_PASSWORD=plc
21
+
- POSTGRES_DB=plc
22
+
volumes:
23
+
- plc_data:/var/lib/postgresql/data
24
+
healthcheck:
25
+
test: ["CMD-SHELL", "pg_isready -U plc"]
26
+
interval: 2s
27
+
timeout: 5s
28
+
retries: 10
29
+
30
+
volumes:
31
+
plc_data:
+19
-214
scripts/setup.js
+19
-214
scripts/setup.js
···
4
4
* PDS Setup Script
5
5
*
6
6
* Registers a did:plc, initializes the PDS, and notifies the relay.
7
-
* Zero dependencies - uses Node.js built-ins only.
8
7
*
9
8
* Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev
10
9
*/
11
10
12
-
import { webcrypto } from 'node:crypto';
13
11
import { writeFileSync } from 'node:fs';
12
+
import {
13
+
base32Encode,
14
+
base64UrlEncode,
15
+
bytesToHex,
16
+
cborEncodeDagCbor,
17
+
generateKeyPair,
18
+
importPrivateKey,
19
+
sign,
20
+
} from '../src/pds.js';
14
21
15
22
// === ARGUMENT PARSING ===
16
23
···
57
64
return opts;
58
65
}
59
66
60
-
// === KEY GENERATION ===
61
-
62
-
async function generateP256Keypair() {
63
-
const keyPair = await webcrypto.subtle.generateKey(
64
-
{ name: 'ECDSA', namedCurve: 'P-256' },
65
-
true,
66
-
['sign', 'verify'],
67
-
);
68
-
69
-
// Export private key as raw 32 bytes
70
-
const privateJwk = await webcrypto.subtle.exportKey(
71
-
'jwk',
72
-
keyPair.privateKey,
73
-
);
74
-
const privateBytes = base64UrlDecode(privateJwk.d);
75
-
76
-
// Export public key as uncompressed point (65 bytes)
77
-
const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey);
78
-
const publicBytes = new Uint8Array(publicRaw);
79
-
80
-
// Compress public key to 33 bytes
81
-
const compressedPublic = compressPublicKey(publicBytes);
82
-
83
-
return {
84
-
privateKey: privateBytes,
85
-
publicKey: compressedPublic,
86
-
cryptoKey: keyPair.privateKey,
87
-
};
88
-
}
89
-
90
-
function compressPublicKey(uncompressed) {
91
-
// uncompressed is 65 bytes: 0x04 + x(32) + y(32)
92
-
const x = uncompressed.slice(1, 33);
93
-
const y = uncompressed.slice(33, 65);
94
-
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03;
95
-
const compressed = new Uint8Array(33);
96
-
compressed[0] = prefix;
97
-
compressed.set(x, 1);
98
-
return compressed;
99
-
}
100
-
101
-
function base64UrlDecode(str) {
102
-
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
103
-
const binary = atob(base64);
104
-
const bytes = new Uint8Array(binary.length);
105
-
for (let i = 0; i < binary.length; i++) {
106
-
bytes[i] = binary.charCodeAt(i);
107
-
}
108
-
return bytes;
109
-
}
110
-
111
-
function bytesToHex(bytes) {
112
-
return Array.from(bytes)
113
-
.map((b) => b.toString(16).padStart(2, '0'))
114
-
.join('');
115
-
}
116
67
117
68
// === DID:KEY ENCODING ===
118
69
···
164
115
return result;
165
116
}
166
117
167
-
// === CBOR ENCODING (dag-cbor compliant for PLC operations) ===
168
-
169
-
function cborEncodeKey(key) {
170
-
// Encode a string key to CBOR bytes (for sorting)
171
-
const bytes = new TextEncoder().encode(key);
172
-
const parts = [];
173
-
const mt = 3 << 5; // major type 3 = text string
174
-
if (bytes.length < 24) {
175
-
parts.push(mt | bytes.length);
176
-
} else if (bytes.length < 256) {
177
-
parts.push(mt | 24, bytes.length);
178
-
} else if (bytes.length < 65536) {
179
-
parts.push(mt | 25, bytes.length >> 8, bytes.length & 0xff);
180
-
}
181
-
parts.push(...bytes);
182
-
return new Uint8Array(parts);
183
-
}
184
-
185
-
function compareBytes(a, b) {
186
-
// dag-cbor: bytewise lexicographic order of encoded keys
187
-
const minLen = Math.min(a.length, b.length);
188
-
for (let i = 0; i < minLen; i++) {
189
-
if (a[i] !== b[i]) return a[i] - b[i];
190
-
}
191
-
return a.length - b.length;
192
-
}
193
-
194
-
function cborEncode(value) {
195
-
const parts = [];
196
-
197
-
function encode(val) {
198
-
if (val === null) {
199
-
parts.push(0xf6);
200
-
} else if (typeof val === 'string') {
201
-
const bytes = new TextEncoder().encode(val);
202
-
encodeHead(3, bytes.length);
203
-
parts.push(...bytes);
204
-
} else if (typeof val === 'number') {
205
-
if (Number.isInteger(val) && val >= 0) {
206
-
encodeHead(0, val);
207
-
}
208
-
} else if (val instanceof Uint8Array) {
209
-
encodeHead(2, val.length);
210
-
parts.push(...val);
211
-
} else if (Array.isArray(val)) {
212
-
encodeHead(4, val.length);
213
-
for (const item of val) encode(item);
214
-
} else if (typeof val === 'object') {
215
-
// dag-cbor: sort keys by their CBOR-encoded bytes (length first, then lexicographic)
216
-
const keys = Object.keys(val);
217
-
const keysSorted = keys.sort((a, b) =>
218
-
compareBytes(cborEncodeKey(a), cborEncodeKey(b)),
219
-
);
220
-
encodeHead(5, keysSorted.length);
221
-
for (const key of keysSorted) {
222
-
encode(key);
223
-
encode(val[key]);
224
-
}
225
-
}
226
-
}
227
-
228
-
function encodeHead(majorType, length) {
229
-
const mt = majorType << 5;
230
-
if (length < 24) {
231
-
parts.push(mt | length);
232
-
} else if (length < 256) {
233
-
parts.push(mt | 24, length);
234
-
} else if (length < 65536) {
235
-
parts.push(mt | 25, length >> 8, length & 0xff);
236
-
}
237
-
}
238
-
239
-
encode(value);
240
-
return new Uint8Array(parts);
241
-
}
242
-
243
118
// === HASHING ===
244
119
245
120
async function sha256(data) {
246
-
const hash = await webcrypto.subtle.digest('SHA-256', data);
121
+
const hash = await crypto.subtle.digest('SHA-256', data);
247
122
return new Uint8Array(hash);
248
123
}
249
124
250
125
// === PLC OPERATIONS ===
251
126
252
-
async function signPlcOperation(operation, privateKey) {
127
+
async function signPlcOperation(operation, cryptoKey) {
253
128
// Encode operation without sig field
254
129
const { sig, ...opWithoutSig } = operation;
255
-
const encoded = cborEncode(opWithoutSig);
256
-
257
-
// Sign with P-256
258
-
const signature = await webcrypto.subtle.sign(
259
-
{ name: 'ECDSA', hash: 'SHA-256' },
260
-
privateKey,
261
-
encoded,
262
-
);
263
-
264
-
// Convert to low-S form and base64url encode
265
-
const sigBytes = ensureLowS(new Uint8Array(signature));
266
-
return base64UrlEncode(sigBytes);
267
-
}
268
-
269
-
function ensureLowS(sig) {
270
-
// P-256 order N
271
-
const N = BigInt(
272
-
'0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
273
-
);
274
-
const halfN = N / 2n;
275
-
276
-
const r = sig.slice(0, 32);
277
-
const s = sig.slice(32, 64);
278
-
279
-
// Convert s to BigInt
280
-
let sInt = BigInt(`0x${bytesToHex(s)}`);
281
-
282
-
// If s > N/2, replace with N - s
283
-
if (sInt > halfN) {
284
-
sInt = N - sInt;
285
-
const newS = hexToBytes(sInt.toString(16).padStart(64, '0'));
286
-
const result = new Uint8Array(64);
287
-
result.set(r);
288
-
result.set(newS, 32);
289
-
return result;
290
-
}
130
+
const encoded = cborEncodeDagCbor(opWithoutSig);
291
131
292
-
return sig;
293
-
}
294
-
295
-
function hexToBytes(hex) {
296
-
const bytes = new Uint8Array(hex.length / 2);
297
-
for (let i = 0; i < hex.length; i += 2) {
298
-
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
299
-
}
300
-
return bytes;
301
-
}
302
-
303
-
function base64UrlEncode(bytes) {
304
-
const binary = String.fromCharCode(...bytes);
305
-
return btoa(binary)
306
-
.replace(/\+/g, '-')
307
-
.replace(/\//g, '_')
308
-
.replace(/=+$/, '');
132
+
// Sign with P-256 (sign() handles low-S normalization)
133
+
const signature = await sign(cryptoKey, encoded);
134
+
return base64UrlEncode(signature);
309
135
}
310
136
311
137
async function createGenesisOperation(opts) {
···
339
165
340
166
async function deriveDidFromOperation(operation) {
341
167
// DID is computed from the FULL operation INCLUDING the signature
342
-
const encoded = cborEncode(operation);
168
+
const encoded = cborEncodeDagCbor(operation);
343
169
const hash = await sha256(encoded);
344
170
// DID is base32 of first 15 bytes of hash (= 24 base32 chars)
345
171
return `did:plc:${base32Encode(hash.slice(0, 15))}`;
346
-
}
347
-
348
-
function base32Encode(bytes) {
349
-
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
350
-
let result = '';
351
-
let bits = 0;
352
-
let value = 0;
353
-
354
-
for (const byte of bytes) {
355
-
value = (value << 8) | byte;
356
-
bits += 8;
357
-
while (bits >= 5) {
358
-
bits -= 5;
359
-
result += alphabet[(value >> bits) & 31];
360
-
}
361
-
}
362
-
363
-
if (bits > 0) {
364
-
result += alphabet[(value << (5 - bits)) & 31];
365
-
}
366
-
367
-
return result;
368
172
}
369
173
370
174
// === PLC DIRECTORY REGISTRATION ===
···
479
283
480
284
// Step 1: Generate keypair
481
285
console.log('Generating P-256 keypair...');
482
-
const keyPair = await generateP256Keypair();
286
+
const keyPair = await generateKeyPair();
287
+
const cryptoKey = await importPrivateKey(keyPair.privateKey);
483
288
const didKey = publicKeyToDidKey(keyPair.publicKey);
484
289
console.log(` did:key: ${didKey}`);
485
290
console.log('');
···
490
295
didKey,
491
296
handle: opts.handle,
492
297
pdsUrl: opts.pds,
493
-
cryptoKey: keyPair.cryptoKey,
298
+
cryptoKey,
494
299
});
495
300
const did = await deriveDidFromOperation(operation);
496
301
console.log(` DID: ${did}`);