mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
feat(trust): keep previous mint signing key verifiable
This commit is contained in:
@@ -164,6 +164,10 @@ export const confirmMintRekey = (mints: TrustedMint[], server: string): MintTran
|
|||||||
...mint,
|
...mint,
|
||||||
mintPubkey: pending,
|
mintPubkey: pending,
|
||||||
pendingMintPubkey: undefined,
|
pendingMintPubkey: undefined,
|
||||||
|
// the retiring pin stays verifiable for the notes it already
|
||||||
|
// signed (landed-mutation verification accepts current OR
|
||||||
|
// previous); it is never a target for new trust decisions
|
||||||
|
previousMintPubkey: mint.mintPubkey,
|
||||||
unconfirmed: undefined,
|
unconfirmed: undefined,
|
||||||
}
|
}
|
||||||
: mint,
|
: mint,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
confirmTrustedMintRekey,
|
confirmTrustedMintRekey,
|
||||||
dismissTrustedMintRekey,
|
dismissTrustedMintRekey,
|
||||||
getTrustedMintPubkey,
|
getTrustedMintPubkey,
|
||||||
|
getTrustedMintVerificationKeys,
|
||||||
grandfatherTrustedMint,
|
grandfatherTrustedMint,
|
||||||
isMintTrusted,
|
isMintTrusted,
|
||||||
isMintUnconfirmed,
|
isMintUnconfirmed,
|
||||||
@@ -88,6 +89,29 @@ describe('rekey staging', () => {
|
|||||||
expect(getTrustedMintPubkey(SERVER, OWNER_ID)).toBe(KEY_B)
|
expect(getTrustedMintPubkey(SERVER, OWNER_ID)).toBe(KEY_B)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the retiring key verifiable as previous after a confirmed rekey', async () => {
|
||||||
|
await lockTrustedMint(SERVER, KEY_A, OWNER_ID)
|
||||||
|
expect(getTrustedMintVerificationKeys(SERVER, OWNER_ID)).toEqual([KEY_A])
|
||||||
|
|
||||||
|
await lockTrustedMint(SERVER, KEY_B, OWNER_ID)
|
||||||
|
// while the rekey is only STAGED, the candidate is not a verification key
|
||||||
|
expect(getTrustedMintVerificationKeys(SERVER, OWNER_ID)).toEqual([KEY_A])
|
||||||
|
|
||||||
|
await confirmTrustedMintRekey(SERVER, OWNER_ID)
|
||||||
|
expect(getTrustedMintVerificationKeys(SERVER, OWNER_ID)).toEqual([KEY_B, KEY_A])
|
||||||
|
|
||||||
|
// dismissing a later staged candidate keeps the previous key intact
|
||||||
|
await lockTrustedMint(SERVER, KEY_C, OWNER_ID)
|
||||||
|
await dismissTrustedMintRekey(SERVER, OWNER_ID)
|
||||||
|
expect(getTrustedMintVerificationKeys(SERVER, OWNER_ID)).toEqual([KEY_B, KEY_A])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excludes unconfirmed pins from verification keys', async () => {
|
||||||
|
await grandfatherTrustedMint(SERVER, KEY_A, OWNER_ID)
|
||||||
|
expect(getTrustedMintVerificationKeys(SERVER, OWNER_ID)).toEqual([])
|
||||||
|
expect(getTrustedMintVerificationKeys('unknown.example', OWNER_ID)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it('drops the staged key on dismissal, keeping the original pin', async () => {
|
it('drops the staged key on dismissal, keeping the original pin', async () => {
|
||||||
await lockTrustedMint(SERVER, KEY_A, OWNER_ID)
|
await lockTrustedMint(SERVER, KEY_A, OWNER_ID)
|
||||||
await lockTrustedMint(SERVER, KEY_B, OWNER_ID)
|
await lockTrustedMint(SERVER, KEY_B, OWNER_ID)
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ export type TrustedMint = {
|
|||||||
// would defeat the entire pinning model (a compromised mint could sign
|
// would defeat the entire pinning model (a compromised mint could sign
|
||||||
// unbacked notes that then show the "signed" badge).
|
// unbacked notes that then show the "signed" badge).
|
||||||
pendingMintPubkey?: string
|
pendingMintPubkey?: string
|
||||||
|
// the key this mint signed under immediately BEFORE the holder confirmed
|
||||||
|
// the current pin (set by confirmTrustedMintRekey): notes issued just
|
||||||
|
// before a legitimate rotation stay verifiable against it. Never imported
|
||||||
|
// from a backup (a crafted file could otherwise plant a forgeable
|
||||||
|
// "previous" key, exactly like pendingMintPubkey) and never staged from
|
||||||
|
// an advertisement - only a confirmed rekey creates it, from the key that
|
||||||
|
// was actually pinned.
|
||||||
|
previousMintPubkey?: string
|
||||||
// best-effort node identity/capacity, cached from the mint-address
|
// best-effort node identity/capacity, cached from the mint-address
|
||||||
// discovery endpoint (see the kit's fetchMintAddress) purely for display -
|
// discovery endpoint (see the kit's fetchMintAddress) purely for display -
|
||||||
// absent for a mint that doesn't support it, or one trusted before this
|
// absent for a mint that doesn't support it, or one trusted before this
|
||||||
@@ -132,6 +140,20 @@ export const getTrustedMintPubkey = (server: string, ownerId?: string): string |
|
|||||||
readTrustedMints(ownerId).find((mint) => mint.server === server && !mint.unconfirmed)
|
readTrustedMints(ownerId).find((mint) => mint.server === server && !mint.unconfirmed)
|
||||||
?.mintPubkey ?? null
|
?.mintPubkey ?? null
|
||||||
|
|
||||||
|
// the keys a landed mutation signature may be checked against for this
|
||||||
|
// server: the pinned current key plus the previous one (kept across a
|
||||||
|
// confirmed rekey so the old key's last notes still verify). Unconfirmed
|
||||||
|
// pins contribute nothing - see TrustedMint.unconfirmed.
|
||||||
|
export const getTrustedMintVerificationKeys = (server: string, ownerId?: string): string[] => {
|
||||||
|
const mint = readTrustedMints(ownerId).find(
|
||||||
|
(entry) => entry.server === server && !entry.unconfirmed,
|
||||||
|
)
|
||||||
|
if (!mint) return []
|
||||||
|
return mint.previousMintPubkey
|
||||||
|
? [mint.mintPubkey, mint.previousMintPubkey]
|
||||||
|
: [mint.mintPubkey]
|
||||||
|
}
|
||||||
|
|
||||||
// true when a server has a pin that came from a file/storage rather than a
|
// true when a server has a pin that came from a file/storage rather than a
|
||||||
// live response (see TrustedMint.unconfirmed) - callers should treat a
|
// live response (see TrustedMint.unconfirmed) - callers should treat a
|
||||||
// bearer's own cached mintPubkey for such a server as equally
|
// bearer's own cached mintPubkey for such a server as equally
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ const isTrustedMint = (value: unknown): value is TrustedMint => {
|
|||||||
(value.pendingMintPubkey === undefined ||
|
(value.pendingMintPubkey === undefined ||
|
||||||
(typeof value.pendingMintPubkey === 'string' &&
|
(typeof value.pendingMintPubkey === 'string' &&
|
||||||
isValidMintPubkey(value.pendingMintPubkey))) &&
|
isValidMintPubkey(value.pendingMintPubkey))) &&
|
||||||
|
(value.previousMintPubkey === undefined ||
|
||||||
|
(typeof value.previousMintPubkey === 'string' &&
|
||||||
|
isValidMintPubkey(value.previousMintPubkey))) &&
|
||||||
isOptionalString(value.nodeAlias) &&
|
isOptionalString(value.nodeAlias) &&
|
||||||
isOptionalString(value.nodeColor) &&
|
isOptionalString(value.nodeColor) &&
|
||||||
isOptionalNumber(value.nodeCapacityMsat) &&
|
isOptionalNumber(value.nodeCapacityMsat) &&
|
||||||
|
|||||||
Reference in New Issue
Block a user