@dotrino/vaultd 0.117.0 → 0.119.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/lib/src/enroll.js +39 -12
- package/lib/src/service.js +17 -14
- package/package.json +4 -4
- package/src/client.js +12 -22
- package/src/ctl.js +8 -1
package/lib/src/enroll.js
CHANGED
|
@@ -349,23 +349,50 @@ export function createEnrollDesk ({
|
|
|
349
349
|
return { ok: true, deviceId: pend.deviceId, adopting: true }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
353
|
-
|
|
354
352
|
// Aprobar un emparejamiento ES admitir al dispositivo en el perfil: el cert es la
|
|
355
353
|
// credencial y el acta es la política, y no tiene sentido emitir una sin la otra.
|
|
356
354
|
// Las capacidades salen del scope que se pidió al emparejar (cert ∩ acta, §2.3).
|
|
357
|
-
|
|
355
|
+
//
|
|
356
|
+
// NADA DE REPLIEGUES (2026-09-17). Esto antes estaba envuelto en un `catch` que solo
|
|
357
|
+
// anotaba el error, y además se saltaba en silencio si la identidad no sabía admitir o si
|
|
358
|
+
// el scope no daba ninguna capacidad: el aparato recibía su certificado SIN estar en el
|
|
359
|
+
// acta, y el fallo aparecía después y en otro sitio. Ahora, si no se puede admitir, no
|
|
360
|
+
// se entrega nada y se dice.
|
|
361
|
+
if (typeof identity.admitMember !== 'function') {
|
|
362
|
+
throw Object.assign(new Error('this vault cannot add devices to the account record: no certificate was issued'), { code: 'admit-unavailable' })
|
|
363
|
+
}
|
|
364
|
+
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
365
|
+
// cajón (`secrets:<ns>`) suma `secrets` y fija el CN; no borra lo demás — un bot
|
|
366
|
+
// con `sign,secrets:eco` firma como aparato del acta Y lee solo su cajón.
|
|
367
|
+
const cn = scopeToCn(pend.scope)
|
|
368
|
+
const caps = [...new Set([...scopeToCaps(pend.scope), ...(cn ? ['secrets'] : [])])]
|
|
369
|
+
if (!caps.length) {
|
|
370
|
+
throw Object.assign(new Error('the pairing scope grants no permission: no certificate was issued'), { code: 'empty-scope' })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
374
|
+
|
|
358
375
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// El certificado ya está firmado y no se entrega: se revoca, para que no quede uno
|
|
379
|
+
// válido suelto. Si ni eso sale, se dice también.
|
|
380
|
+
let revoked = true
|
|
381
|
+
try { await identity.revokeDelegation(cert.nonce) } catch (re) {
|
|
382
|
+
revoked = false
|
|
383
|
+
log('[vault] could not revoke the undelivered certificate %s: %s', cert.nonce, re.message)
|
|
366
384
|
}
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
audit('rejected', { what: 'approve', device: pend.deviceId, reason: 'admit-failed' })
|
|
386
|
+
reply(pend.from, { type: MSG_ERROR, error: 'the vault could not add this device to the account: pairing failed, try again' })
|
|
387
|
+
pending.delete(pend.token)
|
|
388
|
+
fire(onPendingChange)
|
|
389
|
+
log('[vault] could not add %s to the record, no certificate was delivered: %s', pend.deviceId, e.message)
|
|
390
|
+
throw Object.assign(
|
|
391
|
+
new Error(`could not add the device to the account record (${e.message}): no certificate was delivered${revoked ? '' : ', and the signed one could not be revoked'}`),
|
|
392
|
+
{ code: 'admit-failed', cause: e }
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
const record = (await identity.profileActa?.())?.acta || null
|
|
369
396
|
|
|
370
397
|
audit('enroll', { device: pend.deviceId, label: pend.label || '', scope: pend.scope })
|
|
371
398
|
// Echamos el código tipeado junto al cert: el DISPOSITIVO acepta solo si coincide
|
package/lib/src/service.js
CHANGED
|
@@ -22,10 +22,10 @@ import fs from 'node:fs'
|
|
|
22
22
|
import path from 'node:path'
|
|
23
23
|
import { createHash } from 'node:crypto'
|
|
24
24
|
import {
|
|
25
|
-
makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice,
|
|
25
|
+
makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice,
|
|
26
26
|
verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
|
|
27
27
|
import { openWrap, wrapForMember, decryptWithCek } from '@dotrino/identity/content'
|
|
28
|
-
import { verifyActa, sealKeyAt,
|
|
28
|
+
import { verifyActa, sealKeyAt, memberCan, checkVaultReply } from '@dotrino/identity/acta'
|
|
29
29
|
import { MSG, secretsScope, isValidSecretsNs, VAULT_PROTOCOL, VAULT_SPEAKS } from './protocol.js'
|
|
30
30
|
import { PKG_VERSION } from './version.js'
|
|
31
31
|
import { makeEphemeralKey, openSealed } from './sealed.js'
|
|
@@ -400,18 +400,15 @@ export async function enrollWithVault ({ qr, label = 'agent', expectedScope = nu
|
|
|
400
400
|
// por reloj, dice el `seq` del acta con el que se emitió, y quien lo emitió tiene que
|
|
401
401
|
// ser SELLADORA de ese acta. Sin acta no se puede comprobar nada de eso, y entrar a
|
|
402
402
|
// ciegas en un perfil es justo lo que no se hace.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
cert: res.cert, expectedSub: device.publickey,
|
|
411
|
-
actaSeq: acta.seq, sealers: sealersOf(acta),
|
|
412
|
-
...(expectedScope ? { expectedScope } : {})
|
|
403
|
+
//
|
|
404
|
+
// Se juzga contra la llave de LA BÓVEDA del QR, con la regla del pilar. Antes se exigía
|
|
405
|
+
// `acta.profileId === qr.iss`, que solo es verdad en una cuenta que nació en esa bóveda:
|
|
406
|
+
// un servicio no podía emparejarse con una segunda bóveda ni con una que adoptó la cuenta.
|
|
407
|
+
const chk = await checkVaultReply({
|
|
408
|
+
acta: res.acta, cert: res.cert, vault: qr.iss, sub: device.publickey,
|
|
409
|
+
scope: expectedScope || null, justSealed: true
|
|
413
410
|
})
|
|
414
|
-
if (!
|
|
411
|
+
if (!chk.ok) throw new Error('the vault reply does not check out: ' + chk.reason)
|
|
415
412
|
|
|
416
413
|
return { device, enc: enc ? { publickey: enc.encPublickey, privateJwk: enc.encPrivateJwk } : null, cert: res.cert, iss: qr.iss, proxy: qr.proxy || 'wss://proxy.dotrino.com' }
|
|
417
414
|
} finally { client.close() }
|
|
@@ -599,7 +596,13 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
|
|
|
599
596
|
const pending = waitForMsg(client, (p) => p.type === MSG.RENEWED || p.type === MSG.ERROR, 15000)
|
|
600
597
|
client.sendByPubkey(masterPubkey, { type: MSG.RENEW, data, signature, cert })
|
|
601
598
|
const res = await pending
|
|
602
|
-
|
|
599
|
+
// SE VERIFICA ANTES DE GUARDARLO. Aquí solo se miraba que fuera para esta llave: un
|
|
600
|
+
// papel cualquiera, sin firma que valga, sustituía al bueno y el servicio se quedaba
|
|
601
|
+
// fuera hasta el siguiente arranque. Misma regla que al enrolarse, sin `justSealed`:
|
|
602
|
+
// la última acta pudo sellarla otra selladora de la cuenta.
|
|
603
|
+
const ok = res.type === MSG.RENEWED &&
|
|
604
|
+
(await checkVaultReply({ acta: res.acta, cert: res.cert, vault: masterPubkey, sub: device.publickey })).ok
|
|
605
|
+
if (ok) {
|
|
603
606
|
cert = res.cert
|
|
604
607
|
recordar({ cert })
|
|
605
608
|
// QUIEN NO PASA `dir` GUARDA ÉL. Un servicio que lleva su identidad en su propio
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.119.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@dotrino/compat": "0.4.0",
|
|
25
|
-
"@dotrino/identity": "0.
|
|
25
|
+
"@dotrino/identity": "0.92.0",
|
|
26
26
|
"@dotrino/passmanager": "^0.10.1",
|
|
27
27
|
"@dotrino/proxy-client": "0.18.2",
|
|
28
|
-
"@dotrino/roadmap": "0.
|
|
28
|
+
"@dotrino/roadmap": "0.3.3",
|
|
29
29
|
"@dotrino/store": "0.11.0",
|
|
30
30
|
"ws": "^8.18.0"
|
|
31
31
|
},
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"!lib/src/types.d.ts"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@dotrino/remote-agent": "0.
|
|
51
|
+
"@dotrino/remote-agent": "0.10.0",
|
|
52
52
|
"@types/node": "^22.0.0",
|
|
53
53
|
"typescript": "5.9.3"
|
|
54
54
|
},
|
package/src/client.js
CHANGED
|
@@ -11,19 +11,10 @@
|
|
|
11
11
|
* estrictamente (firmado por la maestra que vio en el QR, y para SU clave) antes de
|
|
12
12
|
* guardarlo. La maestra nunca sale del vault.
|
|
13
13
|
*/
|
|
14
|
-
import { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice,
|
|
14
|
+
import { makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
|
|
15
15
|
import { myContentKey, encryptWithCek, decryptWithKeyring } from '@dotrino/identity/content'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Con qué se juzga un papel: el ACTA que la bóveda manda junto a él. Sustituye a comparar
|
|
20
|
-
* `cert.iss` con la maestra — con varias selladoras el emisor puede ser otra llave del mismo
|
|
21
|
-
* perfil, así que lo que se fija es el PERFIL, no la llave.
|
|
22
|
-
*/
|
|
23
|
-
function contexto (acta) {
|
|
24
|
-
if (!acta) throw new Error('the vault did not send its record: cannot check who signed this cert')
|
|
25
|
-
return { actaSeq: acta.seq, sealers: sealersOf(acta) }
|
|
26
|
-
}
|
|
16
|
+
// Con qué se juzga lo que devuelve la bóveda (papel + acta): la regla del pilar, una sola.
|
|
17
|
+
import { checkVaultReply } from '@dotrino/identity/acta'
|
|
27
18
|
import { installNodeGlobals } from './node-globals.js'
|
|
28
19
|
import { MSG } from './protocol.js'
|
|
29
20
|
|
|
@@ -102,11 +93,11 @@ export async function enroll ({ qr, label = '', dir, onChallenge, approveTimeout
|
|
|
102
93
|
client.sendByPubkey(qr.iss, { type: MSG.ENROLL, data, signature })
|
|
103
94
|
const res = await enrolled
|
|
104
95
|
|
|
105
|
-
// VALIDACIÓN ESTRICTA antes de persistir (cierra inyección de cert / sustitución de maestra)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
96
|
+
// VALIDACIÓN ESTRICTA antes de persistir (cierra inyección de cert / sustitución de maestra),
|
|
97
|
+
// contra la llave de la bóveda del QR y con la regla del pilar: vale también para una
|
|
98
|
+
// segunda bóveda o una que adoptó la cuenta, donde esa llave no es el `profileId`.
|
|
99
|
+
const chk = await checkVaultReply({ acta: res.acta, cert: res.cert, vault: qr.iss, sub: device.publickey, justSealed: true })
|
|
100
|
+
if (!chk.ok) throw new Error('the vault reply does not check out: ' + chk.reason)
|
|
110
101
|
// OJO: devolvemos qr.iss (la maestra que el usuario VIO), NO res.iss.
|
|
111
102
|
return { device: { ...device, encPublickey: enc.encPublickey, encPrivateJwk: enc.encPrivateJwk }, cert: res.cert, iss: qr.iss, acta: res.acta }
|
|
112
103
|
} finally { client.close() }
|
|
@@ -168,11 +159,10 @@ export async function requestRenew ({ masterPubkey, proxyUrl, device, cert, dir
|
|
|
168
159
|
client.sendByPubkey(masterPubkey, { type: MSG.RENEW, data, signature, cert })
|
|
169
160
|
const res = await pending
|
|
170
161
|
if (res.type === MSG.ERROR) throw new Error(res.error)
|
|
171
|
-
// Se valida antes de devolverlo, igual que en el enrolamiento: un cert que no
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!v.ok) throw new Error('invalid cert: ' + v.reason)
|
|
162
|
+
// Se valida antes de devolverlo, igual que en el enrolamiento: un cert que no firmó la
|
|
163
|
+
// bóveda que ya conocemos, o que no es para esta llave, no se guarda.
|
|
164
|
+
const chk = await checkVaultReply({ acta: res.acta, cert: res.cert, vault: masterPubkey, sub: device.publickey })
|
|
165
|
+
if (!chk.ok) throw new Error('invalid renewed cert: ' + chk.reason)
|
|
176
166
|
return { cert: res.cert }
|
|
177
167
|
} finally { client.close() }
|
|
178
168
|
}
|
package/src/ctl.js
CHANGED
|
@@ -1864,7 +1864,14 @@ export async function runCtl (argv) {
|
|
|
1864
1864
|
case 'caps': return cmdCaps(rest)
|
|
1865
1865
|
case 'revoke': return cmdRevoke(rest[0])
|
|
1866
1866
|
case 'secret': return cmdSecret(rest)
|
|
1867
|
-
|
|
1867
|
+
// `approval` se quitó el 2026-09-01 (ahora es el permiso `unattended`), pero la ruta se
|
|
1868
|
+
// quedó apuntando a una función que ya no existía: reventaba con un ReferenceError. Se
|
|
1869
|
+
// rechaza en voz alta, igual que `caps +approval`.
|
|
1870
|
+
case 'approval':
|
|
1871
|
+
console.error('`dotrino-vault approval` ya no existe. Pedir aprobación es el DEFECTO; lo que se concede es lo contrario:')
|
|
1872
|
+
console.error(' dotrino-vault caps <ID> +desatendido ← se lleva las claves privadas sin aprobación')
|
|
1873
|
+
console.error(' dotrino-vault caps <ID> -desatendido ← vuelve a pedir aprobación')
|
|
1874
|
+
process.exit(2)
|
|
1868
1875
|
case 'atrest': return cmdAtrest(rest)
|
|
1869
1876
|
case 'activity': return cmdActivity(Number(rest.find((a) => /^\d+$/.test(a))) || 30, {
|
|
1870
1877
|
verify: rest.includes('--verify'), exportar: rest.includes('--export')
|