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,
|
||||
mintPubkey: pending,
|
||||
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,
|
||||
}
|
||||
: mint,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
confirmTrustedMintRekey,
|
||||
dismissTrustedMintRekey,
|
||||
getTrustedMintPubkey,
|
||||
getTrustedMintVerificationKeys,
|
||||
grandfatherTrustedMint,
|
||||
isMintTrusted,
|
||||
isMintUnconfirmed,
|
||||
@@ -88,6 +89,29 @@ describe('rekey staging', () => {
|
||||
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 () => {
|
||||
await lockTrustedMint(SERVER, KEY_A, 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
|
||||
// unbacked notes that then show the "signed" badge).
|
||||
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
|
||||
// discovery endpoint (see the kit's fetchMintAddress) purely for display -
|
||||
// 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)
|
||||
?.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
|
||||
// live response (see TrustedMint.unconfirmed) - callers should treat a
|
||||
// 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 ||
|
||||
(typeof value.pendingMintPubkey === 'string' &&
|
||||
isValidMintPubkey(value.pendingMintPubkey))) &&
|
||||
(value.previousMintPubkey === undefined ||
|
||||
(typeof value.previousMintPubkey === 'string' &&
|
||||
isValidMintPubkey(value.previousMintPubkey))) &&
|
||||
isOptionalString(value.nodeAlias) &&
|
||||
isOptionalString(value.nodeColor) &&
|
||||
isOptionalNumber(value.nodeCapacityMsat) &&
|
||||
|
||||
Reference in New Issue
Block a user