Pular para o conteúdo
Escalibur Wallet
Auditoria

O assinador

Kit/Sources/EscaliburKeys/Signer.swift

O único lugar do app onde uma chave privada assina alguma coisa.

151 linhasSHA-256 ddba80d818eb185e…Abrir no GitHub, no commit 015f5f5
1import EscaliburChains
2import EscaliburCore
3import Foundation
4
5/// O unico lugar do app onde uma chave privada assina alguma coisa.
6///
7/// Recebe um `SigningPlan` (que so existe se passou pela validacao da rede) e a RK
8/// recem-destrancada por presenca do dono. Sincrono de ponta a ponta, sem `await`:
9/// funcao assincrona guarda variaveis locais num contexto no heap que e liberado sem
10/// zerar, e aqui as variaveis locais sao a seed e as chaves (docs/seguranca.md §2.9).
11///
12/// ```
13/// rk ─► DEK ─► registro ─► frase ─► seed ─► chave de cada pedido ─► assinatura
14/// cada etapa zera a anterior; a seed e as chaves somem no fim do plano
15/// ```
16public enum Signer {
17 public enum Failure: Error, Equatable, Sendable {
18 case expired
19 /// A chave derivada nao e a que a transacao espera: caminho errado ou
20 /// carteira errada. Nada foi assinado.
21 case unexpectedKey
22 /// A assinatura produzida nao verificou. Com nonce deterministico, uma
23 /// assinatura defeituosa pode entregar a chave; ela nunca sai daqui.
24 case verificationFailed
25 case unsupportedScheme(SignatureScheme)
26 /// A passphrase digitada nao e a da carteira (carteira com passphrase e "pedir a
27 /// passphrase para enviar"). Nada foi assinado.
28 case passphraseMismatch
29 }
30
31 /// Assina todas as transacoes do plano, na ordem. Consome e zera a RK.
32 ///
33 /// `passphrase`: a passphrase que o dono acabou de digitar, numa carteira com
34 /// passphrase que pede a passphrase para enviar (pedido do dono, 16/10/2026). Ela e
35 /// conferida com a do cofre, as duas em NFKD e em tempo constante, antes de nascer
36 /// qualquer chave; nao confere, nada e assinado. O chamador continua dono do buffer.
37 /// Sem valor padrao: cada chamador diz se pede a passphrase (nil so quando a carteira nao
38 /// pede), e um chamador novo nao fura a regra esquecendo o parametro (revisao de 16/10/2026).
39 public static func sign(_ plan: SigningPlan, rootKey rk: SecureBytes, vault: WalletVault,
40 passphrase typed: SecureBytes?) throws -> [SignedTransaction] {
41 defer { rk.wipe() }
42 guard !plan.isExpired() else { throw Failure.expired }
43
44 let secret = try vault.open(walletID: plan.walletID, rk: rk)
45 rk.wipe()
46 defer { secret.wipe() }
47 if let typed {
48 // As duas em NFKD, como a conferencia da passphrase (ConfirmPassphraseView): a do
49 // cofre ja e NFKD desde a criacao, a digitada vem do teclado em NFC.
50 let stored = BIP39.nfkd(secret.passphrase), given = BIP39.nfkd(typed)
51 defer { stored.wipe(); given.wipe() }
52 guard stored.count > 0, Hash.constantTimeEqual(stored, given) else { throw Failure.passphraseMismatch }
53 }
54 // Cardano: a mestra Icarus sai da entropia (CIP-3), antes de o segredo ser zerado.
55 let cardano = try CardanoSigning.master(for: plan, secret: secret)
56 defer { cardano?.wipe() }
57 let seed = try secret.seed()
58 secret.wipe()
59 defer { seed.wipe() }
60
61 var masters: [Curve: HDKey] = [:]
62 defer { masters.values.forEach { $0.wipe() } }
63
64 var signed: [SignedTransaction] = []
65 for transaction in plan.transactions {
66 var signatures: [ProducedSignature] = []
67 for request in transaction.signingRequests {
68 if request.scheme == .ed25519Cardano {
69 signatures.append(try CardanoSigning.sign(request, master: cardano))
70 continue
71 }
72 let master: HDKey
73 if let cached = masters[request.curve] {
74 master = cached
75 } else {
76 master = try HDKey.master(seed: seed, curve: request.curve)
77 masters[request.curve] = master
78 }
79 let key = try master.derive(request.path)
80 defer { key.wipe() }
81 signatures.append(try sign(request, with: key))
82 }
83 signed.append(try transaction.assemble(with: signatures))
84 }
85 return signed
86 }
87
88 static func sign(_ request: SigningRequest, with key: HDKey) throws -> ProducedSignature {
89 let publicKey = try key.publicKey()
90 guard Hash.constantTimeEqual(publicKey, request.expectedPublicKey) else { throw Failure.unexpectedKey }
91
92 switch request.scheme {
93 case .ecdsaDER:
94 let der = try Secp256k1.signDER(digest: request.payload, privateKey: key.key)
95 guard Secp256k1.verifyDER(signature: der, digest: request.payload, publicKey: publicKey) else {
96 throw Failure.verificationFailed
97 }
98 return ProducedSignature(bytes: der)
99 case .ecdsaRecoverable:
100 let (compact, recovery) = try Secp256k1.signRecoverable(digest: request.payload, privateKey: key.key)
101 let recovered = try Secp256k1.recover(digest: request.payload, compact: compact, recoveryID: recovery, compressed: true)
102 guard recovered == publicKey else { throw Failure.verificationFailed }
103 return ProducedSignature(bytes: compact, recoveryID: recovery)
104 case .ed25519:
105 let signature = try Ed25519.sign(request.payload, seed: key.key)
106 guard Ed25519.verify(signature: signature, message: request.payload, publicKey: publicKey) else {
107 throw Failure.verificationFailed
108 }
109 return ProducedSignature(bytes: signature)
110 case .schnorrBIP340:
111 throw Failure.unsupportedScheme(.schnorrBIP340)
112 case .ed25519Cardano:
113 // Chave BIP32-Ed25519, que o HDKey nao deriva: so pelo CardanoSigning.
114 throw Failure.unsupportedScheme(.ed25519Cardano)
115 }
116 }
117}
118
119public enum AccountDeriver {
120 /// Deriva o endereco padrao de cada rede.
121 public static func derive(_ secret: WalletSecret, chains: [Chain] = Chain.all) throws -> (accounts: [DerivedAccount], fingerprint: [UInt8]) {
122 let seed = try secret.seed()
123 defer { seed.wipe() }
124 let secp = try HDKey.master(seed: seed, curve: .secp256k1)
125 let ed = try HDKey.master(seed: seed, curve: .ed25519)
126 defer { secp.wipe(); ed.wipe() }
127
128 var accounts: [DerivedAccount] = []
129 for chain in chains {
130 if chain.family == .cardano {
131 if let account = try? CardanoKey.derivedAccount(secret, chain: chain) { accounts.append(account) }
132 continue
133 }
134 let path = DefaultPaths.path(for: chain)
135 let master = chain.family.curve == .secp256k1 ? secp : ed
136 let key = try master.derive(path)
137 defer { key.wipe() }
138 let publicKey = try key.publicKey()
139 guard let address = try? Address.from(publicKey: publicKey, chain: chain) else { continue }
140 var xpub: ExtendedPublicKey?
141 if chain.family == .utxo {
142 let accountPath = DerivationPath(components: Array(path.components.prefix(3)))
143 let account = try master.derive(accountPath)
144 defer { account.wipe() }
145 xpub = try ExtendedPublicKey(publicKey: account.publicKey(), chainCode: account.chainCode.withUnsafeBytes { Array($0) })
146 }
147 accounts.append(DerivedAccount(chainID: chain.id, path: path, address: address, publicKey: publicKey, accountXPub: xpub))
148 }
149 return (accounts, try secp.fingerprint())
150 }
151}