Auditoria
O assinador
Kit/Sources/EscaliburKeys/Signer.swift
O único lugar do app onde uma chave privada assina alguma coisa.
| 1 | import EscaliburChains |
| 2 | import EscaliburCore |
| 3 | import 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 | /// ``` |
| 16 | public 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 | |
| 119 | public 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 | } |