@dotrino/identity 0.91.0 → 0.92.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/package.json +1 -1
- package/vault/acta.js +48 -2
- package/vault/remote.js +15 -12
package/package.json
CHANGED
package/vault/acta.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* Módulo PURO: sin kv, sin red, sin disco. Cripto de `./capabilities.js`.
|
|
36
36
|
*/
|
|
37
37
|
import { canonicalStringify } from './core.js'
|
|
38
|
-
import { signWithDevice, verifyDeviceSig, pubkeyId } from './capabilities.js'
|
|
38
|
+
import { signWithDevice, verifyDeviceSig, verifyDelegation, pubkeyId } from './capabilities.js'
|
|
39
39
|
|
|
40
40
|
export const ACTA_V = 5
|
|
41
41
|
|
|
@@ -649,6 +649,52 @@ export async function verifyActa ({ acta, expectedProfileId } = {}) {
|
|
|
649
649
|
return mal ? { ok: false, reason: mal } : { ok: true }
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
/**
|
|
653
|
+
* ¿ES DE FIAR LO QUE CONTESTA LA BÓVEDA CON LA QUE TE EMPAREJASTE? El papel y el acta que
|
|
654
|
+
* llegan al enrolarse o al renovar, juzgados contra la llave de ESA bóveda (`vault`: la
|
|
655
|
+
* `iss` del QR, o la que quedó guardada al enrolarse).
|
|
656
|
+
*
|
|
657
|
+
* Antes cada cliente comparaba `acta.profileId` con esa llave. Eso solo es verdad en una
|
|
658
|
+
* cuenta de una bóveda que además nació en ella: el `profileId` es la llave del GÉNESIS, y
|
|
659
|
+
* en una cuenta que la bóveda ADOPTÓ, o en la SEGUNDA bóveda de un multivault, la llave de
|
|
660
|
+
* la bóveda es otra. Ahí ningún aparato podía entrar ni renovar. Y encima no protegía: el
|
|
661
|
+
* acta no se verificaba, así que bastaba con escribir ese `profileId` en una inventada.
|
|
662
|
+
*
|
|
663
|
+
* Lo que se comprueba ahora, y por qué alcanza:
|
|
664
|
+
* 1. el acta está bien firmada por quien dice haberla sellado;
|
|
665
|
+
* 2. esa bóveda PUEDE SELLAR esta acta — es de la cuenta, con el permiso, y una llave
|
|
666
|
+
* está en una sola cuenta, así que esto también fija la cuenta;
|
|
667
|
+
* 3. el papel lo firmó ESA bóveda y no otra selladora: la respuesta es suya, porque nadie
|
|
668
|
+
* por el camino puede firmar con su llave;
|
|
669
|
+
* 4. y el papel vale según esa acta (para esta llave, este permiso, un `seq` que no viene
|
|
670
|
+
* del futuro).
|
|
671
|
+
*
|
|
672
|
+
* `justSealed` es para el ENROLAMIENTO: aprobar es admitir al aparato, así que el acta que
|
|
673
|
+
* vuelve la acaba de sellar esa misma bóveda. Exigirlo ata el acta a su llave y no solo el
|
|
674
|
+
* papel — el llavero que el aparato va a usar viaja ahí dentro. Al RENOVAR no se pide: la
|
|
675
|
+
* última acta pudo sellarla otra selladora de la cuenta.
|
|
676
|
+
*
|
|
677
|
+
* @returns {Promise<{ok:true}|{ok:false, reason:string}>}
|
|
678
|
+
*/
|
|
679
|
+
export async function checkVaultReply ({ acta, cert, vault, sub, scope = null, justSealed = false } = {}) {
|
|
680
|
+
if (!acta) return { ok: false, reason: 'sin-acta' }
|
|
681
|
+
if (!isPub(vault)) return { ok: false, reason: 'sin-boveda' }
|
|
682
|
+
// Para QUÉ llave es el papel no es opcional: sin ella `verifyDelegation` no lo mira, y un
|
|
683
|
+
// papel emitido para otra llave pasaría por bueno.
|
|
684
|
+
if (!isPub(sub)) return { ok: false, reason: 'sin-llave-del-aparato' }
|
|
685
|
+
const v = await verifyActa({ acta })
|
|
686
|
+
if (!v.ok) return { ok: false, reason: 'acta:' + v.reason }
|
|
687
|
+
if (!canSeal(acta, vault)) return { ok: false, reason: 'boveda-no-sella' }
|
|
688
|
+
if (justSealed && !samePubkey(acta.sealedBy, vault)) return { ok: false, reason: 'acta-de-otra-selladora' }
|
|
689
|
+
if (!cert || !samePubkey(cert.iss, vault)) return { ok: false, reason: 'papel-de-otra-llave' }
|
|
690
|
+
const d = await verifyDelegation({
|
|
691
|
+
cert, expectedSub: sub, actaSeq: acta.seq, sealers: sealersOf(acta),
|
|
692
|
+
...(scope ? { expectedScope: scope } : {})
|
|
693
|
+
})
|
|
694
|
+
if (!d.ok) return { ok: false, reason: 'papel:' + d.reason }
|
|
695
|
+
return { ok: true }
|
|
696
|
+
}
|
|
697
|
+
|
|
652
698
|
/**
|
|
653
699
|
* Aplica cambios y devuelve el acta SIGUIENTE, **sin firmar** (hay que `sealActa`).
|
|
654
700
|
* `by` es quien va a sellar: si no es el sellador vigente, se rechaza (regla 1).
|
|
@@ -1218,7 +1264,7 @@ export async function canAdopt ({ candidate, current }) {
|
|
|
1218
1264
|
|
|
1219
1265
|
export default {
|
|
1220
1266
|
ACTA_V, CAPS, CAP_SCOPE, genesisActa, actaBody, actaHash, memberId, checkShape, verifySealerChain, verifySignedBy,
|
|
1221
|
-
sealActa, verifyActa, applyChanges, makeRenounce, verifyRenounce,
|
|
1267
|
+
sealActa, verifyActa, checkVaultReply, applyChanges, makeRenounce, verifyRenounce,
|
|
1222
1268
|
makeContinuity, verifyContinuity,
|
|
1223
1269
|
cardBody, makeProfileCard, verifyProfileCard, canAdoptCard, sealersOf, canSeal,
|
|
1224
1270
|
effectiveCaps, memberCan, memberCanSign, memberCanScope, memberCanReadSecrets, memberScopes, isService, capScope, isValidCn, canAdopt,
|
package/vault/remote.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* No reimplementa cripto: usa `@dotrino/identity/capabilities`. Transporte:
|
|
14
14
|
* `@dotrino/proxy-client` (importado perezosamente; solo se carga al emparejar).
|
|
15
15
|
*/
|
|
16
|
-
import { makeDeviceKey, signWithDevice,
|
|
17
|
-
import {
|
|
16
|
+
import { makeDeviceKey, signWithDevice, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
|
|
17
|
+
import { checkVaultReply } from './acta.js'
|
|
18
18
|
|
|
19
19
|
const MSG = {
|
|
20
20
|
HELLO: 'vault.hello',
|
|
@@ -218,14 +218,12 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
|
|
|
218
218
|
// (Aquí vivía el margen de reloj: el cert lo sellaba la bóveda con SU reloj y lo validaba
|
|
219
219
|
// este aparato con el suyo, y 850 ms de diferencia bastaban para no poder enrolarse. Sin
|
|
220
220
|
// vencimiento no hay ventana que ajustar y el problema no puede volver.)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!v.ok) throw new Error('invalid cert: ' + v.reason)
|
|
228
|
-
if (res.cert.sub !== dev.publickey) throw new Error('cert issued for a different device')
|
|
221
|
+
//
|
|
222
|
+
// Se juzga contra la llave de LA BÓVEDA con la que hablas, no contra el `profileId`:
|
|
223
|
+
// eso solo coincidía en una cuenta que nació en esa bóveda, y dejaba fuera a quien se
|
|
224
|
+
// emparejaba con una segunda bóveda o con una que adoptó la cuenta (`checkVaultReply`).
|
|
225
|
+
const chk = await checkVaultReply({ acta: res.acta, cert: res.cert, vault: qr.iss, sub: dev.publickey, justSealed: true })
|
|
226
|
+
if (!chk.ok) throw new Error('the vault reply does not check out: ' + chk.reason)
|
|
229
227
|
return { device: dev, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId, acta: res.acta || null }
|
|
230
228
|
} finally { try { client.close() } catch (_) {} }
|
|
231
229
|
}
|
|
@@ -381,12 +379,17 @@ export async function requestDevices ({ master, proxy, device, cert, sinceSeq, o
|
|
|
381
379
|
*/
|
|
382
380
|
export async function requestRenew ({ master, proxy, device, cert, onRevoked } = {}) {
|
|
383
381
|
const res = await vaultRpc({ master, proxy, device, cert, onRevoked, sendType: 'vault.renew', okType: 'vault.renewed', data: { op: 'renew' } })
|
|
384
|
-
if (!res.cert || res.cert.sub !== device.publickey) throw new Error('invalid renewed cert')
|
|
385
382
|
// EL ACTA VIAJA CON EL PAPEL y hay que dejarla pasar: quien lo recibe la necesita para
|
|
386
383
|
// comprobar que lo firmó una SELLADORA de este perfil. Aquí se comparaba `cert.iss` con
|
|
387
384
|
// la maestra y se tiraba el acta, así que arriba no había con qué juzgar y la renovación
|
|
388
385
|
// fallaba con «the vault did not send its record» — con el papel correcto en la mano.
|
|
389
|
-
|
|
386
|
+
//
|
|
387
|
+
// Y se juzga AQUÍ, en el pilar: antes lo hacía cada cliente por su cuenta —uno comparando
|
|
388
|
+
// el `profileId`, que con dos bóvedas no vale, y el navegador guardando el papel sin mirar
|
|
389
|
+
// nada—. `master` es la bóveda a la que se le pidió.
|
|
390
|
+
const chk = await checkVaultReply({ acta: res.acta, cert: res.cert, vault: master, sub: device.publickey })
|
|
391
|
+
if (!chk.ok) throw new Error('invalid renewed cert: ' + chk.reason)
|
|
392
|
+
return { cert: res.cert, acta: res.acta }
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
/**
|