├── .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 | 
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 | 
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 | [](https://codecov.io/gh/hyperledger/firefly-signer)
2 | [](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 |
--------------------------------------------------------------------------------