├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── settings.yml └── workflows │ ├── docker_main.yml │ ├── docker_release.yml │ └── go.yml ├── .gitignore ├── .golangci.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── SECURITY.md ├── cmd ├── config.go ├── config_docs_generate_test.go ├── config_docs_test.go ├── config_test.go ├── ffsigner.go ├── ffsigner_test.go ├── version.go └── version_test.go ├── codecov.yml ├── config.md ├── ffsigner └── main.go ├── go.mod ├── go.sum ├── internal ├── rpcserver │ ├── rpchandler.go │ ├── rpchandler_test.go │ ├── rpcprocessor.go │ ├── rpcprocessor_test.go │ ├── server.go │ └── server_test.go ├── signerconfig │ ├── signerconfig.go │ └── signerconfig_test.go └── signermsgs │ ├── en_api_translations.go │ ├── en_config_descriptions.go │ ├── en_error_messges.go │ └── en_field_descriptions.go ├── mocks ├── ethsignermocks │ └── wallet.go ├── rpcbackendmocks │ └── backend.go ├── rpcservermocks │ └── server.go └── secp256k1mocks │ ├── signer.go │ └── signer_direct.go ├── pkg ├── abi │ ├── abi.go │ ├── abi_test.go │ ├── abidecode.go │ ├── abidecode_test.go │ ├── abiencode.go │ ├── abiencode_test.go │ ├── ethers.interface.sample.json │ ├── inputparsing.go │ ├── inputparsing_test.go │ ├── outputserialization.go │ ├── outputserialization_test.go │ ├── signedi256.go │ ├── signedi256_test.go │ ├── typecomponents.go │ └── typecomponents_test.go ├── eip712 │ ├── abi_to_typed_data.go │ ├── abi_to_typed_data_test.go │ ├── typed_data_v4.go │ └── typed_data_v4_test.go ├── ethereum │ └── ethereum.go ├── ethsigner │ ├── transaction.go │ ├── transaction_test.go │ ├── typed_data.go │ ├── typed_data_test.go │ └── wallet.go ├── ethtypes │ ├── address.go │ ├── address_test.go │ ├── hexbytes.go │ ├── hexbytes_test.go │ ├── hexinteger.go │ ├── hexinteger_test.go │ ├── hexuint64.go │ ├── hexuint64_test.go │ ├── integer_parsing.go │ └── integer_parsing_test.go ├── ffi2abi │ ├── ffi.go │ ├── ffi_param_validator.go │ ├── ffi_param_validator_test.go │ └── ffi_test.go ├── fswallet │ ├── config.go │ ├── fslistener.go │ ├── fslistener_test.go │ ├── fswallet.go │ └── fswallet_test.go ├── keystorev3 │ ├── aes128ctr.go │ ├── aes128ctr_test.go │ ├── pbkdf2.go │ ├── pbkdf2_test.go │ ├── scrypt.go │ ├── scrypt_test.go │ ├── wallet.go │ ├── wallet_test.go │ └── walletfile.go ├── rlp │ ├── decode.go │ ├── decode_test.go │ ├── encode.go │ ├── encode_test.go │ ├── rlp.go │ └── rlp_test.go ├── rpcbackend │ ├── backend.go │ ├── backend_test.go │ ├── wsbackend.go │ └── wsbackend_test.go └── secp256k1 │ ├── keypair.go │ ├── keypair_test.go │ ├── signer.go │ └── signer_test.go └── test ├── bad-config.ffsigner.yaml ├── bad-wallet.ffsigner.yaml ├── firefly.ffsigner.yaml ├── keystore_toml ├── 1f185718734552d08278aa70f804580bab5fd2b4.key.json ├── 1f185718734552d08278aa70f804580bab5fd2b4.pwd ├── 1f185718734552d08278aa70f804580bab5fd2b4.toml ├── 497eedc4299dea2f2a364be10025d0ad0f702de3.toml ├── 5d093e9b41911be5f5c4cf91b108bac5d130fa83.toml ├── abcd1234.key.json ├── abcd1234abcd1234abcd1234abcd1234abcd1234.key.json ├── abcd1234abcd1234abcd1234abcd1234abcd1234.pwd ├── file_with_wrong_name.toml └── ignore_dir │ └── readme.txt ├── no-wallet.ffsigner.yaml └── quick-fail.ffsigner.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | **/.nyc_output 4 | firefly-signer -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{yaml,yml,json}] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go licensefile=.githooks/license-maintainer/LICENSE-go -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: firefly-signer 3 | default_branch: main 4 | has_downloads: false 5 | has_issues: true 6 | has_projects: false 7 | has_wiki: false 8 | archived: false 9 | private: false 10 | allow_squash_merge: false 11 | allow_merge_commit: false 12 | allow_rebase_merge: true 13 | -------------------------------------------------------------------------------- /.github/workflows/docker_main.yml: -------------------------------------------------------------------------------- 1 | name: Docker Main Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set build tag 20 | id: build_tag_generator 21 | run: | 22 | RELEASE_TAG=$(curl https://api.github.com/repos/hyperledger/firefly-signer/releases/latest -s | jq .tag_name -r) 23 | BUILD_TAG=$RELEASE_TAG-$(date +"%Y%m%d")-$GITHUB_RUN_NUMBER 24 | echo ::set-output name=BUILD_TAG::$BUILD_TAG 25 | 26 | - name: Build 27 | run: | 28 | make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ 29 | --label commit=$GITHUB_SHA \ 30 | --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 31 | --label tag=${{ steps.build_tag_generator.outputs.BUILD_TAG }} \ 32 | --tag ghcr.io/hyperledger/firefly-signer:${{ steps.build_tag_generator.outputs.BUILD_TAG }}" \ 33 | docker 34 | 35 | - name: Tag release 36 | run: docker tag ghcr.io/hyperledger/firefly-signer:${{ steps.build_tag_generator.outputs.BUILD_TAG }} ghcr.io/hyperledger/firefly-signer:head 37 | 38 | - name: Push docker image 39 | run: | 40 | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 41 | docker push ghcr.io/hyperledger/firefly-signer:${{ steps.build_tag_generator.outputs.BUILD_TAG }} 42 | 43 | - name: Push head tag 44 | run: | 45 | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 46 | docker push ghcr.io/hyperledger/firefly-signer:head 47 | -------------------------------------------------------------------------------- /.github/workflows/docker_release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Release Build 2 | 3 | on: 4 | release: 5 | types: [released, prereleased] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Build 16 | run: | 17 | make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ 18 | --label commit=$GITHUB_SHA \ 19 | --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 20 | --label tag=${GITHUB_REF##*/} \ 21 | --tag ghcr.io/hyperledger/firefly-signer:${GITHUB_REF##*/}" \ 22 | docker 23 | 24 | - name: Tag release 25 | if: github.event.action == 'released' 26 | run: docker tag ghcr.io/hyperledger/firefly-signer:${GITHUB_REF##*/} ghcr.io/hyperledger/firefly-signer:latest 27 | 28 | - name: Push docker image 29 | run: | 30 | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 31 | docker push ghcr.io/hyperledger/firefly-signer:${GITHUB_REF##*/} 32 | 33 | - name: Push latest tag 34 | if: github.event.action == 'released' 35 | run: | 36 | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 37 | docker push ghcr.io/hyperledger/firefly-signer:latest 38 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: "1.23" 22 | check-latest: true 23 | 24 | - name: Build and Test 25 | run: make 26 | 27 | - name: Upload coverage 28 | run: bash <(curl -s https://codecov.io/bash) 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.jar 2 | firefly-signer 3 | coverage.txt 4 | **/debug.test 5 | .DS_Store 6 | __debug* 7 | .vscode/*.log 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | skip-dirs: 4 | - "mocks" 5 | - "ffconfig" 6 | linters-settings: 7 | golint: {} 8 | gocritic: 9 | enabled-checks: [] 10 | disabled-checks: 11 | - regexpMust 12 | revive: 13 | rules: 14 | - name: unused-parameter 15 | disabled: true 16 | gosec: 17 | excludes: 18 | - G601 # Appears not to handle taking an address of a sub-structure, within a pointer to a structure within a loop. Which is valid and safe. 19 | goheader: 20 | values: 21 | regexp: 22 | COMPANY: .* 23 | YEAR_LAX: '202\d' 24 | template: |- 25 | Copyright © {{ YEAR_LAX }} {{ COMPANY }} 26 | 27 | SPDX-License-Identifier: Apache-2.0 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | linters: 41 | disable-all: false 42 | disable: 43 | - structcheck 44 | enable: 45 | - bodyclose 46 | - dogsled 47 | - errcheck 48 | - goconst 49 | - gocritic 50 | - gocyclo 51 | - gofmt 52 | - goheader 53 | - goimports 54 | - goprintffuncname 55 | - gosec 56 | - gosimple 57 | - govet 58 | - ineffassign 59 | - misspell 60 | - nakedret 61 | - revive 62 | - staticcheck 63 | - stylecheck 64 | - typecheck 65 | - unconvert 66 | - unused 67 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.formatFlags": [ 3 | "-s" 4 | ], 5 | "go.lintTool": "golangci-lint", 6 | "cSpell.words": [ 7 | "btcec", 8 | "ccache", 9 | "Debugf", 10 | "dklen", 11 | "ethsigner", 12 | "ethsignermocks", 13 | "ethtypes", 14 | "ffiinputtype", 15 | "ffresty", 16 | "ffsigner", 17 | "fftypes", 18 | "filewallet", 19 | "fsnotify", 20 | "fswallet", 21 | "GJSON", 22 | "httpserver", 23 | "hyperledger", 24 | "Infof", 25 | "Kaleido", 26 | "kdfparams", 27 | "Keccak", 28 | "keypair", 29 | "keystorev", 30 | "logrus", 31 | "pluggable", 32 | "proxying", 33 | "resty", 34 | "rpcbackendmocks", 35 | "secp", 36 | "signerconfig", 37 | "signermsgs", 38 | "stretchr", 39 | "Tracef", 40 | "ufixed", 41 | "unmarshalled", 42 | "unmarshalling", 43 | "Vyper", 44 | "Warnf", 45 | "wsclient" 46 | ], 47 | "go.testTimeout": "10s" 48 | } 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [FireFly Signer Releases](https://github.com/hyperledger/firefly-signer/releases) 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | - @hyperledger/firefly-signer-maintainers 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct Guidelines 2 | 3 | Please review the Hyperledger [Code of 4 | Conduct](https://wiki.hyperledger.org/community/hyperledger-project-code-of-conduct) 5 | before participating. It is important that we keep things civil. 6 | 7 | Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | We welcome contributions to the FireFly Project in many forms, and 4 | there's always plenty to do! 5 | 6 | Please visit the 7 | [contributors guide](https://hyperledger.github.io/firefly/contributors/) in the 8 | docs to learn how to make contributions to this exciting project. 9 | 10 | Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS builder 2 | ARG BUILD_VERSION 3 | ENV BUILD_VERSION=${BUILD_VERSION} 4 | ADD . /ffsigner 5 | WORKDIR /ffsigner 6 | RUN make 7 | 8 | FROM debian:bookworm-slim 9 | WORKDIR /ffsigner 10 | RUN apt update -y \ 11 | && apt install -y curl jq \ 12 | && rm -rf /var/lib/apt/lists/* 13 | COPY --from=builder /ffsigner/firefly-signer /usr/bin/ffsigner 14 | USER 1001 15 | 16 | ENTRYPOINT [ "/usr/bin/ffsigner" ] 17 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | The following is the list of current maintainers this repo: 4 | 5 | | Name | GitHub | Email | LFID | 6 | | ----------------- | --------------- | ---------------------------- | ----------------- | 7 | | Peter Broadhurst | peterbroadhurst | peter.broadhurst@kaleido.io | peterbroadhurst | 8 | | Enrique Lacal | enriquel8 | enrique.lacal@kaleido.io | enrique.lacal | 9 | | Andrew Richardson | awrichar | andrew.richardson@kaleido.io | Andrew.Richardson | 10 | | Vinod Damle | vdamle | vinod.damle@fmr.com | reddevil | 11 | 12 | This list is to be kept up to date as maintainers are added or removed. 13 | 14 | For the full list of maintainers across all repos, the expectations of a maintainer and the process for becoming a maintainer, please see the [FireFly Maintainers page on the Hyperledger Wiki](https://wiki.hyperledger.org/display/FIR/Maintainers). 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VGO=go 2 | GOFILES := $(shell find cmd internal pkg -name '*.go' -print) 3 | GOBIN := $(shell $(VGO) env GOPATH)/bin 4 | LINT := $(GOBIN)/golangci-lint 5 | MOCKERY := $(GOBIN)/mockery 6 | 7 | # Expect that FireFly compiles with CGO disabled 8 | CGO_ENABLED=0 9 | GOGC=30 10 | 11 | .DELETE_ON_ERROR: 12 | 13 | all: build test go-mod-tidy 14 | test: deps lint 15 | $(VGO) test ./internal/... ./cmd/... ./pkg/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s 16 | coverage.html: 17 | $(VGO) tool cover -html=coverage.txt 18 | coverage: test coverage.html 19 | lint: ${LINT} 20 | GOGC=20 $(LINT) run -v --timeout 5m 21 | ${MOCKERY}: 22 | $(VGO) install github.com/vektra/mockery/cmd/mockery@latest 23 | ${LINT}: 24 | $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 25 | 26 | 27 | define makemock 28 | mocks: mocks-$(strip $(1))-$(strip $(2)) 29 | mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} 30 | ${MOCKERY} --case underscore --dir $(1) --name $(2) --outpkg $(3) --output mocks/$(strip $(3)) 31 | endef 32 | 33 | $(eval $(call makemock, pkg/ethsigner, Wallet, ethsignermocks)) 34 | $(eval $(call makemock, pkg/secp256k1, Signer, secp256k1mocks)) 35 | $(eval $(call makemock, pkg/secp256k1, SignerDirect, secp256k1mocks)) 36 | $(eval $(call makemock, internal/rpcserver, Server, rpcservermocks)) 37 | $(eval $(call makemock, pkg/rpcbackend, Backend, rpcbackendmocks)) 38 | 39 | firefly-signer: ${GOFILES} 40 | $(VGO) build -o ./firefly-signer -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v ./ffsigner 41 | go-mod-tidy: .ALWAYS 42 | $(VGO) mod tidy 43 | build: firefly-signer 44 | .ALWAYS: ; 45 | clean: 46 | $(VGO) clean 47 | deps: 48 | $(VGO) get ./ffsigner 49 | reference: 50 | $(VGO) test ./cmd -timeout=10s -tags docs 51 | docker: 52 | docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-signer . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/hyperledger/firefly-signer/branch/main/graph/badge.svg?token=OEI8A08P0R)](https://codecov.io/gh/hyperledger/firefly-signer) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/hyperledger/firefly-signer.svg)](https://pkg.go.dev/github.com/hyperledger/firefly-signer) 3 | 4 | # Hyperledger FireFly Signer 5 | 6 | A set of Ethereum transaction signing utilities designed for use across projects: 7 | 8 | ## Go API libraries 9 | 10 | - RLP Encoding and Decoding 11 | - See `pkg/rlp` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/rlp) 12 | - ABI Encoding and Decoding 13 | - Validation of ABI definitions 14 | - JSON <-> Value Tree <-> ABI Bytes 15 | - Model API exposed, as well as encode/decode APIs 16 | - See `pkg/abi` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/abi) 17 | - Secp256k1 transaction signing for Ethereum transactions 18 | - Original 19 | - EIP-155 20 | - EIP-1559 21 | - EIP-712 (see below) 22 | - See `pkg/ethsigner` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/ethsigner) 23 | - EIP-712 Typed Data implementation 24 | - See `pkg/eip712` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/eip712) 25 | - Keystore V3 key file implementation 26 | - Scrypt - read/write 27 | - pbkdf2 - read 28 | - See `pkg/keystorev3` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/keystorev3) 29 | - Filesystem wallet 30 | - Configurable caching for in-memory keys 31 | - Files in directory with a given extension matching `{{ADDRESS}}.key`/`{{ADDRESS}}.toml` or arbitrary regex 32 | - Files can be TOML/YAML/JSON metadata pointing to Keystore V3 files + password files 33 | - Files can be Keystore V3 files directly, with accompanying `{{ADDRESS}}.pass` files 34 | - Detects newly added files automatically 35 | - See `pkg/fswallet` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/fswallet) 36 | - JSON/RPC client 37 | - HTTP 38 | - WebSockets - with `eth_subscribe` support 39 | - See `pkg/rpcbackend` [go doc](https://pkg.go.dev/github.com/hyperledger/firefly-signer/pkg/rpcbackend) 40 | 41 | ## JSON/RPC proxy server 42 | 43 | A runtime JSON/RPC server/proxy to intercept `eth_sendTransaction` JSON/RPC calls, and pass other 44 | calls through unchanged. 45 | 46 | - Lightweight fast-starting runtime 47 | - HTTP/HTTPS server 48 | - All HTTPS/CORS etc. features from FireFly Microservice framework 49 | - Configured via YAML 50 | - Batch JSON/RPC support 51 | - `eth_sendTransaction` implementation to sign transactions 52 | - If EIP-1559 gas price fields are specified uses `0x02` transactions, otherwise EIP-155 53 | - Makes some JSON/RPC calls on application's behalf 54 | - Queries Chain ID via `net_version` on startup 55 | - `eth_accounts` JSON/RPC method support 56 | - Trivial nonce management built-in (calls `eth_getTransactionCount` for each request) 57 | 58 | ## JSON/RPC proxy server configuration 59 | 60 | For a full list of configuration options see [config.md](./config.md) 61 | 62 | ## Example configuration 63 | 64 | Two examples provided below: 65 | 66 | ### Flat directory of keys 67 | 68 | ```yaml 69 | fileWallet: 70 | path: /data/keystore 71 | filenames: 72 | with0xPrefix: false 73 | primaryExt: '.key.json' 74 | passwordExt: '.password' 75 | server: 76 | address: '127.0.0.1' 77 | port: 8545 78 | backend: 79 | url: https://blockhain.rpc.endpoint/path 80 | ``` 81 | 82 | ### Directory containing TOML configurations 83 | 84 | ```yaml 85 | fileWallet: 86 | path: /data/keystore 87 | filenames: 88 | with0xPrefix: false 89 | primaryExt: '.toml' 90 | metadata: 91 | format: toml 92 | keyFileProperty: '{{ index .signing "key-file" }}' 93 | passwordFileProperty: '{{ index .signing "password-file" }}' 94 | server: 95 | address: '127.0.0.1' 96 | port: 8545 97 | backend: 98 | url: https://blockhain.rpc.endpoint/path 99 | ``` 100 | 101 | Example TOML: 102 | 103 | ```toml 104 | [metadata] 105 | description = "File based configuration" 106 | 107 | [signing] 108 | type = "file-based-signer" 109 | key-file = "/data/keystore/1f185718734552d08278aa70f804580bab5fd2b4.key.json" 110 | password-file = "/data/keystore/1f185718734552d08278aa70f804580bab5fd2b4.pwd" 111 | 112 | ``` 113 | 114 | # License 115 | 116 | Apache 2.0 117 | 118 | # References / credits 119 | 120 | ### JSON/RPC proxy 121 | 122 | The JSON/RPC proxy and RLP encoding code was contributed by Kaleido, Inc. 123 | 124 | ### Cryptography 125 | 126 | secp256k1 cryptography libraries are provided by btcsuite (ISC Licensed): 127 | 128 | https://pkg.go.dev/github.com/btcsuite/btcd/btcec 129 | 130 | ### RLP encoding and keystore 131 | 132 | Reference during implementation was made to the web3j implementation of Ethereum 133 | RLP encoding, and Keystore V3 wallet files (Apache 2.0 licensed): 134 | 135 | https://github.com/web3j/web3j 136 | 137 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Hyperledger Security Policy 2 | 3 | ## Reporting a Security Bug 4 | 5 | If you think you have discovered a security issue in any of the Hyperledger projects, we'd love to 6 | hear from you. We will take all security bugs seriously and if confirmed upon investigation we will 7 | patch it within a reasonable amount of time and release a public security bulletin discussing the 8 | impact and credit the discoverer. 9 | 10 | There are two ways to report a security bug. The easiest is to email a description of the flaw and 11 | any related information (e.g. reproduction steps, version) to 12 | [security at hyperledger dot org](mailto:security@hyperledger.org). 13 | 14 | The other way is to file a confidential security bug in our 15 | [JIRA bug tracking system](https://jira.hyperledger.org). Be sure to set the “Security Level” to 16 | “Security issue”. 17 | 18 | The process by which the Hyperledger Security Team handles security bugs is documented further in 19 | our [Defect Response page](https://wiki.hyperledger.org/display/SEC/Defect+Response) on our 20 | [wiki](https://wiki.hyperledger.org). -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/hyperledger/firefly-common/pkg/config" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | const configReferenceHeader = `--- 28 | layout: default 29 | title: pages.reference 30 | parent: Reference 31 | nav_order: 2 32 | --- 33 | 34 | # Configuration Reference 35 | {: .no_toc } 36 | 37 | 42 | 43 | --- 44 | ` 45 | 46 | func configCommand() *cobra.Command { 47 | versionCmd := &cobra.Command{ 48 | Use: "docs", 49 | Short: "Prints the config info as markdown", 50 | Long: "", 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | initConfig() 53 | b, err := config.GenerateConfigMarkdown(context.Background(), configReferenceHeader, config.GetKnownKeys()) 54 | fmt.Println(string(b)) 55 | return err 56 | }, 57 | } 58 | return versionCmd 59 | } 60 | -------------------------------------------------------------------------------- /cmd/config_docs_generate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | //go:build docs 18 | // +build docs 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "os" 25 | "path/filepath" 26 | "testing" 27 | 28 | "github.com/hyperledger/firefly-common/pkg/config" 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestGenerateConfigDocs(t *testing.T) { 33 | // Initialize config of all plugins 34 | initConfig() 35 | f, err := os.Create(filepath.Join("..", "config.md")) 36 | assert.NoError(t, err) 37 | generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), configReferenceHeader, config.GetKnownKeys()) 38 | assert.NoError(t, err) 39 | _, err = f.Write(generatedConfig) 40 | assert.NoError(t, err) 41 | err = f.Close() 42 | assert.NoError(t, err) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/config_docs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | //go:build !docs 18 | // +build !docs 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "crypto/sha1" 25 | "os" 26 | "path/filepath" 27 | "testing" 28 | 29 | "github.com/hyperledger/firefly-common/pkg/config" 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestConfigDocsUpToDate(t *testing.T) { 34 | // Initialize config of all plugins 35 | initConfig() 36 | generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), configReferenceHeader, config.GetKnownKeys()) 37 | assert.NoError(t, err) 38 | configOnDisk, err := os.ReadFile(filepath.Join("..", "config.md")) 39 | assert.NoError(t, err) 40 | 41 | generatedConfigHash := sha1.New() 42 | generatedConfigHash.Write(generatedConfig) 43 | configOnDiskHash := sha1.New() 44 | configOnDiskHash.Write(configOnDisk) 45 | assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make docs`?") 46 | } 47 | -------------------------------------------------------------------------------- /cmd/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestConfigMarkdown(t *testing.T) { 26 | rootCmd.SetArgs([]string{"docs"}) 27 | defer rootCmd.SetArgs([]string{}) 28 | err := rootCmd.Execute() 29 | assert.NoError(t, err) 30 | } 31 | -------------------------------------------------------------------------------- /cmd/ffsigner.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "os/signal" 24 | "syscall" 25 | 26 | "github.com/hyperledger/firefly-common/pkg/config" 27 | "github.com/hyperledger/firefly-common/pkg/i18n" 28 | "github.com/hyperledger/firefly-common/pkg/log" 29 | "github.com/hyperledger/firefly-signer/internal/rpcserver" 30 | "github.com/hyperledger/firefly-signer/internal/signerconfig" 31 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 32 | "github.com/hyperledger/firefly-signer/pkg/fswallet" 33 | "github.com/sirupsen/logrus" 34 | "github.com/spf13/cobra" 35 | ) 36 | 37 | var sigs = make(chan os.Signal, 1) 38 | 39 | var rootCmd = &cobra.Command{ 40 | Use: "ffsigner", 41 | Short: "Hyperledger FireFly Signer", 42 | Long: ``, 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | return run() 45 | }, 46 | } 47 | 48 | var cfgFile string 49 | 50 | func init() { 51 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file") 52 | rootCmd.AddCommand(versionCommand()) 53 | rootCmd.AddCommand(configCommand()) 54 | } 55 | 56 | func Execute() error { 57 | return rootCmd.Execute() 58 | } 59 | 60 | func initConfig() { 61 | // Read the configuration 62 | signerconfig.Reset() 63 | } 64 | 65 | func run() error { 66 | 67 | initConfig() 68 | err := config.ReadConfig("ffsigner", cfgFile) 69 | 70 | // Setup logging after reading config (even if failed), to output header correctly 71 | ctx, cancelCtx := context.WithCancel(context.Background()) 72 | defer cancelCtx() 73 | ctx = log.WithLogger(ctx, logrus.WithField("pid", fmt.Sprintf("%d", os.Getpid()))) 74 | ctx = log.WithLogger(ctx, logrus.WithField("prefix", "ffsigner")) 75 | 76 | config.SetupLogging(ctx) 77 | 78 | // Deferred error return from reading config 79 | if err != nil { 80 | cancelCtx() 81 | return i18n.WrapError(ctx, err, i18n.MsgConfigFailed) 82 | } 83 | 84 | // Setup signal handling to cancel the context, which shuts down the API Server 85 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 86 | go func() { 87 | sig := <-sigs 88 | log.L(ctx).Infof("Shutting down due to %s", sig.String()) 89 | cancelCtx() 90 | }() 91 | 92 | if !config.GetBool(signerconfig.FileWalletEnabled) { 93 | return i18n.NewError(ctx, signermsgs.MsgNoWalletEnabled) 94 | } 95 | fileWallet, err := fswallet.NewFilesystemWallet(ctx, fswallet.ReadConfig(signerconfig.FileWalletConfig)) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | server, err := rpcserver.NewServer(ctx, fileWallet) 101 | if err != nil { 102 | return err 103 | } 104 | return runServer(server) 105 | } 106 | 107 | func runServer(server rpcserver.Server) error { 108 | err := server.Start() 109 | if err == nil { 110 | err = server.WaitStop() 111 | } 112 | return err 113 | } 114 | -------------------------------------------------------------------------------- /cmd/ffsigner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | "time" 24 | 25 | "github.com/hyperledger/firefly-signer/mocks/rpcservermocks" 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | const configDir = "../test/data/config" 30 | 31 | func TestRunOK(t *testing.T) { 32 | 33 | rootCmd.SetArgs([]string{"-f", "../test/firefly.ffsigner.yaml"}) 34 | defer rootCmd.SetArgs([]string{}) 35 | 36 | done := make(chan struct{}) 37 | go func() { 38 | defer close(done) 39 | err := Execute() 40 | if err != nil { 41 | assert.Error(t, err) 42 | } 43 | }() 44 | 45 | time.Sleep(10 * time.Millisecond) 46 | sigs <- os.Kill 47 | 48 | <-done 49 | 50 | } 51 | 52 | func TestRunNoWallet(t *testing.T) { 53 | 54 | rootCmd.SetArgs([]string{"-f", "../test/no-wallet.ffsigner.yaml"}) 55 | defer rootCmd.SetArgs([]string{}) 56 | 57 | err := Execute() 58 | assert.Regexp(t, "FF22017", err) 59 | 60 | } 61 | 62 | func TestRunBadConfig(t *testing.T) { 63 | 64 | rootCmd.SetArgs([]string{"-f", "../test/bad-config.ffsigner.yaml"}) 65 | defer rootCmd.SetArgs([]string{}) 66 | 67 | err := Execute() 68 | assert.Regexp(t, "FF00101", err) 69 | 70 | } 71 | 72 | func TestRunBadWalletConfig(t *testing.T) { 73 | 74 | rootCmd.SetArgs([]string{"-f", "../test/bad-wallet.ffsigner.yaml"}) 75 | defer rootCmd.SetArgs([]string{}) 76 | 77 | err := Execute() 78 | assert.Regexp(t, "FF22016", err) 79 | 80 | } 81 | 82 | func TestRunFailStartup(t *testing.T) { 83 | rootCmd.SetArgs([]string{"-f", "../test/quick-fail.ffsigner.yaml"}) 84 | defer rootCmd.SetArgs([]string{}) 85 | 86 | err := Execute() 87 | assert.Regexp(t, "FF00151", err) 88 | 89 | } 90 | 91 | func TestRunFailServer(t *testing.T) { 92 | 93 | s := &rpcservermocks.Server{} 94 | s.On("Start").Return(fmt.Errorf("pop")) 95 | err := runServer(s) 96 | assert.Regexp(t, err, "pop") 97 | 98 | } 99 | 100 | func TestRunServerOK(t *testing.T) { 101 | 102 | s := &rpcservermocks.Server{} 103 | s.On("Start").Return(nil) 104 | s.On("WaitStop").Return(nil) 105 | err := runServer(s) 106 | assert.NoError(t, err) 107 | 108 | } 109 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "runtime/debug" 24 | 25 | "github.com/hyperledger/firefly-common/pkg/i18n" 26 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 27 | "github.com/spf13/cobra" 28 | "gopkg.in/yaml.v2" 29 | ) 30 | 31 | var shortened = false 32 | var output = "json" 33 | 34 | var BuildDate string // set by go-releaser 35 | var BuildCommit string // set by go-releaser 36 | var BuildVersionOverride string // set by go-releaser 37 | 38 | type Info struct { 39 | Version string `json:"Version,omitempty" yaml:"Version,omitempty"` 40 | Commit string `json:"Commit,omitempty" yaml:"Commit,omitempty"` 41 | Date string `json:"Date,omitempty" yaml:"Date,omitempty"` 42 | License string `json:"License,omitempty" yaml:"License,omitempty"` 43 | } 44 | 45 | func setBuildInfo(info *Info, buildInfo *debug.BuildInfo, ok bool) { 46 | if ok { 47 | info.Version = buildInfo.Main.Version 48 | } 49 | } 50 | 51 | func versionCommand() *cobra.Command { 52 | versionCmd := &cobra.Command{ 53 | Use: "version", 54 | Short: "Prints the version info", 55 | Long: "", 56 | RunE: func(cmd *cobra.Command, args []string) error { 57 | 58 | info := &Info{ 59 | Version: BuildVersionOverride, 60 | Date: BuildDate, 61 | Commit: BuildCommit, 62 | License: "Apache-2.0", 63 | } 64 | 65 | // Where you are using go install, we will get good version information usefully from Go 66 | // When we're in go-releaser in a Github action, we will have the version passed in explicitly 67 | if info.Version == "" { 68 | buildInfo, ok := debug.ReadBuildInfo() 69 | setBuildInfo(info, buildInfo, ok) 70 | } 71 | 72 | if shortened { 73 | fmt.Println(info.Version) 74 | } else { 75 | var ( 76 | bytes []byte 77 | err error 78 | ) 79 | 80 | switch output { 81 | case "json": 82 | bytes, err = json.MarshalIndent(info, "", " ") 83 | case "yaml": 84 | bytes, err = yaml.Marshal(info) 85 | default: 86 | err = i18n.NewError(context.Background(), signermsgs.MsgInvalidOutputType, output) 87 | } 88 | 89 | if err != nil { 90 | return err 91 | } 92 | 93 | fmt.Println(string(bytes)) 94 | } 95 | 96 | return nil 97 | }, 98 | } 99 | 100 | versionCmd.Flags().BoolVarP(&shortened, "short", "s", false, "print only the version") 101 | versionCmd.Flags().StringVarP(&output, "output", "o", "json", "output format (\"yaml\"|\"json\")") 102 | return versionCmd 103 | } 104 | -------------------------------------------------------------------------------- /cmd/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package cmd 18 | 19 | import ( 20 | "runtime/debug" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestVersionCmdDefault(t *testing.T) { 27 | rootCmd.SetArgs([]string{"version"}) 28 | defer rootCmd.SetArgs([]string{}) 29 | err := rootCmd.Execute() 30 | assert.NoError(t, err) 31 | } 32 | 33 | func TestVersionCmdYAML(t *testing.T) { 34 | rootCmd.SetArgs([]string{"version", "-o", "yaml"}) 35 | defer rootCmd.SetArgs([]string{}) 36 | err := rootCmd.Execute() 37 | assert.NoError(t, err) 38 | } 39 | 40 | func TestVersionCmdJSON(t *testing.T) { 41 | rootCmd.SetArgs([]string{"version", "-o", "json"}) 42 | defer rootCmd.SetArgs([]string{}) 43 | err := rootCmd.Execute() 44 | assert.NoError(t, err) 45 | } 46 | 47 | func TestVersionCmdInvalidType(t *testing.T) { 48 | rootCmd.SetArgs([]string{"version", "-o", "wrong"}) 49 | defer rootCmd.SetArgs([]string{}) 50 | err := rootCmd.Execute() 51 | assert.Regexp(t, "FF22010", err) 52 | } 53 | 54 | func TestVersionCmdShorthand(t *testing.T) { 55 | rootCmd.SetArgs([]string{"version", "-s"}) 56 | defer rootCmd.SetArgs([]string{}) 57 | err := rootCmd.Execute() 58 | assert.NoError(t, err) 59 | } 60 | 61 | func TestSetBuildInfoWithBI(t *testing.T) { 62 | info := &Info{} 63 | setBuildInfo(info, &debug.BuildInfo{Main: debug.Module{Version: "12345"}}, true) 64 | assert.Equal(t, "12345", info.Version) 65 | } 66 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 0.1% 6 | patch: 7 | default: 8 | threshold: 0.1% 9 | ignore: 10 | - "mocks/**/*.go" 11 | -------------------------------------------------------------------------------- /ffsigner/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/hyperledger/firefly-signer/cmd" 24 | ) 25 | 26 | func main() { 27 | if err := cmd.Execute(); err != nil { 28 | fmt.Fprintf(os.Stderr, "%s\n", err) 29 | os.Exit(1) 30 | } 31 | os.Exit(0) 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hyperledger/firefly-signer 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 7 | github.com/fsnotify/fsnotify v1.7.0 8 | github.com/go-resty/resty/v2 v2.11.0 9 | github.com/gorilla/mux v1.8.1 10 | github.com/hyperledger/firefly-common v1.5.5 11 | github.com/karlseguin/ccache v2.0.3+incompatible 12 | github.com/pelletier/go-toml v1.9.5 13 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/spf13/cobra v1.8.0 16 | github.com/spf13/viper v1.18.2 17 | github.com/stretchr/testify v1.9.0 18 | golang.org/x/crypto v0.35.0 19 | golang.org/x/text v0.22.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | ) 22 | 23 | require ( 24 | github.com/aidarkhanov/nanoid v1.0.8 // indirect 25 | github.com/beorn7/perks v1.0.1 // indirect 26 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 27 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/getkin/kin-openapi v0.131.0 // indirect 31 | github.com/ghodss/yaml v1.0.0 // indirect 32 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 33 | github.com/go-openapi/swag v0.23.0 // indirect 34 | github.com/google/uuid v1.5.0 // indirect 35 | github.com/gorilla/websocket v1.5.1 // indirect 36 | github.com/hashicorp/hcl v1.0.0 // indirect 37 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 38 | github.com/josharian/intern v1.0.0 // indirect 39 | github.com/magiconair/properties v1.8.7 // indirect 40 | github.com/mailru/easyjson v0.7.7 // indirect 41 | github.com/mattn/go-colorable v0.1.13 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 44 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 45 | github.com/mitchellh/mapstructure v1.5.0 // indirect 46 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 47 | github.com/nxadm/tail v1.4.8 // indirect 48 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 49 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 50 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 51 | github.com/perimeterx/marshmallow v1.1.5 // indirect 52 | github.com/pkg/errors v0.9.1 // indirect 53 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 54 | github.com/prometheus/client_golang v1.18.0 // indirect 55 | github.com/prometheus/client_model v0.5.0 // indirect 56 | github.com/prometheus/common v0.45.0 // indirect 57 | github.com/prometheus/procfs v0.12.0 // indirect 58 | github.com/rs/cors v1.11.1 // indirect 59 | github.com/sagikazarmark/locafero v0.4.0 // indirect 60 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 61 | github.com/sourcegraph/conc v0.3.0 // indirect 62 | github.com/spf13/afero v1.11.0 // indirect 63 | github.com/spf13/cast v1.6.0 // indirect 64 | github.com/spf13/pflag v1.0.5 // indirect 65 | github.com/stretchr/objx v0.5.2 // indirect 66 | github.com/subosito/gotenv v1.6.0 // indirect 67 | github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 // indirect 68 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect 69 | gitlab.com/hfuss/mux-prometheus v0.0.5 // indirect 70 | go.uber.org/multierr v1.11.0 // indirect 71 | golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect 72 | golang.org/x/net v0.36.0 // indirect 73 | golang.org/x/sys v0.30.0 // indirect 74 | golang.org/x/term v0.29.0 // indirect 75 | golang.org/x/time v0.5.0 // indirect 76 | google.golang.org/protobuf v1.33.0 // indirect 77 | gopkg.in/ini.v1 v1.67.0 // indirect 78 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 79 | gopkg.in/yaml.v3 v3.0.1 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /internal/rpcserver/rpchandler.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rpcserver 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "io" 23 | "net/http" 24 | "strconv" 25 | "unicode" 26 | 27 | "github.com/hyperledger/firefly-common/pkg/fftypes" 28 | "github.com/hyperledger/firefly-common/pkg/i18n" 29 | "github.com/hyperledger/firefly-common/pkg/log" 30 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 31 | "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 32 | ) 33 | 34 | func (s *rpcServer) rpcHandler(w http.ResponseWriter, r *http.Request) { 35 | 36 | ctx := r.Context() // will include logging ID from FireFly server framework 37 | 38 | b, err := io.ReadAll(r.Body) 39 | if err != nil { 40 | s.replyRPCParseError(ctx, w, b) 41 | return 42 | } 43 | 44 | log.L(ctx).Tracef("RPC --> %s", b) 45 | 46 | if s.sniffFirstByte(b) == '[' { 47 | s.handleRPCBatch(ctx, w, b) 48 | return 49 | } 50 | 51 | var rpcRequest rpcbackend.RPCRequest 52 | err = json.Unmarshal(b, &rpcRequest) 53 | if err != nil { 54 | s.replyRPCParseError(ctx, w, b) 55 | return 56 | } 57 | rpcResponse, err := s.processRPC(ctx, &rpcRequest) 58 | if err != nil { 59 | s.replyRPC(ctx, w, rpcResponse, http.StatusInternalServerError) 60 | return 61 | } 62 | s.replyRPC(ctx, w, rpcResponse, http.StatusOK) 63 | 64 | } 65 | 66 | func (s *rpcServer) replyRPCParseError(ctx context.Context, w http.ResponseWriter, b []byte) { 67 | log.L(ctx).Errorf("Request could not be parsed: %s", b) 68 | rpcError := rpcbackend.RPCErrorResponse( 69 | i18n.NewError(ctx, signermsgs.MsgInvalidRequest), 70 | fftypes.JSONAnyPtr("1"), // we couldn't parse the request ID 71 | rpcbackend.RPCCodeInvalidRequest, 72 | ) 73 | s.replyRPC(ctx, w, rpcError, http.StatusBadRequest) 74 | } 75 | 76 | func (s *rpcServer) replyRPC(ctx context.Context, w http.ResponseWriter, result interface{}, status int) { 77 | w.Header().Set("Content-Type", "application/json") 78 | b, _ := json.Marshal(result) 79 | log.L(ctx).Tracef("RPC <-- %s", b) 80 | w.Header().Set("Content-Length", strconv.Itoa(len(b))) 81 | w.WriteHeader(status) 82 | _, _ = w.Write(b) 83 | } 84 | 85 | func (s *rpcServer) sniffFirstByte(data []byte) byte { 86 | sniffLen := len(data) 87 | if sniffLen > 100 { 88 | sniffLen = 100 89 | } 90 | for _, b := range data[0:sniffLen] { 91 | if !unicode.IsSpace(rune(b)) { 92 | return b 93 | } 94 | } 95 | return 0x00 96 | } 97 | 98 | func (s *rpcServer) handleRPCBatch(ctx context.Context, w http.ResponseWriter, batchBytes []byte) { 99 | 100 | var rpcArray []*rpcbackend.RPCRequest 101 | err := json.Unmarshal(batchBytes, &rpcArray) 102 | if err != nil || len(rpcArray) == 0 { 103 | log.L(ctx).Errorf("Bad RPC array received %s", batchBytes) 104 | s.replyRPCParseError(ctx, w, batchBytes) 105 | return 106 | } 107 | 108 | // Kick off a routine to fill in each 109 | rpcResponses := make([]*rpcbackend.RPCResponse, len(rpcArray)) 110 | results := make(chan error) 111 | for i, r := range rpcArray { 112 | responseNumber := i 113 | rpcReq := r 114 | go func() { 115 | var err error 116 | rpcResponses[responseNumber], err = s.processRPC(ctx, rpcReq) 117 | results <- err 118 | }() 119 | } 120 | status := 200 121 | for range rpcArray { 122 | err := <-results 123 | if err != nil { 124 | status = http.StatusInternalServerError 125 | } 126 | } 127 | s.replyRPC(ctx, w, rpcResponses, status) 128 | } 129 | -------------------------------------------------------------------------------- /internal/rpcserver/rpcprocessor.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rpcserver 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/fftypes" 25 | "github.com/hyperledger/firefly-common/pkg/i18n" 26 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 27 | "github.com/hyperledger/firefly-signer/pkg/ethsigner" 28 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 29 | "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 30 | ) 31 | 32 | func (s *rpcServer) processRPC(ctx context.Context, rpcReq *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error) { 33 | if rpcReq.ID == nil { 34 | err := i18n.NewError(ctx, signermsgs.MsgMissingRequestID) 35 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInvalidRequest), err 36 | } 37 | 38 | switch rpcReq.Method { 39 | case "eth_accounts", "personal_accounts": 40 | return s.processEthAccounts(ctx, rpcReq) 41 | case "eth_sendTransaction": 42 | return s.processEthSendTransaction(ctx, rpcReq) 43 | default: 44 | return s.backend.SyncRequest(ctx, rpcReq) 45 | } 46 | } 47 | 48 | func (s *rpcServer) processEthAccounts(ctx context.Context, rpcReq *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error) { 49 | accounts, err := s.wallet.GetAccounts(ctx) 50 | if err != nil { 51 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInternalError), err 52 | } 53 | b, _ := json.Marshal(&accounts) 54 | return &rpcbackend.RPCResponse{ 55 | JSONRpc: "2.0", 56 | ID: rpcReq.ID, 57 | Result: fftypes.JSONAnyPtrBytes(b), 58 | }, nil 59 | } 60 | 61 | func (s *rpcServer) processEthSendTransaction(ctx context.Context, rpcReq *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error) { 62 | 63 | if len(rpcReq.Params) < 1 { 64 | err := i18n.NewError(ctx, signermsgs.MsgInvalidParamCount, 1, len(rpcReq.Params)) 65 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInvalidRequest), err 66 | } 67 | 68 | var txn ethsigner.Transaction 69 | err := json.Unmarshal(rpcReq.Params[0].Bytes(), &txn) 70 | if err != nil { 71 | err := i18n.WrapError(ctx, err, signermsgs.MsgInvalidTransaction) 72 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeParseError), err 73 | } 74 | 75 | if txn.From == nil { 76 | err := i18n.NewError(ctx, signermsgs.MsgMissingFrom) 77 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInvalidRequest), err 78 | } 79 | 80 | // We have trivial nonce management built-in for sequential signing API calls, by making a JSON/RPC request 81 | // to the up-stream node. This should not be relied upon for production use cases. 82 | // See FireFly Transaction Manager, or FireFly EthConnect, for more advanced nonce management capabilities. 83 | if txn.Nonce == nil { 84 | var from ethtypes.Address0xHex 85 | err := json.Unmarshal(txn.From, &from) 86 | if err != nil { 87 | return nil, err 88 | } 89 | rpcErr := s.backend.CallRPC(ctx, &txn.Nonce, "eth_getTransactionCount", &from, "pending") 90 | if rpcErr != nil { 91 | return rpcbackend.RPCErrorResponse(rpcErr.Error(), rpcReq.ID, rpcbackend.RPCCodeInternalError), rpcErr.Error() 92 | } 93 | } 94 | 95 | // Sign the transaction 96 | var hexData ethtypes.HexBytes0xPrefix 97 | hexData, err = s.wallet.Sign(ctx, &txn, s.chainID) 98 | if err != nil { 99 | return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInternalError), err 100 | } 101 | 102 | // Progress with the original request, now updated with a raw transaction fully signed 103 | rpcReq.Method = "eth_sendRawTransaction" 104 | rpcReq.Params = []*fftypes.JSONAny{fftypes.JSONAnyPtr(fmt.Sprintf(`"%s"`, hexData))} 105 | return s.backend.SyncRequest(ctx, rpcReq) 106 | 107 | } 108 | -------------------------------------------------------------------------------- /internal/rpcserver/rpcprocessor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rpcserver 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/hyperledger/firefly-common/pkg/fftypes" 24 | "github.com/hyperledger/firefly-signer/mocks/ethsignermocks" 25 | "github.com/hyperledger/firefly-signer/mocks/rpcbackendmocks" 26 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 27 | "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/mock" 30 | ) 31 | 32 | func TestEthAccountsOK(t *testing.T) { 33 | 34 | _, s, done := newTestServer(t) 35 | defer done() 36 | 37 | w := s.wallet.(*ethsignermocks.Wallet) 38 | w.On("GetAccounts", mock.Anything).Return([]*ethtypes.Address0xHex{ 39 | ethtypes.MustNewAddress("0xFB075BB99F2AA4C49955BF703509A227D7A12248"), 40 | }, nil) 41 | 42 | rpcRes, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 43 | ID: fftypes.JSONAnyPtr("1"), 44 | Method: "eth_accounts", 45 | }) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, `["0xfb075bb99f2aa4c49955bf703509a227d7a12248"]`, rpcRes.Result.String()) 49 | 50 | } 51 | 52 | func TestMissingID(t *testing.T) { 53 | 54 | _, s, done := newTestServer(t) 55 | defer done() 56 | 57 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 58 | Method: "net_version", 59 | }) 60 | assert.Regexp(t, "FF22024", err) 61 | 62 | } 63 | 64 | func TestPersonalAccountsFail(t *testing.T) { 65 | 66 | _, s, done := newTestServer(t) 67 | defer done() 68 | 69 | w := s.wallet.(*ethsignermocks.Wallet) 70 | w.On("GetAccounts", mock.Anything).Return(nil, fmt.Errorf("pop")) 71 | 72 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 73 | ID: fftypes.JSONAnyPtr("1"), 74 | Method: "personal_accounts", 75 | }) 76 | assert.Regexp(t, "pop", err) 77 | 78 | } 79 | 80 | func TestPassthrough(t *testing.T) { 81 | 82 | _, s, done := newTestServer(t) 83 | defer done() 84 | 85 | bm := s.backend.(*rpcbackendmocks.Backend) 86 | bm.On("SyncRequest", mock.Anything, mock.MatchedBy(func(rpcReq *rpcbackend.RPCRequest) bool { 87 | return rpcReq.Method == "net_version" 88 | })).Return(&rpcbackend.RPCResponse{ 89 | Result: fftypes.JSONAnyPtr(`"0x12345"`), 90 | }, nil) 91 | 92 | rpcRes, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 93 | ID: fftypes.JSONAnyPtr("1"), 94 | Method: "net_version", 95 | }) 96 | assert.NoError(t, err) 97 | 98 | assert.Equal(t, `"0x12345"`, rpcRes.Result.String()) 99 | 100 | } 101 | 102 | func TestSignMissingParam(t *testing.T) { 103 | 104 | _, s, done := newTestServer(t) 105 | defer done() 106 | 107 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 108 | ID: fftypes.JSONAnyPtr("1"), 109 | Method: "eth_sendTransaction", 110 | }) 111 | assert.Regexp(t, "FF22019", err) 112 | 113 | } 114 | 115 | func TestSignBadTX(t *testing.T) { 116 | 117 | _, s, done := newTestServer(t) 118 | defer done() 119 | 120 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 121 | ID: fftypes.JSONAnyPtr("1"), 122 | Method: "eth_sendTransaction", 123 | Params: []*fftypes.JSONAny{ 124 | fftypes.JSONAnyPtr(`"not an object"`), 125 | }, 126 | }) 127 | assert.Regexp(t, "FF22023", err) 128 | 129 | } 130 | 131 | func TestSignMissingFrom(t *testing.T) { 132 | 133 | _, s, done := newTestServer(t) 134 | defer done() 135 | 136 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 137 | ID: fftypes.JSONAnyPtr("1"), 138 | Method: "eth_sendTransaction", 139 | Params: []*fftypes.JSONAny{ 140 | fftypes.JSONAnyPtr(`{}`), 141 | }, 142 | }) 143 | assert.Regexp(t, "FF22020", err) 144 | 145 | } 146 | 147 | func TestSignGetNonceBadAddress(t *testing.T) { 148 | 149 | _, s, done := newTestServer(t) 150 | defer done() 151 | 152 | bm := s.backend.(*rpcbackendmocks.Backend) 153 | bm.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionCount", mock.Anything, "pending").Return(fmt.Errorf("pop")) 154 | 155 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 156 | ID: fftypes.JSONAnyPtr("1"), 157 | Method: "eth_sendTransaction", 158 | Params: []*fftypes.JSONAny{ 159 | fftypes.JSONAnyPtr(`{ 160 | "from": "bad address" 161 | }`), 162 | }, 163 | }) 164 | assert.Regexp(t, "bad address", err) 165 | 166 | } 167 | 168 | func TestSignGetNonceFail(t *testing.T) { 169 | 170 | _, s, done := newTestServer(t) 171 | defer done() 172 | 173 | bm := s.backend.(*rpcbackendmocks.Backend) 174 | bm.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionCount", mock.Anything, "pending").Return(&rpcbackend.RPCError{Message: "pop"}) 175 | 176 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 177 | ID: fftypes.JSONAnyPtr("1"), 178 | Method: "eth_sendTransaction", 179 | Params: []*fftypes.JSONAny{ 180 | fftypes.JSONAnyPtr(`{ 181 | "from": "0xfb075bb99f2aa4c49955bf703509a227d7a12248" 182 | }`), 183 | }, 184 | }) 185 | assert.Regexp(t, "pop", err) 186 | 187 | } 188 | 189 | func TestSignSignFail(t *testing.T) { 190 | 191 | _, s, done := newTestServer(t) 192 | defer done() 193 | 194 | w := s.wallet.(*ethsignermocks.Wallet) 195 | w.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("pop")) 196 | 197 | _, err := s.processRPC(s.ctx, &rpcbackend.RPCRequest{ 198 | ID: fftypes.JSONAnyPtr("1"), 199 | Method: "eth_sendTransaction", 200 | Params: []*fftypes.JSONAny{ 201 | fftypes.JSONAnyPtr(`{ 202 | "from": "0xfb075bb99f2aa4c49955bf703509a227d7a12248", 203 | "nonce": "0x123" 204 | }`), 205 | }, 206 | }) 207 | assert.Regexp(t, "pop", err) 208 | 209 | } 210 | -------------------------------------------------------------------------------- /internal/rpcserver/server.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rpcserver 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/gorilla/mux" 24 | "github.com/hyperledger/firefly-common/pkg/config" 25 | "github.com/hyperledger/firefly-common/pkg/ffresty" 26 | "github.com/hyperledger/firefly-common/pkg/httpserver" 27 | "github.com/hyperledger/firefly-common/pkg/i18n" 28 | "github.com/hyperledger/firefly-signer/internal/signerconfig" 29 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 30 | "github.com/hyperledger/firefly-signer/pkg/ethsigner" 31 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 32 | "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 33 | ) 34 | 35 | type Server interface { 36 | Start() error 37 | Stop() 38 | WaitStop() error 39 | } 40 | 41 | func NewServer(ctx context.Context, wallet ethsigner.Wallet) (ss Server, err error) { 42 | 43 | httpClient, err := ffresty.New(ctx, signerconfig.BackendConfig) 44 | if err != nil { 45 | return nil, err 46 | } 47 | s := &rpcServer{ 48 | backend: rpcbackend.NewRPCClient(httpClient), 49 | apiServerDone: make(chan error), 50 | wallet: wallet, 51 | chainID: config.GetInt64(signerconfig.BackendChainID), 52 | } 53 | s.ctx, s.cancelCtx = context.WithCancel(ctx) 54 | 55 | s.apiServer, err = httpserver.NewHTTPServer(ctx, "server", s.router(), s.apiServerDone, signerconfig.ServerConfig, signerconfig.CorsConfig) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return s, err 61 | } 62 | 63 | type rpcServer struct { 64 | ctx context.Context 65 | cancelCtx func() 66 | backend rpcbackend.Backend 67 | 68 | started bool 69 | apiServer httpserver.HTTPServer 70 | apiServerDone chan error 71 | 72 | chainID int64 73 | wallet ethsigner.Wallet 74 | } 75 | 76 | func (s *rpcServer) router() *mux.Router { 77 | mux := mux.NewRouter() 78 | mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(s.rpcHandler)) 79 | return mux 80 | } 81 | 82 | func (s *rpcServer) runAPIServer() { 83 | s.apiServer.ServeHTTP(s.ctx) 84 | } 85 | 86 | func (s *rpcServer) Start() error { 87 | if s.chainID < 0 { 88 | var chainID ethtypes.HexInteger 89 | rpcErr := s.backend.CallRPC(s.ctx, &chainID, "net_version") 90 | if rpcErr != nil { 91 | return i18n.WrapError(s.ctx, rpcErr.Error(), signermsgs.MsgQueryChainID) 92 | } 93 | s.chainID = chainID.BigInt().Int64() 94 | } 95 | 96 | err := s.wallet.Initialize(s.ctx) 97 | if err != nil { 98 | return err 99 | } 100 | go s.runAPIServer() 101 | s.started = true 102 | return nil 103 | } 104 | 105 | func (s *rpcServer) Stop() { 106 | s.cancelCtx() 107 | } 108 | 109 | func (s *rpcServer) WaitStop() (err error) { 110 | if s.started { 111 | s.started = false 112 | err = <-s.apiServerDone 113 | } 114 | return err 115 | } 116 | -------------------------------------------------------------------------------- /internal/rpcserver/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rpcserver 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/hyperledger/firefly-common/pkg/fftls" 27 | "github.com/hyperledger/firefly-common/pkg/httpserver" 28 | "github.com/hyperledger/firefly-signer/internal/signerconfig" 29 | "github.com/hyperledger/firefly-signer/mocks/ethsignermocks" 30 | "github.com/hyperledger/firefly-signer/mocks/rpcbackendmocks" 31 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 32 | "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 33 | "github.com/stretchr/testify/assert" 34 | "github.com/stretchr/testify/mock" 35 | ) 36 | 37 | func newTestServer(t *testing.T) (string, *rpcServer, func()) { 38 | signerconfig.Reset() 39 | 40 | ln, err := net.Listen("tcp", "127.0.0.1:0") 41 | assert.NoError(t, err) 42 | serverPort := strings.Split(ln.Addr().String(), ":")[1] 43 | ln.Close() 44 | signerconfig.ServerConfig.Set(httpserver.HTTPConfPort, serverPort) 45 | signerconfig.ServerConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") 46 | 47 | w := ðsignermocks.Wallet{} 48 | 49 | ss, err := NewServer(context.Background(), w) 50 | assert.NoError(t, err) 51 | s := ss.(*rpcServer) 52 | s.backend = &rpcbackendmocks.Backend{} 53 | 54 | return fmt.Sprintf("http://127.0.0.1:%s", serverPort), 55 | s, 56 | func() { 57 | s.Stop() 58 | _ = s.WaitStop() 59 | } 60 | 61 | } 62 | 63 | func TestBadTLSConfig(t *testing.T) { 64 | signerconfig.Reset() 65 | tlsConf := signerconfig.BackendConfig.SubSection("tls") 66 | tlsConf.Set(fftls.HTTPConfTLSEnabled, true) 67 | tlsConf.Set(fftls.HTTPConfTLSCAFile, "!!!!!badness") 68 | signerconfig.ServerConfig.Set(httpserver.HTTPConfPort, 12345) 69 | signerconfig.ServerConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") 70 | 71 | w := ðsignermocks.Wallet{} 72 | 73 | _, err := NewServer(context.Background(), w) 74 | assert.Regexp(t, "FF00153", err) 75 | } 76 | 77 | func TestStartStop(t *testing.T) { 78 | 79 | _, s, done := newTestServer(t) 80 | defer done() 81 | 82 | bm := s.backend.(*rpcbackendmocks.Backend) 83 | bm.On("CallRPC", mock.Anything, mock.Anything, "net_version").Run(func(args mock.Arguments) { 84 | hi := args[1].(*ethtypes.HexInteger) 85 | hi.BigInt().SetInt64(12345) 86 | }).Return(nil) 87 | 88 | w := s.wallet.(*ethsignermocks.Wallet) 89 | w.On("Initialize", mock.Anything).Return(nil) 90 | err := s.Start() 91 | assert.NoError(t, err) 92 | 93 | assert.Equal(t, int64(12345), s.chainID) 94 | 95 | } 96 | 97 | func TestStartFailChainID(t *testing.T) { 98 | 99 | _, s, done := newTestServer(t) 100 | defer done() 101 | 102 | bm := s.backend.(*rpcbackendmocks.Backend) 103 | bm.On("CallRPC", mock.Anything, mock.Anything, "net_version").Run(func(args mock.Arguments) { 104 | hi := args[1].(*ethtypes.HexInteger) 105 | hi.BigInt().SetInt64(12345) 106 | }).Return(&rpcbackend.RPCError{Message: "pop"}) 107 | 108 | err := s.Start() 109 | assert.Regexp(t, "pop", err) 110 | 111 | } 112 | 113 | func TestStartFailInitialize(t *testing.T) { 114 | 115 | _, s, done := newTestServer(t) 116 | defer done() 117 | 118 | bm := s.backend.(*rpcbackendmocks.Backend) 119 | bm.On("CallRPC", mock.Anything, mock.Anything, "net_version").Run(func(args mock.Arguments) { 120 | hi := args[1].(*ethtypes.HexInteger) 121 | hi.BigInt().SetInt64(12345) 122 | }).Return(nil) 123 | 124 | w := s.wallet.(*ethsignermocks.Wallet) 125 | w.On("Initialize", mock.Anything).Return(fmt.Errorf("pop")) 126 | err := s.Start() 127 | assert.Regexp(t, "pop", err) 128 | 129 | } 130 | 131 | func TestBadConfig(t *testing.T) { 132 | 133 | signerconfig.Reset() 134 | signerconfig.ServerConfig.Set(httpserver.HTTPConfAddress, ":::::") 135 | _, err := NewServer(context.Background(), ðsignermocks.Wallet{}) 136 | assert.Error(t, err) 137 | 138 | } 139 | -------------------------------------------------------------------------------- /internal/signerconfig/signerconfig.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package signerconfig 18 | 19 | import ( 20 | "github.com/hyperledger/firefly-common/pkg/config" 21 | "github.com/hyperledger/firefly-common/pkg/httpserver" 22 | "github.com/hyperledger/firefly-common/pkg/wsclient" 23 | "github.com/hyperledger/firefly-signer/pkg/fswallet" 24 | "github.com/spf13/viper" 25 | ) 26 | 27 | var ffc = config.AddRootKey 28 | 29 | var ( 30 | // BackendChainID optionally set the Chain ID manually (usually queries network ID) 31 | BackendChainID = ffc("backend.chainId") 32 | // FileWalletEnabled if the Keystore V3 wallet is enabled 33 | FileWalletEnabled = ffc("fileWallet.enabled") 34 | ) 35 | 36 | var ServerConfig config.Section 37 | 38 | var CorsConfig config.Section 39 | 40 | var BackendConfig config.Section 41 | 42 | var FileWalletConfig config.Section 43 | 44 | func setDefaults() { 45 | viper.SetDefault(string(BackendChainID), -1) 46 | viper.SetDefault(string(FileWalletEnabled), true) 47 | } 48 | 49 | func Reset() { 50 | config.RootConfigReset(setDefaults) 51 | 52 | ServerConfig = config.RootSection("server") 53 | httpserver.InitHTTPConfig(ServerConfig, 8545) 54 | 55 | CorsConfig = config.RootSection("cors") 56 | httpserver.InitCORSConfig(CorsConfig) 57 | 58 | BackendConfig = config.RootSection("backend") 59 | wsclient.InitConfig(BackendConfig) 60 | 61 | FileWalletConfig = config.RootSection("fileWallet") 62 | fswallet.InitConfig(FileWalletConfig) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /internal/signerconfig/signerconfig_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package signerconfig 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/hyperledger/firefly-common/pkg/config" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | const configDir = "../../test/data/config" 27 | 28 | func TestInitConfigOK(t *testing.T) { 29 | Reset() 30 | 31 | assert.True(t, config.GetBool(FileWalletEnabled)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/signermsgs/en_api_translations.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package signermsgs 18 | 19 | import ( 20 | "github.com/hyperledger/firefly-common/pkg/i18n" 21 | "golang.org/x/text/language" 22 | ) 23 | 24 | var ffm = func(key, translation string) i18n.MessageKey { 25 | return i18n.FFM(language.AmericanEnglish, key, translation) 26 | } 27 | 28 | //revive:disable 29 | var ( 30 | APIIntegerDescription = ffm("api.integer", "An integer. You are recommended to use a JSON string. A JSON number can be used for values up to the safe maximum.") 31 | APIBoolDescription = ffm("api.bool", "A boolean. You can use a boolean or a string true/false as input") 32 | APIFloatDescription = ffm("api.float", "A floating point number, which will be converted to a fixed point number. You are recommended to use a JSON string. A JSON number can be used for values up to the safe maximum.") 33 | APIHexDescription = ffm("api.hex", "A hex encoded set of bytes, with an optional '0x' prefix") 34 | ) 35 | -------------------------------------------------------------------------------- /internal/signermsgs/en_config_descriptions.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package signermsgs 18 | 19 | import ( 20 | "github.com/hyperledger/firefly-common/pkg/i18n" 21 | "golang.org/x/text/language" 22 | ) 23 | 24 | var ffc = func(key, translation, fieldType string) i18n.ConfigMessageKey { 25 | return i18n.FFC(language.AmericanEnglish, key, translation, fieldType) 26 | } 27 | 28 | //revive:disable 29 | var ( 30 | ConfigFileWalletEnabled = ffc("config.fileWallet.enabled", "Whether the Keystore V3 filesystem wallet is enabled", "boolean") 31 | ConfigFileWalletPath = ffc("config.fileWallet.path", "Path on the filesystem where the metadata files (and/or key files) are located", "string") 32 | ConfigFileWalletFilenamesPrimaryBatchRegex = ffc("config.fileWallet.filenames.primaryMatchRegex", "Regular expression run against key/metadata filenames to extract the address (takes precedence over primaryExt)", "regexp") 33 | ConfigFileWalletFilenamesWith0xPrefix = ffc("config.fileWallet.filenames.with0xPrefix", "When true and passwordExt is used, password filenames will be generated with an 0x prefix", "boolean") 34 | ConfigFileWalletFilenamesPrimaryExt = ffc("config.fileWallet.filenames.primaryExt", "Extension for key/metadata files named by
.", "string") 35 | ConfigFileWalletFilenamesPasswordExt = ffc("config.fileWallet.filenames.passwordExt", "Optional to use to look up password files, that sit next to the key files directly. Alternative to metadata when you have a password per keystore", "string") 36 | ConfigFileWalletFilenamesPasswordPath = ffc("config.fileWallet.filenames.passwordPath", "Optional directory in which to look for the password files, when passwordExt is configured. Default is the wallet directory", "string") 37 | ConfigFileWalletFilenamesPasswordTrimSpace = ffc("config.fileWallet.filenames.passwordTrimSpace", "Whether to trim leading/trailing whitespace (such as a newline) from the password when loaded from file", "boolean") 38 | ConfigFileWalletDefaultPasswordFile = ffc("config.fileWallet.defaultPasswordFile", "Optional default password file to use, if one is not specified individually for the key (via metadata, or file extension)", "string") 39 | ConfigFileWalletDisableListener = ffc("config.fileWallet.disableListener", "Disable the filesystem listener that automatically detects the creation of new keystore files", "boolean") 40 | ConfigFileWalletSignerCacheSize = ffc("config.fileWallet.signerCacheSize", "Maximum of signing keys to hold in memory", "number") 41 | ConfigFileWalletSignerCacheTTL = ffc("config.fileWallet.signerCacheTTL", "How long ot leave an unused signing key in memory", "duration") 42 | ConfigFileWalletMetadataFormat = ffc("config.fileWallet.metadata.format", "Set this if the primary key file is a metadata file. Supported formats: auto (from extension) / filename / toml / yaml / json (please quote \"0x...\" strings in YAML)", "string") 43 | ConfigFileWalletMetadataKeyFileProperty = ffc("config.fileWallet.metadata.keyFileProperty", "Go template to look up the key-file path from the metadata. Example: '{{ index .signing \"key-file\" }}'", "go-template") 44 | ConfigFileWalletMetadataPasswordFileProperty = ffc("config.fileWallet.metadata.passwordFileProperty", "Go template to look up the password-file path from the metadata", "go-template") 45 | 46 | ConfigServerAddress = ffc("config.server.address", "Local address for the JSON/RPC server to listen on", "string") 47 | ConfigServerPort = ffc("config.server.port", "Port for the JSON/RPC server to listen on", "number") 48 | ConfigAPIPublicURL = ffc("config.server.publicURL", "External address callers should access API over", "string") 49 | ConfigServerReadTimeout = ffc("config.server.readTimeout", "The maximum time to wait when reading from an HTTP connection", "duration") 50 | ConfigServerWriteTimeout = ffc("config.server.writeTimeout", "The maximum time to wait when writing to a HTTP connection", "duration") 51 | ConfigAPIShutdownTimeout = ffc("config.server.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) 52 | 53 | ConfigBackendChainID = ffc("config.backend.chainId", "Optionally set the Chain ID of the blockchain. Otherwise the Network ID will be queried, and used as the Chain ID in signing", "number") 54 | ConfigBackendURL = ffc("config.backend.url", "URL for the backend JSON/RPC server / blockchain node", "url") 55 | ConfigBackendProxyURL = ffc("config.backend.proxy.url", "Optional HTTP proxy URL", "url") 56 | ) 57 | -------------------------------------------------------------------------------- /internal/signermsgs/en_field_descriptions.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package signermsgs 18 | 19 | var ( 20 | ABIEntryAnonymous = ffm("EthABIEntry.anonymous", "If the event is anonymous then the signature of the event does not take up a topic slot") 21 | ABIEntryType = ffm("EthABIEntry.type", "The type of the ABI entry: 'event', 'error', 'function', 'constructor', 'receive', or 'fallback'") 22 | ABIEntryName = ffm("EthABIEntry.name", "The name of the ABI entry") 23 | ABIEntryInputs = ffm("EthABIEntry.inputs", "Array of ABI parameter definitions") 24 | ABIEntryOutputs = ffm("EthABIEntry.outputs", "Array of ABI parameter definitions") 25 | ABIEntryStateMutability = ffm("EthABIEntry.stateMutability", "The state mutability of the function: 'pure', 'view', 'nonpayable' (the default) and 'payable'") 26 | ABIEntryConstant = ffm("EthABIEntry.constant", "Functions only: Superseded by stateMutability payable/nonpayable") 27 | ABIEntryPayable = ffm("EthABIEntry.payable", "Functions only: Superseded by stateMutability pure/view") 28 | 29 | ABIParameterName = ffm("EthABIParameter.name", "The name of the parameter") 30 | ABIParameterType = ffm("EthABIParameter.type", "The type of the parameter per the ABI specification") 31 | ABIParameterComponents = ffm("EthABIParameter.components", "An array of components, if the parameter is a tuple") 32 | ABIParameterIndexed = ffm("EthABIParameter.indexed", "Whether this parameter uses one of the topics, or is in the data area") 33 | ABIParameterInternalType = ffm("EthABIParameter.internalType", "Used by the solc compiler to include additional details - importantly the struct name for tuples") 34 | 35 | EthTransactionFrom = ffm("EthTransaction.from", "The from address (not encoded into the transaction directly, but used on this structure on input)") 36 | EthTransactionNonce = ffm("EthTransaction.nonce", "Number used once (nonce) that specifies the sequence of this transaction in all transactions sent to the chain from this signing address") 37 | EthTransactionGasPrice = ffm("EthTransaction.gasPrice", "The price per unit offered for the gas used when executing this transaction, if submitting to a chain that requires gas fees (in wei of the native chain token)") 38 | EthTransactionMaxPriorityFeePerGas = ffm("EthTransaction.maxPriorityFeePerGas", "Part of the EIP-1559 extension to transaction pricing. The amount provided to the miner of the block per unit of gas, in addition to the base fee (which is burned when the block is mined)") 39 | EthTransactionMaxFeePerGas = ffm("EthTransaction.maxFeePerGas", "Part of the EIP-1559 extension to transaction pricing. The total amount you are willing to pay per unit of gas used by your contract, which is the total of the baseFeePerGas (determined by the chain at execution time) and the maxPriorityFeePerGas") 40 | EthTransactionGas = ffm("EthTransaction.gas", "The gas limit for execution of your transaction. Must be provided regardless of whether you paying a fee for the gas") 41 | EthTransactionTo = ffm("EthTransaction.to", "The target address of the transaction. Omitted for contract deployments") 42 | EthTransactionValue = ffm("EthTransaction.value", "An optional amount of native token to transfer along with the transaction (in wei)") 43 | EthTransactionData = ffm("EthTransaction.data", "The encoded and signed transaction payload") 44 | 45 | EIP712ResultHash = ffm("EIP712Result.hash", "The EIP-712 hash generated according to the Typed Data V4 algorithm") 46 | EIP712ResultSignatureRSV = ffm("EIP712Result.signatureRSV", "Hex encoded array of 65 bytes containing the R, S & V of the ECDSA signature. This is the standard signature encoding used in Ethereum recover utilities (note that some other utilities might expect a different encoding/packing of the data)") 47 | EIP712ResultV = ffm("EIP712Result.v", "The V value of the ECDSA signature as a hex encoded integer") 48 | EIP712ResultR = ffm("EIP712Result.r", "The R value of the ECDSA signature as a 32byte hex encoded array") 49 | EIP712ResultS = ffm("EIP712Result.s", "The S value of the ECDSA signature as a 32byte hex encoded array") 50 | 51 | TypedDataDomain = ffm("TypedData.domain", "The data to encode into the EIP712Domain as part fo signing the transaction") 52 | TypedDataMessage = ffm("TypedData.message", "The data to encode into primaryType structure, with nested values for any sub-structures") 53 | TypedDataTypes = ffm("TypedData.types", "Array of types to use when encoding, which must include the primaryType and the EIP712Domain (noting the primary type can be EIP712Domain if the message is empty)") 54 | TypedDataPrimaryType = ffm("TypedData.primaryType", "The primary type to begin encoding the EIP-712 hash from in the list of types, using the input message (unless set directly to EIP712Domain, in which case the message can be omitted)") 55 | ) 56 | -------------------------------------------------------------------------------- /mocks/ethsignermocks/wallet.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package ethsignermocks 4 | 5 | import ( 6 | context "context" 7 | 8 | ethsigner "github.com/hyperledger/firefly-signer/pkg/ethsigner" 9 | ethtypes "github.com/hyperledger/firefly-signer/pkg/ethtypes" 10 | 11 | mock "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | // Wallet is an autogenerated mock type for the Wallet type 15 | type Wallet struct { 16 | mock.Mock 17 | } 18 | 19 | // Close provides a mock function with given fields: 20 | func (_m *Wallet) Close() error { 21 | ret := _m.Called() 22 | 23 | var r0 error 24 | if rf, ok := ret.Get(0).(func() error); ok { 25 | r0 = rf() 26 | } else { 27 | r0 = ret.Error(0) 28 | } 29 | 30 | return r0 31 | } 32 | 33 | // GetAccounts provides a mock function with given fields: ctx 34 | func (_m *Wallet) GetAccounts(ctx context.Context) ([]*ethtypes.Address0xHex, error) { 35 | ret := _m.Called(ctx) 36 | 37 | var r0 []*ethtypes.Address0xHex 38 | var r1 error 39 | if rf, ok := ret.Get(0).(func(context.Context) ([]*ethtypes.Address0xHex, error)); ok { 40 | return rf(ctx) 41 | } 42 | if rf, ok := ret.Get(0).(func(context.Context) []*ethtypes.Address0xHex); ok { 43 | r0 = rf(ctx) 44 | } else { 45 | if ret.Get(0) != nil { 46 | r0 = ret.Get(0).([]*ethtypes.Address0xHex) 47 | } 48 | } 49 | 50 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 51 | r1 = rf(ctx) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | 59 | // Initialize provides a mock function with given fields: ctx 60 | func (_m *Wallet) Initialize(ctx context.Context) error { 61 | ret := _m.Called(ctx) 62 | 63 | var r0 error 64 | if rf, ok := ret.Get(0).(func(context.Context) error); ok { 65 | r0 = rf(ctx) 66 | } else { 67 | r0 = ret.Error(0) 68 | } 69 | 70 | return r0 71 | } 72 | 73 | // Refresh provides a mock function with given fields: ctx 74 | func (_m *Wallet) Refresh(ctx context.Context) error { 75 | ret := _m.Called(ctx) 76 | 77 | var r0 error 78 | if rf, ok := ret.Get(0).(func(context.Context) error); ok { 79 | r0 = rf(ctx) 80 | } else { 81 | r0 = ret.Error(0) 82 | } 83 | 84 | return r0 85 | } 86 | 87 | // Sign provides a mock function with given fields: ctx, txn, chainID 88 | func (_m *Wallet) Sign(ctx context.Context, txn *ethsigner.Transaction, chainID int64) ([]byte, error) { 89 | ret := _m.Called(ctx, txn, chainID) 90 | 91 | var r0 []byte 92 | var r1 error 93 | if rf, ok := ret.Get(0).(func(context.Context, *ethsigner.Transaction, int64) ([]byte, error)); ok { 94 | return rf(ctx, txn, chainID) 95 | } 96 | if rf, ok := ret.Get(0).(func(context.Context, *ethsigner.Transaction, int64) []byte); ok { 97 | r0 = rf(ctx, txn, chainID) 98 | } else { 99 | if ret.Get(0) != nil { 100 | r0 = ret.Get(0).([]byte) 101 | } 102 | } 103 | 104 | if rf, ok := ret.Get(1).(func(context.Context, *ethsigner.Transaction, int64) error); ok { 105 | r1 = rf(ctx, txn, chainID) 106 | } else { 107 | r1 = ret.Error(1) 108 | } 109 | 110 | return r0, r1 111 | } 112 | 113 | // NewWallet creates a new instance of Wallet. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 114 | // The first argument is typically a *testing.T value. 115 | func NewWallet(t interface { 116 | mock.TestingT 117 | Cleanup(func()) 118 | }) *Wallet { 119 | mock := &Wallet{} 120 | mock.Mock.Test(t) 121 | 122 | t.Cleanup(func() { mock.AssertExpectations(t) }) 123 | 124 | return mock 125 | } 126 | -------------------------------------------------------------------------------- /mocks/rpcbackendmocks/backend.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package rpcbackendmocks 4 | 5 | import ( 6 | context "context" 7 | 8 | rpcbackend "github.com/hyperledger/firefly-signer/pkg/rpcbackend" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // Backend is an autogenerated mock type for the Backend type 13 | type Backend struct { 14 | mock.Mock 15 | } 16 | 17 | // CallRPC provides a mock function with given fields: ctx, result, method, params 18 | func (_m *Backend) CallRPC(ctx context.Context, result interface{}, method string, params ...interface{}) *rpcbackend.RPCError { 19 | var _ca []interface{} 20 | _ca = append(_ca, ctx, result, method) 21 | _ca = append(_ca, params...) 22 | ret := _m.Called(_ca...) 23 | 24 | var r0 *rpcbackend.RPCError 25 | if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) *rpcbackend.RPCError); ok { 26 | r0 = rf(ctx, result, method, params...) 27 | } else { 28 | if ret.Get(0) != nil { 29 | r0 = ret.Get(0).(*rpcbackend.RPCError) 30 | } 31 | } 32 | 33 | return r0 34 | } 35 | 36 | // SyncRequest provides a mock function with given fields: ctx, rpcReq 37 | func (_m *Backend) SyncRequest(ctx context.Context, rpcReq *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error) { 38 | ret := _m.Called(ctx, rpcReq) 39 | 40 | var r0 *rpcbackend.RPCResponse 41 | var r1 error 42 | if rf, ok := ret.Get(0).(func(context.Context, *rpcbackend.RPCRequest) (*rpcbackend.RPCResponse, error)); ok { 43 | return rf(ctx, rpcReq) 44 | } 45 | if rf, ok := ret.Get(0).(func(context.Context, *rpcbackend.RPCRequest) *rpcbackend.RPCResponse); ok { 46 | r0 = rf(ctx, rpcReq) 47 | } else { 48 | if ret.Get(0) != nil { 49 | r0 = ret.Get(0).(*rpcbackend.RPCResponse) 50 | } 51 | } 52 | 53 | if rf, ok := ret.Get(1).(func(context.Context, *rpcbackend.RPCRequest) error); ok { 54 | r1 = rf(ctx, rpcReq) 55 | } else { 56 | r1 = ret.Error(1) 57 | } 58 | 59 | return r0, r1 60 | } 61 | 62 | // NewBackend creates a new instance of Backend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 63 | // The first argument is typically a *testing.T value. 64 | func NewBackend(t interface { 65 | mock.TestingT 66 | Cleanup(func()) 67 | }) *Backend { 68 | mock := &Backend{} 69 | mock.Mock.Test(t) 70 | 71 | t.Cleanup(func() { mock.AssertExpectations(t) }) 72 | 73 | return mock 74 | } 75 | -------------------------------------------------------------------------------- /mocks/rpcservermocks/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package rpcservermocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Server is an autogenerated mock type for the Server type 8 | type Server struct { 9 | mock.Mock 10 | } 11 | 12 | // Start provides a mock function with given fields: 13 | func (_m *Server) Start() error { 14 | ret := _m.Called() 15 | 16 | var r0 error 17 | if rf, ok := ret.Get(0).(func() error); ok { 18 | r0 = rf() 19 | } else { 20 | r0 = ret.Error(0) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // Stop provides a mock function with given fields: 27 | func (_m *Server) Stop() { 28 | _m.Called() 29 | } 30 | 31 | // WaitStop provides a mock function with given fields: 32 | func (_m *Server) WaitStop() error { 33 | ret := _m.Called() 34 | 35 | var r0 error 36 | if rf, ok := ret.Get(0).(func() error); ok { 37 | r0 = rf() 38 | } else { 39 | r0 = ret.Error(0) 40 | } 41 | 42 | return r0 43 | } 44 | 45 | // NewServer creates a new instance of Server. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | // The first argument is typically a *testing.T value. 47 | func NewServer(t interface { 48 | mock.TestingT 49 | Cleanup(func()) 50 | }) *Server { 51 | mock := &Server{} 52 | mock.Mock.Test(t) 53 | 54 | t.Cleanup(func() { mock.AssertExpectations(t) }) 55 | 56 | return mock 57 | } 58 | -------------------------------------------------------------------------------- /mocks/secp256k1mocks/signer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package secp256k1mocks 4 | 5 | import ( 6 | secp256k1 "github.com/hyperledger/firefly-signer/pkg/secp256k1" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // Signer is an autogenerated mock type for the Signer type 11 | type Signer struct { 12 | mock.Mock 13 | } 14 | 15 | // Sign provides a mock function with given fields: msgToHashAndSign 16 | func (_m *Signer) Sign(msgToHashAndSign []byte) (*secp256k1.SignatureData, error) { 17 | ret := _m.Called(msgToHashAndSign) 18 | 19 | var r0 *secp256k1.SignatureData 20 | var r1 error 21 | if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { 22 | return rf(msgToHashAndSign) 23 | } 24 | if rf, ok := ret.Get(0).(func([]byte) *secp256k1.SignatureData); ok { 25 | r0 = rf(msgToHashAndSign) 26 | } else { 27 | if ret.Get(0) != nil { 28 | r0 = ret.Get(0).(*secp256k1.SignatureData) 29 | } 30 | } 31 | 32 | if rf, ok := ret.Get(1).(func([]byte) error); ok { 33 | r1 = rf(msgToHashAndSign) 34 | } else { 35 | r1 = ret.Error(1) 36 | } 37 | 38 | return r0, r1 39 | } 40 | 41 | // NewSigner creates a new instance of Signer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 42 | // The first argument is typically a *testing.T value. 43 | func NewSigner(t interface { 44 | mock.TestingT 45 | Cleanup(func()) 46 | }) *Signer { 47 | mock := &Signer{} 48 | mock.Mock.Test(t) 49 | 50 | t.Cleanup(func() { mock.AssertExpectations(t) }) 51 | 52 | return mock 53 | } 54 | -------------------------------------------------------------------------------- /mocks/secp256k1mocks/signer_direct.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.37.1. DO NOT EDIT. 2 | 3 | package secp256k1mocks 4 | 5 | import ( 6 | secp256k1 "github.com/hyperledger/firefly-signer/pkg/secp256k1" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // SignerDirect is an autogenerated mock type for the SignerDirect type 11 | type SignerDirect struct { 12 | mock.Mock 13 | } 14 | 15 | // Sign provides a mock function with given fields: msgToHashAndSign 16 | func (_m *SignerDirect) Sign(msgToHashAndSign []byte) (*secp256k1.SignatureData, error) { 17 | ret := _m.Called(msgToHashAndSign) 18 | 19 | var r0 *secp256k1.SignatureData 20 | var r1 error 21 | if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { 22 | return rf(msgToHashAndSign) 23 | } 24 | if rf, ok := ret.Get(0).(func([]byte) *secp256k1.SignatureData); ok { 25 | r0 = rf(msgToHashAndSign) 26 | } else { 27 | if ret.Get(0) != nil { 28 | r0 = ret.Get(0).(*secp256k1.SignatureData) 29 | } 30 | } 31 | 32 | if rf, ok := ret.Get(1).(func([]byte) error); ok { 33 | r1 = rf(msgToHashAndSign) 34 | } else { 35 | r1 = ret.Error(1) 36 | } 37 | 38 | return r0, r1 39 | } 40 | 41 | // SignDirect provides a mock function with given fields: message 42 | func (_m *SignerDirect) SignDirect(message []byte) (*secp256k1.SignatureData, error) { 43 | ret := _m.Called(message) 44 | 45 | var r0 *secp256k1.SignatureData 46 | var r1 error 47 | if rf, ok := ret.Get(0).(func([]byte) (*secp256k1.SignatureData, error)); ok { 48 | return rf(message) 49 | } 50 | if rf, ok := ret.Get(0).(func([]byte) *secp256k1.SignatureData); ok { 51 | r0 = rf(message) 52 | } else { 53 | if ret.Get(0) != nil { 54 | r0 = ret.Get(0).(*secp256k1.SignatureData) 55 | } 56 | } 57 | 58 | if rf, ok := ret.Get(1).(func([]byte) error); ok { 59 | r1 = rf(message) 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // NewSignerDirect creates a new instance of SignerDirect. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 68 | // The first argument is typically a *testing.T value. 69 | func NewSignerDirect(t interface { 70 | mock.TestingT 71 | Cleanup(func()) 72 | }) *SignerDirect { 73 | mock := &SignerDirect{} 74 | mock.Mock.Test(t) 75 | 76 | t.Cleanup(func() { mock.AssertExpectations(t) }) 77 | 78 | return mock 79 | } 80 | -------------------------------------------------------------------------------- /pkg/abi/ethers.interface.sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "payable": false, 5 | "inputs": [ 6 | { 7 | "type": "uint256", 8 | "name": "initVal" 9 | } 10 | ] 11 | }, 12 | { 13 | "type": "event", 14 | "anonymous": false, 15 | "name": "Changed", 16 | "inputs": [ 17 | { 18 | "type": "uint256", 19 | "name": "data", 20 | "indexed": false 21 | } 22 | ] 23 | }, 24 | { 25 | "type": "function", 26 | "name": "get", 27 | "constant": true, 28 | "stateMutability": "view", 29 | "payable": false, 30 | "gas": 29000000, 31 | "inputs": [], 32 | "outputs": [ 33 | { 34 | "type": "uint256", 35 | "name": "retVal" 36 | } 37 | ] 38 | }, 39 | { 40 | "type": "function", 41 | "name": "query", 42 | "constant": true, 43 | "stateMutability": "view", 44 | "payable": false, 45 | "gas": 29000000, 46 | "inputs": [], 47 | "outputs": [ 48 | { 49 | "type": "uint256", 50 | "name": "retVal" 51 | } 52 | ] 53 | }, 54 | { 55 | "type": "function", 56 | "name": "set", 57 | "constant": false, 58 | "payable": false, 59 | "gas": 29000000, 60 | "inputs": [ 61 | { 62 | "type": "uint256", 63 | "name": "x" 64 | } 65 | ], 66 | "outputs": [ 67 | { 68 | "type": "uint256", 69 | "name": "value" 70 | } 71 | ] 72 | }, 73 | { 74 | "type": "function", 75 | "name": "storedData", 76 | "constant": true, 77 | "stateMutability": "view", 78 | "payable": false, 79 | "gas": 29000000, 80 | "inputs": [], 81 | "outputs": [ 82 | { 83 | "type": "uint256" 84 | } 85 | ] 86 | } 87 | ] -------------------------------------------------------------------------------- /pkg/abi/signedi256.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package abi 18 | 19 | import "math/big" 20 | 21 | var singleBit = big.NewInt(1) 22 | var oneMoreThanMaxUint256 = new(big.Int).Lsh(singleBit, 256) // 2^256 - a one then 256 zeros 23 | var fullBits256 = new(big.Int).Sub(oneMoreThanMaxUint256, big.NewInt(1)) // all ones for 256 bits 24 | var oneThen255Zeros = new(big.Int).Lsh(singleBit, 255) 25 | var posMax = map[uint16]*big.Int{} 26 | var negMax = map[uint16]*big.Int{} 27 | 28 | func init() { 29 | for i := uint16(8); i <= uint16(256); i += 8 { 30 | posMax[i] = maxPositiveSignedInt(uint(i)) 31 | negMax[i] = maxNegativeSignedInt(uint(i)) 32 | } 33 | } 34 | 35 | func maxPositiveSignedInt(bitLen uint) *big.Int { 36 | return new(big.Int).Sub(new(big.Int).Lsh(singleBit, bitLen-1), big.NewInt(1)) 37 | } 38 | 39 | func maxNegativeSignedInt(bitLen uint) *big.Int { 40 | return new(big.Int).Neg(new(big.Int).Lsh(singleBit, bitLen-1)) 41 | } 42 | 43 | func checkSignedIntFits(i *big.Int, bitlen uint16) bool { 44 | switch i.Sign() { 45 | case 0: 46 | return true 47 | case 1: 48 | max, ok := posMax[bitlen] 49 | return ok && i.Cmp(max) <= 0 50 | default: // -1 51 | max, ok := negMax[bitlen] 52 | return ok && i.Cmp(max) >= 0 53 | } 54 | } 55 | 56 | func SerializeInt256TwosComplementBytes(i *big.Int) []byte { 57 | // Go doesn't have a function to serialize bytes in two's compliment, 58 | // but you can do a bitwise AND to get a positive integer containing 59 | // the bits of the two's compliment value (for the number of bits you provide) 60 | tcI := new(big.Int).And(i, fullBits256) 61 | b := make([]byte, 32) 62 | return tcI.FillBytes(b) 63 | } 64 | 65 | func ParseInt256TwosComplementBytes(b []byte) *big.Int { 66 | // Parse the two's complement bytes as a positive number 67 | i := new(big.Int).SetBytes(b) 68 | // If the sign bit is not set, this is a positive number 69 | if i.Cmp(oneThen255Zeros) < 0 { 70 | return i 71 | } 72 | // Otherwise negate the value 73 | i.Sub(i, oneMoreThanMaxUint256) 74 | return i 75 | } 76 | -------------------------------------------------------------------------------- /pkg/abi/signedi256_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package abi 18 | 19 | import ( 20 | "math/big" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestInt256TwosCompliment(t *testing.T) { 27 | 28 | i := big.NewInt(-12345) 29 | b := SerializeInt256TwosComplementBytes(i) 30 | i2 := ParseInt256TwosComplementBytes(b) 31 | assert.Equal(t, int64(-12345), i2.Int64()) 32 | 33 | // Largest negative two's compliment - 2^255 34 | i = new(big.Int).Exp(big.NewInt(2), big.NewInt(255), nil) 35 | i = i.Neg(i) 36 | b = SerializeInt256TwosComplementBytes(i) 37 | i3 := ParseInt256TwosComplementBytes(b) 38 | assert.Zero(t, i.Cmp(i3)) 39 | 40 | // Largest positive two's compliment - 2^255-1 41 | i = new(big.Int).Exp(big.NewInt(2), big.NewInt(255), nil) 42 | i = i.Sub(i, big.NewInt(1)) 43 | b = SerializeInt256TwosComplementBytes(i) 44 | i4 := ParseInt256TwosComplementBytes(b) 45 | assert.Zero(t, i.Cmp(i4)) 46 | 47 | } 48 | 49 | func TestBitLen(t *testing.T) { 50 | 51 | assert.True(t, checkSignedIntFits(big.NewInt(0), 0)) 52 | assert.False(t, checkSignedIntFits(big.NewInt(1), 0)) 53 | 54 | assert.True(t, checkSignedIntFits(big.NewInt(-32768), 16)) 55 | assert.False(t, checkSignedIntFits(big.NewInt(-32769), 16)) 56 | 57 | assert.True(t, checkSignedIntFits(big.NewInt(32767), 16)) 58 | assert.False(t, checkSignedIntFits(big.NewInt(32768), 16)) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /pkg/eip712/abi_to_typed_data.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package eip712 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "regexp" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/i18n" 25 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 26 | "github.com/hyperledger/firefly-signer/pkg/abi" 27 | ) 28 | 29 | var internalTypeStructExtractor = regexp.MustCompile(`^struct (.*\.)?([^.\[\]]+)(\[\d*\])*$`) 30 | 31 | // Convert an ABI tuple definition, into the EIP-712 structure that's embedded into the 32 | // "eth_signTypedData" signing request payload. It's a much simpler structure that 33 | // flattens out a map of types (requiring each type to be named by a struct definition) 34 | func ABItoTypedDataV4(ctx context.Context, tc abi.TypeComponent) (primaryType string, typeSet TypeSet, err error) { 35 | if tc.ComponentType() != abi.TupleComponent { 36 | return "", nil, i18n.NewError(ctx, signermsgs.MsgEIP712PrimaryNotTuple, tc.String()) 37 | } 38 | primaryType, err = extractSolidityTypeName(ctx, tc.Parameter()) 39 | if err != nil { 40 | return "", nil, err 41 | } 42 | // First we need to build the sorted array of types for `encodeType` 43 | typeSet = make(TypeSet) 44 | if err := addABITypes(ctx, tc, typeSet); err != nil { 45 | return "", nil, err 46 | } 47 | return primaryType, typeSet, nil 48 | } 49 | 50 | // Maps a parsed ABI component type to an EIP-712 type string 51 | // - Subset of elementary types, with aliases resolved 52 | // - Struct types are simply the name of the type 53 | // - Fixed and dynamic array suffixes are supported 54 | func mapABIType(ctx context.Context, tc abi.TypeComponent) (string, error) { 55 | switch tc.ComponentType() { 56 | case abi.TupleComponent: 57 | return extractSolidityTypeName(ctx, tc.Parameter()) 58 | case abi.DynamicArrayComponent, abi.FixedArrayComponent: 59 | child, err := mapABIType(ctx, tc.ArrayChild()) 60 | if err != nil { 61 | return "", err 62 | } 63 | if tc.ComponentType() == abi.FixedArrayComponent { 64 | return fmt.Sprintf("%s[%d]", child, tc.FixedArrayLen()), nil 65 | } 66 | return child + "[]", nil 67 | default: 68 | return mapElementaryABIType(ctx, tc) 69 | } 70 | } 71 | 72 | // Maps one of the parsed ABI elementary types to an EIP-712 elementary type 73 | func mapElementaryABIType(ctx context.Context, tc abi.TypeComponent) (string, error) { 74 | if tc.ComponentType() != abi.ElementaryComponent { 75 | return "", i18n.NewError(ctx, signermsgs.MsgNotElementary, tc) 76 | } 77 | et := tc.ElementaryType() 78 | switch et.BaseType() { 79 | case abi.BaseTypeAddress, abi.BaseTypeBool, abi.BaseTypeString, abi.BaseTypeInt, abi.BaseTypeUInt: 80 | // Types that need no transposition 81 | return string(et.BaseType()) + tc.ElementarySuffix(), nil 82 | case abi.BaseTypeBytes: 83 | // Bytes is special 84 | if tc.ElementaryFixed() { 85 | return string(et.BaseType()) + tc.ElementarySuffix(), nil 86 | } 87 | return string(et.BaseType()), nil 88 | default: 89 | // EIP-712 does not support the other types 90 | return "", i18n.NewError(ctx, signermsgs.MsgEIP712UnsupportedABIType, tc) 91 | } 92 | } 93 | 94 | // ABI does not formally contain the Struct name - as it's not required for encoding the value. 95 | // EIP-712 requires the Struct name as it is used through the standard, including to de-dup definitions 96 | // 97 | // Solidity uses the "internalType" field by convention as an extension to ABI, so we require 98 | // that here for EIP-712 encoding to be successful. 99 | func extractSolidityTypeName(ctx context.Context, param *abi.Parameter) (string, error) { 100 | match := internalTypeStructExtractor.FindStringSubmatch(param.InternalType) 101 | if match == nil { 102 | return "", i18n.NewError(ctx, signermsgs.MsgEIP712BadInternalType, param.InternalType) 103 | } 104 | return match[2], nil 105 | } 106 | 107 | // Recursively find all types, with a name -> encoded name map. 108 | func addABITypes(ctx context.Context, tc abi.TypeComponent, typeSet TypeSet) error { 109 | switch tc.ComponentType() { 110 | case abi.TupleComponent: 111 | typeName, err := extractSolidityTypeName(ctx, tc.Parameter()) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if _, mapped := typeSet[typeName]; mapped { 117 | // we've already mapped this type 118 | return nil 119 | } 120 | t := make(Type, len(tc.TupleChildren())) 121 | for i, child := range tc.TupleChildren() { 122 | ts, err := mapABIType(ctx, child) 123 | if err != nil { 124 | return err 125 | } 126 | t[i] = &TypeMember{ 127 | Name: child.KeyName(), 128 | Type: ts, 129 | } 130 | } 131 | typeSet[typeName] = t 132 | // recurse 133 | for _, child := range tc.TupleChildren() { 134 | if err := addABITypes(ctx, child, typeSet); err != nil { 135 | return err 136 | } 137 | } 138 | return nil 139 | case abi.DynamicArrayComponent, abi.FixedArrayComponent: 140 | // recurse into the child 141 | return addABITypes(ctx, tc.ArrayChild(), typeSet) 142 | default: 143 | // from a type collection perspective, this is a leaf - nothing to do 144 | return nil 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/ethereum/ethereum.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethereum 18 | 19 | import ( 20 | "math/big" 21 | 22 | "github.com/hyperledger/firefly-common/pkg/fftypes" 23 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 24 | ) 25 | 26 | // txReceiptJSONRPC is the receipt obtained over JSON/RPC from the ethereum client, with gas used, logs and contract address 27 | type TXReceiptJSONRPC struct { 28 | BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"` 29 | BlockNumber *ethtypes.HexInteger `json:"blockNumber"` 30 | ContractAddress *ethtypes.Address0xHex `json:"contractAddress"` 31 | CumulativeGasUsed *ethtypes.HexInteger `json:"cumulativeGasUsed"` 32 | From *ethtypes.Address0xHex `json:"from"` 33 | GasUsed *ethtypes.HexInteger `json:"gasUsed"` 34 | Logs []*LogJSONRPC `json:"logs"` 35 | Status *ethtypes.HexInteger `json:"status"` 36 | To *ethtypes.Address0xHex `json:"to"` 37 | TransactionHash ethtypes.HexBytes0xPrefix `json:"transactionHash"` 38 | TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"` 39 | } 40 | 41 | // receiptExtraInfo is the version of the receipt we store under the TX. 42 | // - We omit the full logs from the JSON/RPC 43 | // - We omit fields already in the standardized cross-blockchain section 44 | // - We format numbers as decimals 45 | type ReceiptExtraInfo struct { 46 | ContractAddress *ethtypes.Address0xHex `json:"contractAddress"` 47 | CumulativeGasUsed *fftypes.FFBigInt `json:"cumulativeGasUsed"` 48 | From *ethtypes.Address0xHex `json:"from"` 49 | To *ethtypes.Address0xHex `json:"to"` 50 | GasUsed *fftypes.FFBigInt `json:"gasUsed"` 51 | Status *fftypes.FFBigInt `json:"status"` 52 | ErrorMessage *string `json:"errorMessage"` 53 | } 54 | 55 | // txInfoJSONRPC is the transaction info obtained over JSON/RPC from the ethereum client, with input data 56 | type TXInfoJSONRPC struct { 57 | BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"` // null if pending 58 | BlockNumber *ethtypes.HexInteger `json:"blockNumber"` // null if pending 59 | From *ethtypes.Address0xHex `json:"from"` 60 | ChainID *ethtypes.HexInteger `json:"chainID"` 61 | Gas *ethtypes.HexInteger `json:"gas"` 62 | GasPrice *ethtypes.HexInteger `json:"gasPrice"` 63 | Hash ethtypes.HexBytes0xPrefix `json:"hash"` 64 | Input ethtypes.HexBytes0xPrefix `json:"input"` 65 | Nonce *ethtypes.HexInteger `json:"nonce"` 66 | R *ethtypes.HexInteger `json:"r"` 67 | S *ethtypes.HexInteger `json:"s"` 68 | To *ethtypes.Address0xHex `json:"to"` 69 | TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"` // null if pending 70 | Type *ethtypes.HexInteger `json:"type"` 71 | V *ethtypes.HexInteger `json:"v"` 72 | Value *ethtypes.HexInteger `json:"value"` 73 | } 74 | 75 | func (t *TXInfoJSONRPC) Cost() *big.Int { 76 | return big.NewInt(0).Mul(t.GasPrice.BigInt(), t.Gas.BigInt()) 77 | } 78 | 79 | type LogFilterJSONRPC struct { 80 | FromBlock *ethtypes.HexInteger `json:"fromBlock,omitempty"` 81 | ToBlock *ethtypes.HexInteger `json:"toBlock,omitempty"` 82 | Address *ethtypes.Address0xHex `json:"address,omitempty"` 83 | Topics [][]ethtypes.HexBytes0xPrefix `json:"topics,omitempty"` 84 | } 85 | 86 | type LogJSONRPC struct { 87 | Removed bool `json:"removed"` 88 | LogIndex *ethtypes.HexInteger `json:"logIndex"` 89 | TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"` 90 | BlockNumber *ethtypes.HexInteger `json:"blockNumber"` 91 | TransactionHash ethtypes.HexBytes0xPrefix `json:"transactionHash"` 92 | BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"` 93 | Address *ethtypes.Address0xHex `json:"address"` 94 | Data ethtypes.HexBytes0xPrefix `json:"data"` 95 | Topics []ethtypes.HexBytes0xPrefix `json:"topics"` 96 | } 97 | -------------------------------------------------------------------------------- /pkg/ethsigner/typed_data.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethsigner 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/hyperledger/firefly-signer/pkg/eip712" 23 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 24 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 25 | ) 26 | 27 | type EIP712Result struct { 28 | Hash ethtypes.HexBytes0xPrefix `ffstruct:"EIP712Result" json:"hash"` 29 | SignatureRSV ethtypes.HexBytes0xPrefix `ffstruct:"EIP712Result" json:"signatureRSV"` 30 | V ethtypes.HexInteger `ffstruct:"EIP712Result" json:"v"` 31 | R ethtypes.HexBytes0xPrefix `ffstruct:"EIP712Result" json:"r"` 32 | S ethtypes.HexBytes0xPrefix `ffstruct:"EIP712Result" json:"s"` 33 | } 34 | 35 | func SignTypedDataV4(ctx context.Context, signer secp256k1.SignerDirect, payload *eip712.TypedData) (*EIP712Result, error) { 36 | encodedData, err := eip712.EncodeTypedDataV4(ctx, payload) 37 | if err != nil { 38 | return nil, err 39 | } 40 | // Note that signer.Sign performs the hash 41 | sig, err := signer.SignDirect(encodedData) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | signatureBytes := make([]byte, 65) 47 | sig.R.FillBytes(signatureBytes[0:32]) 48 | sig.S.FillBytes(signatureBytes[32:64]) 49 | signatureBytes[64] = byte(sig.V.Int64()) 50 | 51 | return &EIP712Result{ 52 | Hash: encodedData, 53 | // Include the clearly distinguished V, R & S values of the signature 54 | V: ethtypes.HexInteger(*sig.V), 55 | R: sig.R.FillBytes(make([]byte, 32)), 56 | S: sig.S.FillBytes(make([]byte, 32)), 57 | // the Ethereum convention (which is different to the Golang convention) is to encode compact signatures as 58 | // 65 bytes - R (32B), S (32B), V (1B) 59 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7294d34c17ca215c201b3772ff67036fa4b1ef12/contracts/utils/cryptography/ECDSA.sol#L56-L73 60 | SignatureRSV: signatureBytes, 61 | }, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/ethsigner/typed_data_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethsigner 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "math/big" 24 | "testing" 25 | 26 | "github.com/btcsuite/btcd/btcec/v2/ecdsa" 27 | "github.com/hyperledger/firefly-common/pkg/ffapi" 28 | "github.com/hyperledger/firefly-common/pkg/log" 29 | "github.com/hyperledger/firefly-signer/mocks/secp256k1mocks" 30 | "github.com/hyperledger/firefly-signer/pkg/eip712" 31 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 32 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 33 | "github.com/sirupsen/logrus" 34 | "github.com/stretchr/testify/assert" 35 | "github.com/stretchr/testify/mock" 36 | ) 37 | 38 | func TestSignTypedDataV4(t *testing.T) { 39 | 40 | // We use a simple empty message payload 41 | payload := &eip712.TypedData{ 42 | PrimaryType: eip712.EIP712Domain, 43 | } 44 | keypair, err := secp256k1.GenerateSecp256k1KeyPair() 45 | assert.NoError(t, err) 46 | 47 | ctx := context.Background() 48 | sig, err := SignTypedDataV4(ctx, keypair, payload) 49 | assert.NoError(t, err) 50 | 51 | b, err := json.Marshal(sig) 52 | assert.NoError(t, err) 53 | log.L(context.Background()).Infof("Signature: %s", b) 54 | 55 | foundSig := &secp256k1.SignatureData{ 56 | V: sig.V.BigInt(), 57 | R: new(big.Int), 58 | S: new(big.Int), 59 | } 60 | foundSig.R.SetBytes(sig.R) 61 | foundSig.S.SetBytes(sig.S) 62 | 63 | signaturePayload := ethtypes.HexBytes0xPrefix(sig.Hash) 64 | addr, err := foundSig.RecoverDirect(signaturePayload, -1 /* chain id is in the domain (not applied EIP-155 style to the V value) */) 65 | assert.NoError(t, err) 66 | assert.Equal(t, keypair.Address.String(), addr.String()) 67 | 68 | encoded, err := eip712.EncodeTypedDataV4(ctx, payload) 69 | assert.NoError(t, err) 70 | 71 | // Check all is as we expect 72 | assert.Equal(t, "0x8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38", encoded.String()) 73 | assert.Equal(t, "0x8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38", signaturePayload.String()) 74 | } 75 | 76 | func TestSignTypedDataV4BadPayload(t *testing.T) { 77 | 78 | payload := &eip712.TypedData{ 79 | PrimaryType: "missing", 80 | } 81 | 82 | keypair, err := secp256k1.GenerateSecp256k1KeyPair() 83 | assert.NoError(t, err) 84 | 85 | ctx := context.Background() 86 | _, err = SignTypedDataV4(ctx, keypair, payload) 87 | assert.Regexp(t, "FF22073", err) 88 | } 89 | 90 | func TestSignTypedDataV4SignFail(t *testing.T) { 91 | 92 | payload := &eip712.TypedData{ 93 | PrimaryType: eip712.EIP712Domain, 94 | } 95 | 96 | msn := &secp256k1mocks.SignerDirect{} 97 | msn.On("SignDirect", mock.Anything).Return(nil, fmt.Errorf("pop")) 98 | 99 | ctx := context.Background() 100 | _, err := SignTypedDataV4(ctx, msn, payload) 101 | assert.Regexp(t, "pop", err) 102 | } 103 | 104 | func TestMessage_2(t *testing.T) { 105 | logrus.SetLevel(logrus.TraceLevel) 106 | 107 | var p eip712.TypedData 108 | err := json.Unmarshal([]byte(`{ 109 | "domain": { 110 | "name": "test-app", 111 | "version": "1", 112 | "chainId": 31337, 113 | "verifyingContract": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0" 114 | }, 115 | "types": { 116 | "Issuance": [ 117 | { 118 | "name": "amount", 119 | "type": "uint256" 120 | }, 121 | { 122 | "name": "to", 123 | "type": "address" 124 | } 125 | ], 126 | "EIP712Domain": [ 127 | { 128 | "name": "name", 129 | "type": "string" 130 | }, 131 | { 132 | "name": "version", 133 | "type": "string" 134 | }, 135 | { 136 | "name": "chainId", 137 | "type": "uint256" 138 | }, 139 | { 140 | "name": "verifyingContract", 141 | "type": "address" 142 | } 143 | ] 144 | }, 145 | "primaryType": "Issuance", 146 | "message": { 147 | "amount": "1000", 148 | "to": "0xce3a47d24140cca16f8839357ca5fada44a1baef" 149 | } 150 | }`), &p) 151 | assert.NoError(t, err) 152 | 153 | ctx := context.Background() 154 | ed, err := eip712.EncodeTypedDataV4(ctx, &p) 155 | assert.NoError(t, err) 156 | assert.Equal(t, "0xb0132202fa81cafac0e405917f86705728ba02912d185065697cc4ba4e61aec3", ed.String()) 157 | 158 | keys, err := secp256k1.NewSecp256k1KeyPair([]byte(`8d01666832be7eb2dbd57cd3d4410d0231a91533f895de76d0930c689618aefd`)) 159 | assert.NoError(t, err) 160 | assert.Equal(t, "0xbcef501facf72ddacdb055acc2716786ff038728", keys.Address.String()) 161 | 162 | signed, err := SignTypedDataV4(ctx, keys, &p) 163 | assert.NoError(t, err) 164 | 165 | assert.Equal(t, "0xb0132202fa81cafac0e405917f86705728ba02912d185065697cc4ba4e61aec3", signed.Hash.String()) 166 | 167 | // The golang convention is V, R, S for the compact signature (differing from Ethereum's convention of R, S, V) 168 | golangCompactSignature := make([]byte, 65) 169 | golangCompactSignature[0] = signed.SignatureRSV[64] 170 | copy(golangCompactSignature[1:33], signed.SignatureRSV[0:32]) 171 | copy(golangCompactSignature[33:65], signed.SignatureRSV[32:64]) 172 | 173 | fmt.Printf("%s\n", ethtypes.HexBytes0xPrefix(golangCompactSignature)) 174 | fmt.Printf("%s\n", ethtypes.HexBytes0xPrefix(signed.SignatureRSV)) 175 | 176 | pubKey, _, err := ecdsa.RecoverCompact(golangCompactSignature, signed.Hash) 177 | assert.NoError(t, err) 178 | assert.Equal(t, "0xbcef501facf72ddacdb055acc2716786ff038728", secp256k1.PublicKeyToAddress(pubKey).String()) 179 | } 180 | 181 | func TestEIP712ResultDocumented(t *testing.T) { 182 | ffapi.CheckObjectDocumented(&EIP712Result{}) 183 | } 184 | -------------------------------------------------------------------------------- /pkg/ethsigner/wallet.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethsigner 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/hyperledger/firefly-signer/pkg/eip712" 23 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 24 | ) 25 | 26 | // Wallet is the common interface can be implemented across wallet/signing capabilities 27 | type Wallet interface { 28 | Sign(ctx context.Context, txn *Transaction, chainID int64) ([]byte, error) 29 | // SignPrivateTxn(ctx context.Context, addr ethtypes.Address, ptx *Transaction, chainID int64) ([]byte, error) 30 | Initialize(ctx context.Context) error 31 | GetAccounts(ctx context.Context) ([]*ethtypes.Address0xHex /* no checksum on returned values */, error) 32 | Refresh(ctx context.Context) error 33 | Close() error 34 | } 35 | 36 | type WalletTypedData interface { 37 | Wallet 38 | SignTypedDataV4(ctx context.Context, from ethtypes.Address0xHex, payload *eip712.TypedData) (*EIP712Result, error) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/ethtypes/address.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "encoding/hex" 21 | "encoding/json" 22 | "fmt" 23 | "strconv" 24 | "strings" 25 | "unicode" 26 | 27 | "golang.org/x/crypto/sha3" 28 | ) 29 | 30 | // Address0xHex formats with an 0x prefix, but no checksum (lower case) 31 | type Address0xHex [20]byte 32 | 33 | // AddressWithChecksum uses full 0x prefixed checksum address format 34 | type AddressWithChecksum Address0xHex 35 | 36 | // AddressPlainHex can parse the same, but formats as just flat hex (no prefix) 37 | type AddressPlainHex AddressWithChecksum 38 | 39 | func (a *Address0xHex) UnmarshalJSON(b []byte) error { 40 | var s string 41 | if err := json.Unmarshal(b, &s); err != nil { 42 | return err 43 | } 44 | return a.SetString(s) 45 | } 46 | 47 | func (a *Address0xHex) SetString(s string) error { 48 | b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) 49 | if err != nil { 50 | return fmt.Errorf("bad address: %s", err) 51 | } 52 | if len(b) != 20 { 53 | return fmt.Errorf("bad address - must be 20 bytes (len=%d)", len(b)) 54 | } 55 | copy(a[0:20], b) 56 | return nil 57 | } 58 | 59 | func (a AddressWithChecksum) MarshalJSON() ([]byte, error) { 60 | return []byte(fmt.Sprintf(`"%s"`, a.String())), nil 61 | } 62 | 63 | func (a AddressWithChecksum) String() string { 64 | 65 | // EIP-55: Mixed-case checksum address encoding 66 | // https://eips.ethereum.org/EIPS/eip-55 67 | 68 | hexAddr := hex.EncodeToString(a[0:20]) 69 | hash := sha3.NewLegacyKeccak256() 70 | hash.Write([]byte(hexAddr)) 71 | hexHash := hex.EncodeToString(hash.Sum(nil)) 72 | 73 | buff := strings.Builder{} 74 | buff.WriteString("0x") 75 | for i := 0; i < 40; i++ { 76 | hexHashDigit, _ := strconv.ParseInt(string([]byte{hexHash[i]}), 16, 64) 77 | if hexHashDigit >= 8 { 78 | buff.WriteRune(unicode.ToUpper(rune(hexAddr[i]))) 79 | } else { 80 | buff.WriteRune(unicode.ToLower(rune(hexAddr[i]))) 81 | } 82 | } 83 | return buff.String() 84 | } 85 | 86 | func (a *AddressPlainHex) UnmarshalJSON(b []byte) error { 87 | return ((*Address0xHex)(a)).UnmarshalJSON(b) 88 | } 89 | 90 | func (a AddressPlainHex) MarshalJSON() ([]byte, error) { 91 | return []byte(fmt.Sprintf(`"%s"`, a.String())), nil 92 | } 93 | 94 | func (a AddressPlainHex) String() string { 95 | return hex.EncodeToString(a[0:20]) 96 | } 97 | 98 | func (a *AddressWithChecksum) UnmarshalJSON(b []byte) error { 99 | return ((*Address0xHex)(a)).UnmarshalJSON(b) 100 | } 101 | 102 | func (a Address0xHex) MarshalJSON() ([]byte, error) { 103 | return []byte(fmt.Sprintf(`"%s"`, a.String())), nil 104 | } 105 | 106 | func (a Address0xHex) String() string { 107 | return "0x" + hex.EncodeToString(a[0:20]) 108 | } 109 | 110 | func NewAddress(s string) (*Address0xHex, error) { 111 | a := new(Address0xHex) 112 | return a, a.SetString(s) 113 | } 114 | 115 | func NewAddressWithChecksum(s string) (*AddressWithChecksum, error) { 116 | a := new(AddressWithChecksum) 117 | return a, (*Address0xHex)(a).SetString(s) 118 | } 119 | 120 | func MustNewAddress(s string) *Address0xHex { 121 | a, err := NewAddress(s) 122 | if err != nil { 123 | panic(err) 124 | } 125 | return a 126 | } 127 | -------------------------------------------------------------------------------- /pkg/ethtypes/address_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "encoding/json" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestAddressCheckSum(t *testing.T) { 27 | 28 | testStruct := struct { 29 | Addr1 AddressWithChecksum `json:"addr1"` 30 | Addr2 AddressWithChecksum `json:"addr2"` 31 | Addr3 AddressPlainHex `json:"addr3"` 32 | Addr4 AddressPlainHex `json:"addr4"` 33 | Addr5 Address0xHex `json:"addr5"` 34 | Addr6 Address0xHex `json:"addr6"` 35 | }{} 36 | 37 | testData := `{ 38 | "addr1": "0x3CCb85578722B5B9250C1a76b4967166a6Ff7B8b", 39 | "addr2": "162534E1aE19712499CE4CB05263D074D7F7aF90", 40 | "addr3": "0xEF15BBAB59891537E9FF75EB5E15D860D0E64117", 41 | "addr4": "A0361F594d5bb261Bc066458805d7aefFC4Ec94a", 42 | "addr5": "0xbD9E8378c52741943FCcDE9283db12aA8841a9F2", 43 | "addr6": "06942dc1fC868aF18132C0916dA3ae4ab58142a4" 44 | }` 45 | 46 | err := json.Unmarshal([]byte(testData), &testStruct) 47 | assert.NoError(t, err) 48 | 49 | assert.Equal(t, "0x3CCb85578722B5B9250C1a76b4967166a6Ff7B8b", testStruct.Addr1.String()) 50 | assert.Equal(t, "0x162534E1aE19712499CE4CB05263D074D7F7aF90", testStruct.Addr2.String()) 51 | assert.Equal(t, "ef15bbab59891537e9ff75eb5e15d860d0e64117", testStruct.Addr3.String()) 52 | assert.Equal(t, "a0361f594d5bb261bc066458805d7aeffc4ec94a", testStruct.Addr4.String()) 53 | assert.Equal(t, "0xbd9e8378c52741943fccde9283db12aa8841a9f2", testStruct.Addr5.String()) 54 | assert.Equal(t, "0x06942dc1fc868af18132c0916da3ae4ab58142a4", testStruct.Addr6.String()) 55 | 56 | jsonSerialized, err := json.Marshal(&testStruct) 57 | assert.JSONEq(t, `{ 58 | "addr1": "0x3CCb85578722B5B9250C1a76b4967166a6Ff7B8b", 59 | "addr2": "0x162534E1aE19712499CE4CB05263D074D7F7aF90", 60 | "addr3": "ef15bbab59891537e9ff75eb5e15d860d0e64117", 61 | "addr4": "a0361f594d5bb261bc066458805d7aeffc4ec94a", 62 | "addr5": "0xbd9e8378c52741943fccde9283db12aa8841a9f2", 63 | "addr6": "0x06942dc1fc868af18132c0916da3ae4ab58142a4" 64 | }`, string(jsonSerialized)) 65 | 66 | } 67 | 68 | func TestAddressFailLen(t *testing.T) { 69 | 70 | testStruct := struct { 71 | Addr1 AddressWithChecksum `json:"addr1"` 72 | }{} 73 | 74 | testData := `{ 75 | "addr1": "0x00" 76 | }` 77 | 78 | err := json.Unmarshal([]byte(testData), &testStruct) 79 | assert.Regexp(t, "bad address - must be 20 bytes", err) 80 | } 81 | 82 | func TestAddressFailNonHex(t *testing.T) { 83 | 84 | testStruct := struct { 85 | Addr1 AddressWithChecksum `json:"addr1"` 86 | }{} 87 | 88 | testData := `{ 89 | "addr1": "wrong" 90 | }` 91 | 92 | err := json.Unmarshal([]byte(testData), &testStruct) 93 | assert.Regexp(t, "bad address", err) 94 | } 95 | 96 | func TestAddressFailNonString(t *testing.T) { 97 | 98 | testStruct := struct { 99 | Addr1 AddressWithChecksum `json:"addr1"` 100 | }{} 101 | 102 | testData := `{ 103 | "addr1": {} 104 | }` 105 | 106 | err := json.Unmarshal([]byte(testData), &testStruct) 107 | assert.Error(t, err) 108 | } 109 | 110 | func TestAddressConstructors(t *testing.T) { 111 | assert.Equal(t, "0x497eedc4299dea2f2a364be10025d0ad0f702de3", MustNewAddress("497EEDC4299DEA2F2A364BE10025D0AD0F702DE3").String()) 112 | assert.Panics(t, func() { 113 | MustNewAddress("!Bad") 114 | }) 115 | 116 | a, err := NewAddressWithChecksum("497EEDC4299DEA2F2A364BE10025D0AD0F702DE3") 117 | assert.NoError(t, err) 118 | assert.Equal(t, "0x497EEdc4299Dea2f2A364Be10025d0aD0f702De3", a.String()) 119 | } 120 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexbytes.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "bytes" 21 | "encoding/hex" 22 | "encoding/json" 23 | "fmt" 24 | "strings" 25 | ) 26 | 27 | // HexBytesPlain is simple bytes that are JSON stored/retrieved as hex 28 | type HexBytesPlain []byte 29 | 30 | // HexBytes0xPrefix are serialized to JSON as hex with an `0x` prefix 31 | type HexBytes0xPrefix []byte 32 | 33 | func (h HexBytesPlain) Equals(h2 HexBytesPlain) bool { 34 | return bytes.Equal(h, h2) 35 | } 36 | 37 | func (h *HexBytesPlain) UnmarshalJSON(b []byte) error { 38 | var s string 39 | err := json.Unmarshal(b, &s) 40 | if err != nil { 41 | return err 42 | } 43 | *h, err = hex.DecodeString(strings.TrimPrefix(s, "0x")) 44 | if err != nil { 45 | return fmt.Errorf("bad hex: %s", err) 46 | } 47 | return nil 48 | } 49 | 50 | func (h HexBytesPlain) String() string { 51 | return hex.EncodeToString(h) 52 | } 53 | 54 | func (h HexBytesPlain) MarshalJSON() ([]byte, error) { 55 | return []byte(fmt.Sprintf(`"%s"`, h.String())), nil 56 | } 57 | 58 | func (h *HexBytes0xPrefix) UnmarshalJSON(b []byte) error { 59 | return ((*HexBytesPlain)(h)).UnmarshalJSON(b) 60 | } 61 | 62 | func (h HexBytes0xPrefix) Equals(h2 HexBytes0xPrefix) bool { 63 | return bytes.Equal(h, h2) 64 | } 65 | 66 | func (h HexBytes0xPrefix) String() string { 67 | return "0x" + hex.EncodeToString(h) 68 | } 69 | 70 | func (h HexBytes0xPrefix) MarshalJSON() ([]byte, error) { 71 | return []byte(fmt.Sprintf(`"%s"`, h.String())), nil 72 | } 73 | 74 | func NewHexBytes0xPrefix(s string) (HexBytes0xPrefix, error) { 75 | h, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return HexBytes0xPrefix(h), nil 80 | } 81 | 82 | func MustNewHexBytes0xPrefix(s string) HexBytes0xPrefix { 83 | h, err := NewHexBytes0xPrefix(s) 84 | if err != nil { 85 | panic(err) 86 | } 87 | return h 88 | } 89 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexbytes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "encoding/json" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestHexBytes(t *testing.T) { 27 | 28 | testStruct := struct { 29 | H1 HexBytesPlain `json:"h1"` 30 | H2 HexBytesPlain `json:"h2"` 31 | H3 HexBytes0xPrefix `json:"h3"` 32 | H4 HexBytes0xPrefix `json:"h4"` 33 | }{} 34 | 35 | testData := `{ 36 | "h1": "0xabcd1234", 37 | "h2": "ffff0000", 38 | "h3": "0xFEEDBEEF", 39 | "h4": "9009a00e" 40 | }` 41 | 42 | err := json.Unmarshal([]byte(testData), &testStruct) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, "abcd1234", testStruct.H1.String()) 46 | assert.Equal(t, "ffff0000", testStruct.H2.String()) 47 | assert.Equal(t, "0xfeedbeef", testStruct.H3.String()) 48 | assert.Equal(t, "0x9009a00e", testStruct.H4.String()) 49 | 50 | jsonSerialized, err := json.Marshal(&testStruct) 51 | assert.JSONEq(t, `{ 52 | "h1": "abcd1234", 53 | "h2": "ffff0000", 54 | "h3": "0xfeedbeef", 55 | "h4": "0x9009a00e" 56 | }`, string(jsonSerialized)) 57 | } 58 | 59 | func TestHexBytesFailNonHex(t *testing.T) { 60 | 61 | testStruct := struct { 62 | H1 HexBytesPlain `json:"h1"` 63 | }{} 64 | 65 | testData := `{ 66 | "h1": "wrong" 67 | }` 68 | 69 | err := json.Unmarshal([]byte(testData), &testStruct) 70 | assert.Regexp(t, "bad hex", err) 71 | } 72 | 73 | func TestHexBytesFailNonString(t *testing.T) { 74 | 75 | testStruct := struct { 76 | H1 HexBytesPlain `json:"h1"` 77 | }{} 78 | 79 | testData := `{ 80 | "h1": {} 81 | }` 82 | 83 | err := json.Unmarshal([]byte(testData), &testStruct) 84 | assert.Error(t, err) 85 | } 86 | 87 | func TestHexByteConstructors(t *testing.T) { 88 | assert.Equal(t, HexBytes0xPrefix{0x01, 0x02}, MustNewHexBytes0xPrefix("0x0102")) 89 | assert.Panics(t, func() { 90 | MustNewHexBytes0xPrefix("!wrong") 91 | }) 92 | } 93 | 94 | func TestHexByteEqual(t *testing.T) { 95 | assert.True(t, HexBytesPlain(nil).Equals(nil)) 96 | assert.False(t, HexBytesPlain(nil).Equals(HexBytesPlain{0x00})) 97 | assert.False(t, (HexBytesPlain{0x00}).Equals(nil)) 98 | assert.True(t, (HexBytesPlain{0x00}).Equals(HexBytesPlain{0x00})) 99 | 100 | assert.True(t, HexBytes0xPrefix(nil).Equals(nil)) 101 | assert.False(t, HexBytes0xPrefix(nil).Equals(HexBytes0xPrefix{0x00})) 102 | assert.False(t, (HexBytes0xPrefix{0x00}).Equals(nil)) 103 | assert.True(t, (HexBytes0xPrefix{0x00}).Equals(HexBytes0xPrefix{0x00})) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexinteger.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/big" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/i18n" 25 | ) 26 | 27 | // HexInteger is a positive integer - serializes to JSON as an 0x hex string (no leading zeros), and parses flexibly depending on the prefix (so 0x for hex, or base 10 for plain string / float64) 28 | type HexInteger big.Int 29 | 30 | func (h *HexInteger) String() string { 31 | return "0x" + (*big.Int)(h).Text(16) 32 | } 33 | 34 | func (h HexInteger) MarshalJSON() ([]byte, error) { 35 | return []byte(fmt.Sprintf(`"%s"`, h.String())), nil 36 | } 37 | 38 | func (h *HexInteger) UnmarshalJSON(b []byte) error { 39 | bi, err := UnmarshalBigInt(context.Background(), b) 40 | if err != nil { 41 | return err 42 | } 43 | if bi.Sign() < 0 { 44 | return fmt.Errorf("negative values are not supported: %s", b) 45 | } 46 | *h = HexInteger(*bi) 47 | return nil 48 | } 49 | 50 | func (h *HexInteger) BigInt() *big.Int { 51 | if h == nil { 52 | return new(big.Int) 53 | } 54 | return (*big.Int)(h) 55 | } 56 | 57 | func (h *HexInteger) Uint64() uint64 { 58 | return h.BigInt().Uint64() 59 | } 60 | 61 | func (h *HexInteger) Int64() int64 { 62 | return h.BigInt().Int64() 63 | } 64 | 65 | func NewHexIntegerU64(i uint64) *HexInteger { 66 | return (*HexInteger)(big.NewInt(0).SetUint64(i)) 67 | } 68 | 69 | func NewHexInteger64(i int64) *HexInteger { 70 | return (*HexInteger)(big.NewInt(i)) 71 | } 72 | 73 | func NewHexInteger(i *big.Int) *HexInteger { 74 | return (*HexInteger)(i) 75 | } 76 | 77 | func (h *HexInteger) Scan(src interface{}) error { 78 | switch src := src.(type) { 79 | case nil: 80 | return nil 81 | case int64: 82 | *h = *NewHexInteger64(src) 83 | return nil 84 | case uint64: 85 | *h = *NewHexIntegerU64(src) 86 | return nil 87 | default: 88 | return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, h) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexinteger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "encoding/json" 21 | "math/big" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestHexIntegerOk(t *testing.T) { 28 | 29 | testStruct := struct { 30 | I1 *HexInteger `json:"i1"` 31 | I2 *HexInteger `json:"i2"` 32 | I3 *HexInteger `json:"i3"` 33 | I4 *HexInteger `json:"i4"` 34 | I5 *HexInteger `json:"i5,omitempty"` 35 | }{} 36 | 37 | testData := `{ 38 | "i1": "0xabcd1234", 39 | "i2": "54321", 40 | "i3": 12345 41 | }` 42 | 43 | err := json.Unmarshal([]byte(testData), &testStruct) 44 | assert.NoError(t, err) 45 | 46 | assert.Equal(t, int64(0xabcd1234), testStruct.I1.BigInt().Int64()) 47 | assert.Equal(t, int64(54321), testStruct.I2.BigInt().Int64()) 48 | assert.Equal(t, int64(12345), testStruct.I3.BigInt().Int64()) 49 | assert.Nil(t, testStruct.I4) 50 | assert.Equal(t, int64(0), testStruct.I4.BigInt().Int64()) // BigInt() safe on nils 51 | assert.Nil(t, testStruct.I5) 52 | assert.Equal(t, int64(12345), testStruct.I3.Int64()) 53 | assert.Equal(t, uint64(12345), testStruct.I3.Uint64()) 54 | 55 | jsonSerialized, err := json.Marshal(&testStruct) 56 | assert.JSONEq(t, `{ 57 | "i1": "0xabcd1234", 58 | "i2": "0xd431", 59 | "i3": "0x3039", 60 | "i4": null 61 | }`, string(jsonSerialized)) 62 | 63 | } 64 | 65 | func TestHexIntegerMissingBytes(t *testing.T) { 66 | 67 | testStruct := struct { 68 | I1 HexInteger `json:"i1"` 69 | }{} 70 | 71 | testData := `{ 72 | "i1": "0x" 73 | }` 74 | 75 | err := json.Unmarshal([]byte(testData), &testStruct) 76 | assert.Regexp(t, "FF22088", err) 77 | 78 | err = testStruct.I1.UnmarshalJSON([]byte(`{!badJSON`)) 79 | assert.Regexp(t, "invalid", err) 80 | } 81 | 82 | func TestHexIntegerBadType(t *testing.T) { 83 | 84 | testStruct := struct { 85 | I1 HexInteger `json:"i1"` 86 | }{} 87 | 88 | testData := `{ 89 | "i1": {} 90 | }` 91 | 92 | err := json.Unmarshal([]byte(testData), &testStruct) 93 | assert.Regexp(t, "FF22091", err) 94 | } 95 | 96 | func TestHexIntegerBadJSON(t *testing.T) { 97 | 98 | testStruct := struct { 99 | I1 HexInteger `json:"i1"` 100 | }{} 101 | 102 | testData := `{ 103 | "i1": null 104 | }` 105 | 106 | err := json.Unmarshal([]byte(testData), &testStruct) 107 | assert.Error(t, err) 108 | } 109 | 110 | func TestHexIntegerBadNegative(t *testing.T) { 111 | 112 | testStruct := struct { 113 | I1 HexInteger `json:"i1"` 114 | }{} 115 | 116 | testData := `{ 117 | "i1": "-12345" 118 | }` 119 | 120 | err := json.Unmarshal([]byte(testData), &testStruct) 121 | assert.Regexp(t, "negative values are not supported", err) 122 | } 123 | 124 | func TestHexIntConstructors(t *testing.T) { 125 | assert.Equal(t, int64(12345), NewHexInteger64(12345).BigInt().Int64()) 126 | assert.Equal(t, int64(12345), NewHexInteger(big.NewInt(12345)).BigInt().Int64()) 127 | assert.Equal(t, "0x0", NewHexInteger(big.NewInt(0)).String()) 128 | assert.Equal(t, "0x1", NewHexInteger(big.NewInt(1)).String()) 129 | assert.Equal(t, "0x1", NewHexIntegerU64(1).String()) 130 | } 131 | 132 | func TestScan(t *testing.T) { 133 | i := &HexInteger{} 134 | err := i.Scan(false) 135 | err = i.Scan(nil) 136 | assert.NoError(t, err) 137 | assert.Equal(t, "0x0", i.String()) 138 | i.Scan(int64(5555)) 139 | assert.Equal(t, "0x15b3", i.String()) 140 | i.Scan(uint64(9999)) 141 | assert.Equal(t, "0x270f", i.String()) 142 | } 143 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexuint64.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strconv" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/i18n" 25 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 26 | ) 27 | 28 | // HexUint64 is a positive integer - serializes to JSON as an 0x hex string (no leading zeros), and parses flexibly depending on the prefix (so 0x for hex, or base 10 for plain string / float64) 29 | type HexUint64 uint64 30 | 31 | func (h *HexUint64) String() string { 32 | if h == nil { 33 | return "0x0" 34 | } 35 | return "0x" + strconv.FormatUint(uint64(*h), 16) 36 | } 37 | 38 | func (h HexUint64) MarshalJSON() ([]byte, error) { 39 | return []byte(fmt.Sprintf(`"%s"`, h.String())), nil 40 | } 41 | 42 | func (h *HexUint64) UnmarshalJSON(b []byte) error { 43 | bi, err := UnmarshalBigInt(context.Background(), b) 44 | if err != nil { 45 | return err 46 | } 47 | if !bi.IsUint64() { 48 | return i18n.NewError(context.Background(), signermsgs.MsgInvalidUint64PrecisionLoss, b) 49 | } 50 | *h = HexUint64(bi.Uint64()) 51 | return nil 52 | } 53 | 54 | func (h HexUint64) Uint64() uint64 { 55 | return uint64(h) 56 | } 57 | 58 | func (h *HexUint64) Uint64OrZero() uint64 { 59 | if h == nil { 60 | return 0 61 | } 62 | return uint64(*h) 63 | } 64 | 65 | func (h *HexUint64) Scan(src interface{}) error { 66 | switch src := src.(type) { 67 | case nil: 68 | return nil 69 | case int64: 70 | if src < 0 { 71 | return i18n.NewError(context.Background(), signermsgs.MsgHexUintNegative, src) 72 | } 73 | *h = HexUint64(src) 74 | return nil 75 | case uint64: 76 | *h = HexUint64(src) 77 | return nil 78 | default: 79 | return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, h) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/ethtypes/hexuint64_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "encoding/json" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestHexUint64Ok(t *testing.T) { 27 | 28 | testStruct := struct { 29 | I1 *HexUint64 `json:"i1"` 30 | I2 *HexUint64 `json:"i2"` 31 | I3 *HexUint64 `json:"i3"` 32 | I4 *HexUint64 `json:"i4"` 33 | I5 *HexUint64 `json:"i5,omitempty"` 34 | }{} 35 | 36 | testData := `{ 37 | "i1": "0xabcd1234", 38 | "i2": "54321", 39 | "i3": 12345 40 | }` 41 | 42 | err := json.Unmarshal([]byte(testData), &testStruct) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, uint64(0xabcd1234), testStruct.I1.Uint64()) 46 | assert.Equal(t, uint64(0xabcd1234), testStruct.I1.Uint64OrZero()) 47 | assert.Equal(t, uint64(54321), testStruct.I2.Uint64()) 48 | assert.Equal(t, uint64(12345), testStruct.I3.Uint64()) 49 | assert.Nil(t, testStruct.I4) 50 | assert.Equal(t, uint64(0), testStruct.I4.Uint64OrZero()) // BigInt() safe on nils 51 | assert.Equal(t, "0x0", testStruct.I4.String()) 52 | assert.Nil(t, testStruct.I5) 53 | assert.Equal(t, uint64(12345), testStruct.I3.Uint64()) 54 | 55 | jsonSerialized, err := json.Marshal(&testStruct) 56 | assert.JSONEq(t, `{ 57 | "i1": "0xabcd1234", 58 | "i2": "0xd431", 59 | "i3": "0x3039", 60 | "i4": null 61 | }`, string(jsonSerialized)) 62 | 63 | } 64 | 65 | func TestHexUint64MissingBytes(t *testing.T) { 66 | 67 | testStruct := struct { 68 | I1 HexUint64 `json:"i1"` 69 | }{} 70 | 71 | testData := `{ 72 | "i1": "0x" 73 | }` 74 | 75 | err := json.Unmarshal([]byte(testData), &testStruct) 76 | assert.Regexp(t, "FF22088", err) 77 | } 78 | 79 | func TestHexUint64BadType(t *testing.T) { 80 | 81 | testStruct := struct { 82 | I1 HexUint64 `json:"i1"` 83 | }{} 84 | 85 | testData := `{ 86 | "i1": {} 87 | }` 88 | 89 | err := json.Unmarshal([]byte(testData), &testStruct) 90 | assert.Regexp(t, "FF22091", err) 91 | } 92 | 93 | func TestHexUint64BadJSON(t *testing.T) { 94 | 95 | testStruct := struct { 96 | I1 HexUint64 `json:"i1"` 97 | }{} 98 | 99 | testData := `{ 100 | "i1": null 101 | }` 102 | 103 | err := json.Unmarshal([]byte(testData), &testStruct) 104 | assert.Error(t, err) 105 | } 106 | 107 | func TestHexUint64BadNegative(t *testing.T) { 108 | 109 | testStruct := struct { 110 | I1 HexUint64 `json:"i1"` 111 | }{} 112 | 113 | testData := `{ 114 | "i1": "-12345" 115 | }` 116 | 117 | err := json.Unmarshal([]byte(testData), &testStruct) 118 | assert.Regexp(t, "FF22090", err) 119 | } 120 | 121 | func TestHexUint64BadTooLarge(t *testing.T) { 122 | 123 | testStruct := struct { 124 | I1 HexUint64 `json:"i1"` 125 | }{} 126 | 127 | testData := `{ 128 | "i1": "18446744073709551616" 129 | }` 130 | 131 | err := json.Unmarshal([]byte(testData), &testStruct) 132 | assert.Regexp(t, "FF22090", err) 133 | } 134 | 135 | func TestHexUint64Constructor(t *testing.T) { 136 | assert.Equal(t, uint64(12345), HexUint64(12345).Uint64()) 137 | } 138 | 139 | func TestScanUint64(t *testing.T) { 140 | var i HexUint64 141 | pI := &i 142 | err := pI.Scan(false) 143 | err = pI.Scan(nil) 144 | assert.NoError(t, err) 145 | assert.Equal(t, "0x0", pI.String()) 146 | err = pI.Scan(int64(5555)) 147 | assert.Equal(t, "0x15b3", pI.String()) 148 | assert.NoError(t, err) 149 | err = pI.Scan(uint64(9999)) 150 | assert.Equal(t, "0x270f", pI.String()) 151 | assert.NoError(t, err) 152 | err = pI.Scan(int64(-9999)) 153 | assert.Regexp(t, "FF22092", err) 154 | } 155 | -------------------------------------------------------------------------------- /pkg/ethtypes/integer_parsing.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "math/big" 24 | 25 | "github.com/hyperledger/firefly-common/pkg/i18n" 26 | "github.com/hyperledger/firefly-common/pkg/log" 27 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 28 | ) 29 | 30 | func BigIntegerFromString(ctx context.Context, s string) (*big.Int, error) { 31 | // We use Go's default '0' base integer parsing, where `0x` means hex, 32 | // no prefix means decimal etc. 33 | i, ok := new(big.Int).SetString(s, 0) 34 | if !ok { 35 | f, _, err := big.ParseFloat(s, 10, 256, big.ToNearestEven) 36 | if err != nil { 37 | log.L(ctx).Errorf("Error parsing numeric string '%s': %s", s, err) 38 | return nil, i18n.NewError(ctx, signermsgs.MsgInvalidNumberString, s) 39 | } 40 | i, accuracy := f.Int(i) 41 | if accuracy != big.Exact { 42 | // If we weren't able to decode without losing precision, return an error 43 | return nil, i18n.NewError(ctx, signermsgs.MsgInvalidIntPrecisionLoss, s) 44 | } 45 | 46 | return i, nil 47 | } 48 | return i, nil 49 | } 50 | 51 | func UnmarshalBigInt(ctx context.Context, b []byte) (*big.Int, error) { 52 | var i interface{} 53 | d := json.NewDecoder(bytes.NewReader(b)) 54 | d.UseNumber() 55 | err := d.Decode(&i) 56 | if err != nil { 57 | return nil, err 58 | } 59 | switch i := i.(type) { 60 | case json.Number: 61 | return BigIntegerFromString(context.Background(), i.String()) 62 | case string: 63 | return BigIntegerFromString(context.Background(), i) 64 | default: 65 | return nil, i18n.NewError(ctx, signermsgs.MsgInvalidJSONTypeForBigInt, i) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/ethtypes/integer_parsing_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ethtypes 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestIntegerParsing(t *testing.T) { 27 | ctx := context.Background() 28 | 29 | i, err := BigIntegerFromString(ctx, "1.0000000000000000000000001e+25") 30 | assert.NoError(t, err) 31 | assert.Equal(t, "10000000000000000000000001", i.String()) 32 | 33 | i, err = BigIntegerFromString(ctx, "10000000000000000000000000000001") 34 | assert.NoError(t, err) 35 | assert.Equal(t, "10000000000000000000000000000001", i.String()) 36 | 37 | i, err = BigIntegerFromString(ctx, "20000000000000000000000000000002") 38 | assert.NoError(t, err) 39 | assert.Equal(t, "20000000000000000000000000000002", i.String()) 40 | 41 | _, err = BigIntegerFromString(ctx, "0xGG") 42 | assert.Regexp(t, "FF22088", err) 43 | 44 | _, err = BigIntegerFromString(ctx, "3.0000000000000000000000000000003") 45 | assert.Regexp(t, "FF22089", err) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/ffi2abi/ffi_param_validator.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package ffi2abi 18 | 19 | import ( 20 | "github.com/santhosh-tekuri/jsonschema/v5" 21 | ) 22 | 23 | type ParamValidator struct{} 24 | 25 | var compiledMetaSchema = jsonschema.MustCompileString("ffiParamDetails.json", `{ 26 | "$ref": "#/$defs/ethereumParam", 27 | "$defs": { 28 | "ethereumParam": { 29 | "oneOf": [ 30 | { 31 | "type": "object", 32 | "properties": { 33 | "type": { 34 | "type": "string", 35 | "not": { 36 | "const": "object" 37 | } 38 | }, 39 | "details": { 40 | "$ref": "#/$defs/details" 41 | } 42 | }, 43 | "required": [ 44 | "type", 45 | "details" 46 | ] 47 | }, 48 | { 49 | "type": "object", 50 | "properties": { 51 | "oneOf": { 52 | "type": "array" 53 | }, 54 | "details": { 55 | "$ref": "#/$defs/details" 56 | } 57 | }, 58 | "required": [ 59 | "oneOf", 60 | "details" 61 | ] 62 | }, 63 | { 64 | "type": "object", 65 | "properties": { 66 | "type": { 67 | "const": "object" 68 | }, 69 | "details": { 70 | "$ref": "#/$defs/details" 71 | }, 72 | "properties": { 73 | "type": "object", 74 | "patternProperties": { 75 | ".*": { 76 | "$ref": "#/$defs/ethereumObjectChildParam" 77 | } 78 | } 79 | } 80 | }, 81 | "required": [ 82 | "type", 83 | "details" 84 | ] 85 | } 86 | ] 87 | }, 88 | "ethereumObjectChildParam": { 89 | "oneOf": [ 90 | { 91 | "type": "object", 92 | "properties": { 93 | "type": { 94 | "type": "string", 95 | "not": { 96 | "const": "object" 97 | } 98 | }, 99 | "details": { 100 | "$ref": "#/$defs/objectFieldDetails" 101 | } 102 | }, 103 | "required": [ 104 | "type", 105 | "details" 106 | ] 107 | }, 108 | { 109 | "type": "object", 110 | "properties": { 111 | "oneOf": { 112 | "type": "array" 113 | }, 114 | "details": { 115 | "$ref": "#/$defs/objectFieldDetails" 116 | } 117 | }, 118 | "required": [ 119 | "oneOf", 120 | "details" 121 | ] 122 | }, 123 | { 124 | "type": "object", 125 | "properties": { 126 | "type": { 127 | "const": "object" 128 | }, 129 | "details": { 130 | "$ref": "#/$defs/objectFieldDetails" 131 | }, 132 | "properties": { 133 | "type": "object", 134 | "patternProperties": { 135 | ".*": { 136 | "$ref": "#/$defs/ethereumObjectChildParam" 137 | } 138 | } 139 | } 140 | }, 141 | "required": [ 142 | "type", 143 | "details" 144 | ] 145 | } 146 | ] 147 | }, 148 | "details": { 149 | "type": "object", 150 | "properties": { 151 | "type": { 152 | "type": "string" 153 | }, 154 | "internalType": { 155 | "type": "string" 156 | }, 157 | "indexed": { 158 | "type": "boolean" 159 | } 160 | }, 161 | "required": [ 162 | "type" 163 | ] 164 | }, 165 | "objectFieldDetails": { 166 | "type": "object", 167 | "properties": { 168 | "type": { 169 | "type": "string" 170 | }, 171 | "internalType": { 172 | "type": "string" 173 | }, 174 | "indexed": { 175 | "type": "boolean" 176 | }, 177 | "index": { 178 | "type": "integer" 179 | } 180 | }, 181 | "required": [ 182 | "type", 183 | "index" 184 | ] 185 | } 186 | } 187 | }`) 188 | 189 | func (v *ParamValidator) Compile(_ jsonschema.CompilerContext, _ map[string]interface{}) (jsonschema.ExtSchema, error) { 190 | return nil, nil 191 | } 192 | 193 | func (v *ParamValidator) GetMetaSchema() *jsonschema.Schema { 194 | return compiledMetaSchema 195 | } 196 | 197 | func (v *ParamValidator) GetExtensionName() string { 198 | return "details" 199 | } 200 | -------------------------------------------------------------------------------- /pkg/fswallet/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package fswallet 18 | 19 | import ( 20 | "github.com/hyperledger/firefly-common/pkg/config" 21 | ) 22 | 23 | const ( 24 | // ConfigPath the path of the Keystore V3 wallet path 25 | ConfigPath = "path" 26 | // ConfigFilenamesWith0xPrefix whether or not to use the 0x prefix on filenames, when using passwordExt password 27 | ConfigFilenamesWith0xPrefix = "filenames.with0xPrefix" 28 | // ConfigFilenamesPrimaryExt extension to append to the "from" address string to find the file (see metadata section for file types). All filenames must be lower case on disk. 29 | ConfigFilenamesPrimaryExt = "filenames.primaryExt" 30 | // ConfigFilenamesPrimaryMatchRegex allows filenames where the address can be extracted with a regular expression. Takes precedence over primaryExt 31 | ConfigFilenamesPrimaryMatchRegex = "filenames.primaryMatchRegex" 32 | // ConfigFilenamesPasswordExt extension to append to the "from" address string to find the password file (if not using a metadata file to specify the password file) 33 | ConfigFilenamesPasswordExt = "filenames.passwordExt" 34 | // ConfigFilenamesPasswordPath directory path where the password files should be found - default is the same path as the primary file 35 | ConfigFilenamesPasswordPath = "filenames.passwordPath" 36 | // ConfigFilenamesPasswordTrimSpace whether to trim whitespace from passwords loaded from files (such as trailing newline characters) 37 | ConfigFilenamesPasswordTrimSpace = "filenames.passwordTrimSpace" 38 | // ConfigDefaultPasswordFile default password file to use if neither the metadata, or passwordExtension find a password 39 | ConfigDefaultPasswordFile = "defaultPasswordFile" 40 | // ConfigDisableListener disable the filesystem listener that detects newly added keys automatically 41 | ConfigDisableListener = "disableListener" 42 | // ConfigSignerCacheSize the number of signing keys to keep in memory 43 | ConfigSignerCacheSize = "signerCacheSize" 44 | // ConfigSignerCacheTTL the time to keep an unused signing key in memory 45 | ConfigSignerCacheTTL = "signerCacheTTL" 46 | // ConfigMetadataFormat format to parse the metadata - supported: auto (from extension) / filename / toml / yaml / json (please quote "0x..." strings in YAML) 47 | ConfigMetadataFormat = "metadata.format" 48 | // ConfigMetadataKeyFileProperty use for toml/yaml/json to find the name of the file containing the keystorev3 file 49 | ConfigMetadataKeyFileProperty = "metadata.keyFileProperty" 50 | // ConfigMetadataPasswordFileProperty use for toml/yaml to find the name of the file containing the keystorev3 file 51 | ConfigMetadataPasswordFileProperty = "metadata.passwordFileProperty" 52 | ) 53 | 54 | type Config struct { 55 | Path string 56 | DefaultPasswordFile string 57 | SignerCacheSize string 58 | SignerCacheTTL string 59 | DisableListener bool 60 | Filenames FilenamesConfig 61 | Metadata MetadataConfig 62 | } 63 | 64 | type FilenamesConfig struct { 65 | PrimaryMatchRegex string 66 | PrimaryExt string 67 | PasswordExt string 68 | PasswordPath string 69 | PasswordTrimSpace bool 70 | With0xPrefix bool 71 | } 72 | 73 | type MetadataConfig struct { 74 | Format string 75 | KeyFileProperty string 76 | PasswordFileProperty string 77 | } 78 | 79 | func InitConfig(section config.Section) { 80 | section.AddKnownKey(ConfigPath) 81 | section.AddKnownKey(ConfigFilenamesPrimaryExt) 82 | section.AddKnownKey(ConfigFilenamesPrimaryMatchRegex) 83 | section.AddKnownKey(ConfigFilenamesPasswordExt) 84 | section.AddKnownKey(ConfigFilenamesPasswordPath) 85 | section.AddKnownKey(ConfigFilenamesPasswordTrimSpace, true) 86 | section.AddKnownKey(ConfigFilenamesWith0xPrefix) 87 | section.AddKnownKey(ConfigDisableListener) 88 | section.AddKnownKey(ConfigDefaultPasswordFile) 89 | section.AddKnownKey(ConfigSignerCacheSize, 250) 90 | section.AddKnownKey(ConfigSignerCacheTTL, "24h") 91 | section.AddKnownKey(ConfigMetadataFormat, `auto`) 92 | section.AddKnownKey(ConfigMetadataKeyFileProperty) 93 | section.AddKnownKey(ConfigMetadataPasswordFileProperty) 94 | } 95 | 96 | func ReadConfig(section config.Section) *Config { 97 | return &Config{ 98 | Path: section.GetString(ConfigPath), 99 | DefaultPasswordFile: section.GetString(ConfigDefaultPasswordFile), 100 | SignerCacheSize: section.GetString(ConfigSignerCacheSize), 101 | SignerCacheTTL: section.GetString(ConfigSignerCacheTTL), 102 | DisableListener: section.GetBool(ConfigDisableListener), 103 | Filenames: FilenamesConfig{ 104 | PrimaryExt: section.GetString(ConfigFilenamesPrimaryExt), 105 | PrimaryMatchRegex: section.GetString(ConfigFilenamesPrimaryMatchRegex), 106 | PasswordExt: section.GetString(ConfigFilenamesPasswordExt), 107 | PasswordPath: section.GetString(ConfigFilenamesPasswordPath), 108 | PasswordTrimSpace: section.GetBool(ConfigFilenamesPasswordTrimSpace), 109 | With0xPrefix: section.GetBool(ConfigFilenamesWith0xPrefix), 110 | }, 111 | Metadata: MetadataConfig{ 112 | Format: section.GetString(ConfigMetadataFormat), 113 | KeyFileProperty: section.GetString(ConfigMetadataKeyFileProperty), 114 | PasswordFileProperty: section.GetString(ConfigMetadataPasswordFileProperty), 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /pkg/fswallet/fslistener.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package fswallet 18 | 19 | import ( 20 | "context" 21 | "os" 22 | 23 | "github.com/fsnotify/fsnotify" 24 | "github.com/hyperledger/firefly-common/pkg/i18n" 25 | "github.com/hyperledger/firefly-common/pkg/log" 26 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 27 | ) 28 | 29 | func (w *fsWallet) startFilesystemListener(ctx context.Context) error { 30 | if w.conf.DisableListener { 31 | log.L(ctx).Debugf("Filesystem listener disabled") 32 | close(w.fsListenerDone) 33 | return nil 34 | } 35 | watcher, err := fsnotify.NewWatcher() 36 | if err == nil { 37 | go w.fsListenerLoop(ctx, func() { 38 | _ = watcher.Close() 39 | close(w.fsListenerDone) 40 | }, watcher.Events, watcher.Errors) 41 | err = watcher.Add(w.conf.Path) 42 | } 43 | if err != nil { 44 | log.L(ctx).Errorf("Failed to start filesystem listener: %s", err) 45 | return i18n.WrapError(ctx, err, signermsgs.MsgFailedToStartListener, err) 46 | } 47 | return nil 48 | } 49 | 50 | func (w *fsWallet) fsListenerLoop(ctx context.Context, done func(), events chan fsnotify.Event, errors chan error) { 51 | defer done() 52 | 53 | for { 54 | select { 55 | case <-ctx.Done(): 56 | log.L(ctx).Infof("File listener exiting") 57 | return 58 | case event, ok := <-events: 59 | if ok { 60 | log.L(ctx).Tracef("FSEvent [%s]: %s", event.Op, event.Name) 61 | fi, err := os.Stat(event.Name) 62 | if err == nil { 63 | _ = w.notifyNewFiles(ctx, fi) 64 | } 65 | } 66 | case err, ok := <-errors: 67 | if ok { 68 | log.L(ctx).Errorf("FSEvent error: %s", err) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/fswallet/fslistener_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package fswallet 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "path" 24 | "testing" 25 | "time" 26 | 27 | "github.com/fsnotify/fsnotify" 28 | "github.com/hyperledger/firefly-common/pkg/config" 29 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 30 | "github.com/sirupsen/logrus" 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func newEmptyWalletTestDir(t *testing.T, init bool) (context.Context, *fsWallet, chan ethtypes.Address0xHex, func()) { 35 | config.RootConfigReset() 36 | logrus.SetLevel(logrus.TraceLevel) 37 | 38 | unitTestConfig := config.RootSection("ut_fs_config") 39 | InitConfig(unitTestConfig) 40 | unitTestConfig.Set(ConfigPath, t.TempDir()) 41 | unitTestConfig.Set(ConfigFilenamesPrimaryMatchRegex, "^((0x)?[0-9a-z]+).key.json$") 42 | unitTestConfig.Set(ConfigFilenamesPasswordExt, ".pwd") 43 | ctx := context.Background() 44 | 45 | listener := make(chan ethtypes.Address0xHex, 1) 46 | ff, err := NewFilesystemWallet(ctx, ReadConfig(unitTestConfig), listener) 47 | assert.NoError(t, err) 48 | if init { 49 | err = ff.Initialize(ctx) 50 | assert.NoError(t, err) 51 | } 52 | 53 | return ctx, ff.(*fsWallet), listener, func() { 54 | ff.Close() 55 | } 56 | } 57 | 58 | func TestFileListener(t *testing.T) { 59 | 60 | ctx, f, listener1, done := newEmptyWalletTestDir(t, true) 61 | defer done() 62 | 63 | // add a 2nd listener 64 | listener2 := make(chan ethtypes.Address0xHex, 1) 65 | f.AddListener(listener2) 66 | 67 | testPWFIle, err := os.ReadFile("../../test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.pwd") 68 | assert.NoError(t, err) 69 | 70 | err = os.WriteFile(path.Join(f.conf.Path, "1f185718734552d08278aa70f804580bab5fd2b4.pwd"), testPWFIle, 0644) 71 | assert.NoError(t, err) 72 | 73 | testKeyFIle, err := os.ReadFile("../../test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.key.json") 74 | assert.NoError(t, err) 75 | 76 | err = os.WriteFile(path.Join(f.conf.Path, "1f185718734552d08278aa70f804580bab5fd2b4.key.json"), testKeyFIle, 0644) 77 | assert.NoError(t, err) 78 | 79 | newAddr1 := <-listener1 80 | assert.Equal(t, `0x1f185718734552d08278aa70f804580bab5fd2b4`, newAddr1.String()) 81 | newAddr2 := <-listener2 82 | assert.Equal(t, `0x1f185718734552d08278aa70f804580bab5fd2b4`, newAddr2.String()) 83 | 84 | addr := *ethtypes.MustNewAddress(`1f185718734552d08278aa70f804580bab5fd2b4`) 85 | wf, err := f.GetWalletFile(ctx, addr) 86 | assert.NoError(t, err) 87 | assert.Equal(t, wf.KeyPair().Address, addr) 88 | 89 | } 90 | 91 | func TestFileListenerStartFail(t *testing.T) { 92 | 93 | ctx, f, _, done := newEmptyWalletTestDir(t, false) 94 | defer done() 95 | 96 | os.RemoveAll(f.conf.Path) 97 | err := f.Initialize(ctx) 98 | assert.Regexp(t, "FF22060", err) 99 | 100 | } 101 | 102 | func TestFileListenerRemoveDirWhileListening(t *testing.T) { 103 | 104 | ctx, f, _, done := newEmptyWalletTestDir(t, true) 105 | defer done() 106 | 107 | errs := make(chan error, 1) 108 | errs <- fmt.Errorf("pop") 109 | ctx, cancelCtx := context.WithCancel(ctx) 110 | go func() { 111 | time.Sleep(10 * time.Millisecond) 112 | cancelCtx() 113 | }() 114 | f.fsListenerLoop(ctx, func() {}, make(chan fsnotify.Event), errs) 115 | 116 | } 117 | -------------------------------------------------------------------------------- /pkg/keystorev3/aes128ctr.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "crypto/aes" 21 | "crypto/cipher" 22 | "fmt" 23 | ) 24 | 25 | func mustAES128CtrEncrypt(key []byte, iv []byte, plaintext []byte) []byte { 26 | 27 | // Per https://go.dev/src/crypto/cipher/example_test.go ExampleNewCTR 28 | 29 | block, err := aes.NewCipher(key) 30 | if err != nil { 31 | panic(fmt.Sprintf("AES initialization failed: %s", err)) 32 | } 33 | 34 | ciphertext := make([]byte, len(plaintext)) 35 | stream := cipher.NewCTR(block, iv) 36 | stream.XORKeyStream(ciphertext, plaintext) 37 | 38 | return ciphertext 39 | 40 | } 41 | 42 | func aes128CtrDecrypt(key []byte, iv []byte, ciphertext []byte) ([]byte, error) { 43 | 44 | // Per https://go.dev/src/crypto/cipher/example_test.go ExampleNewCTR 45 | 46 | block, err := aes.NewCipher(key) 47 | if err != nil { 48 | return nil, fmt.Errorf("AES initialization failed: %s", err) 49 | } 50 | 51 | plaintext := make([]byte, len(ciphertext)) 52 | stream := cipher.NewCTR(block, iv) 53 | stream.XORKeyStream(plaintext, ciphertext) 54 | 55 | return plaintext, nil 56 | 57 | } 58 | -------------------------------------------------------------------------------- /pkg/keystorev3/aes128ctr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestAES128CTRInitFail(t *testing.T) { 26 | 27 | assert.Panics(t, func() { 28 | mustAES128CtrEncrypt([]byte{}, nil, nil) 29 | }) 30 | 31 | _, err := aes128CtrDecrypt([]byte{}, nil, nil) 32 | assert.Error(t, err) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /pkg/keystorev3/pbkdf2.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "crypto/sha256" 21 | "encoding/json" 22 | "fmt" 23 | 24 | "golang.org/x/crypto/pbkdf2" 25 | ) 26 | 27 | const ( 28 | prfHmacSHA256 = "hmac-sha256" 29 | ) 30 | 31 | func readPbkdf2WalletFile(jsonWallet []byte, password []byte, metadata map[string]interface{}) (WalletFile, error) { 32 | var w *walletFilePbkdf2 33 | if err := json.Unmarshal(jsonWallet, &w); err != nil { 34 | return nil, fmt.Errorf("invalid pbkdf2 keystore: %s", err) 35 | } 36 | w.metadata = metadata 37 | return w, w.decrypt(password) 38 | } 39 | 40 | func (w *walletFilePbkdf2) decrypt(password []byte) (err error) { 41 | if w.Crypto.KDFParams.PRF != prfHmacSHA256 { 42 | return fmt.Errorf("invalid pbkdf2 wallet file: unsupported prf '%s'", w.Crypto.KDFParams.PRF) 43 | } 44 | 45 | derivedKey := pbkdf2.Key(password, w.Crypto.KDFParams.Salt, w.Crypto.KDFParams.C, w.Crypto.KDFParams.DKLen, sha256.New) 46 | 47 | w.privateKey, err = w.Crypto.decryptCommon(derivedKey) 48 | return err 49 | 50 | } 51 | -------------------------------------------------------------------------------- /pkg/keystorev3/pbkdf2_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "crypto/rand" 21 | "crypto/sha256" 22 | "encoding/json" 23 | "testing" 24 | 25 | "github.com/hyperledger/firefly-common/pkg/fftypes" 26 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 27 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 28 | "github.com/stretchr/testify/assert" 29 | "golang.org/x/crypto/pbkdf2" 30 | ) 31 | 32 | func TestPbkdf2Wallet(t *testing.T) { 33 | keypair, err := secp256k1.GenerateSecp256k1KeyPair() 34 | assert.NoError(t, err) 35 | 36 | salt := mustReadBytes(32, rand.Reader) 37 | derivedKey := pbkdf2.Key([]byte("myPrecious"), salt, 4096, 32, sha256.New) 38 | iv := mustReadBytes(16 /* 128bit */, rand.Reader) 39 | encryptKey := derivedKey[0:16] 40 | cipherText := mustAES128CtrEncrypt(encryptKey, iv, keypair.PrivateKeyBytes()) 41 | mac := generateMac(derivedKey[16:32], cipherText) 42 | 43 | w1 := &walletFilePbkdf2{ 44 | walletFileBase: walletFileBase{ 45 | walletFileCoreFields: walletFileCoreFields{ 46 | ID: fftypes.NewUUID(), 47 | Version: version3, 48 | }, 49 | walletFileMetadata: walletFileMetadata{ 50 | metadata: map[string]interface{}{ 51 | "address": ethtypes.AddressPlainHex(keypair.Address).String(), 52 | }, 53 | }, 54 | }, 55 | Crypto: cryptoPbkdf2{ 56 | cryptoCommon: cryptoCommon{ 57 | Cipher: cipherAES128ctr, 58 | CipherText: cipherText, 59 | CipherParams: cipherParams{ 60 | IV: iv, 61 | }, 62 | KDF: kdfTypePbkdf2, 63 | MAC: mac, 64 | }, 65 | KDFParams: kdfParamsPbkdf2{ 66 | PRF: prfHmacSHA256, 67 | DKLen: 32, 68 | C: 4096, 69 | Salt: salt, 70 | }, 71 | }, 72 | } 73 | 74 | wb1, err := json.Marshal(&w1) 75 | assert.NoError(t, err) 76 | 77 | w2, err := ReadWalletFile(wb1, []byte("myPrecious")) 78 | assert.NoError(t, err) 79 | 80 | assert.Equal(t, keypair.PrivateKeyBytes(), w2.KeyPair().PrivateKeyBytes()) 81 | 82 | } 83 | 84 | func TestPbkdf2WalletFileDecryptInvalid(t *testing.T) { 85 | 86 | _, err := readPbkdf2WalletFile([]byte(`!! not json`), []byte(""), nil) 87 | assert.Regexp(t, "invalid pbkdf2 keystore", err) 88 | 89 | } 90 | 91 | func TestPbkdf2WalletFileUnsupportedPRF(t *testing.T) { 92 | 93 | _, err := readPbkdf2WalletFile([]byte(`{}`), []byte(""), nil) 94 | assert.Regexp(t, "invalid pbkdf2 wallet file: unsupported prf", err) 95 | 96 | } 97 | -------------------------------------------------------------------------------- /pkg/keystorev3/scrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "crypto/rand" 21 | "encoding/json" 22 | "fmt" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/fftypes" 25 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 26 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 27 | "golang.org/x/crypto/scrypt" 28 | ) 29 | 30 | const defaultR = 8 31 | 32 | func readScryptWalletFile(jsonWallet []byte, password []byte, metadata map[string]interface{}) (WalletFile, error) { 33 | var w *walletFileScrypt 34 | if err := json.Unmarshal(jsonWallet, &w); err != nil { 35 | return nil, fmt.Errorf("invalid scrypt wallet file: %s", err) 36 | } 37 | w.metadata = metadata 38 | return w, w.decrypt(password) 39 | } 40 | 41 | func mustGenerateDerivedScryptKey(password string, salt []byte, n, p int) []byte { 42 | b, err := scrypt.Key([]byte(password), salt, n, defaultR, p, 16) 43 | if err != nil { 44 | panic(fmt.Sprintf("Scrypt failed: %s", err)) 45 | } 46 | return b 47 | } 48 | 49 | // creates an ethereum address wallet file 50 | func newScryptWalletFileSecp256k1(password string, keypair *secp256k1.KeyPair, n int, p int) WalletFile { 51 | wf := newScryptWalletFileBytes(password, keypair.PrivateKeyBytes(), n, p) 52 | wf.Metadata()["address"] = ethtypes.AddressPlainHex(keypair.Address).String() 53 | return wf 54 | } 55 | 56 | // this allows creation of any size/type of key in the store 57 | func newScryptWalletFileBytes(password string, privateKey []byte, n int, p int) *walletFileScrypt { 58 | 59 | // Generate a sale for the scrypt 60 | salt := mustReadBytes(32, rand.Reader) 61 | 62 | // Do the scrypt derivation of the key with the salt from the password 63 | derivedKey := mustGenerateDerivedScryptKey(password, salt, n, p) 64 | 65 | // Generate a random Initialization Vector (IV) for the AES/CTR/128 key encryption 66 | iv := mustReadBytes(16 /* 128bit */, rand.Reader) 67 | 68 | // First 16 bytes of derived key are used as the encryption key 69 | encryptKey := derivedKey[0:16] 70 | 71 | // Encrypt the private key with the encryption key 72 | cipherText := mustAES128CtrEncrypt(encryptKey, iv, privateKey) 73 | 74 | // Last 16 bytes of derived key are used for the MAC 75 | mac := generateMac(derivedKey[16:32], cipherText) 76 | 77 | return &walletFileScrypt{ 78 | walletFileBase: walletFileBase{ 79 | walletFileCoreFields: walletFileCoreFields{ 80 | ID: fftypes.NewUUID(), 81 | Version: version3, 82 | }, 83 | walletFileMetadata: walletFileMetadata{ 84 | metadata: map[string]interface{}{}, 85 | }, 86 | privateKey: privateKey, 87 | }, 88 | Crypto: cryptoScrypt{ 89 | cryptoCommon: cryptoCommon{ 90 | Cipher: cipherAES128ctr, 91 | CipherText: cipherText, 92 | CipherParams: cipherParams{ 93 | IV: iv, 94 | }, 95 | KDF: kdfTypeScrypt, 96 | MAC: mac, 97 | }, 98 | KDFParams: kdfParamsScrypt{ 99 | DKLen: 32, 100 | N: n, 101 | R: defaultR, 102 | P: p, 103 | Salt: salt, 104 | }, 105 | }, 106 | } 107 | } 108 | 109 | func (w *walletFileScrypt) decrypt(password []byte) error { 110 | derivedKey, err := scrypt.Key(password, w.Crypto.KDFParams.Salt, w.Crypto.KDFParams.N, w.Crypto.KDFParams.R, w.Crypto.KDFParams.P, w.Crypto.KDFParams.DKLen) 111 | if err != nil { 112 | return fmt.Errorf("invalid scrypt keystore: %s", err) 113 | } 114 | w.privateKey, err = w.Crypto.decryptCommon(derivedKey) 115 | return err 116 | } 117 | -------------------------------------------------------------------------------- /pkg/keystorev3/scrypt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "encoding/json" 21 | "testing" 22 | 23 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestScryptWalletRoundTripLight(t *testing.T) { 28 | keypair, err := secp256k1.GenerateSecp256k1KeyPair() 29 | assert.NoError(t, err) 30 | 31 | w1 := NewWalletFileLight("waltsentme", keypair) 32 | assert.Equal(t, keypair.PrivateKeyBytes(), w1.KeyPair().PrivateKeyBytes()) 33 | 34 | w1b, err := json.Marshal(&w1) 35 | assert.NoError(t, err) 36 | 37 | w2, err := ReadWalletFile(w1b, []byte("waltsentme")) 38 | assert.NoError(t, err) 39 | assert.Equal(t, keypair.PrivateKeyBytes(), w2.KeyPair().PrivateKeyBytes()) 40 | 41 | } 42 | 43 | func TestScryptWalletRoundTripStandard(t *testing.T) { 44 | keypair, err := secp256k1.GenerateSecp256k1KeyPair() 45 | assert.NoError(t, err) 46 | 47 | w1 := NewWalletFileStandard("TrustNo1", keypair) 48 | assert.Equal(t, keypair.PrivateKeyBytes(), w1.KeyPair().PrivateKeyBytes()) 49 | 50 | w1b, err := json.Marshal(&w1) 51 | assert.NoError(t, err) 52 | 53 | w2, err := ReadWalletFile(w1b, []byte("TrustNo1")) 54 | assert.NoError(t, err) 55 | assert.Equal(t, keypair.PrivateKeyBytes(), w2.KeyPair().PrivateKeyBytes()) 56 | 57 | } 58 | 59 | func TestScryptReadInvalidFile(t *testing.T) { 60 | 61 | _, err := readScryptWalletFile([]byte(`!bad JSON`), []byte(""), nil) 62 | assert.Error(t, err) 63 | 64 | } 65 | 66 | func TestMustGenerateDerivedScryptKeyPanic(t *testing.T) { 67 | 68 | assert.Panics(t, func() { 69 | mustGenerateDerivedScryptKey("", nil, 0, 1) 70 | }) 71 | 72 | } 73 | 74 | func TestScryptWalletFileDecryptInvalid(t *testing.T) { 75 | 76 | w := &walletFileScrypt{} 77 | err := w.decrypt([]byte("")) 78 | assert.Regexp(t, "invalid scrypt keystore", err) 79 | 80 | } 81 | 82 | func TestScryptWalletFileDecryptInvalidDKLen(t *testing.T) { 83 | 84 | var w *walletFileScrypt 85 | err := json.Unmarshal([]byte(sampleWallet), &w) 86 | assert.NoError(t, err) 87 | 88 | w.Crypto.KDFParams.DKLen = 16 89 | err = w.decrypt([]byte("test")) 90 | assert.Regexp(t, "derived key length", err) 91 | 92 | } 93 | 94 | func TestScryptWalletFileDecryptBadPassword(t *testing.T) { 95 | 96 | var w *walletFileScrypt 97 | err := json.Unmarshal([]byte(sampleWallet), &w) 98 | assert.NoError(t, err) 99 | 100 | err = w.decrypt([]byte("wrong")) 101 | assert.Regexp(t, "invalid password", err) 102 | 103 | } 104 | -------------------------------------------------------------------------------- /pkg/keystorev3/wallet.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | 24 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 25 | "golang.org/x/crypto/sha3" 26 | ) 27 | 28 | const ( 29 | nLight int = 1 << 12 30 | nStandard int = 1 << 10 31 | pDefault int = 1 32 | ) 33 | 34 | func NewWalletFileLight(password string, keypair *secp256k1.KeyPair) WalletFile { 35 | return newScryptWalletFileSecp256k1(password, keypair, nLight, pDefault) 36 | } 37 | 38 | func NewWalletFileStandard(password string, keypair *secp256k1.KeyPair) WalletFile { 39 | return newScryptWalletFileSecp256k1(password, keypair, nStandard, pDefault) 40 | } 41 | 42 | func NewWalletFileCustomBytesLight(password string, privateKey []byte) WalletFile { 43 | return newScryptWalletFileBytes(password, privateKey, nStandard, pDefault) 44 | } 45 | 46 | func NewWalletFileCustomBytesStandard(password string, privateKey []byte) WalletFile { 47 | return newScryptWalletFileBytes(password, privateKey, nStandard, pDefault) 48 | } 49 | 50 | func ReadWalletFile(jsonWallet []byte, password []byte) (WalletFile, error) { 51 | var w walletFileCommon 52 | err := json.Unmarshal(jsonWallet, &w) 53 | if err == nil { 54 | err = json.Unmarshal(jsonWallet, &w.metadata) 55 | } 56 | if err != nil { 57 | return nil, fmt.Errorf("invalid wallet file: %s", err) 58 | } 59 | if w.ID == nil { 60 | return nil, fmt.Errorf("missing keyfile id") 61 | } 62 | if w.Version != version3 { 63 | return nil, fmt.Errorf("incorrect keyfile version (only V3 supported): %d", w.Version) 64 | } 65 | switch w.Crypto.KDF { 66 | case kdfTypeScrypt: 67 | return readScryptWalletFile(jsonWallet, password, w.metadata) 68 | case kdfTypePbkdf2: 69 | return readPbkdf2WalletFile(jsonWallet, password, w.metadata) 70 | default: 71 | return nil, fmt.Errorf("unsupported kdf: %s", w.Crypto.KDF) 72 | } 73 | } 74 | 75 | func mustReadBytes(size int, r io.Reader) []byte { 76 | b := make([]byte, size) 77 | n, err := io.ReadFull(r, b) 78 | if err != nil || n != size { 79 | panic(fmt.Sprintf("Read failed (len=%d): %s", n, err)) 80 | } 81 | return b 82 | } 83 | 84 | func generateMac(derivedKeyMacBytes []byte, cipherText []byte) []byte { 85 | hash := sha3.NewLegacyKeccak256() 86 | hash.Write(derivedKeyMacBytes) 87 | hash.Write(cipherText) 88 | return hash.Sum(nil) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/keystorev3/walletfile.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package keystorev3 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | 24 | "github.com/hyperledger/firefly-common/pkg/fftypes" 25 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 26 | "github.com/hyperledger/firefly-signer/pkg/secp256k1" 27 | ) 28 | 29 | const ( 30 | version3 = 3 31 | cipherAES128ctr = "aes-128-ctr" 32 | kdfTypeScrypt = "scrypt" 33 | kdfTypePbkdf2 = "pbkdf2" 34 | ) 35 | 36 | type WalletFile interface { 37 | PrivateKey() []byte 38 | KeyPair() *secp256k1.KeyPair 39 | JSON() []byte 40 | GetID() *fftypes.UUID 41 | GetVersion() int 42 | 43 | // Any fields set into this that do not conflict with the base fields (id/version/crypto) will 44 | // be serialized into the JSON when it is marshalled. 45 | // This includes setting the "address" field (which is not a core part of the V3 standard) to 46 | // an arbitrary string, adding new fields for different key identifiers (like "bjj" or "btc" for 47 | // different public key compression algos). 48 | // If you want to remove the address field completely, simple set "address": nil in the map. 49 | Metadata() map[string]interface{} 50 | } 51 | 52 | type kdfParamsScrypt struct { 53 | DKLen int `json:"dklen"` 54 | N int `json:"n"` 55 | P int `json:"p"` 56 | R int `json:"r"` 57 | Salt ethtypes.HexBytesPlain `json:"salt"` 58 | } 59 | 60 | type kdfParamsPbkdf2 struct { 61 | DKLen int `json:"dklen"` 62 | C int `json:"c"` 63 | PRF string `json:"prf"` 64 | Salt ethtypes.HexBytesPlain `json:"salt"` 65 | } 66 | 67 | type cipherParams struct { 68 | IV ethtypes.HexBytesPlain `json:"iv"` 69 | } 70 | 71 | type cryptoCommon struct { 72 | Cipher string `json:"cipher"` 73 | CipherText ethtypes.HexBytesPlain `json:"ciphertext"` 74 | CipherParams cipherParams `json:"cipherparams"` 75 | KDF string `json:"kdf"` 76 | MAC ethtypes.HexBytesPlain `json:"mac"` 77 | } 78 | 79 | type cryptoScrypt struct { 80 | cryptoCommon 81 | KDFParams kdfParamsScrypt `json:"kdfparams"` 82 | } 83 | 84 | type cryptoPbkdf2 struct { 85 | cryptoCommon 86 | KDFParams kdfParamsPbkdf2 `json:"kdfparams"` 87 | } 88 | 89 | type walletFileCoreFields struct { 90 | ID *fftypes.UUID `json:"id"` 91 | Version int `json:"version"` 92 | } 93 | 94 | type walletFileMetadata struct { 95 | // arbitrary additional fields that can be stored in the JSON, including overriding/removing the "address" field (other core fields cannot be overridden) 96 | metadata map[string]interface{} 97 | } 98 | 99 | type walletFileBase struct { 100 | walletFileCoreFields 101 | walletFileMetadata 102 | privateKey []byte 103 | } 104 | 105 | type walletFileCommon struct { 106 | walletFileBase 107 | Crypto cryptoCommon `json:"crypto"` 108 | } 109 | 110 | type walletFilePbkdf2 struct { 111 | walletFileBase 112 | Crypto cryptoPbkdf2 `json:"crypto"` 113 | } 114 | 115 | func (w *walletFilePbkdf2) MarshalJSON() ([]byte, error) { 116 | return marshalWalletJSON(&w.walletFileBase, w.Crypto) 117 | } 118 | 119 | type walletFileScrypt struct { 120 | walletFileBase 121 | Crypto cryptoScrypt `json:"crypto"` 122 | } 123 | 124 | func (w *walletFileScrypt) MarshalJSON() ([]byte, error) { 125 | return marshalWalletJSON(&w.walletFileBase, w.Crypto) 126 | } 127 | 128 | func (w *walletFileBase) GetVersion() int { 129 | return w.Version 130 | } 131 | 132 | func (w *walletFileBase) GetID() *fftypes.UUID { 133 | return w.ID 134 | } 135 | 136 | func (w *walletFileBase) Metadata() map[string]interface{} { 137 | return w.metadata 138 | } 139 | 140 | func marshalWalletJSON(wc *walletFileBase, crypto interface{}) ([]byte, error) { 141 | cryptoJSON, err := json.Marshal(crypto) 142 | if err != nil { 143 | return nil, err 144 | } 145 | jsonMap := map[string]interface{}{} 146 | for k, v := range wc.metadata { 147 | if v != nil { 148 | jsonMap[k] = v 149 | } 150 | } 151 | // cannot override these fields 152 | jsonMap["id"] = wc.ID 153 | jsonMap["version"] = wc.Version 154 | jsonMap["crypto"] = json.RawMessage(cryptoJSON) 155 | return json.Marshal(jsonMap) 156 | } 157 | 158 | func (w *walletFileBase) KeyPair() *secp256k1.KeyPair { 159 | return secp256k1.KeyPairFromBytes(w.privateKey) 160 | } 161 | 162 | func (w *walletFileBase) PrivateKey() []byte { 163 | return w.privateKey 164 | } 165 | 166 | func (w *walletFilePbkdf2) JSON() []byte { 167 | b, _ := json.Marshal(w) 168 | return b 169 | } 170 | 171 | func (w *walletFileScrypt) JSON() []byte { 172 | b, _ := json.Marshal(w) 173 | return b 174 | } 175 | 176 | func (c *cryptoCommon) decryptCommon(derivedKey []byte) ([]byte, error) { 177 | if len(derivedKey) != 32 { 178 | return nil, fmt.Errorf("invalid scrypt keystore: derived key length %d != 32", len(derivedKey)) 179 | } 180 | // Last 16 bytes of derived key are used for MAC 181 | derivedMac := generateMac(derivedKey[16:32], c.CipherText) 182 | if !bytes.Equal(derivedMac, c.MAC) { 183 | return nil, fmt.Errorf("invalid password provided") 184 | } 185 | // First 16 bytes of derived key are used as the encryption key 186 | encryptKey := derivedKey[0:16] 187 | return aes128CtrDecrypt(encryptKey, c.CipherParams.IV, c.CipherText) 188 | } 189 | -------------------------------------------------------------------------------- /pkg/rlp/encode.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rlp 18 | 19 | func encodeBytes(inBytes []byte, isList bool) []byte { 20 | shortOffset := shortString 21 | if isList { 22 | shortOffset = shortList 23 | } 24 | if len(inBytes) == 1 && 25 | !isList && 26 | inBytes[0] <= 0x7f { 27 | // We don't need the offset, this can be sent as a single byte 28 | return inBytes 29 | } 30 | if len(inBytes) <= 55 { 31 | // Add the length to same byte as the offset 32 | outBytes := make([]byte, len(inBytes)+1) 33 | outBytes[0] = shortOffset + byte(len(inBytes)) 34 | copy(outBytes[1:], inBytes[0:]) 35 | return outBytes 36 | } 37 | // The length is too long to fit in a single byte, we have to encode it 38 | encodedByteLen := int64ToMinimalBytes(int64(len(inBytes))) 39 | outBytes := make([]byte, 1+len(encodedByteLen)+len(inBytes)) 40 | outBytes[0] = shortOffset + shortToLong + byte(len(encodedByteLen)) 41 | copy(outBytes[1:], encodedByteLen) 42 | copy(outBytes[1+len(encodedByteLen):], inBytes) 43 | return outBytes 44 | } 45 | 46 | func int64ToMinimalBytes(v int64) []byte { 47 | vb := int64ToBytes(v) 48 | for i := 0; i < len(vb); i++ { 49 | if vb[i] != 0x00 { 50 | return vb[i:] 51 | } 52 | } 53 | return []byte{} 54 | } 55 | 56 | func int64ToBytes(v int64) [8]byte { 57 | return [8]byte{ 58 | (byte)((v >> 56) & 0xff), 59 | (byte)((v >> 48) & 0xff), 60 | (byte)((v >> 40) & 0xff), 61 | (byte)((v >> 32) & 0xff), 62 | (byte)((v >> 24) & 0xff), 63 | (byte)((v >> 16) & 0xff), 64 | (byte)((v >> 8) & 0xff), 65 | (byte)(v & 0xff), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/rlp/encode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rlp 18 | 19 | import ( 20 | "encoding/hex" 21 | "math/big" 22 | "testing" 23 | 24 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestWrapHexFail(t *testing.T) { 29 | assert.Panics(t, func() { 30 | MustWrapHex("! not hex") 31 | }) 32 | } 33 | 34 | func TestEncodeData(t *testing.T) { 35 | 36 | d := Data{} 37 | assert.False(t, d.IsList()) 38 | 39 | assert.Equal(t, []byte{}, int64ToMinimalBytes(0)) 40 | 41 | assert.Equal(t, big.NewInt(0x7FFFFFFFFFFFFFF0).Bytes(), int64ToMinimalBytes(0x7FFFFFFFFFFFFFF0)) 42 | 43 | assert.Equal(t, []byte{0x80}, WrapString("").Encode()) 44 | 45 | assert.Equal(t, []byte{0x0f}, Data([]byte{0x0f}).Encode()) 46 | 47 | assert.Equal(t, []byte{0x83, 'd', 'o', 'g'}, WrapString("dog").Encode()) 48 | 49 | assert.Equal(t, []byte{0x00}, Data{0x00}.Encode()) 50 | 51 | assert.Equal(t, loremIpsumRLPBytes, WrapString(loremIpsumString).Encode()) 52 | 53 | expected := make([]byte, 56) 54 | expected[0] = 0xb7 55 | assert.Equal(t, expected, make(Data, 55).Encode()) 56 | } 57 | 58 | func TestEncodeIntegers(t *testing.T) { 59 | 60 | assert.Equal(t, []byte{0x0f}, WrapInt(big.NewInt(0x0f)).Encode()) 61 | 62 | assert.Equal(t, []byte{0x82, 0x04, 0x00}, WrapInt(big.NewInt(0x400)).Encode()) 63 | 64 | assert.Equal(t, []byte{0x80}, WrapInt(big.NewInt(0)).Encode()) 65 | 66 | assert.Equal(t, int64(0xfeedbeef), Data{0xfe, 0xed, 0xbe, 0xef}.Int().Int64()) 67 | 68 | assert.Nil(t, Data(nil).Int()) 69 | 70 | } 71 | 72 | func TestEncodeList(t *testing.T) { 73 | 74 | l := List{} 75 | assert.True(t, l.IsList()) 76 | 77 | assert.Equal(t, []byte{0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g'}, 78 | List{WrapString("cat"), WrapString("dog")}.Encode()) 79 | 80 | assert.Equal(t, []byte{0xc0}, List{}.Encode()) 81 | 82 | assert.Equal(t, []byte{ 83 | 0xc7, 84 | 0xc0, 85 | 0xc1, 86 | 0xc0, 87 | 0xc3, 88 | 0xc0, 89 | 0xc1, 90 | 0xc0, 91 | }, List{ 92 | List{}, 93 | List{ 94 | List{}, 95 | }, 96 | List{ 97 | List{}, 98 | List{ 99 | List{}, 100 | }, 101 | }, 102 | }.Encode()) 103 | 104 | assert.Equal(t, []byte{ 105 | 0xc6, 106 | 0x82, 107 | 0x7a, 108 | 0x77, 109 | 0xc1, 110 | 0x04, 111 | 0x01, 112 | }, List{ 113 | WrapString("zw"), 114 | List{ 115 | WrapInt(big.NewInt(4)), 116 | }, 117 | WrapInt(big.NewInt(1)), 118 | }.Encode()) 119 | 120 | } 121 | 122 | func TestEncodeNil(t *testing.T) { 123 | 124 | assert.Equal(t, []byte{0x80}, (Data)(nil).Encode()) 125 | 126 | } 127 | 128 | func TestEncodeZero(t *testing.T) { 129 | 130 | assert.Equal(t, []byte{0x80}, WrapInt(big.NewInt(0)).Encode()) 131 | 132 | } 133 | 134 | func TestEncodeAddress(t *testing.T) { 135 | 136 | b, err := hex.DecodeString("497eedc4299dea2f2a364be10025d0ad0f702de3") 137 | assert.NoError(t, err) 138 | var a ethtypes.Address0xHex 139 | copy(a[0:20], b[0:20]) 140 | 141 | d := WrapAddress(&a) 142 | aa, _, err := Decode(d.Encode()) 143 | assert.NoError(t, err) 144 | assert.Equal(t, Data(b), aa.(Data)) 145 | 146 | d1 := WrapAddress((*ethtypes.Address0xHex)(nil)) 147 | assert.Equal(t, Data{}, d1) 148 | 149 | } 150 | -------------------------------------------------------------------------------- /pkg/rlp/rlp.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rlp 18 | 19 | import ( 20 | "encoding/hex" 21 | "math/big" 22 | "strings" 23 | 24 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 25 | ) 26 | 27 | // Data is an individual RLP Data element - or an "RLP string" 28 | type Data []byte 29 | 30 | // List is a list of RLP elements, which could be either Data or List elements 31 | type List []Element 32 | 33 | // Element is an interface implemented by both Data and List elements 34 | type Element interface { 35 | // When true the Element can safely be cast to List, and when false the Element can safely be cast to Data 36 | IsList() bool 37 | // Encode converts the element to a byte array 38 | Encode() []byte 39 | // Safe function that will give an entry as data, to use the nil-safe functions on it to get the value (will be treated as nil data for list) 40 | ToData() Data 41 | } 42 | 43 | // WrapString converts a plain string to an RLP Data element for encoding 44 | func WrapString(s string) Data { 45 | return Data(s) 46 | } 47 | 48 | // WrapString converts a positive integer to an RLP Data element for encoding 49 | func WrapInt(i *big.Int) Data { 50 | return Data(i.Bytes()) 51 | } 52 | 53 | // WrapAddress wraps an address, or writes empty data if the address is nil 54 | func WrapAddress(a *ethtypes.Address0xHex) Data { 55 | if a == nil { 56 | return Data{} 57 | } 58 | return Data(a[0:20]) 59 | } 60 | 61 | // WrapHex converts a hex encoded string (with or without 0x prefix) to an RLP Data element for encoding 62 | func WrapHex(s string) (Data, error) { 63 | b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return Data(b), nil 68 | } 69 | 70 | // MustWrapHex panics if hex decoding fails 71 | func MustWrapHex(s string) Data { 72 | b, err := WrapHex(s) 73 | if err != nil { 74 | panic(err) 75 | } 76 | return b 77 | } 78 | 79 | // Int is a convenience function to convert the bytes within an RLP Data element to an integer (big endian encoding) 80 | func (r Data) Int() *big.Int { 81 | if r == nil { 82 | return nil 83 | } 84 | i := new(big.Int) 85 | return i.SetBytes(r) 86 | } 87 | 88 | func (r Data) IntOrZero() *big.Int { 89 | if r == nil { 90 | return big.NewInt(0) 91 | } 92 | i := new(big.Int) 93 | return i.SetBytes(r) 94 | } 95 | 96 | func (r Data) BytesNotNil() []byte { 97 | if r == nil { 98 | return []byte{} 99 | } 100 | return r 101 | } 102 | 103 | func (r Data) Address() *ethtypes.Address0xHex { 104 | if r == nil || len(r) != 20 { 105 | return nil 106 | } 107 | return (*ethtypes.Address0xHex)(r) 108 | } 109 | 110 | // Encode encodes this individual RLP Data element 111 | func (r Data) Encode() []byte { 112 | return encodeBytes(r, false) 113 | } 114 | 115 | // IsList is false for individual RLP Data elements 116 | func (r Data) IsList() bool { 117 | return false 118 | } 119 | 120 | func (r Data) ToData() Data { 121 | return r 122 | } 123 | 124 | // Encode encodes the RLP List to a byte array, including recursing into child arrays 125 | func (l List) Encode() []byte { 126 | if len(l) == 0 { 127 | return encodeBytes([]byte{}, true) 128 | } 129 | var concatenation []byte 130 | for _, entry := range l { 131 | concatenation = append(concatenation, entry.Encode()...) 132 | } 133 | return encodeBytes(concatenation, true) 134 | 135 | } 136 | 137 | // IsList returns true for list elements 138 | func (l List) IsList() bool { 139 | return true 140 | } 141 | 142 | func (l List) ToData() Data { 143 | // This allows code to not worry about lots of type checking - a list is treated as nil data 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /pkg/rlp/rlp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package rlp 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestDataBytes(t *testing.T) { 27 | 28 | assert.Nil(t, ((List)(nil)).ToData()) 29 | assert.Nil(t, ((Data)(nil)).ToData()) 30 | assert.Equal(t, Data{0xff}, ((Data)([]byte{0xff})).ToData()) 31 | 32 | } 33 | 34 | func TestDataIntOrZero(t *testing.T) { 35 | 36 | assert.Equal(t, int64(0), ((List)(nil)).ToData().IntOrZero().Int64()) 37 | assert.Equal(t, int64(0xff), ((Data)([]byte{0xff})).ToData().IntOrZero().Int64()) 38 | 39 | } 40 | 41 | func TestDataBytesNotNil(t *testing.T) { 42 | 43 | assert.Equal(t, []byte{}, ((List)(nil)).ToData().BytesNotNil()) 44 | assert.Equal(t, []byte{0xff}, ((Data)([]byte{0xff})).ToData().BytesNotNil()) 45 | 46 | } 47 | 48 | func TestAddress(t *testing.T) { 49 | 50 | assert.Nil(t, ((List)(nil)).ToData().Address()) 51 | assert.Nil(t, (Data{0x00}).Address()) 52 | assert.Equal(t, "0x4f78181c7fdc267d953a3cba8079f899d7f5ba78", (Data)(ethtypes.MustNewAddress("0x4F78181C7fdC267d953A3cBa8079f899D7F5BA78")[:]).Address().String()) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /pkg/secp256k1/keypair.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package secp256k1 18 | 19 | import ( 20 | btcec "github.com/btcsuite/btcd/btcec/v2" // ISC licensed 21 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 22 | "golang.org/x/crypto/sha3" 23 | ) 24 | 25 | type KeyPair struct { 26 | PrivateKey *btcec.PrivateKey 27 | PublicKey *btcec.PublicKey 28 | Address ethtypes.Address0xHex 29 | } 30 | 31 | func (k *KeyPair) PrivateKeyBytes() []byte { 32 | return k.PrivateKey.Serialize() 33 | } 34 | 35 | func (k *KeyPair) PublicKeyBytes() []byte { 36 | // Remove the "04" Prefix byte when computing the address. This byte indicates that it is an uncompressed public key. 37 | return k.PublicKey.SerializeUncompressed()[1:] 38 | } 39 | 40 | func GenerateSecp256k1KeyPair() (*KeyPair, error) { 41 | // Generates key of curve S256() by default 42 | key, _ := btcec.NewPrivateKey() 43 | return wrapSecp256k1Key(key, key.PubKey()), nil 44 | } 45 | 46 | // Deprecated: Note there is no error condition returned by this function (use KeyPairFromBytes) 47 | func NewSecp256k1KeyPair(b []byte) (*KeyPair, error) { 48 | return KeyPairFromBytes(b), nil 49 | } 50 | 51 | func KeyPairFromBytes(b []byte) *KeyPair { 52 | key, pubKey := btcec.PrivKeyFromBytes(b) 53 | return wrapSecp256k1Key(key, pubKey) 54 | } 55 | 56 | func wrapSecp256k1Key(key *btcec.PrivateKey, pubKey *btcec.PublicKey) *KeyPair { 57 | return &KeyPair{ 58 | PrivateKey: key, 59 | PublicKey: pubKey, 60 | Address: *PublicKeyToAddress(pubKey), 61 | } 62 | } 63 | 64 | func PublicKeyToAddress(pubKey *btcec.PublicKey) *ethtypes.Address0xHex { 65 | // Take the hash of the public key to generate the address 66 | hash := sha3.NewLegacyKeccak256() 67 | hash.Write(pubKey.SerializeUncompressed()[1:]) 68 | // Ethereum addresses only use the lower 20 bytes, so toss the rest away 69 | a := new(ethtypes.Address0xHex) 70 | copy(a[:], hash.Sum(nil)[12:32]) 71 | return a 72 | } 73 | -------------------------------------------------------------------------------- /pkg/secp256k1/keypair_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package secp256k1 18 | 19 | import ( 20 | "context" 21 | "math/big" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestGeneratedKeyRoundTrip(t *testing.T) { 28 | 29 | keypair, err := GenerateSecp256k1KeyPair() 30 | assert.NoError(t, err) 31 | 32 | b := keypair.PrivateKeyBytes() 33 | keypair2, err := NewSecp256k1KeyPair(b) 34 | assert.NoError(t, err) 35 | 36 | assert.Equal(t, keypair.PrivateKeyBytes(), keypair2.PrivateKeyBytes()) 37 | assert.True(t, keypair.PublicKey.IsEqual(keypair2.PublicKey)) 38 | 39 | data := []byte("hello world") 40 | sig, err := keypair.Sign(data) 41 | assert.NoError(t, err) 42 | 43 | // Legacy 27/28 - pre EIP-155 44 | addr, err := sig.Recover(data, 0) 45 | assert.NoError(t, err) 46 | assert.Equal(t, keypair.Address, *addr) 47 | 48 | // Latest 0/1 - EIP-1559 / EIP-2930 49 | sig.UpdateEIP2930() 50 | addr, err = sig.Recover(data, 0) 51 | assert.NoError(t, err) 52 | assert.Equal(t, keypair.Address, *addr) 53 | sig.V.SetInt64(sig.V.Int64() + 27) 54 | 55 | // Chain ID encoded in V value - EIP-155 56 | sig.UpdateEIP155(1001) 57 | addr, err = sig.Recover(data, 1001) 58 | assert.NoError(t, err) 59 | assert.Equal(t, keypair.Address, *addr) 60 | 61 | sigRSV := sig.CompactRSV() 62 | sig2, err := DecodeCompactRSV(context.Background(), sigRSV) 63 | assert.NoError(t, err) 64 | addr, err = sig2.Recover(data, 1001) 65 | assert.NoError(t, err) 66 | assert.Equal(t, keypair.Address, *addr) 67 | 68 | _, err = DecodeCompactRSV(context.Background(), []byte("wrong")) 69 | assert.Regexp(t, "FF22087", err) 70 | 71 | _, err = sig.Recover(data, 42) 72 | assert.Regexp(t, "invalid V value in signature", err) 73 | 74 | sigBad := &SignatureData{ 75 | V: big.NewInt(27), 76 | R: new(big.Int), 77 | S: new(big.Int), 78 | } 79 | _, err = sigBad.Recover(data, 0) 80 | assert.Error(t, err) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /pkg/secp256k1/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2024 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package secp256k1 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/big" 23 | 24 | ecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" 25 | "github.com/hyperledger/firefly-common/pkg/i18n" 26 | "github.com/hyperledger/firefly-signer/internal/signermsgs" 27 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 28 | "golang.org/x/crypto/sha3" 29 | ) 30 | 31 | type SignatureData struct { 32 | V *big.Int 33 | R *big.Int 34 | S *big.Int 35 | } 36 | 37 | // Signer is the low level common interface that can be implemented by any module which provides signature capability 38 | type Signer interface { 39 | Sign(msgToHashAndSign []byte) (*SignatureData, error) 40 | } 41 | 42 | type SignerDirect interface { 43 | Signer 44 | SignDirect(message []byte) (*SignatureData, error) 45 | } 46 | 47 | // getVNormalized returns the original 27/28 parity 48 | func (s *SignatureData) getVNormalized(chainID int64) (byte, error) { 49 | v := s.V.Int64() 50 | var vB byte 51 | switch v { 52 | case 0, 1: 53 | vB = byte(v + 27) 54 | case 27, 28: 55 | vB = byte(v) 56 | default: 57 | vB = byte(v - 35 - (chainID * 2) + 27) 58 | } 59 | if vB != 27 && vB != 28 { 60 | return 0, fmt.Errorf("invalid V value in signature (chain ID = %d, V = %d)", chainID, v) 61 | } 62 | return vB, nil 63 | } 64 | 65 | // EIP-155 rules - 2xChainID + 35 - starting point must be legacy 27/28 66 | func (s *SignatureData) UpdateEIP155(chainID int64) { 67 | chainIDx2 := big.NewInt(chainID) 68 | chainIDx2 = chainIDx2.Mul(chainIDx2, big.NewInt(2)) 69 | s.V = s.V.Add(s.V, chainIDx2).Add(s.V, big.NewInt(35-27)) 70 | 71 | } 72 | 73 | // EIP-2930 (/ EIP-1559) rules - 0 or 1 V value for raw Y-parity value (chainID goes into the payload) 74 | func (s *SignatureData) UpdateEIP2930() { 75 | vi64 := s.V.Int64() 76 | if vi64 == 27 || vi64 == 28 { 77 | s.V = s.V.Sub(s.V, big.NewInt(27)) 78 | } 79 | } 80 | 81 | // Recover obtains the original signer from the hash of the message 82 | func (s *SignatureData) Recover(message []byte, chainID int64) (a *ethtypes.Address0xHex, err error) { 83 | msgHash := sha3.NewLegacyKeccak256() 84 | msgHash.Write(message) 85 | return s.RecoverDirect(msgHash.Sum(nil), chainID) 86 | } 87 | 88 | // Recover obtains the original signer 89 | func (s *SignatureData) RecoverDirect(message []byte, chainID int64) (a *ethtypes.Address0xHex, err error) { 90 | 91 | signatureBytes := make([]byte, 65) 92 | signatureBytes[0], err = s.getVNormalized(chainID) 93 | if err != nil { 94 | return nil, err 95 | } 96 | s.R.FillBytes(signatureBytes[1:33]) 97 | s.S.FillBytes(signatureBytes[33:65]) 98 | pubKey, _, err := ecdsa.RecoverCompact(signatureBytes, message) // uses S256() by default 99 | if err != nil { 100 | return nil, err 101 | } 102 | return PublicKeyToAddress(pubKey), nil 103 | } 104 | 105 | // We use the ethereum convention of R,S,V for compact packing (mentioned because Golang tends to prefer V,R,S) 106 | func (s *SignatureData) CompactRSV() []byte { 107 | signatureBytes := make([]byte, 65) 108 | s.R.FillBytes(signatureBytes[0:32]) 109 | s.S.FillBytes(signatureBytes[32:64]) 110 | signatureBytes[64] = byte(s.V.Int64()) 111 | return signatureBytes 112 | } 113 | 114 | func DecodeCompactRSV(ctx context.Context, compactRSV []byte) (*SignatureData, error) { 115 | if len(compactRSV) != 65 { 116 | return nil, i18n.NewError(ctx, signermsgs.MsgSigningInvalidCompactRSV, len(compactRSV)) 117 | } 118 | var sig SignatureData 119 | sig.R = new(big.Int).SetBytes(compactRSV[0:32]) 120 | sig.S = new(big.Int).SetBytes(compactRSV[32:64]) 121 | sig.V = new(big.Int).SetBytes(compactRSV[64:65]) 122 | return &sig, nil 123 | } 124 | 125 | // Sign hashes the input then signs it 126 | func (k *KeyPair) Sign(message []byte) (ethSig *SignatureData, err error) { 127 | msgHash := sha3.NewLegacyKeccak256() 128 | msgHash.Write(message) 129 | hashed := msgHash.Sum(nil) 130 | return k.SignDirect(hashed) 131 | } 132 | 133 | // SignDirect performs raw signing - give legacy 27/28 V values 134 | func (k *KeyPair) SignDirect(message []byte) (ethSig *SignatureData, err error) { 135 | if k == nil { 136 | return nil, fmt.Errorf("nil signer") 137 | } 138 | sig, err := ecdsa.SignCompact(k.PrivateKey, message, false) // uses S256() by default 139 | if err == nil { 140 | // btcec does all the hard work for us. However, the interface of btcec is such 141 | // that we need to unpack the result for Ethereum encoding. 142 | ethSig = &SignatureData{ 143 | V: new(big.Int), 144 | R: new(big.Int), 145 | S: new(big.Int), 146 | } 147 | ethSig.V = ethSig.V.SetInt64(int64(sig[0])) 148 | ethSig.R = ethSig.R.SetBytes(sig[1:33]) 149 | ethSig.S = ethSig.S.SetBytes(sig[33:65]) 150 | } 151 | return ethSig, err 152 | } 153 | -------------------------------------------------------------------------------- /pkg/secp256k1/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Kaleido, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package secp256k1 18 | 19 | import ( 20 | "bytes" 21 | "encoding/hex" 22 | "strconv" 23 | "testing" 24 | 25 | "github.com/hyperledger/firefly-signer/pkg/ethtypes" 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | const ethMessagePrefix = "\u0019Ethereum Signed Message:\n" 30 | 31 | // Test data directly taken from: 32 | // https://github.com/web3j/web3j/blob/master/crypto/src/test/java/org/web3j/crypto/SignTest.java 33 | var ( 34 | sampleMessage = "A test message" 35 | samplePrivateKey = "a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6" 36 | samplePublicKey = "0x506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aab" + 37 | "a645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76" 38 | sampleAddress = "0xef678007d18427e6022059dbc264f27507cd1ffc" 39 | ) 40 | 41 | func addEthMessagePrefix(message []byte) []byte { 42 | b := new(bytes.Buffer) 43 | b.Write([]byte(ethMessagePrefix)) 44 | b.Write([]byte(strconv.FormatInt(int64(len(message)), 10))) 45 | b.Write(message) 46 | return b.Bytes() 47 | } 48 | 49 | func testKeyPair(t *testing.T) *KeyPair { 50 | keyBytes, err := hex.DecodeString(samplePrivateKey) 51 | assert.NoError(t, err) 52 | keypair, err := NewSecp256k1KeyPair(keyBytes) 53 | assert.NoError(t, err) 54 | return keypair 55 | } 56 | 57 | func TestValidateSampleData(t *testing.T) { 58 | // Validate the above sample data is consistent in the base secp256k1 key management layer 59 | keypair := testKeyPair(t) 60 | assert.Equal(t, samplePrivateKey, ((ethtypes.HexBytesPlain)(keypair.PrivateKeyBytes())).String()) 61 | var pubkey ethtypes.HexBytes0xPrefix = keypair.PublicKeyBytes() 62 | assert.Equal(t, samplePublicKey, pubkey.String()) 63 | var addr ethtypes.Address0xHex = ethtypes.Address0xHex(keypair.Address) 64 | assert.Equal(t, sampleAddress, addr.String()) 65 | } 66 | 67 | func TestSignMessage(t *testing.T) { 68 | 69 | keypair := testKeyPair(t) 70 | sig, err := keypair.Sign(addEthMessagePrefix([]byte(sampleMessage))) 71 | assert.NoError(t, err) 72 | 73 | assert.Equal(t, int64(28), sig.V.Int64()) 74 | assert.Equal(t, "0464eee9e2fe1a10ffe48c78b80de1ed8dcf996f3f60955cb2e03cb21903d930", ((ethtypes.HexBytesPlain)(sig.R.Bytes())).String()) 75 | assert.Equal(t, "06624da478b3f862582e85b31c6a21c6cae2eee2bd50f55c93c4faad9d9c8d7f", ((ethtypes.HexBytesPlain)(sig.S.Bytes())).String()) 76 | 77 | sig.UpdateEIP155(1001) 78 | assert.Equal(t, int64(2038), sig.V.Int64()) 79 | } 80 | 81 | func TestSignFailNil(t *testing.T) { 82 | 83 | _, err := (*KeyPair)(nil).Sign(addEthMessagePrefix([]byte(sampleMessage))) 84 | assert.Regexp(t, "nil signer", err) 85 | 86 | } 87 | -------------------------------------------------------------------------------- /test/bad-config.ffsigner.yaml: -------------------------------------------------------------------------------- 1 | !!!{ Not parsable -------------------------------------------------------------------------------- /test/bad-wallet.ffsigner.yaml: -------------------------------------------------------------------------------- 1 | fileWallet: 2 | path: "../test/keystore_toml" 3 | metadata: 4 | format: toml 5 | keyFileProperty: '{{ !!! }}' 6 | backend: 7 | chainId: 0 8 | -------------------------------------------------------------------------------- /test/firefly.ffsigner.yaml: -------------------------------------------------------------------------------- 1 | fileWallet: 2 | path: "./test/keystore_toml" 3 | disableListener: true 4 | filenames: 5 | primaryExt: ".toml" 6 | metadata: 7 | format: auto 8 | keyFileProperty: '{{ index .signing "key-file" }}' 9 | passwordFileProperty: '{{ index .signing "password-file" }}' 10 | backend: 11 | chainId: 0 12 | -------------------------------------------------------------------------------- /test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.key.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1f185718734552d08278aa70f804580bab5fd2b4", 3 | "crypto": { 4 | "cipher": "aes-128-ctr", 5 | "ciphertext": "a46921125177b8eb91710802085cdb91e26318220bd7a0aaa831e180265dfdda", 6 | "cipherparams": { 7 | "iv": "ab38123496886642e19c08c563ab294b" 8 | }, 9 | "kdf": "scrypt", 10 | "kdfparams": { 11 | "dklen": 32, 12 | "n": 262144, 13 | "p": 1, 14 | "r": 8, 15 | "salt": "89a0f233b592e0a38dec3884a04092529ebcacd9be7d713de28b44da937f8df0" 16 | }, 17 | "mac": "e2c13ed7992bb4931cd791a5ecb305a5f2acc535457ce73819d7b30764d333f3" 18 | }, 19 | "id": "24071c43-203a-4524-9c64-98847aa80945", 20 | "version": 3 21 | } -------------------------------------------------------------------------------- /test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.pwd: -------------------------------------------------------------------------------- 1 | correcthorsebatterystaple -------------------------------------------------------------------------------- /test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | createdAt = 2019-11-05T08:15:30-05:00 3 | description = "File based configuration" 4 | 5 | [signing] 6 | type = "file-based-signer" 7 | key-file = "../../test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.key.json" 8 | password-file = "../../test/keystore_toml/1f185718734552d08278aa70f804580bab5fd2b4.pwd" 9 | -------------------------------------------------------------------------------- /test/keystore_toml/497eedc4299dea2f2a364be10025d0ad0f702de3.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | createdAt = 2019-11-05T08:15:30-05:00 3 | description = "This is missing all the useful info" 4 | 5 | -------------------------------------------------------------------------------- /test/keystore_toml/5d093e9b41911be5f5c4cf91b108bac5d130fa83.toml: -------------------------------------------------------------------------------- 1 | [metadata] 2 | createdAt = 2019-11-05T08:15:30-05:00 3 | description = "This has a bad location" 4 | 5 | [signing] 6 | type = "file-based-signer" 7 | key-file = "!!!" 8 | password-file = "!!!" 9 | -------------------------------------------------------------------------------- /test/keystore_toml/abcd1234.key.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "abcd1234", 3 | "description": "This file just exists to show negative matching if the address in the filename is invalid" 4 | } -------------------------------------------------------------------------------- /test/keystore_toml/abcd1234abcd1234abcd1234abcd1234abcd1234.key.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "1f185718734552d08278aa70f804580bab5fd2b4", 3 | "description": "This file will be ignored, because the address does not match", 4 | "crypto": { 5 | "cipher": "aes-128-ctr", 6 | "ciphertext": "a46921125177b8eb91710802085cdb91e26318220bd7a0aaa831e180265dfdda", 7 | "cipherparams": { 8 | "iv": "ab38123496886642e19c08c563ab294b" 9 | }, 10 | "kdf": "scrypt", 11 | "kdfparams": { 12 | "dklen": 32, 13 | "n": 262144, 14 | "p": 1, 15 | "r": 8, 16 | "salt": "89a0f233b592e0a38dec3884a04092529ebcacd9be7d713de28b44da937f8df0" 17 | }, 18 | "mac": "e2c13ed7992bb4931cd791a5ecb305a5f2acc535457ce73819d7b30764d333f3" 19 | }, 20 | "id": "24071c43-203a-4524-9c64-98847aa80945", 21 | "version": 3 22 | } -------------------------------------------------------------------------------- /test/keystore_toml/abcd1234abcd1234abcd1234abcd1234abcd1234.pwd: -------------------------------------------------------------------------------- 1 | correcthorsebatterystaple -------------------------------------------------------------------------------- /test/keystore_toml/file_with_wrong_name.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperledger/firefly-signer/e44058a32899656780b5f45def67167b3ca8b7fc/test/keystore_toml/file_with_wrong_name.toml -------------------------------------------------------------------------------- /test/keystore_toml/ignore_dir/readme.txt: -------------------------------------------------------------------------------- 1 | This directory ensures the code ignores subdirectories -------------------------------------------------------------------------------- /test/no-wallet.ffsigner.yaml: -------------------------------------------------------------------------------- 1 | fileWallet: 2 | enabled: false 3 | backend: 4 | chainId: 0 5 | -------------------------------------------------------------------------------- /test/quick-fail.ffsigner.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | address: ":::::::::" 3 | backend: 4 | chainId: 0 5 | --------------------------------------------------------------------------------