@dotrino/vault 0.20.0 → 0.22.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/README.md +12 -1
- package/package.json +1 -1
- package/src/admin.js +13 -4
- package/src/enroll.js +5 -1
- package/src/index.js +8 -2
- package/src/revocation.js +67 -0
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ si la máquina se compromete, revocas el cert y no había nada que robar.
|
|
|
50
50
|
```bash
|
|
51
51
|
# en el VAULT (tu PC): abres el emparejamiento del servicio y cargas sus secretos
|
|
52
52
|
dotrino-vault pair --service miapp # invitación con scope SOLO vault:secrets:miapp
|
|
53
|
-
dotrino-vault secret set miapp API_KEY sk-…
|
|
53
|
+
dotrino-vault secret set miapp API_KEY sk-… # la comparten TODAS las máquinas del ns
|
|
54
54
|
|
|
55
55
|
# en el PROYECTO/servidor: enrola esta máquina (pega la invitación)
|
|
56
56
|
npx dotrino-env enroll --ns miapp
|
|
@@ -60,6 +60,17 @@ npx dotrino-env enroll --ns miapp
|
|
|
60
60
|
dotrino-vault approve 7K3F-92Q1
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
Si la misma app corre en **varias máquinas**, lo que cambia de una a otra (el puerto,
|
|
64
|
+
la URL pública) va en el cajón **por aparato**, sin partir el `ns`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
dotrino-vault devices # el ID del aparato: AB12-CD34
|
|
68
|
+
dotrino-vault secret device set AB12-CD34 PORT 8443 # solo la lee ESA máquina
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Llegan **mezcladas en el mismo bundle** —y por lo tanto en el mismo `process.env`—:
|
|
72
|
+
las del scope, con las del aparato **encima** si se llaman igual.
|
|
73
|
+
|
|
63
74
|
El código lo **genera el servicio** y **no viaja** por la red: el vault solo puede
|
|
64
75
|
echarlo de vuelta si un humano lo tipeó. Así, un vault falso no puede enrolarte y
|
|
65
76
|
aprobar a ciegas no enrola a nadie. Queda `~/.dotrino/service/<ns>/service-identity.json`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/admin.js
CHANGED
|
@@ -127,13 +127,22 @@ export function createAdminDesk ({
|
|
|
127
127
|
return { ok: true, result: { ok: true } }
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
130
|
+
// QUITAR UN DISPOSITIVO se hace por `sub` (su llave): sale del acta Y se le retiran
|
|
131
|
+
// todos los certificados. Las dos cosas o ninguna.
|
|
132
|
+
//
|
|
133
|
+
// `certNonce` retira UN PAPEL y nada más — el aparato sigue siendo miembro. Sirve
|
|
134
|
+
// para eso y solo para eso, y se conserva por compatibilidad, pero no es «quitar»:
|
|
135
|
+
// usarlo para quitar dejaba un miembro sin certificados al que la bóveda ya nunca le
|
|
136
|
+
// mandaba el aviso de expulsión (mientras siga en el acta, un papel retirado
|
|
137
|
+
// significa «renueva»). Ese era el dispositivo fantasma.
|
|
132
138
|
if (data.op === 'revoke') {
|
|
133
139
|
if (data.sub) {
|
|
140
|
+
const deviceId = await deviceIdOf(String(data.sub)).catch(() => null)
|
|
134
141
|
const r = await desk.revokeDevice(String(data.sub))
|
|
135
|
-
audit('admin.revoke-device', { by, certs: r?.nonces?.length ?? null })
|
|
136
|
-
|
|
142
|
+
audit('admin.revoke-device', { by, device: deviceId, certs: r?.nonces?.length ?? null })
|
|
143
|
+
// Con el `deviceId`: el aviso a los demás dispositivos tiene que decir a QUIÉN
|
|
144
|
+
// quitaron, o no se puede saber si el que sobra eres tú.
|
|
145
|
+
await notify('revoked', { deviceId, by })
|
|
137
146
|
return { ok: true, result: r || { ok: true } }
|
|
138
147
|
}
|
|
139
148
|
const r = await desk.revoke(String(data.certNonce || ''))
|
package/src/enroll.js
CHANGED
|
@@ -104,13 +104,16 @@ export async function deviceIdOf (pub) {
|
|
|
104
104
|
* @param {(...a:any[])=>void} [opts.log]
|
|
105
105
|
* @param {(c:{deviceId:string, scope:any, label:string})=>void} [opts.onChallenge] un dispositivo espera aprobación.
|
|
106
106
|
* @param {()=>void} [opts.onPendingChange]
|
|
107
|
+
* @param {(sub:string)=>void} [opts.onDeviceRemoved] se quitó un aparato (fuera del acta y sin papeles):
|
|
108
|
+
* para que quien guarde algo indexado por esa llave lo suelte. Se avisa desde AQUÍ y no desde
|
|
109
|
+
* quien llama porque a `revokeDevice` se entra por dos puertas (el PC y la consola remota).
|
|
107
110
|
* @param {string[]} [opts.defaultScope]
|
|
108
111
|
* @param {number} [opts.defaultTtlMs]
|
|
109
112
|
*/
|
|
110
113
|
export function createEnrollDesk ({
|
|
111
114
|
identity, iss, proxy, send, sendByPubkey,
|
|
112
115
|
audit = () => {}, log = () => {},
|
|
113
|
-
onChallenge = () => {}, onPendingChange = () => {}, onAdopted = () => {},
|
|
116
|
+
onChallenge = () => {}, onPendingChange = () => {}, onAdopted = () => {}, onDeviceRemoved = () => {},
|
|
114
117
|
defaultScope = ['vault:sign'], defaultTtlMs = DEVICE_TTL_MS,
|
|
115
118
|
// Camino A: lo que ESTA bóveda le manda al aparato para que la meta en su acta. `encPub`
|
|
116
119
|
// es su llave de CIFRADO — sin ella entra mandando pero sin poder leer el contenido.
|
|
@@ -461,6 +464,7 @@ export function createEnrollDesk ({
|
|
|
461
464
|
return done
|
|
462
465
|
})() }
|
|
463
466
|
await emitRevoke(sub, mine[0]?.nonce || null)
|
|
467
|
+
fire(onDeviceRemoved, sub)
|
|
464
468
|
return res
|
|
465
469
|
}
|
|
466
470
|
|
package/src/index.js
CHANGED
|
@@ -206,8 +206,14 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
206
206
|
reject: (deviceId) => desk.reject(deviceId),
|
|
207
207
|
listPending: desk.listPending,
|
|
208
208
|
listMachines,
|
|
209
|
-
//
|
|
210
|
-
//
|
|
209
|
+
// QUITA LA MÁQUINA entera (por su llave): fuera del acta y sin ningún certificado
|
|
210
|
+
// vigente, que son las dos caras del mismo acto. Y AVISA con un REVOKED firmado para
|
|
211
|
+
// que se auto-borre (ahora si está online, o al reaparecer vía handleDevices).
|
|
212
|
+
revokeDevice: (sub) => desk.revokeDevice(sub),
|
|
213
|
+
// Retira UN certificado. No es quitar la máquina: sigue siendo miembro del acta, y
|
|
214
|
+
// quien queda así ya no recibe el aviso de expulsión (mientras siga en el acta, un
|
|
215
|
+
// papel retirado significa «renueva»). Usar `revokeDevice` salvo que quieras
|
|
216
|
+
// exactamente esto.
|
|
211
217
|
revoke: (nonce) => desk.revoke(nonce),
|
|
212
218
|
getSelfCert,
|
|
213
219
|
onPendingChange (fn) { _onPendingChange = fn || (() => {}) },
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* revocation.js — CUÁNDO se le dice a un aparato que ya no es de casa.
|
|
3
|
+
*
|
|
4
|
+
* Módulo PURO (sin red, sin disco, sin `node:*`), como `enroll.js` y `admin.js`: la regla
|
|
5
|
+
* vive en un solo sitio, se lee entera y se prueba sin levantar nada.
|
|
6
|
+
*
|
|
7
|
+
* EL PROBLEMA QUE RESUELVE. Un dispositivo al que quitaron del perfil no se entera por su
|
|
8
|
+
* cuenta: lo único que le borra la cuenta es un aviso FIRMADO por la maestra
|
|
9
|
+
* (`vault.revoked`). Un «unauthorized» suelto no vale y no debe valer — no va firmado, así
|
|
10
|
+
* que cualquiera podría destruir datos ajenos con un mensaje (el wipe-DoS de
|
|
11
|
+
* `docs/pairing-protocol.md §2.3`). Por eso la bóveda tiene que **atender** la conexión de
|
|
12
|
+
* un aparato que fue suyo, aunque su papel ya no sirva, precisamente para poder mandarlo a
|
|
13
|
+
* paseo. Si no lo hace, el aparato se queda enseñando una cuenta que ya no existe.
|
|
14
|
+
*
|
|
15
|
+
* LA REGLA, en una línea: papel nuestro + ya no está en el acta ⇒ se le avisa.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Motivos de rechazo de `verifyChain` que significan «este papel ya no sirve».
|
|
20
|
+
*
|
|
21
|
+
* Los tres se producen DESPUÉS de que `verifyChain` haya comprobado la firma del propio
|
|
22
|
+
* aparato y la del certificado, así que quien llega hasta aquí demostró tener la llave que
|
|
23
|
+
* el certificado nombra. El resto de motivos (`shape`, `bad-signature`,
|
|
24
|
+
* `bad-action-signature`, `cert-device-mismatch`, `untrusted-issuer`…) son ruido o gente
|
|
25
|
+
* ajena: ahí no hay a quién avisar de nada.
|
|
26
|
+
*/
|
|
27
|
+
export const STALE_PAPER = Object.freeze(['revoked', 'expired', 'scope'])
|
|
28
|
+
const STALE = new Set(STALE_PAPER)
|
|
29
|
+
|
|
30
|
+
/** ¿El motivo del rechazo es «tu papel ya no sirve»? */
|
|
31
|
+
export const isStalePaper = (reason) => STALE.has(reason)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ¿Hay que mandarle el aviso firmado de expulsión?
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} o
|
|
37
|
+
* @param {string} o.reason por qué falló `verifyChain`.
|
|
38
|
+
* @param {string} o.pubkey la llave del aparato que acaba de escribir.
|
|
39
|
+
* @param {string} o.master la maestra de ESTA bóveda.
|
|
40
|
+
* @param {string|null} [o.certIss] quién firmó el certificado que presentó.
|
|
41
|
+
* @param {Array<{pub:string}>|null} [o.members] miembros del acta; `null` = no hay acta.
|
|
42
|
+
* @param {boolean} [o.knownRevoked] consta explícitamente como revocado en las
|
|
43
|
+
* delegaciones. Es el único criterio que queda cuando no hay acta.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function shouldNotifyRevoked ({ reason, pubkey, master, certIss = null, members = null, knownRevoked = false } = {}) {
|
|
47
|
+
if (typeof pubkey !== 'string' || !pubkey) return false
|
|
48
|
+
if (!isStalePaper(reason)) return false
|
|
49
|
+
// Que el papel sea NUESTRO. No hace falta que siga siendo válido —justo por eso estamos
|
|
50
|
+
// aquí— pero sí que lo haya firmado esta maestra: es lo que separa a un aparato que fue
|
|
51
|
+
// de casa de uno que pasaba por ahí.
|
|
52
|
+
if (certIss && master && certIss !== master) return false
|
|
53
|
+
// El aparato no puede echarse a sí mismo, y la maestra tampoco se echa.
|
|
54
|
+
if (master && pubkey === master) return false
|
|
55
|
+
|
|
56
|
+
// EL ACTA MANDA, y es lo único que manda. Un certificado retirado o vencido NO significa
|
|
57
|
+
// «estás fuera»: renovar retira el anterior y cambiar permisos obliga a renovar, así que
|
|
58
|
+
// un aparato de casa con un papel viejo solo tiene que renovar. Decirle «estás fuera»
|
|
59
|
+
// ahí lo borraba solo: dabas «administra» y el dispositivo desaparecía.
|
|
60
|
+
if (Array.isArray(members)) return !members.some((m) => m?.pub === pubkey)
|
|
61
|
+
|
|
62
|
+
// Sin acta (bóveda anterior al acta) no se puede saber quién es del perfil: solo se
|
|
63
|
+
// avisa a quien conste explícitamente como revocado.
|
|
64
|
+
return !!knownRevoked
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default { STALE_PAPER, isStalePaper, shouldNotifyRevoked }
|