diff --git a/README.md b/README.md index 9f34e1a..20d0f8d 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,9 @@ and ensure, that it does nothing you wouldn't want it to do. To change some defaults, take a look at `config.go`. Signatures are created without the help of a node, to avoid your seed or -private keys being stolen by a node operator. Apparently this is not to -be taken for granted, since the node API offers, for example, a [method -for signing](https://docs.nano.org/commands/rpc-protocol/#sign) your -blocks. +private keys being stolen by a node operator. The received account info +is always validated using block signatures to ensure the node operator +cannot manipulate atto by, for example, reporting wrong balances. atto does not have any persistance and writes nothing to your file system. This makes atto very portable, but also means, that diff --git a/account.go b/account.go index b5878fd..3680e8a 100644 --- a/account.go +++ b/account.go @@ -12,6 +12,11 @@ type accountInfo struct { Balance string `json:"balance"` } +type blockInfo struct { + Error string `json:"error"` + Contents block `json:"contents"` +} + func getAccountInfo(address string) (info accountInfo, err error) { requestBody := fmt.Sprintf(`{`+ `"action": "account_info",`+ @@ -33,6 +38,41 @@ func getAccountInfo(address string) (info accountInfo, err error) { info.Balance = "0" } else if info.Error != "" { err = fmt.Errorf("could not fetch balance: %s", info.Error) + return } + err = verifyInfo(info, address) return } + +// verifyInfo gets the frontier block of info, ensures that Hash, +// Representative and Balance match and verifies it's signature. +func verifyInfo(info accountInfo, address string) error { + requestBody := fmt.Sprintf(`{`+ + `"action": "block_info",`+ + `"json_block": "true",`+ + `"hash": "%s"`+ + `}`, info.Frontier) + responseBytes, err := doRPC(requestBody) + if err != nil { + return err + } + var block blockInfo + if err = json.Unmarshal(responseBytes, &block); err != nil { + return err + } + if info.Error != "" { + return fmt.Errorf("could not get block info: %s", info.Error) + } + publicKey, err := getPublicKeyFromAddress(address) + if err != nil { + return err + } + if err = block.Contents.verifySignature(publicKey); err == errInvalidSignature || + info.Frontier != block.Contents.Hash || + info.Representative != block.Contents.Representative || + info.Balance != block.Contents.Balance { + return fmt.Errorf("the received account info has been manipulated; " + + "change your node immediately!") + } + return err +} diff --git a/balance.go b/balance.go index 25e3ef6..e30a784 100644 --- a/balance.go +++ b/balance.go @@ -106,7 +106,7 @@ func receivePendingSends(info accountInfo, privateKey *big.Int) (updatedBalance } fmt.Fprintln(os.Stderr, "done") - previousBlock = block.Hash + previousBlock = block.Hash // Hash was computed during signing. } return } diff --git a/block.go b/block.go index b9256b3..c6abcc9 100644 --- a/block.go +++ b/block.go @@ -9,6 +9,8 @@ import ( "golang.org/x/crypto/blake2b" ) +var errInvalidSignature = fmt.Errorf("invalid block signature") + type block struct { Type string `json:"type"` Account string `json:"account"` @@ -19,6 +21,7 @@ type block struct { Signature string `json:"signature"` Work string `json:"work"` Hash string `json:"-"` + HashBytes []byte `json:"-"` } type workGenerateResponse struct { @@ -28,12 +31,10 @@ type workGenerateResponse struct { func (b *block) sign(privateKey *big.Int) error { publicKey := derivePublicKey(privateKey) - hash, err := b.hash(publicKey) - if err != nil { + if err := b.addHashIfUnhashed(publicKey); err != nil { return err } - b.Hash = fmt.Sprintf("%064X", hash) - signature, err := sign(privateKey, hash) + signature, err := sign(privateKey, b.HashBytes) if err != nil { return err } @@ -41,8 +42,35 @@ func (b *block) sign(privateKey *big.Int) error { return nil } +func (b *block) verifySignature(publicKey *big.Int) (err error) { + if err = b.addHashIfUnhashed(publicKey); err != nil { + return + } + sig, ok := big.NewInt(0).SetString(b.Signature, 16) + if !ok { + return fmt.Errorf("cannot parse '%s' as an integer", b.Signature) + } + if !isValidSignature(publicKey, b.HashBytes, bigIntToBytes(sig, 64)) { + err = errInvalidSignature + } + return +} + +func (b *block) addHashIfUnhashed(publicKey *big.Int) error { + if b.Hash == "" || len(b.HashBytes) == 0 { + hashBytes, err := b.hash(publicKey) + if err != nil { + return err + } + b.HashBytes = hashBytes + b.Hash = fmt.Sprintf("%064X", b.HashBytes) + } + return nil +} + func (b *block) hash(publicKey *big.Int) ([]byte, error) { - // Look at https://nanoo.tools/block for a reference. + // See https://nanoo.tools/block for a reference. + msg := make([]byte, 176, 176) msg[31] = 0x6 // block preamble diff --git a/ed25519.go b/ed25519.go index 3c30a0f..ef3f2b4 100644 --- a/ed25519.go +++ b/ed25519.go @@ -48,3 +48,49 @@ func sign(privateKey *big.Int, msg []byte) ([]byte, error) { return signature, nil } + +func isValidSignature(publicKey *big.Int, msg, sig []byte) bool { + // This implementation based on the one from github.com/iotaledger/iota.go. + + publicKeyBytes := bigIntToBytes(publicKey, 32) + + // ZIP215: this works because SetBytes does not check that encodings are canonical + A, err := new(edwards25519.Point).SetBytes(publicKeyBytes) + if err != nil { + return false + } + A.Negate(A) + + h, err := blake2b.New512(nil) + if err != nil { + return false + } + h.Write(sig[:32]) + h.Write(publicKeyBytes) + h.Write(msg) + var digest [64]byte + h.Sum(digest[:0]) + hReduced := new(edwards25519.Scalar).SetUniformBytes(digest[:]) + + // ZIP215: this works because SetBytes does not check that encodings are canonical + checkR, err := new(edwards25519.Point).SetBytes(sig[:32]) + if err != nil { + return false + } + + // https://tools.ietf.org/html/rfc8032#section-5.1.7 requires that s be in + // the range [0, order) in order to prevent signature malleability + s, err := new(edwards25519.Scalar).SetCanonicalBytes(sig[32:]) + if err != nil { + return false + } + + R := new(edwards25519.Point).VarTimeDoubleScalarBaseMult(hReduced, A, s) + + // ZIP215: We want to check [8](R - checkR) == 0 + p := new(edwards25519.Point).Subtract(R, checkR) // p = R - checkR + p.Add(p, p) // p = [2]p + p.Add(p, p) // p = [4]p + p.Add(p, p) // p = [8]p + return p.Equal(edwards25519.NewIdentityPoint()) == 1 // p == 0 +}