@dotrino/vaultd 0.117.0 → 0.118.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.
@@ -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, verifyDelegation,
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, sealersOf, memberCan } from '@dotrino/identity/acta'
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
- const acta = res.acta
404
- if (!acta) throw new Error('the vault did not send its record: cannot check who signed this cert')
405
- // Y el acta tiene que ser la DEL PERFIL DEL QR. Antes esto se comprobaba mirando que el
406
- // cert lo firmara `qr.iss`; con varias selladoras el emisor puede ser otra llave del
407
- // mismo perfil, así que lo que se fija es el perfil, no la llave.
408
- if (acta.profileId !== qr.iss) throw new Error('the record is from a profile other than the one in the QR')
409
- const v = await verifyDelegation({
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 (!v.ok) throw new Error('invalid cert: ' + v.reason)
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
- if (res.type === MSG.RENEWED && res.cert?.sub === device.publickey) {
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.117.0",
3
+ "version": "0.118.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.81.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.2.0",
28
+ "@dotrino/roadmap": "0.3.3",
29
29
  "@dotrino/store": "0.11.0",
30
30
  "ws": "^8.18.0"
31
31
  },
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, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from '@dotrino/identity/capabilities'
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
- import { sealersOf } from '@dotrino/identity/acta'
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
- if (res.acta?.profileId !== qr.iss) throw new Error('the record is from a profile other than the one you saw (possible malicious proxy)')
107
- const v = await verifyDelegation({ cert: res.cert, expectedSub: device.publickey, ...contexto(res.acta) })
108
- if (!v.ok) throw new Error('invalid cert: ' + v.reason)
109
- if (res.cert.sub !== device.publickey) throw new Error('cert issued for a different device')
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 está
172
- // firmado por la maestra que ya conocemos, o que no es para esta llave, no se guarda.
173
- if (res.acta?.profileId !== masterPubkey) throw new Error('the record is from a profile other than the pinned one')
174
- const v = await verifyDelegation({ cert: res.cert, expectedSub: device.publickey, ...contexto(res.acta) })
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
  }