diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml new file mode 100644 index 0000000..5b7cdc1 --- /dev/null +++ b/.github/workflows/docker-test.yml @@ -0,0 +1,18 @@ +name: Dockerized Tests + +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "**" ] + +jobs: + all-tests: + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Run tests in docker container + run: make docker-test-all diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..2e2f883 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,22 @@ +name: golangci-lint + +on: + push: + tags: [ "v*" ] + branches: [ "master", "main" ] + pull_request: + branches: [ "**" ] + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.20.3 + - uses: actions/checkout@v3 + - name: install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: run golangci-lint + run: golangci-lint run diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000..2562312 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,204 @@ +name: Build Release Artifacts + +on: + push: + tags: + - v* + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + VAULT_IMAGE_NAME: nydig-oss/vault-plugin-lndsigner + GO_VERSION: 1.20.3 + +jobs: + # This job fetches the latest minor revision for each currently supported vault version. + # The output of this job is used to construct the version matrix in the build-docker-vault job. + vault_latest_versions: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.generate.outputs.versions }} + steps: + - name: "Generate matrix" + id: generate + run: | + VAULT_VERSIONS=`curl https://raw.githubusercontent.com/docker-library/official-images/master/library/vault | grep -Eo '1\.(9|1[0123])\.[0-9]+' | jq --raw-input --raw-output --slurp 'split("\n") | del(.[] | select(. == "")) | tojson'` + echo "versions=$VAULT_VERSIONS" >> "$GITHUB_OUTPUT" + + # This job builds the Vault plugin, and pushes it to the GitHub release + build-vault-plugin: + strategy: + matrix: + os: [ "linux" ] + arch: [ "amd64", "arm64" ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: Build plugin + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -buildvcs=false -o "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}" ./cmd/vault-plugin-lndsigner/ + + - name: Create the SHA256 checksum file + run: shasum -a 256 "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}" | cut -d " " -f1 > "vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM" + + - name: Upload plugin assets to release + uses: ncipollo/release-action@v1 + with: + artifacts: | + vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }} + vault-plugin-lndsigner-${{ matrix.os }}-${{ matrix.arch }}.SHA256SUM + + body: | + ## Docker Images + All images are built for both `linux/amd64` and `linux/arm64` architectures. + + ### lndsignerd + Pre-built docker images for the `lndsignerd` server are available in multiple flavours: + + - Debian (Bullseye): `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}` + - Alpine Linux (3.17): `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-alpine` + + ### vault-plugin-lndsigner + Pre-built extensions of the latest [base Vault images](https://hub.docker.com/_/vault) with `vault-plugin-lndsigner` pre-installed are available here: + + [${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }}](https://github.com/nydig-oss/lndsigner/pkgs/container/vault-plugin-lndsigner) + + Note that the images within this package are provided for testing purposes only. Running a Vault image from an untrusted source in production is not recommended. + + generateReleaseNotes: true + prerelease: true + allowUpdates: true + + # This job extends the base Vault docker image by pre-installing the lndsigner plugin created + # by the build-vault-plugin job + build-docker-vault: + runs-on: ubuntu-latest + needs: + - vault_latest_versions + - build-vault-plugin + strategy: + matrix: + vault_version: ${{ fromJSON(needs.vault_latest_versions.outputs.versions) }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "Extract metadata (tags, labels) for Docker" + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.VAULT_IMAGE_NAME }} + tags: | + type=ref,event=tag + flavor: | + prefix=${{ matrix.vault_version }}-lndsigner- + latest=${{ startsWith(matrix.vault_version, '1.12') }} + labels: | + org.opencontainers.image.title=Vault (with lndsigner) + org.opencontainers.image.description=The base Hashicorp Vault image (library/vault), with the lndsigner plugin pre-installed. + org.opencontainers.image.vendor=Hashicorp (Vault), NYDIG (lndsigner) + org.opencontainers.image.source=https://github.com/nydig-oss/lndsigner + org.opencontainers.image.version=${{ github.ref_name }} + org.opencontainers.image.licenses=(MPL-2.0 AND MIT) + org.opencontainers.image.base.name=docker.io/library/vault:${{ matrix.vault_version }} + + - name: Build the Docker image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile.vault + platforms: linux/amd64,linux/arm64 + build-args: | + VAULT_VER=${{ matrix.vault_version }} + VAULT_SIGNER_PLUGIN_VER=${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + + # This job builds the lndsigner server image + build-docker-lndsigner: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build the Docker image + uses: docker/build-push-action@v3 + with: + context: . + target: release-builder + platforms: linux/amd64,linux/arm64 + + - name: "Debian: Extract metadata (tags, labels) for Docker" + id: debian_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + latest=auto + + - name: "Debian: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.debian_meta.outputs.tags }} + labels: ${{ steps.debian_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: debian + + - name: "Alpine: Extract metadata (tags, labels) for Docker" + id: alpine_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + flavor: | + suffix=-alpine + + - name: "Alpine: Push Docker image" + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.alpine_meta.outputs.tags }} + labels: ${{ steps.alpine_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + target: alpine diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4f3cb7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# vim +*.swp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..560ef3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +ARG gover=1.20.3 + +# Build a release binary + +FROM golang:$gover AS release-builder + +COPY . /go/src/github.com/nydig-oss/lndsigner + +RUN cd /go/src/github.com/nydig-oss/lndsigner \ + && CGO_ENABLED=0 go install -buildvcs=false \ + github.com/nydig-oss/lndsigner/cmd/... + +### Build an Alpine image +FROM alpine:3.17 as alpine + +# Update CA certs +RUN apk add --no-cache ca-certificates && rm -rf /var/cache/apk/* + +# Copy over app binary +COPY --from=release-builder /go/bin/lndsignerd /usr/bin/lndsignerd + +# Add a user +RUN mkdir -p /app && adduser -D lndsignerd && chown -R lndsignerd /app +USER lndsignerd + +WORKDIR /app/ + +CMD [ "/usr/bin/lndsignerd" ] + +### Build a Debian image +FROM debian:bullseye-slim as debian + +# Update CA certs +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +# Copy over app binary +COPY --from=release-builder /go/bin/lndsignerd /usr/bin/lndsignerd + +# Add a user +RUN mkdir -p /app && adduser --disabled-login lndsignerd && chown -R lndsignerd /app +USER lndsignerd + +WORKDIR /app + +CMD [ "/usr/bin/lndsignerd" ] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..b47e022 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,29 @@ +ARG gover=1.20.3 + +FROM golang:$gover + +ARG goplatform +ARG cplatform +ARG lnd +ARG bitcoind +ARG vault + +RUN apt update && apt-get install -y zip + +RUN cd /root && \ + wget https://bitcoincore.org/bin/bitcoin-core-$bitcoind/bitcoin-${bitcoind}-${cplatform}-linux-gnu.tar.gz && \ + tar xfz bitcoin-$bitcoind-$cplatform-linux-gnu.tar.gz && \ + mv bitcoin-$bitcoind/bin/* /usr/local/bin/ && \ + wget https://github.com/lightningnetwork/lnd/releases/download/$lnd/lnd-linux-$goplatform-$lnd.tar.gz && \ + tar xfz lnd-linux-$goplatform-$lnd.tar.gz && \ + mv lnd-linux-$goplatform-$lnd/* /usr/local/bin/ && \ + wget https://releases.hashicorp.com/vault/$vault/vault_${vault}_linux_${goplatform}.zip && \ + unzip vault_${vault}_linux_${goplatform}.zip && \ + mv vault /usr/local/bin/ && \ + go install github.com/go-delve/delve/cmd/dlv@latest && \ + git config --global --add safe.directory /app && \ + echo "export PATH='$PATH:/usr/local/go/bin:/root/go/bin'" >> .bashrc + +VOLUME [ "/app" ] + +WORKDIR /app diff --git a/Dockerfile.vault b/Dockerfile.vault new file mode 100644 index 0000000..4649b54 --- /dev/null +++ b/Dockerfile.vault @@ -0,0 +1,14 @@ +ARG VAULT_VER=1.11.7 + +FROM library/vault:${VAULT_VER} +ARG TARGETARCH +ARG VAULT_SIGNER_PLUGIN_VER + +ADD https://github.com/nydig-oss/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH} /vault/plugins/vault-plugin-lndsigner +ADD https://github.com/nydig-oss/lndsigner/releases/download/${VAULT_SIGNER_PLUGIN_VER}/vault-plugin-lndsigner-linux-${TARGETARCH}.SHA256SUM /vault/plugins/vault-plugin-lndsigner.SHA256SUM +ADD LICENSE /vault/plugins/vault-plugin-lndsigner.LICENSE + +RUN \ + chown -R vault:vault /vault/plugins \ + && chmod +x /vault/plugins/vault-plugin-lndsigner \ + && setcap cap_ipc_lock=+ep /vault/plugins/vault-plugin-lndsigner diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96c6989 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (C) 2013-2017 The btcsuite developers +Copyright (C) 2015-2016 The Decred developers +Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30e91d0 --- /dev/null +++ b/Makefile @@ -0,0 +1,65 @@ +.PHONY: docker docker-itest docker-test docker-test-all docker-check docker-shell itest test test-all + +IMG_NAME := lndsigner-builder + +CPLATFORM := $(shell uname -m) + +ifeq ($(CPLATFORM), x86_64) + GOPLATFORM := amd64 +endif + +ifeq ($(CPLATFORM), aarch64) + GOPLATFORM := arm64 +endif + +ifeq ($(CPLATFORM), arm64) + GOPLATFORM := arm64 + CPLATFORM := aarch64 +endif + +GOVER := 1.20.3 +LND := v0.16.2-beta +BITCOIND := 24.0.1 +VAULT := 1.12.2 + +# docker builds a builder image for the host platform if one isn't cached. +docker: + docker build -t $(IMG_NAME):latest --build-arg cplatform=$(CPLATFORM) \ + --build-arg goplatform=$(GOPLATFORM) --build-arg gover=$(GOVER) \ + --build-arg lnd=$(LND) --build-arg bitcoind=$(BITCOIND) \ + --build-arg vault=$(VAULT) -f Dockerfile.dev . + +# docker-itest runs itests in a docker container, then removes the container. +docker-itest: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make itest + +# docker-test runs unit tests in a docker container, then removes the container. +docker-test: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make test + +# docker-test-all runs unit and integration tests in a docker container, then +# removes the container. +docker-test-all: docker + docker run -t --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + make test-all + +# docker-shell opens a shell to a dockerized environment with all dependencies +# and also dlv installed for easy debugging, then removes the container. +docker-shell: docker + docker run -it --rm \ + --mount type=bind,source=$(CURDIR),target=/app $(IMG_NAME):latest \ + bash -l + +itest: + go install -race -buildvcs=false ./cmd/... && go test -v -count=1 -race -tags=itest -cover ./itest + +test: + go test -v -count=1 -race -cover ./... + +test-all: + go install -race -buildvcs=false ./cmd/... && go test -v -count=1 -race -tags=itest -cover ./... diff --git a/README.md b/README.md index f33dd9f..21400eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,171 @@ # lndsigner -lndsigner +`lndsigner` is a [remote signer](https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md) for [lnd](https://github.com/lightningnetwork/lnd). Currently, it can do the following: +- [x] store seeds for multiple nodes in [Hashicorp Vault](https://github.com/hashicorp/vault/) +- [x] securely generate new node seeds in vault +- [x] import seed/pass phrases +- [x] run unit tests +- [x] perform derivation and signing operations in a Vault plugin +- [x] export account list as JSON from vault +- [x] sign messages for network announcements +- [x] derive shared keys for peer connections +- [x] sign PSBTs for on-chain transactions, channel openings/closes, HTLC updates, etc. +- [x] run itests + +There is a [list of issues](https://github.com/nydig-oss/lndsigner/issues?q=is%3Aissue+is%3Aopen+milestone%3Amainnet-ready) that tracks TODO items needed for a mainnet release. + +## Usage + +Ensure you have `bitcoind`, `lnd`, and `vault` installed. Build `signer` using Go 1.18+ from this directory: + +``` +$ go install ./cmd/... +``` + +Create a directory `~/vault_plugins` and then move the `vault-plugin-lndsigner` binary to it. + +Start Vault from your home directory: + +``` +~$ vault server -dev -dev-root-token-id=root -dev-plugin-dir=./vault_plugins -log-level=trace +``` + +Enable the signer plugin: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault secrets enable --path=lndsigner vault-plugin-lndsigner +``` + +Create a new node: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault write lndsigner/lnd-nodes network=regtest + +``` + +Note that this should return a pubkey for the new node: + +``` +Key Value +--- ----- +node 03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 +``` + +You can also list the nodes as follows: + +``` +$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root vault read lndsigner/lnd-nodes +Key Value +--- ----- +03dc60dce282bb96abb4328c3e19640aa4f87defc400458322b80f0b73c2b14263 regtest +``` + +The value is the network specified above. Note that the Vault plugin is multi-tenant (supports multiple nodes), so you can add more nodes by writing as above. + +Create a directory `~/.lndsigner` (Linux) with a `signer.conf` similar to: + +``` +rpclisten=tcp://127.0.0.1:10021 +network=regtest +nodepubkey=*pubkey* +``` + +Use the pubkey from the node you created above. Note that on other platforms, the lndsigner directory you need to create may be different, such as: + +- `C:\Users\\AppData\Local\Lndsigner` on Windows +- `~/Library/Application Support/Lndsigner` on MacOS + +The rest of this README assumes you're working on Linux. Additional documentation for other platforms welcome. + +You'll need to provide a `tls.key` and `tls.cert` for the daemon. This allows it to accept TLS connections and lets `lnd` to authenticate that it's connecting to the correct signer, as configured below. For testing purposes, you can grab some that are auto-generated by a regtest instance of `lnd`. For deploy, you'll want your infrastructure to create these. + +Run the signer binary as follows: + +``` +~/.lndsigner$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root lndsignerd +``` + +Ensure you have a `bitcoind` instance running locally on regtest. Then, create a directory `~/.lnd-watchonly` with a `lnd.conf` similar to: + +``` +[bitcoin] +bitcoin.active=true +bitcoin.regtest=true +bitcoin.node=bitcoind + +[remotesigner] +remotesigner.enable=true +remotesigner.rpchost=127.0.0.1:10021 +remotesigner.tlscertpath=/home/*user*/.lndsigner/tls.cert +remotesigner.macaroonpath=any.macaroon +``` + +Note that `lnd` checks that the macaroon file deserializes correctly but lndsigner ignores the macaroon. + +Next, get the account list for the node (this works on Linux with `jq` installed): + +``` +~/.lnd-watchonly$ VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=root \ + vault read lndsigner/lnd-nodes/accounts node=*pubkey* | \ + tail -n 1 | sed s/acctList\\s*// | jq > accounts.json +``` + +You'll get an `accounts.json` file that starts like: + +``` +{ + "accounts": [ + { + "name": "default", + "address_type": "HYBRID_NESTED_WITNESS_PUBKEY_HASH", + "extended_public_key": "upub... +``` + +Now, run `lnd` in watch-only mode: + +``` +~/.lnd-watchonly$ lnd --lnddir=. +``` + +Create the watch-only wallet using the accounts exported by the signer: + +``` +~$ lncli createwatchonly .lndsigner/accounts.json +``` + +Now you can use your node as usual. Note that MuSig2 isn't supported yet. If you created multiple nodes in the vault, you can create a separate directory for each signer instance (`.lndsigner`) and each watch-only node (`.lnd`) and start each as above. + +You can also import a seedphrase, optionally protected by a passphrase, into the vault if you have a backup from an existing LND installation: +``` +~$ vault write lndsigner/lnd-nodes/import \ + seedphrase="abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet" \ + passphrase=weks1234 \ + network=regtest \ + node=03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf +``` + +Note that the `node` parameter is optional and used to check that the correct node pubkey is derived from the seed and network passed to the vault. You should get output like this if the command succeeds: + +``` +Key Value +--- ----- +node 03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf +``` + +Now you can use the imported key as before. + +## Testing +You can run unit tests and integration tests, together or separately, in Docker or on your host system. To run tests inside Docker, from the project directory, run one of: + + * `$ make docker-test` for unit tests + * `$ make docker-itest` for integration tests + * `$ make docker-test-all` for integration and unit tests + +To run tests directly on your development machine, you can use: + + * `$ make test` for unit tests + * `$ make itest` for integration tests + * `$ make test-all` for integration and unit tests + +Before running integration tests on your development machine, ensure you have all the required binaries (bitcoind, bitcoin-cli, lnd, lncli, vault). + +To get a shell on a container that can run tests, you can use `make docker-shell`. Then, you can `make test`, `make itest`, or `make test-all` inside the container, just like you would directly on the host system. diff --git a/cmd/lndsignerd/main.go b/cmd/lndsignerd/main.go new file mode 100644 index 0000000..0c029bf --- /dev/null +++ b/cmd/lndsignerd/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/jessevdk/go-flags" + "github.com/nydig-oss/lndsigner" +) + +func main() { + // Load the configuration, and parse any command line options. This + // function will also set up logging properly. + loadedConfig, err := lndsigner.LoadConfig() + if err != nil { + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + // Print error if not due to help request. + err = fmt.Errorf("failed to load config: %w", err) + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Help was requested, exit normally. + os.Exit(0) + } + + // Call the "real" main in a nested manner so the defers will properly + // be executed in the case of a graceful shutdown. + if err = lndsigner.Main( + loadedConfig, lndsigner.ListenerCfg{}, + ); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/vault-plugin-lndsigner/main.go b/cmd/vault-plugin-lndsigner/main.go new file mode 100644 index 0000000..1c9b5a0 --- /dev/null +++ b/cmd/vault-plugin-lndsigner/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/plugin" + "github.com/nydig-oss/lndsigner/vault" +) + +func main() { + logger := hclog.New(&hclog.LoggerOptions{}) + + apiClientMeta := &api.PluginAPIClientMeta{} + flags := apiClientMeta.FlagSet() + err := flags.Parse(os.Args[1:]) + if err != nil { + logger.Error("error parsing vault API client flags") + os.Exit(1) + } + + tlsConfig := apiClientMeta.GetTLSConfig() + tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) + + err = plugin.Serve(&plugin.ServeOpts{ + BackendFactoryFunc: vault.Factory, + TLSProviderFunc: tlsProviderFunc, + Logger: logger, + }) + if err != nil { + logger.Error("plugin shutting down", "error", err) + os.Exit(1) + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..0476c73 --- /dev/null +++ b/config.go @@ -0,0 +1,406 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "fmt" + "net" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + flags "github.com/jessevdk/go-flags" + "github.com/nydig-oss/lndsigner/vault" +) + +const ( + defaultConfigFilename = "signer.conf" + defaultTLSCertFilename = "tls.cert" + defaultTLSKeyFilename = "tls.key" + defaultRPCPort = 10009 + defaultRPCHost = "localhost" +) + +var ( + // DefaultSignerDir is the default directory where lndsignerd tries to + // find its configuration file and store its data. This is a directory + // in the user's application data, for example: + // C:\Users\\AppData\Local\Lndsigner on Windows + // ~/.lndsigner on Linux + // ~/Library/Application Support/Lndsigner on MacOS + DefaultSignerDir = btcutil.AppDataDir("lndsigner", false) + + // DefaultConfigFile is the default full path of lndsignerd's + // configuration file. + DefaultConfigFile = filepath.Join(DefaultSignerDir, defaultConfigFilename) + + defaultTLSCertPath = filepath.Join(DefaultSignerDir, defaultTLSCertFilename) + defaultTLSKeyPath = filepath.Join(DefaultSignerDir, defaultTLSKeyFilename) +) + +// Config defines the configuration options for lndsignerd. +// +// See LoadConfig for further details regarding the configuration +// loading+parsing process. +type Config struct { + SignerDir string `long:"signerdir" description:"The base directory that contains signer's data, logs, configuration file, etc."` + ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` + + TLSCertPath string `long:"tlscertpath" description:"Path to write the TLS certificate for lndsignerd's RPC services"` + TLSKeyPath string `long:"tlskeypath" description:"Path to write the TLS private key for lndsignerd's RPC services"` + + // We'll parse these 'raw' string arguments into real net.Addrs in the + // loadConfig function. We need to expose the 'raw' strings so the + // command line library can access them. + // Only the parsed net.Addrs should be used! + RawRPCListeners []string `long:"rpclisten" description:"Add an interface/port/socket to listen for RPC connections"` + RPCListeners []net.Addr + + Network string `long:"network" description:"The network for which the node was created in the vault. One of: 'testnet', 'simnet', 'regtest', 'signet'"` + + // ActiveNetParams contains parameters of the target chain. + ActiveNetParams chaincfg.Params + + // Node contains the node ID as a 66-character hex string. + NodePubKey string `long:"nodepubkey" description:"Node pubkey hex"` +} + +// DefaultConfig returns all default values for the Config struct. +func DefaultConfig() Config { + return Config{ + SignerDir: DefaultSignerDir, + ConfigFile: DefaultConfigFile, + TLSCertPath: defaultTLSCertPath, + TLSKeyPath: defaultTLSKeyPath, + Network: "regtest", + } +} + +// LoadConfig initializes and parses the config using a config file and command +// line options. +// +// The configuration proceeds as follows: +// 1. Start with a default config with sane settings +// 2. Pre-parse the command line to check for an alternative config file +// 3. Load configuration file overwriting defaults with any specified options +// 4. Parse CLI options and overwrite/add any specified options +func LoadConfig() (*Config, error) { + // Pre-parse the command line options to pick up an alternative config + // file. + preCfg := DefaultConfig() + if _, err := flags.Parse(&preCfg); err != nil { + return nil, err + } + + // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) + + // If the config file path has not been modified by the user, then we'll + // use the default config file path. However, if the user has modified + // their signerdir, then we should assume they intend to use the config + // file within it. + configFileDir := CleanAndExpandPath(preCfg.SignerDir) + configFilePath := CleanAndExpandPath(preCfg.ConfigFile) + switch { + // User specified --signerdir but no --configfile. Update the config + // file path to the lndsignerd config directory, but don't require it + // to exist. + case configFileDir != DefaultSignerDir && + configFilePath == DefaultConfigFile: + + configFilePath = filepath.Join( + configFileDir, defaultConfigFilename, + ) + + // User did specify an explicit --configfile, so we check that it does + // exist under that path to avoid surprises. + case configFilePath != DefaultConfigFile: + if !fileExists(configFilePath) { + return nil, fmt.Errorf("specified config file does "+ + "not exist in %s", configFilePath) + } + } + + // Next, load any additional configuration options from the file. + var configFileError error + cfg := preCfg + fileParser := flags.NewParser(&cfg, flags.Default) + err := flags.NewIniParser(fileParser).ParseFile(configFilePath) + if err != nil { + // If it's a parsing related error, then we'll return + // immediately, otherwise we can proceed as possibly the config + // file doesn't exist which is OK. + if _, ok := err.(*flags.IniError); ok { + return nil, err + } + + configFileError = err + } + + // Finally, parse the remaining command line options again to ensure + // they take precedence. + flagParser := flags.NewParser(&cfg, flags.Default) + if _, err := flagParser.Parse(); err != nil { + return nil, err + } + + // Make sure everything we just loaded makes sense. + cleanCfg, err := ValidateConfig( + cfg, fileParser, flagParser, + ) + if usageErr, ok := err.(*usageError); ok { + // The logging system might not yet be initialized, so we also + // write to stderr to make sure the error appears somewhere. + _, _ = fmt.Fprintln(os.Stderr, usageMessage) + signerLog.Warnf("Incorrect usage: %v", usageMessage) + + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + signerLog.Warnf("Error validating config: %v", usageErr.err) + + return nil, usageErr.err + } + if err != nil { + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + signerLog.Warnf("Error validating config: %v", err) + + return nil, err + } + + // Warn about missing config file only after all other configuration is + // done. This prevents the warning on help messages and invalid options. + // Note this should go directly before the return. + if configFileError != nil { + signerLog.Warnf("%v", configFileError) + } + + return cleanCfg, nil +} + +// usageError is an error type that signals a problem with the supplied flags. +type usageError struct { + err error +} + +// Error returns the error string. +// +// NOTE: This is part of the error interface. +func (u *usageError) Error() string { + return u.err.Error() +} + +// ValidateConfig check the given configuration to be sane. This makes sure no +// illegal values or combination of values are set. All file system paths are +// normalized. The cleaned up config is returned on success. +func ValidateConfig(cfg Config, fileParser, flagParser *flags.Parser) ( + *Config, error) { + + // If the provided lndsignerd directory is not the default, we'll + // modify the path to all of the files and directories that will live + // within it. + signerDir := CleanAndExpandPath(cfg.SignerDir) + if signerDir != DefaultSignerDir { + cfg.TLSCertPath = filepath.Join(signerDir, defaultTLSCertFilename) + cfg.TLSKeyPath = filepath.Join(signerDir, defaultTLSKeyFilename) + } + + funcName := "ValidateConfig" + mkErr := func(format string, args ...interface{}) error { + return fmt.Errorf(funcName+": "+format, args...) + } + makeDirectory := func(dir string) error { + err := os.MkdirAll(dir, 0700) + if err != nil { + // Show a nicer error message if it's because a symlink + // is linked to a directory that does not exist + // (probably because it's not mounted). + if e, ok := err.(*os.PathError); ok && os.IsExist(err) { + link, lerr := os.Readlink(e.Path) + if lerr == nil { + str := "is symlink %s -> %s mounted?" + err = fmt.Errorf(str, e.Path, link) + } + } + + str := "Failed to create lndsigner directory '%s': %v" + return mkErr(str, dir, err) + } + + return nil + } + + // As soon as we're done parsing configuration options, ensure all paths + // to directories and files are cleaned and expanded before attempting + // to use them later on. + cfg.TLSCertPath = CleanAndExpandPath(cfg.TLSCertPath) + cfg.TLSKeyPath = CleanAndExpandPath(cfg.TLSKeyPath) + + params, err := vault.GetNet(cfg.Network) + if err != nil { + return nil, err + } + cfg.ActiveNetParams = *params + + // Create the lndsignerd directory and all other sub-directories if + // they don't already exist. This makes sure that directory trees are + // also created for files that point to outside the signerdir. + dirs := []string{ + signerDir, filepath.Dir(cfg.TLSCertPath), + filepath.Dir(cfg.TLSKeyPath), + } + for _, dir := range dirs { + if err := makeDirectory(dir); err != nil { + return nil, err + } + } + + // At least one RPCListener is required. So listen on localhost per + // default. + if len(cfg.RawRPCListeners) == 0 { + addr := fmt.Sprintf("localhost:%d", defaultRPCPort) + cfg.RawRPCListeners = append(cfg.RawRPCListeners, addr) + } + + // Add default port to all RPC listener addresses if needed and remove + // duplicate addresses. + cfg.RPCListeners, err = NormalizeAddresses( + cfg.RawRPCListeners, strconv.Itoa(defaultRPCPort)) + if err != nil { + return nil, mkErr("error normalizing RPC listen addrs: %v", err) + } + + // All good, return the sanitized result. + return &cfg, nil +} + +// CleanAndExpandPath expands environment variables and leading ~ in the +// passed path, cleans the result, and returns it. +// This function is taken from https://github.com/btcsuite/btcd +func CleanAndExpandPath(path string) string { + if path == "" { + return "" + } + + // Expand initial ~ to OS specific home directory. + if strings.HasPrefix(path, "~") { + var homeDir string + u, err := user.Current() + if err == nil { + homeDir = u.HomeDir + } else { + homeDir = os.Getenv("HOME") + } + + path = strings.Replace(path, "~", homeDir, 1) + } + + // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, + // but the variables can still be expanded via POSIX-style $VARIABLE. + return filepath.Clean(os.ExpandEnv(path)) +} + +// NormalizeAddresses returns a new slice with all the passed addresses +// normalized with the given default port and all duplicates removed. +func NormalizeAddresses(addrs []string, defaultPort string) ([]net.Addr, + error) { + + result := make([]net.Addr, 0, len(addrs)) + seen := map[string]struct{}{} + + for _, addr := range addrs { + parsedAddr, err := ParseAddressString(addr, defaultPort) + if err != nil { + return nil, fmt.Errorf("parse address %s failed: %w", + addr, err) + } + + if _, ok := seen[parsedAddr.String()]; !ok { + result = append(result, parsedAddr) + seen[parsedAddr.String()] = struct{}{} + } + } + + return result, nil +} + +// verifyPort makes sure that an address string has both a host and a port. If +// there is no port found, the default port is appended. If the address is just +// a port, then we'll assume that the user is using the short cut to specify a +// localhost:port address. +func verifyPort(address string, defaultPort string) string { + host, port, err := net.SplitHostPort(address) + if err != nil { + // If the address itself is just an integer, then we'll assume + // that we're mapping this directly to a localhost:port pair. + // This ensures we maintain the legacy behavior. + if _, err := strconv.Atoi(address); err == nil { + return net.JoinHostPort("localhost", address) + } + + // Otherwise, we'll assume that the address just failed to + // attach its own port, so we'll use the default port. In the + // case of IPv6 addresses, if the host is already surrounded by + // brackets, then we'll avoid using the JoinHostPort function, + // since it will always add a pair of brackets. + if strings.HasPrefix(address, "[") { + return address + ":" + defaultPort + } + return net.JoinHostPort(address, defaultPort) + } + + // In the case that both the host and port are empty, we'll use the + // default port. + if host == "" && port == "" { + return ":" + defaultPort + } + + return address +} + +// ParseAddressString converts an address in string format to a net.Addr that is +// compatible with lndsignerd. +func ParseAddressString(strAddress string, defaultPort string) (net.Addr, + error) { + + var parsedNetwork, parsedAddr string + + // Addresses can either be in network://address:port format, + // network:address:port, address:port, or just port. We want to support + // all possible types. + if strings.Contains(strAddress, "://") { + parts := strings.Split(strAddress, "://") + parsedNetwork, parsedAddr = parts[0], parts[1] + } else if strings.Contains(strAddress, ":") { + parts := strings.Split(strAddress, ":") + parsedNetwork = parts[0] + parsedAddr = strings.Join(parts[1:], ":") + } + + // Only TCP and Unix socket addresses are valid. We can't use IP or + // UDP only connections for anything we do here. + switch parsedNetwork { + case "unix", "unixpacket": + return net.ResolveUnixAddr(parsedNetwork, parsedAddr) + + case "", "tcp", "tcp4", "tcp6": + return net.ResolveTCPAddr( + parsedNetwork, verifyPort(parsedAddr, defaultPort), + ) + + default: + return nil, fmt.Errorf("only TCP or unix socket "+ + "addresses are supported: %s", parsedAddr) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..feb52bb --- /dev/null +++ b/doc.go @@ -0,0 +1,6 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d9511a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,93 @@ +module github.com/nydig-oss/lndsigner + +require ( + github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 + github.com/btcsuite/btcd v0.23.1 + github.com/btcsuite/btcd/btcec/v2 v2.2.1 + github.com/btcsuite/btcd/btcutil v1.1.2 + github.com/btcsuite/btcd/btcutil/psbt v1.1.8 + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + github.com/hashicorp/go-hclog v1.3.1 + github.com/hashicorp/vault/api v1.8.0 + github.com/hashicorp/vault/sdk v0.6.0 + github.com/jessevdk/go-flags v1.4.0 + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 + github.com/stretchr/testify v1.8.0 + github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 + go.uber.org/zap v1.23.0 + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd + google.golang.org/grpc v1.47.0 + google.golang.org/protobuf v1.28.0 +) + +require ( + github.com/armon/go-metrics v0.3.9 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/evanphx/json-patch/v5 v5.5.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.4.5 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-version v1.2.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +// This replace is for https://github.com/advisories/GHSA-w73w-5m7g-f7qc +replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt v3.2.1+incompatible + +// This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27 +replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.8 + +// This replace is for +// https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1 +replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 + +// If you change this please also update .github/pull_request_template.md and +// docs/INSTALL.md. +go 1.20 + +retract v0.0.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..513dfec --- /dev/null +++ b/go.sum @@ -0,0 +1,447 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= +github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= +github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.1 h1:IB8cVQcC2X5mHbnfirLG5IZnkWYNTPlLZVrxUYSotbE= +github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E= +github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= +github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg= +github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.5.0 h1:bAmFiUJ+o0o2B4OiTFeE3MqCOtyo+jjPP9iZ0VRxYUc= +github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= +github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 h1:pSjQfW3vPtrOTcasTUKgCTQT7OGPPTTMVRrOfU6FJD8= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0/go.mod h1:xvb32K2keAc+R8DSFG2IwDcydK9DBQE+fGA5fsw6hSk= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= +github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.8.0 h1:7765sW1XBt+qf4XKIYE4ebY9qc/yi9V2/egzGSUNMZU= +github.com/hashicorp/vault/api v1.8.0/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/sdk v0.6.0 h1:6Z+In5DXHiUfZvIZdMx7e2loL1PPyDjA4bVh9ZTIAhs= +github.com/hashicorp/vault/sdk v0.6.0/go.mod h1:+DRpzoXIdMvKc88R4qxr+edwy/RvH5QK8itmxLiDHLc= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 h1:5Tbluzus3QxoAJx4IefGt1W0HQZW4nuMrVk684jI74Q= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/itest/gen_protos.sh b/itest/gen_protos.sh new file mode 100755 index 0000000..101ca2a --- /dev/null +++ b/itest/gen_protos.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +# Copyright (C) 2022-2023 Bottlepay and The Lightning Network Developers + +set -e + +# generate compiles the *.pb.go stubs from the *.proto files. +function generate() { + echo "Generating root gRPC server protos" + + PROTOS="walletunlocker.proto" + + # For each of the sub-servers, we then generate their protos, but a restricted + # set as they don't yet require REST proxies, or swagger docs. + for file in $PROTOS; do + DIRECTORY=$(dirname "${file}") + echo "Generating protos from ${file}, into ${DIRECTORY}" + + # Generate the protos. + protoc -I/usr/local/include -I. \ + --go_out . --go_opt paths=source_relative \ + --go-grpc_out . --go-grpc_opt paths=source_relative \ + "${file}" + done +} + +# format formats the *.proto files with the clang-format utility. +function format() { + find . -name "*.proto" -print0 | xargs -0 clang-format --style=file -i +} + +# Compile and format the itest package. +pushd itest +format +generate +popd diff --git a/itest/itest_context_test.go b/itest/itest_context_test.go new file mode 100644 index 0000000..79419ed --- /dev/null +++ b/itest/itest_context_test.go @@ -0,0 +1,397 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/fs" + "net" + "os" + "os/exec" + "path" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +// testContext manages the test environment. +type testContext struct { + t *testing.T + log *zap.SugaredLogger + cancel context.CancelFunc + + tmpRoot string + + vaultPort string + vaultCmd *exec.Cmd + vaultClient *api.Logical + + bitcoinDir string + bitcoinRPC string + bitcoinZB *net.TCPAddr + bitcoinZT *net.TCPAddr + bitcoindCmd *exec.Cmd + bitcoindClient *api.Logical + blocksMined uint32 + + lndPath string + lndSignerPath string + lncliPath string + bitcoincliPath string + + lnds []*lndHarness +} + +//newTestContext creates a new test context. +func newTestContext(t *testing.T) *testContext { + t.Helper() + + tctx := &testContext{ + t: t, + log: zaptest.NewLogger(t).Sugar(), + lnds: make([]*lndHarness, 0, 3), + } + + ctx, cancel := context.WithCancel(context.Background()) + tctx.cancel = cancel + + // Create temp directory for test context. + tmpRoot, err := os.MkdirTemp("", "lndsigner-itest") + require.NoError(t, err) + tctx.tmpRoot = tmpRoot + + // Get binary paths + bitcoindPath, err := exec.LookPath("bitcoind") + require.NoError(t, err) + + tctx.lndPath, err = exec.LookPath("lnd") + require.NoError(tctx.t, err) + + tctx.lndSignerPath, err = exec.LookPath("lndsignerd") + require.NoError(tctx.t, err) + + tctx.lncliPath, err = exec.LookPath("lncli") + require.NoError(tctx.t, err) + + tctx.bitcoincliPath, err = exec.LookPath("bitcoin-cli") + require.NoError(tctx.t, err) + + // Start bitcoind + tctx.bitcoinDir = path.Join(tctx.tmpRoot, "bitcoin") + err = os.Mkdir(tctx.bitcoinDir, fs.ModeDir|0700) + require.NoError(t, err) + + tctx.bitcoinRPC = newPortString() + tctx.bitcoinZB = newPort() + tctx.bitcoinZT = newPort() + + tctx.bitcoindCmd = exec.CommandContext(ctx, bitcoindPath, "-server=1", + "-datadir="+tctx.bitcoinDir, "-listen=0", "-txindex=1", + "-regtest=1", "-rpcuser=user", "-rpcpassword=password", + "-rpcport="+tctx.bitcoinRPC, + "-zmqpubrawblock=tcp://"+tctx.bitcoinZB.String(), + "-zmqpubrawtx=tcp://"+tctx.bitcoinZT.String()) + + go waitProc(tctx.bitcoindCmd) + + // Wait for bitcoind to start. Log file is ~6300 bytes when regtest + // bitcoind with our options is started. + waitFile( + t, + path.Join(tctx.bitcoinDir, "/regtest/debug.log"), + "init message: Done loading", + ) + + // Mine blocks to give us funds and activate soft forks. + go func() { + tctx.bitcoinCli("createwallet", "default") + tctx.mine(1000) + }() + + // Start vault. + vaultPath, err := exec.LookPath("vault") + require.NoError(t, err) + + pluginPath, err := exec.LookPath("vault-plugin-lndsigner") + require.NoError(t, err) + + pluginDir := path.Join(tmpRoot, "vault_plugins") + err = os.Mkdir(pluginDir, fs.ModeDir|0700) + require.NoError(t, err) + + mustCopyFile(pluginPath, path.Join(pluginDir, "vault-plugin-lndsigner"), + 0700) + + tctx.vaultPort = newPortString() + tctx.vaultCmd = exec.CommandContext(ctx, vaultPath, "server", "-dev", + "-dev-root-token-id=root", "-dev-plugin-dir="+pluginDir, + "-dev-listen-address=127.0.0.1:"+tctx.vaultPort) + + go waitProc(tctx.vaultCmd) + + vaultClientConf := api.DefaultConfig() + vaultClientConf.Address = "http://127.0.0.1:" + tctx.vaultPort + + vaultClient, err := api.NewClient(vaultClientConf) + require.NoError(t, err) + + vaultClient.SetToken("root") + + tctx.vaultClient = vaultClient.Logical() + + vaultSys := vaultClient.Sys() + err = vaultSys.Mount("lndsigner", &api.MountInput{ + Type: "vault-plugin-lndsigner", + }) + require.NoError(t, err) + + return tctx +} + +// bitcoinCli sends a command to the test context's bitcoind. +func (tctx *testContext) bitcoinCli(args ...string) map[string]interface{} { + tctx.t.Helper() + + bitcoinCliCmd := exec.CommandContext(context.Background(), + tctx.bitcoincliPath, + append([]string{"-datadir=" + tctx.bitcoinDir, + "-rpcport=" + tctx.bitcoinRPC, "-rpcuser=user", + "-rpcpassword=password", "-rpcwaittimeout=5"}, + args...)...) + + stdErrBuf := bytes.NewBuffer(make([]byte, 0)) + bitcoinCliCmd.Stderr = stdErrBuf + + stdOutBuf := bytes.NewBuffer(make([]byte, 0)) + bitcoinCliCmd.Stdout = stdOutBuf + + err := bitcoinCliCmd.Start() + require.NoError(tctx.t, err) + + // If there's an error on exit, show stderr. + err = bitcoinCliCmd.Wait() + require.NoError(tctx.t, err, string(stdErrBuf.Bytes())) + + stdout := string(stdOutBuf.Bytes()) + + // sendtoaddress only returns a txid on success. In this case, the + // first argument is "-named". + if len(args) > 1 && args[1] == "sendtoaddress" { + return map[string]interface{}{ + "txid": stdout[:64], + } + } + + // If we're stopping, we won't get JSON back. + if args[0] == "stop" { + return nil + } + + // If there's an error parsing the JSON, show stdout to see the issue. + resp := make(map[string]interface{}) + err = json.Unmarshal([]byte(stdout), &resp) + require.NoError(tctx.t, err, stdout) + + return resp +} + +// Close cleans up the test context. +func (tctx *testContext) Close() { + tctx.t.Helper() + + for _, lnd := range tctx.lnds { + lnd.Close() + } + + _ = tctx.bitcoinCli("stop") + _ = tctx.vaultCmd.Process.Signal(os.Interrupt) + + tctx.cancel() + + os.RemoveAll(tctx.tmpRoot) +} + +// addNode adds a new LND node to the test context, complete with its own +// lndsignerd. reqPath can be used to specify create or import, reqData must +// have a network and optional seed/passphrase, and unixSocket may be used to +// specify that a UNIX socket should be used to communicate between LND and +// lndsignerd. +func (tctx *testContext) addNode(reqPath string, + reqData map[string]interface{}, unixSocket bool) string { + + tctx.t.Helper() + + resp, err := tctx.vaultClient.Write(reqPath, reqData) + require.NoError(tctx.t, err) + + pubKey, ok := resp.Data["node"].(string) + require.True(tctx.t, ok) + require.Equal(tctx.t, 66, len(pubKey)) + + lnd := &lndHarness{ + tctx: tctx, + idPubKey: pubKey, + unixSocket: unixSocket, + } + + lnd.Start() + + tctx.lnds = append(tctx.lnds, lnd) + + return pubKey +} + +// mine mines the specified number of blocks, and ensures `getblockchaininfo` +// returns the correct number. +func (tctx *testContext) mine(blocks int) { + tctx.t.Helper() + + // Ensure all TXs are accepted to mempool. + time.Sleep(mineDelay) + + require.Equal(tctx.t, blocks, + len(tctx.bitcoinCli("-generate", fmt.Sprintf( + "%d", blocks))["blocks"].([]interface{}))) + + reqMined := int(atomic.AddUint32(&tctx.blocksMined, uint32(blocks))) + + var ( + mined int + resp map[string]interface{} + ) + + for mined < reqMined || mined < 1000 { + time.Sleep(waitDelay) + + resp = tctx.bitcoinCli("getblockchaininfo") + mined = int(resp["blocks"].(float64)) + } +} + +// waitForSync ensures that each LND has caught up to the blocks that have been +// mined on bitcoind. +func (tctx *testContext) waitForSync() { + tctx.t.Helper() + + // Ensure everyone has time to catch up. + time.Sleep(mineDelay) + + blocks := int(atomic.LoadUint32(&tctx.blocksMined)) + + var resp map[string]interface{} + for _, lnd := range tctx.lnds { + synced := 0 + + for synced != blocks { + time.Sleep(waitDelay) + + resp = lnd.Lncli("getinfo") + require.Equal(tctx.t, resp["identity_pubkey"].(string), + lnd.idPubKey) + + synced = int(resp["block_height"].(float64)) + } + } +} + +// waitForGraphSync ensures that each LND has a synchronized graph. +func (tctx *testContext) waitForGraphSync() { + tctx.t.Helper() + + var ( + nodes, chans int + synced bool + resp map[string]interface{} + ) + + for !synced { + synced = true + + for _, lnd := range tctx.lnds { + + resp = lnd.Lncli("getnetworkinfo") + + gotNodes := int(resp["num_nodes"].(float64)) + if gotNodes != nodes { + synced = false + + if gotNodes > nodes { + nodes = gotNodes + } + } + + gotChans := int(resp["num_channels"].(float64)) + if gotChans != chans { + synced = false + + if gotChans > chans { + chans = gotChans + } + } + } + + time.Sleep(mineDelay) + } + + // One final round of `describegraph` to ensure we've updated our + // routing information. + for _, lnd := range tctx.lnds { + lnd.Lncli("describegraph") + } +} + +// testEach runs a function for each LND instance, in parallel. +func (tctx *testContext) testEach(test func(lnd *lndHarness)) { + tctx.t.Helper() + + var wg sync.WaitGroup + for _, lnd := range tctx.lnds { + innerLnd := lnd + + wg.Add(1) + go func() { + defer wg.Done() + + test(innerLnd) + }() + } + + wg.Wait() +} + +// testEachPair runs a function for each pair of LNDs in parallel, avoiding +// testing an LND instance with itself. +func (tctx *testContext) testEachPair(test func(lnd1, lnd2 *lndHarness)) { + tctx.t.Helper() + + var wg sync.WaitGroup + for i, lnd1 := range tctx.lnds { + for j, lnd2 := range tctx.lnds { + innerLnd1 := lnd1 + innerLnd2 := lnd2 + + if i == j { + continue + } + + wg.Add(1) + go func() { + defer wg.Done() + + test(innerLnd1, innerLnd2) + }() + } + } + + wg.Wait() +} diff --git a/itest/itest_lndharness_test.go b/itest/itest_lndharness_test.go new file mode 100644 index 0000000..16c40f7 --- /dev/null +++ b/itest/itest_lndharness_test.go @@ -0,0 +1,396 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/nydig-oss/lndsigner" + "github.com/nydig-oss/lndsigner/itest" + "io/fs" + "math/big" + "net" + "os" + "os/exec" + "path" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// lndHarness manages a single lndsignerd-backed instance of LND. +type lndHarness struct { + tctx *testContext + idPubKey string + + unixSocket bool + + cancel context.CancelFunc + + lndSignerCmd *exec.Cmd + + lndDir string + lncliPath string + rpc string + p2p string + lndCmd *exec.Cmd + + startChan chan struct{} +} + +// Start takes the initial configuration (tctx, idPubKey, and unixSocket) and +// starts lndsignerd and LND. +func (l *lndHarness) Start() { + l.tctx.t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + l.cancel = cancel + + // Make a channel. We'll close this channel after the node is fully + // started to signal clients it's safe to make calls. + l.startChan = make(chan struct{}) + + // Start lndsignerd. + l.lndDir = path.Join(l.tctx.tmpRoot, fmt.Sprintf("lnd%s", l.idPubKey)) + err := os.Mkdir(l.lndDir, fs.ModeDir|0700) + require.NoError(l.tctx.t, err) + + keyPath := path.Join(l.lndDir, "signer.key") + certPath := path.Join(l.lndDir, "signer.cert") + + mustGenCertPair(l.tctx.t, certPath, keyPath) + + macPath := path.Join(l.lndDir, "dummy.macaroon") + + mustGenMacaroon(l.tctx.t, macPath) + + signerAddr := "127.0.0.1:" + newPortString() + fullSignerAddr := "tcp://" + signerAddr + + if l.unixSocket { + signerAddr = "unix://" + path.Join(l.tctx.tmpRoot, l.idPubKey+".socket") + fullSignerAddr = signerAddr + } + + l.lndSignerCmd = exec.CommandContext(ctx, l.tctx.lndSignerPath, + "--rpclisten="+fullSignerAddr, "--nodepubkey="+l.idPubKey, + "--tlscertpath="+certPath, "--tlskeypath="+keyPath, + "--network=regtest", + ) + + l.lndSignerCmd.Env = append(l.lndSignerCmd.Env, + "VAULT_ADDR=http://127.0.0.1:"+l.tctx.vaultPort, + "VAULT_TOKEN=root", + ) + + go waitProc(l.lndSignerCmd) + + // Start lnd. + acctsResp, err := l.tctx.vaultClient.ReadWithData( + "lndsigner/lnd-nodes/accounts", + map[string][]string{ + "node": []string{l.idPubKey}, + }, + ) + require.NoError(l.tctx.t, err) + + acctList, ok := acctsResp.Data["acctList"].(string) + require.True(l.tctx.t, ok) + + accounts, err := lndsigner.GetAccounts(acctList) + require.NoError(l.tctx.t, err) + + grpcAccounts := make([]*itest.WatchOnlyAccount, 0, + len(accounts)) + + for derPath, xPub := range accounts { + grpcAccounts = append(grpcAccounts, + &itest.WatchOnlyAccount{ + Purpose: derPath[0], + CoinType: derPath[1], + Account: derPath[2], + Xpub: xPub, + }) + } + + l.rpc = newPortString() + l.p2p = newPortString() + + l.lndCmd = exec.CommandContext(ctx, l.tctx.lndPath, + "--lnddir="+l.lndDir, "--norest", "--listen="+l.p2p, + "--rpclisten="+l.rpc, "--trickledelay=1", "--bitcoin.active", + "--bitcoin.regtest", "--bitcoin.node=bitcoind", + "--bitcoind.rpcuser=user", "--bitcoind.rpcpass=password", + "--bitcoind.rpchost=127.0.0.1:"+l.tctx.bitcoinRPC, + "--bitcoind.zmqpubrawblock=tcp://"+l.tctx.bitcoinZB.String(), + "--bitcoind.zmqpubrawtx=tcp://"+l.tctx.bitcoinZT.String(), + "--remotesigner.enable", + "--remotesigner.rpchost="+signerAddr, + "--remotesigner.tlscertpath="+certPath, + "--remotesigner.macaroonpath="+macPath, + ) + + go waitProc(l.lndCmd) + + go func() { + // Ensure we wait until lnd has started its wallet unlocker + // server. + waitFile( + l.tctx.t, + path.Join(l.lndDir, "/logs/bitcoin/regtest/lnd.log"), + "Waiting for wallet encryption password", + ) + + // Initialize with the accounts information. We use gRPC for this + // because lncli doesn't run non-interactively, so we have to send a + // wallet password over gRPC. + tlsCreds, err := credentials.NewClientTLSFromFile( + path.Join(l.lndDir, "tls.cert"), "") + require.NoError(l.tctx.t, err) + + tlsCredsOption := grpc.WithTransportCredentials(tlsCreds) + unlockerConn, err := grpc.Dial("127.0.0.1:"+l.rpc, tlsCredsOption) + require.NoError(l.tctx.t, err) + + unlocker := itest.NewWalletUnlockerClient(unlockerConn) + _, err = unlocker.InitWallet( + ctx, + &itest.InitWalletRequest{ + WalletPassword: []byte("weks1234"), + WatchOnly: &itest.WatchOnly{ + Accounts: grpcAccounts, + }, + }, + ) + require.NoError(l.tctx.t, err) + + // Wait for lnd to start the main gRPC server. Log file is + // ~7300 bytes when the RPC server is started. + // TODO(aakselrod): maybe check log file for + // "Auto peer bootstrapping" instead? + waitFile( + l.tctx.t, + path.Join(l.lndDir, "/logs/bitcoin/regtest/lnd.log"), + "Auto peer bootstrapping", + ) + + // Signal any waiting clients that lnd should be initialized + close(l.startChan) + }() +} + +// Close cleans up LND and lndsignerd. +func (l *lndHarness) Close() { + l.tctx.t.Helper() + + _ = l.Lncli("stop") + + l.cancel() +} + +// LnCli calls lncli against the harness' LND instance. +func (l *lndHarness) Lncli(args ...string) map[string]interface{} { + l.tctx.t.Helper() + + <-l.startChan + + lnCliCmd := exec.CommandContext(context.Background(), l.tctx.lncliPath, + append([]string{"--lnddir=" + l.lndDir, + "--rpcserver=127.0.0.1:" + l.rpc, + "--network=regtest", + "--tlscertpath=./testdata/tls.cert"}, args...)...) + + outBuf := bytes.NewBuffer(make([]byte, 0)) + lnCliCmd.Stdout = outBuf + + errBuf := bytes.NewBuffer(make([]byte, 0)) + lnCliCmd.Stderr = errBuf + + err := lnCliCmd.Start() + require.NoError(l.tctx.t, err) + + err = lnCliCmd.Wait() + require.NoError(l.tctx.t, err, + fmt.Sprintf("lncli (args %+v) failed:\n%s\n%s", args, + errBuf.Bytes(), outBuf.Bytes())) + + stdout := string(outBuf.Bytes()) + + // If we're stopping, we won't get JSON back. + if args[0] == "stop" { + return nil + } + + resp := make(map[string]interface{}) + err = json.Unmarshal([]byte(stdout), &resp) + require.NoError(l.tctx.t, err) + + return resp +} + +// waitFile waits for a log file to contain the requested string. +func waitFile(t *testing.T, file, waitStr string) { + var ( + err error + logBytes []byte + ) + + for { + time.Sleep(waitDelay) + + logBytes, err = os.ReadFile(file) + if err != nil { + require.True(t, os.IsNotExist(err), err) + } + + if bytes.Contains(logBytes, []byte(waitStr)) { + break + } + } +} + +// waitProc launches a goroutine to wait for a long-running program, such as +// vault, bitcoind, lndsignerd, or lnd, to stop. If the program returns an +// exit error, the program's entire stderr and stdout are logged. +func waitProc(cmd *exec.Cmd) { + output, err := cmd.CombinedOutput() + if err != nil && err.Error() != "signal: killed" { + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + config.EncoderConfig.EncodeCaller = nil + logger := zap.Must(config.Build()).Sugar() + logger.Warnw( + "WARNING: Service exited with error", + "cmd", cmd.Path, + "err", err, + "stdout/stderr", string(output), + ) + } +} + +// newPort finds an open TCP port to listen on. +func newPort() *net.TCPAddr { + lis, err := net.Listen("tcp", "127.0.0.1:") + if err != nil { + panic(err) + } + defer lis.Close() + return lis.Addr().(*net.TCPAddr) +} + +// newPortString finds an open TCP port to listen on and returns the port +// number as a string. +func newPortString() string { + return fmt.Sprintf("%d", newPort().Port) +} + +// mustCopyFile copies a file and panics on error. +func mustCopyFile(src, dst string, mode os.FileMode) { + fileBytes, err := os.ReadFile(src) + if err != nil { + panic(err) + } + + err = os.WriteFile(dst, fileBytes, mode) + if err != nil { + panic(err) + } +} + +func mustGenCertPair(t *testing.T, certFile, keyFile string) { + now := time.Now() + + // Generate a random serial number. + serialNumber, err := rand.Int(rand.Reader, + new(big.Int).Lsh(big.NewInt(1), 128)) + require.NoError(t, err) + + // Generate a private key for the certificate. + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // Construct the certificate template. + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"test"}, + CommonName: "localhost", + }, + NotBefore: now.Add(-time.Hour * 24), + NotAfter: now.Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | + x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv6loopback, net.IP{127, 0, 0, 1}}, + } + + derBytes, err := x509.CreateCertificate( + rand.Reader, &template, + &template, &priv.PublicKey, priv, + ) + require.NoError(t, err) + + certBuf := &bytes.Buffer{} + require.NoError(t, pem.Encode( + certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}, + )) + + keybytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + + keyBuf := &bytes.Buffer{} + require.NoError(t, pem.Encode( + keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}, + )) + + require.NoError(t, os.WriteFile(certFile, certBuf.Bytes(), 0644)) + + require.NoError(t, os.WriteFile(keyFile, keyBuf.Bytes(), 0600)) +} + +func mustGenMacaroon(t *testing.T, macPath string) { + var macData []byte + + macData = append(macData, 2) // version + macData = append(macData, 1) // field type loc + macData = append(macData, 1) // length + macData = append(macData, 65) // loc ("A") + macData = append(macData, 2) // field type id + macData = append(macData, 1) // length + macData = append(macData, 65) // id ("A") + macData = append(macData, 0) // end of seq + macData = append(macData, 0) // end of seq + macData = append(macData, 6) // field type sig + macData = append(macData, 32) // length + macData = append(macData, // sig (32 * "A") + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + 65, 65, 65, 65, 65, 65, 65, 65, + ) + macData = append(macData, 0) // end of seq + + require.NoError(t, os.WriteFile(macPath, macData, 0644)) +} diff --git a/itest/lndsigner_test.go b/itest/lndsigner_test.go new file mode 100644 index 0000000..937b3f8 --- /dev/null +++ b/itest/lndsigner_test.go @@ -0,0 +1,198 @@ +//go:build itest +// +build itest + +package itest_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + lndCreatePath = "lndsigner/lnd-nodes" + lndImportPath = "lndsigner/lnd-nodes/import" + + mineDelay = 500 * time.Millisecond + waitDelay = 100 * time.Millisecond +) + +// TestIntegration function runs end-to-end tests using all of the required +// binaries. +// +// This assumes we've got `lnd`, `lncli`, `vault`, `bitcoind`, `bitcoin-cli`, +// and the binaries produced by this package installed and available in the +// executable path. These are installed in CI by the GitHub workflow, but +// for now need to be installed manually in the dev environment. +// +// TODO(aakselrod): add Dockerfile to dockerize itests locally. +func TestIntegration(t *testing.T) { + tctx := newTestContext(t) + defer tctx.Close() + + // Create a randomly-initialized node for which nobody's ever seen the + // keys. + + _ = tctx.addNode(lndCreatePath, map[string]interface{}{ + "network": "regtest", + }, true) + + // Import node without passphrase. + lnd2PK := "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6" + require.Equal(t, tctx.addNode(lndImportPath, map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "passphrase": "", + "node": lnd2PK, + }, true), lnd2PK) + + // Import node with passphrase. + lnd3PK := "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf" + require.Equal(t, tctx.addNode(lndImportPath, map[string]interface{}{ + "network": "testnet", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": lnd3PK, + }, false), lnd3PK) + + tctx.waitForSync() + + t.Run("fund each lnd with a p2tr address", tctx.testFundLnds) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep p2tr to p2wkh address", tctx.testSweepToP2WKH) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep p2wkh to np2wkh address", tctx.testSweepToNP2WKH) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("sweep np2wkh to p2tr address", tctx.testSweepToP2TR) + + tctx.mine(1) + tctx.waitForSync() + + t.Run("open channel lnd1 to lnd2", func(t *testing.T) { + _ = tctx.lnds[0].Lncli("connect", + tctx.lnds[1].idPubKey+"@127.0.0.1:"+tctx.lnds[1].p2p) + + resp := tctx.lnds[0].Lncli("openchannel", tctx.lnds[1].idPubKey, + "10000000", "5000000") + require.Equal(t, 64, len(resp["funding_txid"].(string))) + }) + + t.Run("open channel lnd2 to lnd3", func(t *testing.T) { + _ = tctx.lnds[1].Lncli("connect", + tctx.lnds[2].idPubKey+"@127.0.0.1:"+tctx.lnds[2].p2p) + + resp := tctx.lnds[1].Lncli("openchannel", tctx.lnds[2].idPubKey, + "10000000", "5000000") + require.Equal(t, 64, len(resp["funding_txid"].(string))) + }) + + // Confirm our channels. + tctx.mine(5) + tctx.waitForSync() + tctx.mine(5) + tctx.waitForSync() + tctx.waitForGraphSync() + + t.Run("sign and verify messages", tctx.testEachSignVerifyEachOther) + + t.Run("each lnd pays every other lnd", tctx.testEachPaysEachOther) +} + +// testFundLnds funds each lnd instance in the test context with 1 BTC into +// a new P2TR address. +func (tctx *testContext) testFundLnds(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2tr") + address := resp["address"].(string) + + tctx.bitcoinCli("-named", "sendtoaddress", + "address="+address, "amount=1", "fee_rate=25") + }) +} + +// testSweepToP2WKH sweeps all of the nodes' on-chain funds into P2WKH +// addresses +func (tctx *testContext) testSweepToP2WKH(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2wkh") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testSweepToNP2WKH sweeps all of the nodes' on-chain funds into NP2WKH +// addresses +func (tctx *testContext) testSweepToNP2WKH(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "np2wkh") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testSweepToP2TR sweeps all of the nodes' on-chain funds into P2TR +// addresses +func (tctx *testContext) testSweepToP2TR(t *testing.T) { + tctx.testEach(func(lnd *lndHarness) { + resp := lnd.Lncli("newaddress", "p2tr") + address := resp["address"].(string) + + resp = lnd.Lncli("sendcoins", "--sweepall", + address) + require.Equal(t, 64, len(resp["txid"].(string))) + + tctx.log.Infow("swept", "node", lnd.idPubKey) + }) +} + +// testEachPaysEachOther sends LN payments from each LND to each other LND, +// testing both direct and chained payments. +func (tctx *testContext) testEachPaysEachOther(t *testing.T) { + tctx.testEachPair(func(lnd1, lnd2 *lndHarness) { + resp := lnd1.Lncli("addinvoice", "5000") + invoice := resp["payment_request"].(string) + + resp = lnd2.Lncli("payinvoice", "--timeout=10s", "--json", + "-f", invoice) + require.Equal(t, resp["status"].(string), "SUCCEEDED") + + tctx.log.Infow("payment", "src", lnd2.idPubKey, + "dst", lnd1.idPubKey) + }) +} + +// testEachSignVerifyEachOther signs a message from each LND to each other LND, +// verifying the message on the second LND. +func (tctx *testContext) testEachSignVerifyEachOther(t *testing.T) { + tctx.testEachPair(func(lnd1, lnd2 *lndHarness) { + message := lnd1.idPubKey + " to " + lnd2.idPubKey + + resp := lnd1.Lncli("signmessage", message) + sig := resp["signature"].(string) + + resp = lnd2.Lncli("verifymessage", message, sig) + require.True(t, resp["valid"].(bool)) + + tctx.log.Info(message) + }) +} diff --git a/itest/walletunlocker.pb.go b/itest/walletunlocker.pb.go new file mode 100644 index 0000000..0202fb9 --- /dev/null +++ b/itest/walletunlocker.pb.go @@ -0,0 +1,423 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: walletunlocker.proto + +package itest + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InitWalletRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //wallet_password is the passphrase that should be used to encrypt the + //wallet. This MUST be at least 8 chars in length. After creation, this + //password is required to unlock the daemon. When using REST, this field + //must be encoded as base64. + WalletPassword []byte `protobuf:"bytes,1,opt,name=wallet_password,json=walletPassword,proto3" json:"wallet_password,omitempty"` + // + //watch_only is the third option of initializing a wallet: by importing + //account xpubs only and therefore creating a watch-only wallet that does not + //contain any private keys. That means the wallet won't be able to sign for + //any of the keys and _needs_ to be run with a remote signer that has the + //corresponding private keys and can serve signing RPC requests. + WatchOnly *WatchOnly `protobuf:"bytes,9,opt,name=watch_only,json=watchOnly,proto3" json:"watch_only,omitempty"` +} + +func (x *InitWalletRequest) Reset() { + *x = InitWalletRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitWalletRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitWalletRequest) ProtoMessage() {} + +func (x *InitWalletRequest) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitWalletRequest.ProtoReflect.Descriptor instead. +func (*InitWalletRequest) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{0} +} + +func (x *InitWalletRequest) GetWalletPassword() []byte { + if x != nil { + return x.WalletPassword + } + return nil +} + +func (x *InitWalletRequest) GetWatchOnly() *WatchOnly { + if x != nil { + return x.WatchOnly + } + return nil +} + +type InitWalletResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The binary serialized admin macaroon that can be used to access the daemon + //after creating the wallet. If the stateless_init parameter was set to true, + //this is the ONLY copy of the macaroon and MUST be stored safely by the + //caller. Otherwise a copy of this macaroon is also persisted on disk by the + //daemon, together with other macaroon files. + AdminMacaroon []byte `protobuf:"bytes,1,opt,name=admin_macaroon,json=adminMacaroon,proto3" json:"admin_macaroon,omitempty"` +} + +func (x *InitWalletResponse) Reset() { + *x = InitWalletResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitWalletResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitWalletResponse) ProtoMessage() {} + +func (x *InitWalletResponse) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitWalletResponse.ProtoReflect.Descriptor instead. +func (*InitWalletResponse) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{1} +} + +func (x *InitWalletResponse) GetAdminMacaroon() []byte { + if x != nil { + return x.AdminMacaroon + } + return nil +} + +type WatchOnly struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The list of accounts to import. There _must_ be an account for all of lnd's + //main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the + //coin type is always 0, even for testnet/regtest) and lnd's internal key + //scope (m/1017'/'/'), where account is the key family as + //defined in `keychain/derivation.go` (currently indices 0 to 9). + Accounts []*WatchOnlyAccount `protobuf:"bytes,3,rep,name=accounts,proto3" json:"accounts,omitempty"` +} + +func (x *WatchOnly) Reset() { + *x = WatchOnly{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WatchOnly) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchOnly) ProtoMessage() {} + +func (x *WatchOnly) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchOnly.ProtoReflect.Descriptor instead. +func (*WatchOnly) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{2} +} + +func (x *WatchOnly) GetAccounts() []*WatchOnlyAccount { + if x != nil { + return x.Accounts + } + return nil +} + +type WatchOnlyAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //Purpose is the first number in the derivation path, must be either 49, 84 + //or 1017. + Purpose uint32 `protobuf:"varint,1,opt,name=purpose,proto3" json:"purpose,omitempty"` + // + //Coin type is the second number in the derivation path, this is _always_ 0 + //for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on + //testnet or regtest. + CoinType uint32 `protobuf:"varint,2,opt,name=coin_type,json=coinType,proto3" json:"coin_type,omitempty"` + // + //Account is the third number in the derivation path. For purposes 49 and 84 + //at least the default account (index 0) needs to be created but optional + //additional accounts are allowed. For purpose 1017 there needs to be exactly + //one account for each of the key families defined in `keychain/derivation.go` + //(currently indices 0 to 9) + Account uint32 `protobuf:"varint,3,opt,name=account,proto3" json:"account,omitempty"` + // + //The extended public key at depth 3 for the given account. + Xpub string `protobuf:"bytes,4,opt,name=xpub,proto3" json:"xpub,omitempty"` +} + +func (x *WatchOnlyAccount) Reset() { + *x = WatchOnlyAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_walletunlocker_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WatchOnlyAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchOnlyAccount) ProtoMessage() {} + +func (x *WatchOnlyAccount) ProtoReflect() protoreflect.Message { + mi := &file_walletunlocker_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WatchOnlyAccount.ProtoReflect.Descriptor instead. +func (*WatchOnlyAccount) Descriptor() ([]byte, []int) { + return file_walletunlocker_proto_rawDescGZIP(), []int{3} +} + +func (x *WatchOnlyAccount) GetPurpose() uint32 { + if x != nil { + return x.Purpose + } + return 0 +} + +func (x *WatchOnlyAccount) GetCoinType() uint32 { + if x != nil { + return x.CoinType + } + return 0 +} + +func (x *WatchOnlyAccount) GetAccount() uint32 { + if x != nil { + return x.Account + } + return 0 +} + +func (x *WatchOnlyAccount) GetXpub() string { + if x != nil { + return x.Xpub + } + return "" +} + +var File_walletunlocker_proto protoreflect.FileDescriptor + +var file_walletunlocker_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x22, 0x6d, 0x0a, + 0x11, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2f, 0x0a, 0x0a, 0x77, + 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, + 0x79, 0x52, 0x09, 0x77, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x3b, 0x0a, 0x12, + 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x61, 0x63, 0x61, + 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x61, 0x64, 0x6d, 0x69, + 0x6e, 0x4d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, 0x40, 0x0a, 0x09, 0x57, 0x61, 0x74, + 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x77, 0x0a, 0x10, 0x57, + 0x61, 0x74, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x69, + 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63, 0x6f, + 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x78, 0x70, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x78, 0x70, 0x75, 0x62, 0x32, 0x53, 0x0a, 0x0e, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x55, 0x6e, + 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0a, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x12, 0x18, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x57, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, + 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x69, 0x74, 0x65, 0x73, + 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_walletunlocker_proto_rawDescOnce sync.Once + file_walletunlocker_proto_rawDescData = file_walletunlocker_proto_rawDesc +) + +func file_walletunlocker_proto_rawDescGZIP() []byte { + file_walletunlocker_proto_rawDescOnce.Do(func() { + file_walletunlocker_proto_rawDescData = protoimpl.X.CompressGZIP(file_walletunlocker_proto_rawDescData) + }) + return file_walletunlocker_proto_rawDescData +} + +var file_walletunlocker_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_walletunlocker_proto_goTypes = []interface{}{ + (*InitWalletRequest)(nil), // 0: lnrpc.InitWalletRequest + (*InitWalletResponse)(nil), // 1: lnrpc.InitWalletResponse + (*WatchOnly)(nil), // 2: lnrpc.WatchOnly + (*WatchOnlyAccount)(nil), // 3: lnrpc.WatchOnlyAccount +} +var file_walletunlocker_proto_depIdxs = []int32{ + 2, // 0: lnrpc.InitWalletRequest.watch_only:type_name -> lnrpc.WatchOnly + 3, // 1: lnrpc.WatchOnly.accounts:type_name -> lnrpc.WatchOnlyAccount + 0, // 2: lnrpc.WalletUnlocker.InitWallet:input_type -> lnrpc.InitWalletRequest + 1, // 3: lnrpc.WalletUnlocker.InitWallet:output_type -> lnrpc.InitWalletResponse + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_walletunlocker_proto_init() } +func file_walletunlocker_proto_init() { + if File_walletunlocker_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_walletunlocker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitWalletRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InitWalletResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WatchOnly); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletunlocker_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WatchOnlyAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_walletunlocker_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_walletunlocker_proto_goTypes, + DependencyIndexes: file_walletunlocker_proto_depIdxs, + MessageInfos: file_walletunlocker_proto_msgTypes, + }.Build() + File_walletunlocker_proto = out.File + file_walletunlocker_proto_rawDesc = nil + file_walletunlocker_proto_goTypes = nil + file_walletunlocker_proto_depIdxs = nil +} diff --git a/itest/walletunlocker.proto b/itest/walletunlocker.proto new file mode 100644 index 0000000..36ad7a0 --- /dev/null +++ b/itest/walletunlocker.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package lnrpc; + +option go_package = "github.com/nydig-oss/lndsigner/itest"; + +/* + * Comments in this file will be directly parsed into the API + * Documentation as descriptions of the associated method, message, or field. + * These descriptions should go right above the definition of the object, and + * can be in either block or // comment format. + * + * An RPC method can be matched to an lncli command by placing a line in the + * beginning of the description in exactly the following format: + * lncli: `methodname` + * + * Failure to specify the exact name of the command will cause documentation + * generation to fail. + * + * More information on how exactly the gRPC documentation is generated from + * this proto file can be found here: + * https://github.com/lightninglabs/lightning-api + */ + +// WalletUnlocker is a service that is used to set up a wallet password for +// lnd at first startup, and unlock a previously set up wallet. +service WalletUnlocker { + /* + InitWallet is used when lnd is starting up for the first time to fully + initialize the daemon and its internal wallet. At the very least a wallet + password must be provided. This will be used to encrypt sensitive material + on disk. + + In the case of a recovery scenario, the user can also specify their aezeed + mnemonic and passphrase. If set, then the daemon will use this prior state + to initialize its internal wallet. + + Alternatively, this can be used along with the GenSeed RPC to obtain a + seed, then present it to the user. Once it has been verified by the user, + the seed can be fed into this RPC in order to commit the new wallet. + */ + rpc InitWallet(InitWalletRequest) returns (InitWalletResponse); +} + +message InitWalletRequest { + /* + wallet_password is the passphrase that should be used to encrypt the + wallet. This MUST be at least 8 chars in length. After creation, this + password is required to unlock the daemon. When using REST, this field + must be encoded as base64. + */ + bytes wallet_password = 1; + + /* + watch_only is the third option of initializing a wallet: by importing + account xpubs only and therefore creating a watch-only wallet that does not + contain any private keys. That means the wallet won't be able to sign for + any of the keys and _needs_ to be run with a remote signer that has the + corresponding private keys and can serve signing RPC requests. + */ + WatchOnly watch_only = 9; +} +message InitWalletResponse { + /* + The binary serialized admin macaroon that can be used to access the daemon + after creating the wallet. If the stateless_init parameter was set to true, + this is the ONLY copy of the macaroon and MUST be stored safely by the + caller. Otherwise a copy of this macaroon is also persisted on disk by the + daemon, together with other macaroon files. + */ + bytes admin_macaroon = 1; +} + +message WatchOnly { + /* + The list of accounts to import. There _must_ be an account for all of lnd's + main key scopes: BIP49/BIP84 (m/49'/0'/0', m/84'/0'/0', note that the + coin type is always 0, even for testnet/regtest) and lnd's internal key + scope (m/1017'/'/'), where account is the key family as + defined in `keychain/derivation.go` (currently indices 0 to 9). + */ + repeated WatchOnlyAccount accounts = 3; +} + +message WatchOnlyAccount { + /* + Purpose is the first number in the derivation path, must be either 49, 84 + or 1017. + */ + uint32 purpose = 1; + + /* + Coin type is the second number in the derivation path, this is _always_ 0 + for purposes 49 and 84. It only needs to be set to 1 for purpose 1017 on + testnet or regtest. + */ + uint32 coin_type = 2; + + /* + Account is the third number in the derivation path. For purposes 49 and 84 + at least the default account (index 0) needs to be created but optional + additional accounts are allowed. For purpose 1017 there needs to be exactly + one account for each of the key families defined in `keychain/derivation.go` + (currently indices 0 to 9) + */ + uint32 account = 3; + + /* + The extended public key at depth 3 for the given account. + */ + string xpub = 4; +} diff --git a/itest/walletunlocker_grpc.pb.go b/itest/walletunlocker_grpc.pb.go new file mode 100644 index 0000000..da38cfc --- /dev/null +++ b/itest/walletunlocker_grpc.pb.go @@ -0,0 +1,127 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package itest + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// WalletUnlockerClient is the client API for WalletUnlocker service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WalletUnlockerClient interface { + // + //InitWallet is used when lnd is starting up for the first time to fully + //initialize the daemon and its internal wallet. At the very least a wallet + //password must be provided. This will be used to encrypt sensitive material + //on disk. + // + //In the case of a recovery scenario, the user can also specify their aezeed + //mnemonic and passphrase. If set, then the daemon will use this prior state + //to initialize its internal wallet. + // + //Alternatively, this can be used along with the GenSeed RPC to obtain a + //seed, then present it to the user. Once it has been verified by the user, + //the seed can be fed into this RPC in order to commit the new wallet. + InitWallet(ctx context.Context, in *InitWalletRequest, opts ...grpc.CallOption) (*InitWalletResponse, error) +} + +type walletUnlockerClient struct { + cc grpc.ClientConnInterface +} + +func NewWalletUnlockerClient(cc grpc.ClientConnInterface) WalletUnlockerClient { + return &walletUnlockerClient{cc} +} + +func (c *walletUnlockerClient) InitWallet(ctx context.Context, in *InitWalletRequest, opts ...grpc.CallOption) (*InitWalletResponse, error) { + out := new(InitWalletResponse) + err := c.cc.Invoke(ctx, "/lnrpc.WalletUnlocker/InitWallet", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WalletUnlockerServer is the server API for WalletUnlocker service. +// All implementations must embed UnimplementedWalletUnlockerServer +// for forward compatibility +type WalletUnlockerServer interface { + // + //InitWallet is used when lnd is starting up for the first time to fully + //initialize the daemon and its internal wallet. At the very least a wallet + //password must be provided. This will be used to encrypt sensitive material + //on disk. + // + //In the case of a recovery scenario, the user can also specify their aezeed + //mnemonic and passphrase. If set, then the daemon will use this prior state + //to initialize its internal wallet. + // + //Alternatively, this can be used along with the GenSeed RPC to obtain a + //seed, then present it to the user. Once it has been verified by the user, + //the seed can be fed into this RPC in order to commit the new wallet. + InitWallet(context.Context, *InitWalletRequest) (*InitWalletResponse, error) + mustEmbedUnimplementedWalletUnlockerServer() +} + +// UnimplementedWalletUnlockerServer must be embedded to have forward compatible implementations. +type UnimplementedWalletUnlockerServer struct { +} + +func (UnimplementedWalletUnlockerServer) InitWallet(context.Context, *InitWalletRequest) (*InitWalletResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method InitWallet not implemented") +} +func (UnimplementedWalletUnlockerServer) mustEmbedUnimplementedWalletUnlockerServer() {} + +// UnsafeWalletUnlockerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WalletUnlockerServer will +// result in compilation errors. +type UnsafeWalletUnlockerServer interface { + mustEmbedUnimplementedWalletUnlockerServer() +} + +func RegisterWalletUnlockerServer(s grpc.ServiceRegistrar, srv WalletUnlockerServer) { + s.RegisterService(&WalletUnlocker_ServiceDesc, srv) +} + +func _WalletUnlocker_InitWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InitWalletRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletUnlockerServer).InitWallet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/lnrpc.WalletUnlocker/InitWallet", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletUnlockerServer).InitWallet(ctx, req.(*InitWalletRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WalletUnlocker_ServiceDesc is the grpc.ServiceDesc for WalletUnlocker service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WalletUnlocker_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "lnrpc.WalletUnlocker", + HandlerType: (*WalletUnlockerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "InitWallet", + Handler: _WalletUnlocker_InitWallet_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "walletunlocker.proto", +} diff --git a/keyring/errors.go b/keyring/errors.go new file mode 100644 index 0000000..3c1bf16 --- /dev/null +++ b/keyring/errors.go @@ -0,0 +1,12 @@ +// Copyright (C) 2022-2023 Bottlepay and The Lightning Network Developers + +package keyring + +import "errors" + +var ( + ErrNoSharedKeyReturned = errors.New("vault returned no shared key") + ErrBadSharedKey = errors.New("vault returned bad shared key") + ErrNoSignatureReturned = errors.New("vault returned no signature") + ErrNoPubkeyReturned = errors.New("vault returned no pubkey") +) diff --git a/keyring/keyring.go b/keyring/keyring.go new file mode 100644 index 0000000..0a783c4 --- /dev/null +++ b/keyring/keyring.go @@ -0,0 +1,739 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/hashicorp/vault/api" + "github.com/nydig-oss/lndsigner/vault" +) + +// signMethod defines the different ways a signer can sign, given a specific +// input. +type signMethod uint8 + +const ( + // WitnessV0SignMethod denotes that a SegWit v0 (p2wkh, np2wkh, p2wsh) + // input script should be signed. + witnessV0SignMethod signMethod = 0 + + // TaprootKeySpendBIP0086SignMethod denotes that a SegWit v1 (p2tr) + // input should be signed by using the BIP0086 method (commit to + // internal key only). + taprootKeySpendBIP0086SignMethod signMethod = 1 + + // TaprootKeySpendSignMethod denotes that a SegWit v1 (p2tr) + // input should be signed by using a given taproot hash to commit to in + // addition to the internal key. + taprootKeySpendSignMethod signMethod = 2 + + // TaprootScriptSpendSignMethod denotes that a SegWit v1 (p2tr) input + // should be spent using the script path and that a specific leaf script + // should be signed for. + taprootScriptSpendSignMethod signMethod = 3 +) + +var ( + // psbtKeyTypeInputSignatureTweakSingle is a custom/proprietary PSBT key + // for an input that specifies what single tweak should be applied to + // the key before signing the input. The value 51 is leet speak for + // "si", short for "single". + psbtKeyTypeInputSignatureTweakSingle = []byte{0x51} + + // psbtKeyTypeInputSignatureTweakDouble is a custom/proprietary PSBT key + // for an input that specifies what double tweak should be applied to + // the key before signing the input. The value d0 is leet speak for + // "do", short for "double". + psbtKeyTypeInputSignatureTweakDouble = []byte{0xd0} +) + +type KeyLocator struct { + // Family is the family of key being identified. + Family uint32 + + // Index is the precise index of the key being identified. + Index uint32 +} + +type KeyDescriptor struct { + // KeyLocator is the internal KeyLocator of the descriptor. + KeyLocator + + // PubKey is an optional public key that fully describes a target key. + // If this is nil, the KeyLocator MUST NOT be empty. + PubKey *btcec.PublicKey +} + +// logicalWriter is an interface that specifies the relevant methods of the +// vault/api.Logical struct for mocking in tests. +type logicalWriter interface { + Write(string, map[string]interface{}) (*api.Secret, error) +} + +// KeyRing is an HD keyring backed by pre-derived in-memory account keys from +// which index keys can be quickly derived on demand. +type KeyRing struct { + client logicalWriter + node string + coin uint32 +} + +// NewKeyRing returns a vault-backed key ring. +func NewKeyRing(client logicalWriter, node string, coin uint32) *KeyRing { + return &KeyRing{ + client: client, + node: node, + coin: coin, + } +} + +// ECDH performs a scalar multiplication (ECDH-like operation) between the +// target key descriptor and remote public key. The output returned will be +// the sha256 of the resulting shared point serialized in compressed format. If +// k is our private key, and P is the public key, we perform the following +// operation: +// +// sx := k*P s := sha256(sx.SerializeCompressed()) +func (k *KeyRing) ECDH(keyDesc KeyDescriptor, pub *btcec.PublicKey) ([32]byte, + error) { + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyDesc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyDesc.Index), + }, + "peer": hex.EncodeToString(pub.SerializeCompressed()), + } + + if keyDesc.PubKey != nil { + reqData["pubkey"] = hex.EncodeToString( + keyDesc.PubKey.SerializeCompressed(), + ) + } + + log.Debugf("Sending data %+v for shared key request", reqData) + + sharedKeyResp, err := k.client.Write( + "lndsigner/lnd-nodes/ecdh", + reqData, + ) + if err != nil { + return [32]byte{}, err + } + + log.Debugf("Got data %+v in shared key response", sharedKeyResp.Data) + + sharedKeyHex, ok := sharedKeyResp.Data["sharedkey"].(string) + if !ok { + return [32]byte{}, ErrNoSharedKeyReturned + } + + sharedKeyBytes, err := hex.DecodeString(sharedKeyHex) + if err != nil { + return [32]byte{}, err + } + + if len(sharedKeyBytes) != 32 { + return [32]byte{}, ErrBadSharedKey + } + + var sharedKeyByteArray [32]byte + + copy(sharedKeyByteArray[:], sharedKeyBytes) + + return sharedKeyByteArray, nil +} + +// SignMessage signs the given message, single or double SHA256 hashing it +// first, with the private key described in the key locator. +func (k *KeyRing) SignMessage(keyLoc KeyLocator, msg []byte, doubleHash bool, + compact bool) ([]byte, error) { + + var digest []byte + if doubleHash { + digest = chainhash.DoubleHashB(msg) + } else { + digest = chainhash.HashB(msg) + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyLoc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyLoc.Index), + }, + "method": "ecdsa", + "digest": hex.EncodeToString(digest), + } + + if compact { + reqData["method"] = "ecdsa-compact" + } + + log.Debugf("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return nil, err + } + + log.Debugf("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return nil, ErrNoSignatureReturned + } + + return hex.DecodeString(signatureHex) +} + +// SignMessageSchnorr signs the given message, single or double SHA256 +// hashing it first, with the private key described in the key locator +// and the optional Taproot tweak applied to the private key. +func (k *KeyRing) SignMessageSchnorr(keyLoc KeyLocator, msg []byte, + doubleHash bool, taprootTweak []byte) (*schnorr.Signature, error) { + + var digest []byte + if doubleHash { + digest = chainhash.DoubleHashB(msg) + } else { + digest = chainhash.HashB(msg) + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": []int{ + int(vault.Bip0043purpose + hdkeychain.HardenedKeyStart), + int(k.coin + hdkeychain.HardenedKeyStart), + int(keyLoc.Family + hdkeychain.HardenedKeyStart), + 0, // Only external branch in LN purpose. + int(keyLoc.Index), + }, + "method": "schnorr", + "digest": hex.EncodeToString(digest), + } + + if len(taprootTweak) > 0 { + reqData["taptweak"] = hex.EncodeToString(taprootTweak) + } + + log.Debugf("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return nil, err + } + + log.Debugf("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return nil, ErrNoSignatureReturned + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return nil, err + } + + return schnorr.ParseSignature(signatureBytes) +} + +// SignPsbt signs all inputs in the PSBT that can be signed by our keyring. +// We have no state information, so we only attempt to derive the appropriate +// keys for each input and sign if we get a match. +func (k *KeyRing) SignPsbt(packet *psbt.Packet) ([]uint32, error) { + // In signedInputs we return the indices of psbt inputs that were signed + // by our wallet. This way the caller can check if any inputs were signed. + var signedInputs []uint32 + + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return nil, err + } + + strPacket, _ := packet.B64Encode() + log.Debugf("Got PSBT to sign: %s", strPacket) + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. If there is nothing more to sign or + // there are inputs that we don't know how to sign, we won't return any + // error. So it's possible we're not the final signer. We expect all + // required UTXO data as part of the PSBT packet as we have no state. + tx := packet.UnsignedTx + prevOutputFetcher := psbtPrevOutputFetcher(packet) + sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) + for idx := range tx.TxIn { + in := &packet.Inputs[idx] + + // We can only sign if we have UTXO information available. Since + // we don't finalize, we just skip over any input that we know + // we can't do anything with. Since we only support signing + // witness inputs, we only look at the witness UTXO being set. + if in.WitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // Skip this input if there is no BIP32 derivation info + // available. + if len(in.Bip32Derivation) == 0 { + continue + } + + // What kind of signature is expected from us and do we have all + // information we need? + signMethod, err := validateSigningMethod(in) + if err != nil { + return nil, err + } + + switch signMethod { + // For p2wkh, np2wkh and p2wsh. + case witnessV0SignMethod: + err = k.signSegWitV0(in, tx, sigHashes, idx) + + // For p2tr BIP0086 key spend only. + case taprootKeySpendBIP0086SignMethod: + rootHash := make([]byte, 0) + err = k.signSegWitV1KeySpend( + in, tx, sigHashes, idx, rootHash, + ) + + // For p2tr with script commitment key spend path. + case taprootKeySpendSignMethod: + rootHash := in.TaprootMerkleRoot + err = k.signSegWitV1KeySpend( + in, tx, sigHashes, idx, rootHash, + ) + + // For p2tr script spend path. + case taprootScriptSpendSignMethod: + leafScript := in.TaprootLeafScript[0] + leaf := txscript.TapLeaf{ + LeafVersion: leafScript.LeafVersion, + Script: leafScript.Script, + } + err = k.signSegWitV1ScriptSpend( + in, tx, sigHashes, idx, leaf, + ) + + default: + err = fmt.Errorf("unsupported signing method for "+ + "PSBT signing: %v", signMethod) + } + if err != nil { + return nil, err + } + + signedInputs = append(signedInputs, uint32(idx)) + } + + return signedInputs, nil +} + +// signSegWitV0 attempts to generate a signature for a SegWit version 0 input +// and stores it in the PartialSigs (and FinalScriptSig for np2wkh addresses) +// field. +func (k *KeyRing) signSegWitV0(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + // Extract the correct witness and/or legacy scripts now, depending on + // the type of input we sign. The txscript package has the peculiar + // requirement that the PkScript of a P2PKH must be given as the witness + // script in order for it to arrive at the correct sighash. That's why + // we call it subScript here instead of witness script. + subScript := prepareScriptsV0(in) + + // We have everything we need to calculate the digest for signing. + digest, err := txscript.CalcWitnessSigHash(subScript, sigHashes, + in.SighashType, tx, idx, in.WitnessUtxo.Value) + if err != nil { + return fmt.Errorf("error getting sighash for input %d: %v", + idx, err) + } + + log.Debugf("Got input %+v for signing with unknowns %+v", in, + in.Unknowns) + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "ecdsa", + "digest": hex.EncodeToString(digest), + } + + getTweakParams(in.Unknowns, reqData) + + log.Debugf("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + log.Debugf("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return ErrNoSignatureReturned + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + pubKeyHex, ok := signResp.Data["pubkey"].(string) + if !ok { + return ErrNoPubkeyReturned + } + + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + return err + } + + in.PartialSigs = append(in.PartialSigs, &psbt.PartialSig{ + PubKey: pubKeyBytes, + Signature: append(signatureBytes, byte(in.SighashType)), + }) + + return nil +} + +// signSegWitV1KeySpend attempts to generate a signature for a SegWit version 1 +// (p2tr) input and stores it in the TaprootKeySpendSig field. +func (k *KeyRing) signSegWitV1KeySpend(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int, + tapscriptRootHash []byte) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + digest, err := txscript.CalcTaprootSignatureHash( + sigHashes, in.SighashType, tx, idx, + txscript.NewCannedPrevOutputFetcher( + in.WitnessUtxo.PkScript, + in.WitnessUtxo.Value, + ), + ) + if err != nil { + return err + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "schnorr", + "digest": hex.EncodeToString(digest), + "taptweak": hex.EncodeToString(tapscriptRootHash), + } + + getTweakParams(in.Unknowns, reqData) + + log.Debugf("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return ErrNoSignatureReturned + } + + log.Debugf("Got data %+v in signing response", signResp.Data) + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + in.TaprootKeySpendSig = append(signatureBytes, byte(in.SighashType)) + + return nil +} + +// signSegWitV1ScriptSpend attempts to generate a signature for a SegWit version +// 1 (p2tr) input and stores it in the TaprootScriptSpendSig field. +func (k *KeyRing) signSegWitV1ScriptSpend(in *psbt.PInput, tx *wire.MsgTx, + sigHashes *txscript.TxSigHashes, idx int, leaf txscript.TapLeaf) error { + + if len(in.Bip32Derivation) == 0 { + return nil + } + + digest, err := txscript.CalcTapscriptSignaturehash( + sigHashes, in.SighashType, tx, idx, + txscript.NewCannedPrevOutputFetcher( + in.WitnessUtxo.PkScript, + in.WitnessUtxo.Value, + ), + leaf, + ) + if err != nil { + return err + } + + reqData := map[string]interface{}{ + "node": k.node, + "path": sliceUint32ToInt(in.Bip32Derivation[0].Bip32Path), + "method": "schnorr", + "digest": hex.EncodeToString(digest), + } + + getTweakParams(in.Unknowns, reqData) + + log.Debugf("Sending data %+v for signing request", reqData) + + signResp, err := k.client.Write( + "lndsigner/lnd-nodes/sign", + reqData, + ) + if err != nil { + return err + } + + log.Debugf("Got data %+v in signing response", signResp.Data) + + signatureHex, ok := signResp.Data["signature"].(string) + if !ok { + return ErrNoSignatureReturned + } + + signatureBytes, err := hex.DecodeString(signatureHex) + if err != nil { + return err + } + + leafHash := leaf.TapHash() + in.TaprootScriptSpendSig = append( + in.TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ + XOnlyPubKey: in.TaprootBip32Derivation[0].XOnlyPubKey, + LeafHash: leafHash[:], + Signature: signatureBytes, + SigHash: in.SighashType, + }, + ) + + return nil +} + +// prepareScriptsV0 returns the appropriate witness v0 and/or legacy scripts, +// depending on the type of input that should be signed. +func prepareScriptsV0(in *psbt.PInput) []byte { + switch { + // It's a NP2WKH input: + //case len(in.RedeemScript) > 0: + // return in.RedeemScript + + // It's a P2WSH input: + case len(in.WitnessScript) > 0: + return in.WitnessScript + + // It's a P2WKH input: + default: + return in.WitnessUtxo.PkScript + } +} + +// getTweakParams examines if there are any tweak parameters given in the +// custom/proprietary PSBT fields and adds them to a vault request's data +// to use them if populated. +func getTweakParams(unknowns []*psbt.Unknown, reqData map[string]interface{}) { + // There can be other custom/unknown keys in a PSBT that we just ignore. + // Key tweaking is optional and only one tweak (single _or_ double) can + // ever be applied (at least for any use cases described in the BOLT + // spec). + for _, u := range unknowns { + if bytes.Equal(u.Key, psbtKeyTypeInputSignatureTweakSingle) { + reqData["ln1tweak"] = hex.EncodeToString(u.Value) + return + } + + if bytes.Equal(u.Key, psbtKeyTypeInputSignatureTweakDouble) { + reqData["ln2tweak"] = hex.EncodeToString(u.Value) + return + } + } +} + +// validateSigningMethod attempts to detect the signing method that is required +// to sign for the given PSBT input and makes sure all information is available +// to do so. +func validateSigningMethod(in *psbt.PInput) (signMethod, error) { + script, err := txscript.ParsePkScript(in.WitnessUtxo.PkScript) + if err != nil { + return 0, fmt.Errorf("error detecting signing method, "+ + "couldn't parse pkScript: %v", err) + } + + switch script.Class() { + case txscript.WitnessV0PubKeyHashTy, txscript.ScriptHashTy, + txscript.WitnessV0ScriptHashTy: + + return witnessV0SignMethod, nil + + case txscript.WitnessV1TaprootTy: + if len(in.TaprootBip32Derivation) == 0 { + return 0, fmt.Errorf("cannot sign for taproot input " + + "without taproot BIP0032 derivation info") + } + + // Currently, we only support creating one signature per input. + if len(in.TaprootBip32Derivation) > 1 { + return 0, fmt.Errorf("unsupported multiple taproot " + + "BIP0032 derivation info found, can only " + + "sign for one at a time") + } + + derivation := in.TaprootBip32Derivation[0] + switch { + // No leaf hashes means this is the internal key we're signing + // with, so it's a key spend. And no merkle root means this is + // a BIP0086 output we're signing for. + case len(derivation.LeafHashes) == 0 && + len(in.TaprootMerkleRoot) == 0: + + return taprootKeySpendBIP0086SignMethod, nil + + // A non-empty merkle root means we committed to a taproot hash + // that we need to use in the tap tweak. + case len(derivation.LeafHashes) == 0: + // Getting here means the merkle root isn't empty, but + // is it exactly the length we need? + if len(in.TaprootMerkleRoot) != sha256.Size { + return 0, fmt.Errorf("invalid taproot merkle "+ + "root length, got %d expected %d", + len(in.TaprootMerkleRoot), sha256.Size) + } + + return taprootKeySpendSignMethod, nil + + // Currently, we only support signing for one leaf at a time. + case len(derivation.LeafHashes) == 1: + // If we're supposed to be signing for a leaf hash, we + // also expect the leaf script that hashes to that hash + // in the appropriate field. + if len(in.TaprootLeafScript) != 1 { + return 0, fmt.Errorf("specified leaf hash in " + + "taproot BIP0032 derivation but " + + "missing taproot leaf script") + } + + leafScript := in.TaprootLeafScript[0] + leaf := txscript.TapLeaf{ + LeafVersion: leafScript.LeafVersion, + Script: leafScript.Script, + } + leafHash := leaf.TapHash() + if !bytes.Equal(leafHash[:], derivation.LeafHashes[0]) { + return 0, fmt.Errorf("specified leaf hash in" + + "taproot BIP0032 derivation but " + + "corresponding taproot leaf script " + + "was not found") + } + + return taprootScriptSpendSignMethod, nil + + default: + return 0, fmt.Errorf("unsupported number of leaf " + + "hashes in taproot BIP0032 derivation info, " + + "can only sign for one at a time") + } + + default: + return 0, fmt.Errorf("unsupported script class for signing "+ + "PSBT: %v", script.Class()) + } +} + +// psbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func psbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} + +func sliceUint32ToInt(uints []uint32) []int { + ints := make([]int, len(uints)) + + for idx, element := range uints { + ints[idx] = int(element) + } + + return ints +} diff --git a/keyring/keyring_test.go b/keyring/keyring_test.go new file mode 100644 index 0000000..917f5f0 --- /dev/null +++ b/keyring/keyring_test.go @@ -0,0 +1,678 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import ( + "bytes" + "encoding/hex" + "errors" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr" + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/require" +) + +var ( + ourPub = mustParsePubKey("03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf") + + message = []byte("happy hanukkah") + + keyLoc = KeyLocator{ + Family: 6, + Index: 0, + } + + schnorrSigHex = "71b77d9c8a0badfa7c4eca3fbef5da2a552bf032f56b85fbc5c2f3500498fc20d5ab8505ae9733b1b756da7a5dba41dbe069dd0d86793618829c3077df0cd759" + schnorrSig, _ = hex.DecodeString(schnorrSigHex) + + requestError = errors.New("error on request") +) + +type mockClient struct { + writeFunc func(string, map[string]interface{}) (*api.Secret, error) +} + +func (m *mockClient) Write(path string, data map[string]interface{}) (*api.Secret, error) { + return m.writeFunc(path, data) +} + +func newTestKeyRing() *KeyRing { + return NewKeyRing( + &mockClient{}, + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + 1, + ) +} + +func mustParsePubKey(keyHex string) *btcec.PublicKey { + keyBytes, _ := hex.DecodeString(keyHex) + key, _ := btcec.ParsePubKey(keyBytes) + return key +} + +func TestECDH(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + peerPubHex := "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e" + peerPub := mustParsePubKey(peerPubHex) + + keyBytes, _ := hex.DecodeString( + "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + ) + var sharedKey [32]byte + copy(sharedKey[:], keyBytes) + + keyDesc := KeyDescriptor{ + KeyLocator: keyLoc, + PubKey: ourPub, + } + + testCases := []struct { + name string + respData map[string]interface{} + respErr error + key [32]byte + err error + }{ + { + name: "ecdh", + respData: map[string]interface{}{ + "sharedkey": "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + }, + key: sharedKey, + }, + { + name: ErrNoSharedKeyReturned.Error(), + respData: map[string]interface{}{}, + err: ErrNoSharedKeyReturned, + }, + { + name: ErrBadSharedKey.Error(), + respData: map[string]interface{}{ + "sharedkey": "7895", + }, + err: ErrBadSharedKey, + }, + { + name: "shared key not hex", + respData: map[string]interface{}{ + "sharedkey": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "error on request", + respErr: errors.New("request error"), + err: errors.New("request error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/ecdh", + path) + + require.Equal(t, map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "peer": peerPubHex, + "pubkey": keyRing.node, + }, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + key, err := keyRing.ECDH(keyDesc, peerPub) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.key, key) + }) + } +} + +func TestSignMessage(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + doubleHash bool + compact bool + reqData map[string]interface{} + respData map[string]interface{} + respErr error + sig []byte + err error + }{ + { + name: "sign single", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign double", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign single compact", + compact: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa-compact", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "sign double compact", + doubleHash: true, + compact: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa-compact", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + sig: []byte{0xab, 0xcd, 0xef}, + }, + { + name: "error on request", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respErr: requestError, + err: requestError, + }, + { + name: ErrNoSignatureReturned.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + err: ErrNoSignatureReturned, + }, + { + name: "signature not hex", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "ecdsa", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + sig, err := keyRing.SignMessage(keyLoc, message, + testCase.doubleHash, testCase.compact) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.sig, sig) + }) + } +} + +func TestSignMessageSchnorr(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + doubleHash bool + tapTweak []byte + reqData map[string]interface{} + respData map[string]interface{} + respErr error + sig []byte + err error + }{ + { + name: "sign single", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign double", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign single tweaked", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "sign double tweaked", + doubleHash: true, + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "2b81484875960ba2eaea16ae0ecfc2848c2d40944b5c034c609ce95542151f14", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + sig: schnorrSig, + }, + { + name: "error on request", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respErr: requestError, + err: requestError, + }, + { + name: ErrNoSignatureReturned.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + err: ErrNoSignatureReturned, + }, + { + name: "signature not hex", + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: schnorr.ErrSigTooShort.Error(), + reqData: map[string]interface{}{ + "node": keyRing.node, + "path": []int{2147484665, 2147483649, + 2147483654, 0, 0}, + "method": "schnorr", + "digest": "4eacd1f26fe18294c9671e427240e6762e6f021f1e35793cab2850cbec7320f3", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + err: schnorr.Error{ + Err: schnorr.ErrSigTooShort, + Description: "malformed signature: too short: 3 < 64", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + sig, err := keyRing.SignMessageSchnorr(keyLoc, message, + testCase.doubleHash, testCase.tapTweak) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.sig, sig.Serialize()) + }) + } +} + +func TestSignPsbt(t *testing.T) { + t.Parallel() + + keyRing := newTestKeyRing() + client := keyRing.client.(*mockClient) + + testCases := []struct { + name string + packet *psbt.Packet + reqData map[string]interface{} + respData map[string]interface{} + respErr error + inputs []uint32 + err error + }{ + { + name: "nil PSBT", + err: errors.New("PSBT packet cannot be nil"), + }, + { + name: "p2tr spend", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respData: map[string]interface{}{ + "signature": schnorrSigHex, + }, + inputs: []uint32{0}, + }, + { + name: "p2wkh spend", + packet: p2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "2046c479fa1d00033ff7239086071cb4abadc2c99e2dd14e6e1af7ed8060f3ca", + "path": []int{2147483732, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: "np2wkh spend", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: "ln1tweak spend", + packet: tweak1Psbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "acf17dc76b84ab1b061f274ccea1b680e7195d34f138c92bec64f66a6ed11b7c", + "ln1tweak": "cf374dcf99541cff08176226b16e1848eee7f00430da428a74ddc671224bbe8f", + "path": []int{2147484665, 2147483649, 2147483650, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "abcdef", + "signature": "abcdef", + }, + inputs: []uint32{0}, + }, + { + name: ErrNoPubkeyReturned.Error(), + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "signature": "abcdef", + }, + err: ErrNoPubkeyReturned, + }, + { + name: "pubkey not hex", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "pubkey": "g", + "signature": "abcdef", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "p2wkh and np2wkh error on request", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respErr: requestError, + err: requestError, + }, + { + name: "np2wkh and p2wkh " + ErrNoSignatureReturned.Error(), + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + err: ErrNoSignatureReturned, + }, + { + name: "np2wkh and p2wkh signature not hex", + packet: np2wkhPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "5f5c0b1eb31d60a15c0b607ee037b97bbb2ef375d127be13c4718ddc5b67dc70", + "path": []int{2147483697, 2147483648, 2147483648, 0, 0}, + "method": "ecdsa", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + { + name: "p2wkh and np2wkh error on request", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respErr: requestError, + err: requestError, + }, + { + name: "p2tr " + ErrNoSignatureReturned.Error(), + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + err: ErrNoSignatureReturned, + }, + { + name: "p2tr signature not hex", + packet: p2trPsbt, + reqData: map[string]interface{}{ + "node": keyRing.node, + "digest": "6a14f55652583393923a9f6909c9be3ada3e5bd724c324d8a554b823388491ad", + "path": []int{2147483734, 2147483648, 2147483648, 0, 0}, + "method": "schnorr", + "taptweak": "", + }, + respData: map[string]interface{}{ + "signature": "g", + }, + err: hex.InvalidByteError(0x67), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client.writeFunc = func(path string, + data map[string]interface{}) (*api.Secret, + error) { + + require.Equal(t, "lndsigner/lnd-nodes/sign", + path) + + require.Equal(t, testCase.reqData, data) + + return &api.Secret{Data: testCase.respData}, + testCase.respErr + } + + signed, err := keyRing.SignPsbt(testCase.packet) + require.Equal(t, testCase.err, err) + + if err != nil { + return + } + + require.Equal(t, testCase.inputs, signed) + }) + } +} + +func mustGetPacket(pb64 string) *psbt.Packet { + packet, _ := psbt.NewFromRawBytes(bytes.NewBuffer([]byte(pb64)), true) + return packet +} + +var ( + p2trPsbt = mustGetPacket("cHNidP8BAF4CAAAAAReb7pdpQYTQ2CNhvICbAlIE+1c/c+mDcoMwoc8rinenAAAAAAAAAAAAAUbL9QUAAAAAIlEgtfY28ZYCnRSLF8t3TlIu/whJ6214cNnzljDTT1efhbXlCwAAAAEA9gIAAAAAAQFJMZAOE1PK+T6HTes4eGZLMIDd0bzYEZ1oFJVYK/ZiCwAAAAAA/f///wIA4fUFAAAAACJRILX2NvGWAp0UixfLd05SLv8ISetteHDZ85Yw009Xn4W1nR0NjwAAAAAiUSDDFLNZISF+rpRN7aK0J7JM0mT5fqTEKvFKaUMBEw6ZIAJHMEQCIHHymwJU7u3YG6DRJHEBXU7WM7iKpfEyi2RirHkkG3qgAiBlFw4ueMJ7R9UiX7X4kqOkrtnPPbjRFAcV0N2dlwMsqQEhAlwtWnotMY/75da30kBFv6MAP1cQ9+JAtFPHOys8EcVx5AsAAAEBKwDh9QUAAAAAIlEgtfY28ZYCnRSLF8t3TlIu/whJ6214cNnzljDTT1efhbUiBgPNayRhLVP0LyNSLfBQl/5mzCRF1JJj0XfEHSRXn2ocYBgAAAAAVgAAgAAAAIAAAACAAAAAAAAAAAAhFs1rJGEtU/QvI1It8FCX/mbMJEXUkmPRd8QdJFefahxgGQAAAAAAVgAAgAAAAIAAAACAAAAAAAAAAAAAAA==") + p2wkhPsbt = mustGetPacket("cHNidP8BAFICAAAAAY5F4ge3IcjdMnXPFvSlcsJtrWsucxdANZMwuxiBAn9mAAAAAAAAAAAAAXWi9QUAAAAAFgAU9Pc1yrjv1bubTE9iLVshd6CQcBPnCwAAAAEAlgIAAAAAAQFFycXtfec9d3aq5MGiwljDzXdT1b27wxA35E5bQ+fjlgAAAAAAAAAAAAHkt/UFAAAAABYAFPT3Ncq479W7m0xPYi1bIXegkHATAUB2zaC2OeERmiGxi78tR1hE7aDCWUJCMhcSvcs4e7kxarTGXy2IcLzCGuwsJkWiYmcunFp6VQSbpcO23KQNMDkz5gsAAAEBH+S39QUAAAAAFgAU9Pc1yrjv1bubTE9iLVshd6CQcBMBAwQBAAAAAQUWABT09zXKuO/Vu5tMT2ItWyF3oJBwEyIGAwEIoLvhEXJvhEZtTcYV/qv1mWWy6gvKBE+dVfZTNiRDGAAAAABUAACAAAAAgAAAAIAAAAAAAAAAAAAA") + np2wkhPsbt = mustGetPacket("cHNidP8BAFMCAAAAAd5zNp3onh0tGqfG/uWxkO492bU81losB9X4NeyTzD+/AAAAAAAAAAAAAV1w9QUAAAAAF6kUraCbn6aZpvU7PJsguxGajtIBNxOH6QsAAAABAMECAAAAAAEBZd+rb8GaP6GGqU8L9mbFsIQhZg5Tu74ydSOG3Co97GQAAAAAAAAAAAAB1Iz1BQAAAAAXqRStoJufppmm9Ts8myC7EZqO0gE3E4cCSDBFAiEAnd6DdDpgIBspLqYb4c4UxA0OfHH5U6v8MdFqYNGr3qsCICz6j4z50tyrlb6udEg19obEMvxZYapfiTm8b1u+yY7WASEDAQigu+ERcm+ERm1NxhX+q/WZZbLqC8oET51V9lM2JEPoCwAAAQEg1Iz1BQAAAAAXqRStoJufppmm9Ts8myC7EZqO0gE3E4cBAwQBAAAAAQQXFgAUZHoOpcJIIpm9ZZqp4+yUqUbVxlABBRYAFGR6DqXCSCKZvWWaqePslKlG1cZQIgYCjY4A5/M2NnFkZxRH38ob8I0kLvCMqAaLpjV2L4W2DV4YAAAAADEAAIAAAACAAAAAgAAAAAAAAAAAAAA=") + tweak1Psbt = mustGetPacket("cHNidP8BAF4CAAAAAefBDVMh9KyBaUQgDsjV6nUylfgrye5+zKxHOWe2WppIAgAAAAABAAAAAYgTAAAAAAAAIgAgnt22lI7xTfZhaVvG98+/a2fgGpKGx4If+tDif9TNGtIbDAAAAAEBK4gTAAAAAAAAIgAgh3mqXWpXMnilHq2jlNLT1RHiQblrGAcXso10qnIuHo4BAwSDAAAAAQWIdqkUSL7Vs9GdmmyMm/jAw3NWT1cavGSHY6xnIQM9249K7owaTOry2WiTfm5kYbSiKpopzl0v4REiL4tjk3yCASCHZHVSfCEDCrYgRP5lJXfOS1ZXS110yioIRjDn5OEr6yCLWXhajChSrmepFL5fYOxnPqaCwyQdgixZKou3K8d1iKxoUbJ1aCIGA0fj8run5YT4j5ZwAwgD7jOWCKVLiFP7xPUeSQVQotAvGAAAAAD5AwCAAQAAgAIAAIAAAAAAAAAAAAFRIM83Tc+ZVBz/CBdiJrFuGEju5/AEMNpCinTdxnEiS76PAAA=") +) diff --git a/keyring/log.go b/keyring/log.go new file mode 100644 index 0000000..931e554 --- /dev/null +++ b/keyring/log.go @@ -0,0 +1,20 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package keyring + +import ( + "go.uber.org/zap" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log *zap.SugaredLogger = zap.NewNop().Sugar() + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger *zap.SugaredLogger) { + log = logger +} diff --git a/lndsigner.go b/lndsigner.go new file mode 100644 index 0000000..fdb07ec --- /dev/null +++ b/lndsigner.go @@ -0,0 +1,279 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "net" + "os" + "os/signal" + "strconv" + "strings" + "sync" + "syscall" + + "github.com/hashicorp/vault/api" + "github.com/nydig-oss/lndsigner/vault" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// ListenerWithSignal is a net.Listener that has an additional Ready channel +// that will be closed when a server starts listening. +type ListenerWithSignal struct { + net.Listener + + // Ready will be closed by the server listening on Listener. + Ready chan struct{} +} + +// ListenerCfg is a wrapper around custom listeners that can be passed to lnd +// when calling its main method. +type ListenerCfg struct { + // RPCListeners can be set to the listeners to use for the RPC server. + // If empty a regular network listener will be created. + RPCListeners []*ListenerWithSignal +} + +// Main is the true entry point for lnd. It accepts a fully populated and +// validated main configuration struct and an optional listener config struct. +// This function starts all main system components then blocks until a signal +// is received on the shutdownChan at which point everything is shut down again. +func Main(cfg *Config, lisCfg ListenerCfg) error { + // mkErr makes it easy to return logged errors. + mkErr := func(format string, args ...interface{}) error { + signerLog.Errorf("Shutting down because error in main "+ + "method: "+format, args...) + return fmt.Errorf(format, args...) + } + + signerLog.Infow("Active Bitcoin network: ", "net", + cfg.ActiveNetParams.Name) + + // Use defaults for vault client, including getting config from env. + vaultClient, err := api.NewClient(nil) + if err != nil { + return mkErr("error creating vault client: %v", err) + } + + signerClient := vaultClient.Logical() + + serverOpts, err := getTLSConfig(cfg) + if err != nil { + return mkErr("unable to load TLS credentials: %v", err) + } + + // If we have chosen to start with a dedicated listener for the + // rpc server, we set it directly. + grpcListeners := append([]*ListenerWithSignal{}, lisCfg.RPCListeners...) + if len(grpcListeners) == 0 { + // Otherwise we create listeners from the RPCListeners defined + // in the config. + for _, grpcEndpoint := range cfg.RPCListeners { + // Start a gRPC server listening for HTTP/2 + // connections. + lis, err := ListenOnAddress(grpcEndpoint) + if err != nil { + return mkErr("unable to listen on %s: %v", + grpcEndpoint, err) + } + defer lis.Close() + + grpcListeners = append( + grpcListeners, &ListenerWithSignal{ + Listener: lis, + Ready: make(chan struct{}), + }, + ) + } + } + + // Initialize the rpcServer and add its interceptor to the server + // options. + rpcServer := newRPCServer(cfg, signerClient) + serverOpts = append( + serverOpts, + grpc.ChainUnaryInterceptor(rpcServer.intercept), + ) + + // Create the GRPC server with the TLS and interceptor configuration. + grpcServer := grpc.NewServer(serverOpts...) + defer grpcServer.Stop() + + // Register our implementation of the gRPC interface exported by the + // rpcServer. + err = rpcServer.RegisterWithGrpcServer(grpcServer) + if err != nil { + return mkErr("error registering gRPC server: %v", err) + } + + // Now that both the WalletUnlocker and LightningService have been + // registered with the GRPC server, we can start listening. + err = startGrpcListen(cfg, grpcServer, grpcListeners) + if err != nil { + return mkErr("error starting gRPC listener: %v", err) + } + + // Wait for shutdown signal from the interrupt handler. + signerLog.Infof("Press ctrl-c to exit") + + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + <-sigint + + return nil +} + +// getTLSConfig returns a TLS configuration for the gRPC server. +func getTLSConfig(cfg *Config) ([]grpc.ServerOption, error) { + + // The certData returned here is just a wrapper around the PEM blocks + // loaded from the file. The PEM is not yet fully parsed but a basic + // check is performed that the certificate and private key actually + // belong together. + certData, err := tls.LoadX509KeyPair( + cfg.TLSCertPath, cfg.TLSKeyPath, + ) + if err != nil { + return nil, err + } + + // Now parse the the PEM block of the certificate. + _, err = x509.ParseCertificate(certData.Certificate[0]) + if err != nil { + return nil, err + } + + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{certData}, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + }, + } + + serverCreds := credentials.NewTLS(tlsCfg) + serverOpts := []grpc.ServerOption{grpc.Creds(serverCreds)} + + return serverOpts, nil +} + +// fileExists reports whether the named file or directory exists. +// This function is taken from https://github.com/btcsuite/btcd +func fileExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// startGrpcListen starts the GRPC server on the passed listeners. +func startGrpcListen(cfg *Config, grpcServer *grpc.Server, + listeners []*ListenerWithSignal) error { + + // Use a WaitGroup so we can be sure the instructions on how to input the + // password is the last thing to be printed to the console. + var wg sync.WaitGroup + + for _, lis := range listeners { + wg.Add(1) + go func(lis *ListenerWithSignal) { + signerLog.Infof("RPC server listening on %s", lis.Addr()) + + // Close the ready chan to indicate we are listening. + close(lis.Ready) + + wg.Done() + _ = grpcServer.Serve(lis) + }(lis) + } + + // Wait for gRPC servers to be up running. + wg.Wait() + + return nil +} + +// parseNetwork parses the network type of the given address. +func parseNetwork(addr net.Addr) string { + switch addr := addr.(type) { + // TCP addresses resolved through net.ResolveTCPAddr give a default + // network of "tcp", so we'll map back the correct network for the given + // address. This ensures that we can listen on the correct interface + // (IPv4 vs IPv6). + case *net.TCPAddr: + if addr.IP.To4() != nil { + return "tcp4" + } + return "tcp6" + + default: + return addr.Network() + } +} + +// ListenOnAddress creates a listener that listens on the given address. +func ListenOnAddress(addr net.Addr) (net.Listener, error) { + return net.Listen(parseNetwork(addr), addr.String()) +} + +type jsonAcctEl struct { + Xpub string `json:"extended_public_key"` + Path string `json:"derivation_path"` +} + +// GetAccounts is currently used in integration testing, but will soon also +// be used in policy enforcement. For current status, see the branch at +// https://github.com/aakselrod/lndsigner/tree/offchain-ratelimiting +func GetAccounts(acctList string) (map[[3]uint32]string, error) { + accounts := make(map[[3]uint32]string) + + elements := make(map[string][]*jsonAcctEl) + + err := json.Unmarshal([]byte(acctList), &elements) + if err != nil { + return nil, err + } + + acctElements, ok := elements["accounts"] + if !ok { + return nil, fmt.Errorf("no accounts returned in JSON") + } + + for _, acctEl := range acctElements { + pathEls := strings.Split(acctEl.Path, "/") + if len(pathEls) != 4 || pathEls[0] != "m" { + return nil, fmt.Errorf("invalid derivation path") + } + + var derPath [3]uint32 + for idx, el := range pathEls[1:] { + if !strings.HasSuffix(el, "'") { + return nil, vault.ErrElementNotHardened + } + + intEl, err := strconv.ParseUint(el[:len(el)-1], 10, 32) + if err != nil { + return nil, err + } + + derPath[idx] = uint32(intEl) + } + + accounts[derPath] = acctEl.Xpub + } + + return accounts, nil +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..a5407ec --- /dev/null +++ b/log.go @@ -0,0 +1,23 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "github.com/nydig-oss/lndsigner/keyring" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var signerLog *zap.SugaredLogger + +func init() { + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + config.EncoderConfig.EncodeCaller = nil + rawLog := zap.Must(config.Build()) + signerLog = rawLog.Sugar() + keyring.UseLogger(signerLog.With(zap.Any("pkg", "keyring"))) +} diff --git a/proto/gen_protos.sh b/proto/gen_protos.sh new file mode 100755 index 0000000..68f4ec9 --- /dev/null +++ b/proto/gen_protos.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +# Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +set -e + +# generate compiles the *.pb.go stubs from the *.proto files. +function generate() { + echo "Generating root gRPC server protos" + + PROTOS="lightning.proto signer.proto walletkit.proto" + + # For each of the sub-servers, we then generate their protos, but a restricted + # set as they don't yet require REST proxies, or swagger docs. + for file in $PROTOS; do + DIRECTORY=$(dirname "${file}") + echo "Generating protos from ${file}, into ${DIRECTORY}" + + # Generate the protos. + protoc -I/usr/local/include -I. \ + --go_out . --go_opt paths=source_relative \ + --go-grpc_out . --go-grpc_opt paths=source_relative \ + "${file}" + done +} + +# format formats the *.proto files with the clang-format utility. +function format() { + find . -name "*.proto" -print0 | xargs -0 clang-format --style=file -i +} + +# Compile and format the lnrpc package. +pushd proto +format +generate +popd diff --git a/proto/lightning.pb.go b/proto/lightning.pb.go new file mode 100644 index 0000000..5ef4c95 --- /dev/null +++ b/proto/lightning.pb.go @@ -0,0 +1,390 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: lightning.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignMessageRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The message to be signed. When using REST, this field must be encoded as + //base64. + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` + // + //Instead of the default double-SHA256 hashing of the message before signing, + //only use one round of hashing instead. + SingleHash bool `protobuf:"varint,2,opt,name=single_hash,json=singleHash,proto3" json:"single_hash,omitempty"` +} + +func (x *SignMessageRequest) Reset() { + *x = SignMessageRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageRequest) ProtoMessage() {} + +func (x *SignMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageRequest.ProtoReflect.Descriptor instead. +func (*SignMessageRequest) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{0} +} + +func (x *SignMessageRequest) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil +} + +func (x *SignMessageRequest) GetSingleHash() bool { + if x != nil { + return x.SingleHash + } + return false +} + +type SignMessageResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The signature for the given message + Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` +} + +func (x *SignMessageResponse) Reset() { + *x = SignMessageResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResponse) ProtoMessage() {} + +func (x *SignMessageResponse) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResponse.ProtoReflect.Descriptor instead. +func (*SignMessageResponse) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{1} +} + +func (x *SignMessageResponse) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +type KeyLocator struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The family of key being identified. + KeyFamily int32 `protobuf:"varint,1,opt,name=key_family,json=keyFamily,proto3" json:"key_family,omitempty"` + // The precise index of the key being identified. + KeyIndex int32 `protobuf:"varint,2,opt,name=key_index,json=keyIndex,proto3" json:"key_index,omitempty"` +} + +func (x *KeyLocator) Reset() { + *x = KeyLocator{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyLocator) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyLocator) ProtoMessage() {} + +func (x *KeyLocator) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyLocator.ProtoReflect.Descriptor instead. +func (*KeyLocator) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{2} +} + +func (x *KeyLocator) GetKeyFamily() int32 { + if x != nil { + return x.KeyFamily + } + return 0 +} + +func (x *KeyLocator) GetKeyIndex() int32 { + if x != nil { + return x.KeyIndex + } + return 0 +} + +type KeyDescriptor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The raw bytes of the key being identified. + RawKeyBytes []byte `protobuf:"bytes,1,opt,name=raw_key_bytes,json=rawKeyBytes,proto3" json:"raw_key_bytes,omitempty"` + // + //The key locator that identifies which key to use for signing. + KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` +} + +func (x *KeyDescriptor) Reset() { + *x = KeyDescriptor{} + if protoimpl.UnsafeEnabled { + mi := &file_lightning_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KeyDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KeyDescriptor) ProtoMessage() {} + +func (x *KeyDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_lightning_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use KeyDescriptor.ProtoReflect.Descriptor instead. +func (*KeyDescriptor) Descriptor() ([]byte, []int) { + return file_lightning_proto_rawDescGZIP(), []int{3} +} + +func (x *KeyDescriptor) GetRawKeyBytes() []byte { + if x != nil { + return x.RawKeyBytes + } + return nil +} + +func (x *KeyDescriptor) GetKeyLoc() *KeyLocator { + if x != nil { + return x.KeyLoc + } + return nil +} + +var File_lightning_proto protoreflect.FileDescriptor + +var file_lightning_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x47, 0x0a, 0x12, 0x53, 0x69, 0x67, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, + 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x48, 0x61, 0x73, + 0x68, 0x22, 0x33, 0x0a, 0x13, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x48, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x5f, 0x66, 0x61, 0x6d, 0x69, + 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6b, 0x65, 0x79, 0x46, 0x61, 0x6d, + 0x69, 0x6c, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x22, 0x5f, 0x0a, 0x0d, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, + 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x61, 0x77, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x4b, 0x65, 0x79, + 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, + 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, + 0x63, 0x32, 0x51, 0x0a, 0x09, 0x4c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x44, + 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_lightning_proto_rawDescOnce sync.Once + file_lightning_proto_rawDescData = file_lightning_proto_rawDesc +) + +func file_lightning_proto_rawDescGZIP() []byte { + file_lightning_proto_rawDescOnce.Do(func() { + file_lightning_proto_rawDescData = protoimpl.X.CompressGZIP(file_lightning_proto_rawDescData) + }) + return file_lightning_proto_rawDescData +} + +var file_lightning_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_lightning_proto_goTypes = []interface{}{ + (*SignMessageRequest)(nil), // 0: proto.SignMessageRequest + (*SignMessageResponse)(nil), // 1: proto.SignMessageResponse + (*KeyLocator)(nil), // 2: proto.KeyLocator + (*KeyDescriptor)(nil), // 3: proto.KeyDescriptor +} +var file_lightning_proto_depIdxs = []int32{ + 2, // 0: proto.KeyDescriptor.key_loc:type_name -> proto.KeyLocator + 0, // 1: proto.Lightning.SignMessage:input_type -> proto.SignMessageRequest + 1, // 2: proto.Lightning.SignMessage:output_type -> proto.SignMessageResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_lightning_proto_init() } +func file_lightning_proto_init() { + if File_lightning_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_lightning_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyLocator); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lightning_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*KeyDescriptor); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_lightning_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_lightning_proto_goTypes, + DependencyIndexes: file_lightning_proto_depIdxs, + MessageInfos: file_lightning_proto_msgTypes, + }.Build() + File_lightning_proto = out.File + file_lightning_proto_rawDesc = nil + file_lightning_proto_goTypes = nil + file_lightning_proto_depIdxs = nil +} diff --git a/proto/lightning.proto b/proto/lightning.proto new file mode 100644 index 0000000..dd38452 --- /dev/null +++ b/proto/lightning.proto @@ -0,0 +1,76 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +package proto; + +option go_package = "github.com/nydig-oss/lndsigner/proto"; + +/* + * Comments in this file will be directly parsed into the API + * Documentation as descriptions of the associated method, message, or field. + * These descriptions should go right above the definition of the object, and + * can be in either block or // comment format. + * + * An RPC method can be matched to an lncli command by placing a line in the + * beginning of the description in exactly the following format: + * lncli: `methodname` + * + * Failure to specify the exact name of the command will cause documentation + * generation to fail. + * + * More information on how exactly the gRPC documentation is generated from + * this proto file can be found here: + * https://github.com/lightninglabs/lightning-api + */ + +// Lightning is the main RPC server of the daemon. +service Lightning { + /* lncli: `signmessage` + SignMessage signs a message with this node's private key. The returned + signature string is `zbase32` encoded and pubkey recoverable, meaning that + only the message digest and signature are needed for verification. + */ + rpc SignMessage(SignMessageRequest) returns (SignMessageResponse); +} + +message SignMessageRequest { + /* + The message to be signed. When using REST, this field must be encoded as + base64. + */ + bytes msg = 1; + + /* + Instead of the default double-SHA256 hashing of the message before signing, + only use one round of hashing instead. + */ + bool single_hash = 2; +} +message SignMessageResponse { + // The signature for the given message + string signature = 1; +} + +message KeyLocator { + // The family of key being identified. + int32 key_family = 1; + + // The precise index of the key being identified. + int32 key_index = 2; +} + +message KeyDescriptor { + /* + The raw bytes of the key being identified. + */ + bytes raw_key_bytes = 1; + + /* + The key locator that identifies which key to use for signing. + */ + KeyLocator key_loc = 2; +} diff --git a/proto/lightning_grpc.pb.go b/proto/lightning_grpc.pb.go new file mode 100644 index 0000000..ac6f148 --- /dev/null +++ b/proto/lightning_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// LightningClient is the client API for Lightning service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type LightningClient interface { + // lncli: `signmessage` + //SignMessage signs a message with this node's private key. The returned + //signature string is `zbase32` encoded and pubkey recoverable, meaning that + //only the message digest and signature are needed for verification. + SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) +} + +type lightningClient struct { + cc grpc.ClientConnInterface +} + +func NewLightningClient(cc grpc.ClientConnInterface) LightningClient { + return &lightningClient{cc} +} + +func (c *lightningClient) SignMessage(ctx context.Context, in *SignMessageRequest, opts ...grpc.CallOption) (*SignMessageResponse, error) { + out := new(SignMessageResponse) + err := c.cc.Invoke(ctx, "/proto.Lightning/SignMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// LightningServer is the server API for Lightning service. +// All implementations must embed UnimplementedLightningServer +// for forward compatibility +type LightningServer interface { + // lncli: `signmessage` + //SignMessage signs a message with this node's private key. The returned + //signature string is `zbase32` encoded and pubkey recoverable, meaning that + //only the message digest and signature are needed for verification. + SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) + mustEmbedUnimplementedLightningServer() +} + +// UnimplementedLightningServer must be embedded to have forward compatible implementations. +type UnimplementedLightningServer struct { +} + +func (UnimplementedLightningServer) SignMessage(context.Context, *SignMessageRequest) (*SignMessageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} +func (UnimplementedLightningServer) mustEmbedUnimplementedLightningServer() {} + +// UnsafeLightningServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LightningServer will +// result in compilation errors. +type UnsafeLightningServer interface { + mustEmbedUnimplementedLightningServer() +} + +func RegisterLightningServer(s grpc.ServiceRegistrar, srv LightningServer) { + s.RegisterService(&Lightning_ServiceDesc, srv) +} + +func _Lightning_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightningServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Lightning/SignMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightningServer).SignMessage(ctx, req.(*SignMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Lightning_ServiceDesc is the grpc.ServiceDesc for Lightning service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Lightning_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Lightning", + HandlerType: (*LightningServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignMessage", + Handler: _Lightning_SignMessage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lightning.proto", +} diff --git a/proto/signer.pb.go b/proto/signer.pb.go new file mode 100644 index 0000000..96b38f0 --- /dev/null +++ b/proto/signer.pb.go @@ -0,0 +1,443 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: signer.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignMessageReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The message to be signed. When using REST, this field must be encoded as + //base64. + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` + // The key locator that identifies which key to use for signing. + KeyLoc *KeyLocator `protobuf:"bytes,2,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"` + // Double-SHA256 hash instead of just the default single round. + DoubleHash bool `protobuf:"varint,3,opt,name=double_hash,json=doubleHash,proto3" json:"double_hash,omitempty"` + // + //Use the compact (pubkey recoverable) format instead of the raw lnwire + //format. This option cannot be used with Schnorr signatures. + CompactSig bool `protobuf:"varint,4,opt,name=compact_sig,json=compactSig,proto3" json:"compact_sig,omitempty"` + // + //Use Schnorr signature. This option cannot be used with compact format. + SchnorrSig bool `protobuf:"varint,5,opt,name=schnorr_sig,json=schnorrSig,proto3" json:"schnorr_sig,omitempty"` + // + //The optional Taproot tweak bytes to apply to the private key before creating + //a Schnorr signature. The private key is tweaked as described in BIP-341: + //privKey + h_tapTweak(internalKey || tapTweak) + SchnorrSigTapTweak []byte `protobuf:"bytes,6,opt,name=schnorr_sig_tap_tweak,json=schnorrSigTapTweak,proto3" json:"schnorr_sig_tap_tweak,omitempty"` +} + +func (x *SignMessageReq) Reset() { + *x = SignMessageReq{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageReq) ProtoMessage() {} + +func (x *SignMessageReq) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageReq.ProtoReflect.Descriptor instead. +func (*SignMessageReq) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{0} +} + +func (x *SignMessageReq) GetMsg() []byte { + if x != nil { + return x.Msg + } + return nil +} + +func (x *SignMessageReq) GetKeyLoc() *KeyLocator { + if x != nil { + return x.KeyLoc + } + return nil +} + +func (x *SignMessageReq) GetDoubleHash() bool { + if x != nil { + return x.DoubleHash + } + return false +} + +func (x *SignMessageReq) GetCompactSig() bool { + if x != nil { + return x.CompactSig + } + return false +} + +func (x *SignMessageReq) GetSchnorrSig() bool { + if x != nil { + return x.SchnorrSig + } + return false +} + +func (x *SignMessageReq) GetSchnorrSigTapTweak() []byte { + if x != nil { + return x.SchnorrSigTapTweak + } + return nil +} + +type SignMessageResp struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The signature for the given message in the fixed-size LN wire format. + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` +} + +func (x *SignMessageResp) Reset() { + *x = SignMessageResp{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignMessageResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignMessageResp) ProtoMessage() {} + +func (x *SignMessageResp) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignMessageResp.ProtoReflect.Descriptor instead. +func (*SignMessageResp) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{1} +} + +func (x *SignMessageResp) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +type SharedKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ephemeral public key to use for the DH key derivation. + EphemeralPubkey []byte `protobuf:"bytes,1,opt,name=ephemeral_pubkey,json=ephemeralPubkey,proto3" json:"ephemeral_pubkey,omitempty"` + // + //A key descriptor describes the key used for performing ECDH. Either a key + //locator or a raw public key is expected, if neither is supplied, defaults to + //the node's identity private key. + KeyDesc *KeyDescriptor `protobuf:"bytes,3,opt,name=key_desc,json=keyDesc,proto3" json:"key_desc,omitempty"` +} + +func (x *SharedKeyRequest) Reset() { + *x = SharedKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SharedKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedKeyRequest) ProtoMessage() {} + +func (x *SharedKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SharedKeyRequest.ProtoReflect.Descriptor instead. +func (*SharedKeyRequest) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{2} +} + +func (x *SharedKeyRequest) GetEphemeralPubkey() []byte { + if x != nil { + return x.EphemeralPubkey + } + return nil +} + +func (x *SharedKeyRequest) GetKeyDesc() *KeyDescriptor { + if x != nil { + return x.KeyDesc + } + return nil +} + +type SharedKeyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The shared public key, hashed with sha256. + SharedKey []byte `protobuf:"bytes,1,opt,name=shared_key,json=sharedKey,proto3" json:"shared_key,omitempty"` +} + +func (x *SharedKeyResponse) Reset() { + *x = SharedKeyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_signer_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SharedKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedKeyResponse) ProtoMessage() {} + +func (x *SharedKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_signer_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SharedKeyResponse.ProtoReflect.Descriptor instead. +func (*SharedKeyResponse) Descriptor() ([]byte, []int) { + return file_signer_proto_rawDescGZIP(), []int{3} +} + +func (x *SharedKeyResponse) GetSharedKey() []byte { + if x != nil { + return x.SharedKey + } + return nil +} + +var File_signer_proto protoreflect.FileDescriptor + +var file_signer_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe4, 0x01, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x2a, 0x0a, 0x07, 0x6b, + 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x52, + 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, + 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x6f, + 0x75, 0x62, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x70, + 0x61, 0x63, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x63, + 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x53, 0x69, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x68, + 0x6e, 0x6f, 0x72, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, + 0x73, 0x63, 0x68, 0x6e, 0x6f, 0x72, 0x72, 0x53, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x15, 0x73, 0x63, + 0x68, 0x6e, 0x6f, 0x72, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x5f, 0x74, 0x61, 0x70, 0x5f, 0x74, 0x77, + 0x65, 0x61, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x73, 0x63, 0x68, 0x6e, 0x6f, + 0x72, 0x72, 0x53, 0x69, 0x67, 0x54, 0x61, 0x70, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x22, 0x2f, 0x0a, + 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x6e, + 0x0a, 0x10, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, + 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, + 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x22, 0x32, + 0x0a, 0x11, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x32, 0x8c, 0x01, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x3c, 0x0a, + 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x44, 0x0a, 0x0f, 0x44, + 0x65, 0x72, 0x69, 0x76, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x17, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x62, 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_signer_proto_rawDescOnce sync.Once + file_signer_proto_rawDescData = file_signer_proto_rawDesc +) + +func file_signer_proto_rawDescGZIP() []byte { + file_signer_proto_rawDescOnce.Do(func() { + file_signer_proto_rawDescData = protoimpl.X.CompressGZIP(file_signer_proto_rawDescData) + }) + return file_signer_proto_rawDescData +} + +var file_signer_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_signer_proto_goTypes = []interface{}{ + (*SignMessageReq)(nil), // 0: proto.SignMessageReq + (*SignMessageResp)(nil), // 1: proto.SignMessageResp + (*SharedKeyRequest)(nil), // 2: proto.SharedKeyRequest + (*SharedKeyResponse)(nil), // 3: proto.SharedKeyResponse + (*KeyLocator)(nil), // 4: proto.KeyLocator + (*KeyDescriptor)(nil), // 5: proto.KeyDescriptor +} +var file_signer_proto_depIdxs = []int32{ + 4, // 0: proto.SignMessageReq.key_loc:type_name -> proto.KeyLocator + 5, // 1: proto.SharedKeyRequest.key_desc:type_name -> proto.KeyDescriptor + 0, // 2: proto.Signer.SignMessage:input_type -> proto.SignMessageReq + 2, // 3: proto.Signer.DeriveSharedKey:input_type -> proto.SharedKeyRequest + 1, // 4: proto.Signer.SignMessage:output_type -> proto.SignMessageResp + 3, // 5: proto.Signer.DeriveSharedKey:output_type -> proto.SharedKeyResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_signer_proto_init() } +func file_signer_proto_init() { + if File_signer_proto != nil { + return + } + file_lightning_proto_init() + if !protoimpl.UnsafeEnabled { + file_signer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignMessageResp); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SharedKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_signer_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SharedKeyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_signer_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_signer_proto_goTypes, + DependencyIndexes: file_signer_proto_depIdxs, + MessageInfos: file_signer_proto_msgTypes, + }.Build() + File_signer_proto = out.File + file_signer_proto_rawDesc = nil + file_signer_proto_goTypes = nil + file_signer_proto_depIdxs = nil +} diff --git a/proto/signer.proto b/proto/signer.proto new file mode 100644 index 0000000..e995a75 --- /dev/null +++ b/proto/signer.proto @@ -0,0 +1,92 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +import "lightning.proto"; + +package proto; + +option go_package = "github.com/nydig-oss/lndsigner/proto"; + +// Signer is a service that gives access to the signing functionality of the +// daemon's wallet. +service Signer { + /* + SignMessage signs a message with the key specified in the key locator. The + returned signature is fixed-size LN wire format encoded. + + The main difference to SignMessage in the main RPC is that a specific key is + used to sign the message instead of the node identity private key. + */ + rpc SignMessage(SignMessageReq) returns (SignMessageResp); + + /* + DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + derivation between the ephemeral public key in the request and the node's + key specified in the key_desc parameter. Either a key locator or a raw + public key is expected in the key_desc, if neither is supplied, defaults to + the node's identity private key: + P_shared = privKeyNode * ephemeralPubkey + The resulting shared public key is serialized in the compressed format and + hashed with sha256, resulting in the final key length of 256bit. + */ + rpc DeriveSharedKey(SharedKeyRequest) returns (SharedKeyResponse); +} + +message SignMessageReq { + /* + The message to be signed. When using REST, this field must be encoded as + base64. + */ + bytes msg = 1; + + // The key locator that identifies which key to use for signing. + KeyLocator key_loc = 2; + + // Double-SHA256 hash instead of just the default single round. + bool double_hash = 3; + + /* + Use the compact (pubkey recoverable) format instead of the raw lnwire + format. This option cannot be used with Schnorr signatures. + */ + bool compact_sig = 4; + + /* + Use Schnorr signature. This option cannot be used with compact format. + */ + bool schnorr_sig = 5; + + /* + The optional Taproot tweak bytes to apply to the private key before creating + a Schnorr signature. The private key is tweaked as described in BIP-341: + privKey + h_tapTweak(internalKey || tapTweak) + */ + bytes schnorr_sig_tap_tweak = 6; +} +message SignMessageResp { + /* + The signature for the given message in the fixed-size LN wire format. + */ + bytes signature = 1; +} + +message SharedKeyRequest { + // The ephemeral public key to use for the DH key derivation. + bytes ephemeral_pubkey = 1; + + /* + A key descriptor describes the key used for performing ECDH. Either a key + locator or a raw public key is expected, if neither is supplied, defaults to + the node's identity private key. + */ + KeyDescriptor key_desc = 3; +} + +message SharedKeyResponse { + // The shared public key, hashed with sha256. + bytes shared_key = 1; +} diff --git a/proto/signer_grpc.pb.go b/proto/signer_grpc.pb.go new file mode 100644 index 0000000..1480ddf --- /dev/null +++ b/proto/signer_grpc.pb.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SignerClient is the client API for Signer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SignerClient interface { + // + //SignMessage signs a message with the key specified in the key locator. The + //returned signature is fixed-size LN wire format encoded. + // + //The main difference to SignMessage in the main RPC is that a specific key is + //used to sign the message instead of the node identity private key. + SignMessage(ctx context.Context, in *SignMessageReq, opts ...grpc.CallOption) (*SignMessageResp, error) + // + //DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + //derivation between the ephemeral public key in the request and the node's + //key specified in the key_desc parameter. Either a key locator or a raw + //public key is expected in the key_desc, if neither is supplied, defaults to + //the node's identity private key: + //P_shared = privKeyNode * ephemeralPubkey + //The resulting shared public key is serialized in the compressed format and + //hashed with sha256, resulting in the final key length of 256bit. + DeriveSharedKey(ctx context.Context, in *SharedKeyRequest, opts ...grpc.CallOption) (*SharedKeyResponse, error) +} + +type signerClient struct { + cc grpc.ClientConnInterface +} + +func NewSignerClient(cc grpc.ClientConnInterface) SignerClient { + return &signerClient{cc} +} + +func (c *signerClient) SignMessage(ctx context.Context, in *SignMessageReq, opts ...grpc.CallOption) (*SignMessageResp, error) { + out := new(SignMessageResp) + err := c.cc.Invoke(ctx, "/proto.Signer/SignMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *signerClient) DeriveSharedKey(ctx context.Context, in *SharedKeyRequest, opts ...grpc.CallOption) (*SharedKeyResponse, error) { + out := new(SharedKeyResponse) + err := c.cc.Invoke(ctx, "/proto.Signer/DeriveSharedKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SignerServer is the server API for Signer service. +// All implementations must embed UnimplementedSignerServer +// for forward compatibility +type SignerServer interface { + // + //SignMessage signs a message with the key specified in the key locator. The + //returned signature is fixed-size LN wire format encoded. + // + //The main difference to SignMessage in the main RPC is that a specific key is + //used to sign the message instead of the node identity private key. + SignMessage(context.Context, *SignMessageReq) (*SignMessageResp, error) + // + //DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + //derivation between the ephemeral public key in the request and the node's + //key specified in the key_desc parameter. Either a key locator or a raw + //public key is expected in the key_desc, if neither is supplied, defaults to + //the node's identity private key: + //P_shared = privKeyNode * ephemeralPubkey + //The resulting shared public key is serialized in the compressed format and + //hashed with sha256, resulting in the final key length of 256bit. + DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error) + mustEmbedUnimplementedSignerServer() +} + +// UnimplementedSignerServer must be embedded to have forward compatible implementations. +type UnimplementedSignerServer struct { +} + +func (UnimplementedSignerServer) SignMessage(context.Context, *SignMessageReq) (*SignMessageResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignMessage not implemented") +} +func (UnimplementedSignerServer) DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeriveSharedKey not implemented") +} +func (UnimplementedSignerServer) mustEmbedUnimplementedSignerServer() {} + +// UnsafeSignerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SignerServer will +// result in compilation errors. +type UnsafeSignerServer interface { + mustEmbedUnimplementedSignerServer() +} + +func RegisterSignerServer(s grpc.ServiceRegistrar, srv SignerServer) { + s.RegisterService(&Signer_ServiceDesc, srv) +} + +func _Signer_SignMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignMessageReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServer).SignMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Signer/SignMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServer).SignMessage(ctx, req.(*SignMessageReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Signer_DeriveSharedKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SharedKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SignerServer).DeriveSharedKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.Signer/DeriveSharedKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SignerServer).DeriveSharedKey(ctx, req.(*SharedKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Signer_ServiceDesc is the grpc.ServiceDesc for Signer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Signer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.Signer", + HandlerType: (*SignerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignMessage", + Handler: _Signer_SignMessage_Handler, + }, + { + MethodName: "DeriveSharedKey", + Handler: _Signer_DeriveSharedKey_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "signer.proto", +} diff --git a/proto/walletkit.pb.go b/proto/walletkit.pb.go new file mode 100644 index 0000000..3aea118 --- /dev/null +++ b/proto/walletkit.pb.go @@ -0,0 +1,234 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0-devel +// protoc v3.14.0 +// source: walletkit.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SignPsbtRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The PSBT that should be signed. The PSBT must contain all required inputs, + //outputs, UTXO data and custom fields required to identify the signing key. + FundedPsbt []byte `protobuf:"bytes,1,opt,name=funded_psbt,json=fundedPsbt,proto3" json:"funded_psbt,omitempty"` +} + +func (x *SignPsbtRequest) Reset() { + *x = SignPsbtRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_walletkit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtRequest) ProtoMessage() {} + +func (x *SignPsbtRequest) ProtoReflect() protoreflect.Message { + mi := &file_walletkit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtRequest.ProtoReflect.Descriptor instead. +func (*SignPsbtRequest) Descriptor() ([]byte, []int) { + return file_walletkit_proto_rawDescGZIP(), []int{0} +} + +func (x *SignPsbtRequest) GetFundedPsbt() []byte { + if x != nil { + return x.FundedPsbt + } + return nil +} + +type SignPsbtResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The signed transaction in PSBT format. + SignedPsbt []byte `protobuf:"bytes,1,opt,name=signed_psbt,json=signedPsbt,proto3" json:"signed_psbt,omitempty"` + // The indices of signed inputs. + SignedInputs []uint32 `protobuf:"varint,2,rep,packed,name=signed_inputs,json=signedInputs,proto3" json:"signed_inputs,omitempty"` +} + +func (x *SignPsbtResponse) Reset() { + *x = SignPsbtResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_walletkit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtResponse) ProtoMessage() {} + +func (x *SignPsbtResponse) ProtoReflect() protoreflect.Message { + mi := &file_walletkit_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtResponse.ProtoReflect.Descriptor instead. +func (*SignPsbtResponse) Descriptor() ([]byte, []int) { + return file_walletkit_proto_rawDescGZIP(), []int{1} +} + +func (x *SignPsbtResponse) GetSignedPsbt() []byte { + if x != nil { + return x.SignedPsbt + } + return nil +} + +func (x *SignPsbtResponse) GetSignedInputs() []uint32 { + if x != nil { + return x.SignedInputs + } + return nil +} + +var File_walletkit_proto protoreflect.FileDescriptor + +var file_walletkit_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x6b, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x32, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, + 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x66, + 0x75, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0a, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x22, 0x58, 0x0a, 0x10, + 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x50, 0x73, 0x62, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0c, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x49, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x32, 0x48, 0x0a, 0x09, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x4b, 0x69, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x12, + 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, + 0x6f, 0x74, 0x74, 0x6c, 0x65, 0x70, 0x61, 0x79, 0x2f, 0x6c, 0x6e, 0x64, 0x73, 0x69, 0x67, 0x6e, + 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_walletkit_proto_rawDescOnce sync.Once + file_walletkit_proto_rawDescData = file_walletkit_proto_rawDesc +) + +func file_walletkit_proto_rawDescGZIP() []byte { + file_walletkit_proto_rawDescOnce.Do(func() { + file_walletkit_proto_rawDescData = protoimpl.X.CompressGZIP(file_walletkit_proto_rawDescData) + }) + return file_walletkit_proto_rawDescData +} + +var file_walletkit_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_walletkit_proto_goTypes = []interface{}{ + (*SignPsbtRequest)(nil), // 0: proto.SignPsbtRequest + (*SignPsbtResponse)(nil), // 1: proto.SignPsbtResponse +} +var file_walletkit_proto_depIdxs = []int32{ + 0, // 0: proto.WalletKit.SignPsbt:input_type -> proto.SignPsbtRequest + 1, // 1: proto.WalletKit.SignPsbt:output_type -> proto.SignPsbtResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_walletkit_proto_init() } +func file_walletkit_proto_init() { + if File_walletkit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_walletkit_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignPsbtRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletkit_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignPsbtResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_walletkit_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_walletkit_proto_goTypes, + DependencyIndexes: file_walletkit_proto_depIdxs, + MessageInfos: file_walletkit_proto_msgTypes, + }.Build() + File_walletkit_proto = out.File + file_walletkit_proto_rawDesc = nil + file_walletkit_proto_goTypes = nil + file_walletkit_proto_depIdxs = nil +} diff --git a/proto/walletkit.proto b/proto/walletkit.proto new file mode 100644 index 0000000..22fb2b7 --- /dev/null +++ b/proto/walletkit.proto @@ -0,0 +1,45 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +syntax = "proto3"; + +package proto; + +option go_package = "github.com/nydig-oss/lndsigner/proto"; + +// WalletKit is a service that gives access to the core functionalities of the +// daemon's wallet. +service WalletKit { + /* + SignPsbt expects a partial transaction with all inputs and outputs fully + declared and tries to sign all unsigned inputs that have all required fields + (UTXO information, BIP32 derivation information, witness or sig scripts) + set. + If no error is returned, the PSBT is ready to be given to the next signer or + to be finalized if we were the last signer. + + NOTE: This RPC only signs inputs (and only those it can sign), it does not + perform any other tasks (such as coin selection, UTXO locking or + input/output/fee value validation, PSBT finalization). Any input that is + incomplete will be skipped. + */ + rpc SignPsbt(SignPsbtRequest) returns (SignPsbtResponse); +} + +message SignPsbtRequest { + /* + The PSBT that should be signed. The PSBT must contain all required inputs, + outputs, UTXO data and custom fields required to identify the signing key. + */ + bytes funded_psbt = 1; +} + +message SignPsbtResponse { + // The signed transaction in PSBT format. + bytes signed_psbt = 1; + + // The indices of signed inputs. + repeated uint32 signed_inputs = 2; +} diff --git a/proto/walletkit_grpc.pb.go b/proto/walletkit_grpc.pb.go new file mode 100644 index 0000000..3ff8382 --- /dev/null +++ b/proto/walletkit_grpc.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// WalletKitClient is the client API for WalletKit service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WalletKitClient interface { + // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) +} + +type walletKitClient struct { + cc grpc.ClientConnInterface +} + +func NewWalletKitClient(cc grpc.ClientConnInterface) WalletKitClient { + return &walletKitClient{cc} +} + +func (c *walletKitClient) SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) { + out := new(SignPsbtResponse) + err := c.cc.Invoke(ctx, "/proto.WalletKit/SignPsbt", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WalletKitServer is the server API for WalletKit service. +// All implementations must embed UnimplementedWalletKitServer +// for forward compatibility +type WalletKitServer interface { + // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) + mustEmbedUnimplementedWalletKitServer() +} + +// UnimplementedWalletKitServer must be embedded to have forward compatible implementations. +type UnimplementedWalletKitServer struct { +} + +func (UnimplementedWalletKitServer) SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignPsbt not implemented") +} +func (UnimplementedWalletKitServer) mustEmbedUnimplementedWalletKitServer() {} + +// UnsafeWalletKitServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WalletKitServer will +// result in compilation errors. +type UnsafeWalletKitServer interface { + mustEmbedUnimplementedWalletKitServer() +} + +func RegisterWalletKitServer(s grpc.ServiceRegistrar, srv WalletKitServer) { + s.RegisterService(&WalletKit_ServiceDesc, srv) +} + +func _WalletKit_SignPsbt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignPsbtRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletKitServer).SignPsbt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.WalletKit/SignPsbt", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletKitServer).SignPsbt(ctx, req.(*SignPsbtRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WalletKit_ServiceDesc is the grpc.ServiceDesc for WalletKit service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WalletKit_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.WalletKit", + HandlerType: (*WalletKitServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignPsbt", + Handler: _WalletKit_SignPsbt_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "walletkit.proto", +} diff --git a/rpcserver.go b/rpcserver.go new file mode 100644 index 0000000..e973a86 --- /dev/null +++ b/rpcserver.go @@ -0,0 +1,130 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "fmt" + + "github.com/hashicorp/vault/api" + "github.com/nydig-oss/lndsigner/keyring" + "github.com/nydig-oss/lndsigner/proto" + "github.com/tv42/zbase32" + "google.golang.org/grpc" +) + +// keyRingKeyStruct is a struct used to look up a keyring passed in a context. +type keyRingKeyStruct struct{} + +var ( + keyRingKey = keyRingKeyStruct{} +) + +// rpcServer is a gRPC front end to the signer daemon. +type rpcServer struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + // Must be after the atomically used variables to not break struct + // alignment. + proto.UnimplementedLightningServer + + client *api.Logical + + cfg *Config + + keyRing *keyring.KeyRing +} + +// A compile time check to ensure that rpcServer fully implements the +// LightningServer gRPC service. +var _ proto.LightningServer = (*rpcServer)(nil) + +// newRPCServer creates and returns a new instance of the rpcServer. Before +// dependencies are added, this will be an non-functioning RPC server only to +// be used to register the LightningService with the gRPC server. +func newRPCServer(cfg *Config, c *api.Logical) *rpcServer { + return &rpcServer{ + cfg: cfg, + client: c, + keyRing: keyring.NewKeyRing( + c, + cfg.NodePubKey, + cfg.ActiveNetParams.HDCoinType, + ), + } +} + +// intercept allows the RPC server to intercept requests to ensure that they're +// authorized by a macaroon signed by the macaroon root key. +func (r *rpcServer) intercept(ctx context.Context, req interface{}, + info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) ( + interface{}, error) { + + return handler( + context.WithValue(ctx, keyRingKey, r.keyRing), + req, + ) +} + +// RegisterWithGrpcServer registers the rpcServer and any subservers with the +// root gRPC server. +func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { + // Register the main RPC server. + lnDesc := proto.Lightning_ServiceDesc + lnDesc.ServiceName = "lnrpc.Lightning" + grpcServer.RegisterService(&lnDesc, r) + + // Register the wallet subserver. + walletDesc := proto.WalletKit_ServiceDesc + walletDesc.ServiceName = "walletrpc.WalletKit" + grpcServer.RegisterService(&walletDesc, &walletKit{ + server: r, + }) + + // Register the signer subserver. + signerDesc := proto.Signer_ServiceDesc + signerDesc.ServiceName = "signrpc.Signer" + grpcServer.RegisterService(&signerDesc, &signerServer{ + server: r, + }) + + return nil +} + +var ( + // signedMsgPrefix is a special prefix that we'll prepend to any + // messages we sign/verify. We do this to ensure that we don't + // accidentally sign a sighash, or other sensitive material. By + // prepending this fragment, we mind message signing to our particular + // context. + signedMsgPrefix = []byte("Lightning Signed Message:") +) + +// SignMessage signs a message with the resident node's private key. The +// returned signature string is zbase32 encoded and pubkey recoverable, meaning +// that only the message digest and signature are needed for verification. +func (r *rpcServer) SignMessage(_ context.Context, + in *proto.SignMessageRequest) (*proto.SignMessageResponse, error) { + + if in.Msg == nil { + return nil, fmt.Errorf("need a message to sign") + } + + in.Msg = append(signedMsgPrefix, in.Msg...) + keyLoc := keyring.KeyLocator{ + Family: 6, + Index: 0, + } + + sig, err := r.keyRing.SignMessage( + keyLoc, in.Msg, !in.SingleHash, true, + ) + if err != nil { + return nil, err + } + + sigStr := zbase32.EncodeToString(sig) + return &proto.SignMessageResponse{Signature: sigStr}, nil +} diff --git a/signer_server.go b/signer_server.go new file mode 100644 index 0000000..cd5029b --- /dev/null +++ b/signer_server.go @@ -0,0 +1,190 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/nydig-oss/lndsigner/keyring" + "github.com/nydig-oss/lndsigner/proto" + "github.com/nydig-oss/lndsigner/vault" +) + +// Server is a sub-server of the main RPC server: the signer RPC. This sub RPC +// server allows external callers to access the full signing capabilities of +// lndsignerd. This allows callers to create custom protocols, external to the +// signer itself, even backed by multiple distinct signers across independent +// failure domains. +type signerServer struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + proto.UnimplementedSignerServer + + server *rpcServer +} + +// A compile time check to ensure that Server fully implements the SignerServer +// gRPC service. +var _ proto.SignerServer = (*signerServer)(nil) + +// SignMessage signs a message with the key specified in the key locator. The +// returned signature is fixed-size LN wire format encoded. +func (s *signerServer) SignMessage(_ context.Context, + in *proto.SignMessageReq) (*proto.SignMessageResp, error) { + + if in.Msg == nil { + return nil, fmt.Errorf("a message to sign MUST be passed in") + } + if in.KeyLoc == nil { + return nil, fmt.Errorf("a key locator MUST be passed in") + } + if in.SchnorrSig && in.CompactSig { + return nil, fmt.Errorf("compact format can not be used for " + + "Schnorr signatures") + } + + // Describe the private key we'll be using for signing. + keyLocator := keyring.KeyLocator{ + Family: uint32(in.KeyLoc.KeyFamily), + Index: uint32(in.KeyLoc.KeyIndex), + } + + // Use the schnorr signature algorithm to sign the message. + if in.SchnorrSig { + sig, err := s.server.keyRing.SignMessageSchnorr( + keyLocator, in.Msg, in.DoubleHash, + in.SchnorrSigTapTweak, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + + sigParsed, err := schnorr.ParseSignature(sig.Serialize()) + if err != nil { + return nil, fmt.Errorf("can't parse Schnorr "+ + "signature: %v", err) + } + + return &proto.SignMessageResp{ + Signature: sigParsed.Serialize(), + }, nil + } + + // Create the raw ECDSA signature first and convert it to the final wire + // format after. + sig, err := s.server.keyRing.SignMessage( + keyLocator, in.Msg, in.DoubleHash, in.CompactSig, + ) + if err != nil { + return nil, fmt.Errorf("can't sign the hash: %v", err) + } + return &proto.SignMessageResp{ + Signature: sig, + }, nil +} + +// DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key +// derivation between the ephemeral public key in the request and the node's +// key specified in the key_desc parameter. Either a key locator or a raw public +// key is expected in the key_desc, if neither is supplied, defaults to the +// node's identity private key. The old key_loc parameter in the request +// shouldn't be used anymore. +// The resulting shared public key is serialized in the compressed format and +// hashed with sha256, resulting in the final key length of 256bit. +func (s *signerServer) DeriveSharedKey(ctx context.Context, + in *proto.SharedKeyRequest) (*proto.SharedKeyResponse, error) { + + // Check that EphemeralPubkey is valid. + ephemeralPubkey, err := parseRawKeyBytes(in.EphemeralPubkey) + if err != nil { + return nil, fmt.Errorf("error in ephemeral pubkey: %v", err) + } + if ephemeralPubkey == nil { + return nil, fmt.Errorf("must provide ephemeral pubkey") + } + + // When key_desc is used, the key_desc.key_loc is expected as the caller + // needs to specify the KeyFamily. + if in.KeyDesc != nil && in.KeyDesc.KeyLoc == nil { + return nil, fmt.Errorf("when setting key_desc the field " + + "key_desc.key_loc must also be set") + } + + // We extract two params, rawKeyBytes and keyLoc. + rawKeyBytes := in.KeyDesc.GetRawKeyBytes() + keyLoc := in.KeyDesc.GetKeyLoc() + + // When no keyLoc is supplied, defaults to the node's identity private + // key. + if keyLoc == nil { + keyLoc = &proto.KeyLocator{ + KeyFamily: int32(vault.NodeKeyAcct), + KeyIndex: 0, + } + } + + // Check the caller is using either the key index or the raw public key + // to perform the ECDH, we can't have both. + if rawKeyBytes != nil && keyLoc.KeyIndex != 0 { + return nil, fmt.Errorf("use either raw_key_bytes or key_index") + } + + // Check the raw public key is valid. Notice that if the rawKeyBytes is + // empty, the parseRawKeyBytes won't return an error, a nil + // *btcec.PublicKey is returned instead. + pk, err := parseRawKeyBytes(rawKeyBytes) + if err != nil { + return nil, fmt.Errorf("error in raw pubkey: %v", err) + } + + // Create a key descriptor. When the KeyIndex is not specified, it uses + // the empty value 0, and when the raw public key is not specified, the + // pk is nil. + keyDescriptor := keyring.KeyDescriptor{ + KeyLocator: keyring.KeyLocator{ + Family: uint32(keyLoc.KeyFamily), + Index: uint32(keyLoc.KeyIndex), + }, + PubKey: pk, + } + + // Derive the shared key using ECDH and hashing the serialized + // compressed shared point. + sharedKeyHash, err := s.server.keyRing.ECDH( + keyDescriptor, ephemeralPubkey, + ) + if err != nil { + signerLog.Errorf("unable to derive shared key: %+v", err) + return nil, err + } + + return &proto.SharedKeyResponse{SharedKey: sharedKeyHash[:]}, nil +} + +// parseRawKeyBytes checks that the provided raw public key is valid and returns +// the public key. A nil public key is returned if the length of the rawKeyBytes +// is zero. +func parseRawKeyBytes(rawKeyBytes []byte) (*btcec.PublicKey, error) { + switch { + case len(rawKeyBytes) == 33: + // If a proper raw key was provided, then we'll attempt + // to decode and parse it. + return btcec.ParsePubKey(rawKeyBytes) + + case len(rawKeyBytes) == 0: + // No key is provided, return nil. + return nil, nil + + default: + // If the user provided a raw key, but it's of the + // wrong length, then we'll return with an error. + return nil, fmt.Errorf("pubkey must be " + + "serialized in compressed format if " + + "specified") + } +} diff --git a/vault/aezeed.go b/vault/aezeed.go new file mode 100644 index 0000000..8ee0361 --- /dev/null +++ b/vault/aezeed.go @@ -0,0 +1,2154 @@ +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "encoding/binary" + "hash/crc32" + "strings" + + "github.com/Yawning/aez" + "github.com/kkdai/bstream" + "golang.org/x/crypto/scrypt" +) + +var ( + // reverseWordMap maps a word to its position within the default word list. + reverseWordMap map[string]int +) + +func seedFromSeedAndPassPhrases(seedPhrase, passPhrase string) ([]byte, error) { + if passPhrase == "" { + passPhrase = "aezeed" + } + + mnemonic := strings.Split( + strings.ToLower(strings.TrimSpace(seedPhrase)), " ", + ) + + if len(mnemonic) != 24 { + return nil, ErrSeedPhraseWrongLength + } + + cipherBits := bstream.NewBStreamWriter(33) + + for _, word := range mnemonic { + idx, ok := reverseWordMap[word] + if !ok { + return nil, ErrSeedPhraseNotBIP39 + } + + cipherBits.WriteBits(uint64(idx), 11) + } + + cipherText := cipherBits.Bytes() + + if cipherText[0] != byte(0) { + return nil, ErrBadCipherSeedVer + } + + salt := cipherText[24:29] + + checksum := cipherText[29:] + if len(checksum) != 4 { + return nil, ErrWrongLengthChecksum + } + + freshChecksum := crc32.Checksum( + cipherText[:29], crc32.MakeTable(crc32.Castagnoli), + ) + if freshChecksum != binary.BigEndian.Uint32(checksum) { + return nil, ErrChecksumMismatch + } + + cipherSeed := cipherText[1:24] + + key, err := scrypt.Key([]byte(passPhrase), salt, 32768, 8, 1, 32) + if err != nil { + return nil, err + } + + ad := make([]byte, 6) + ad[0] = cipherText[0] + copy(ad[1:], salt) + + plainSeedBytes, ok := aez.Decrypt( + key, nil, [][]byte{ad[:]}, 4, cipherSeed, nil, + ) + if !ok { + return nil, ErrInvalidPassphrase + } + + if plainSeedBytes[0] != byte(1) && plainSeedBytes[0] != byte(0) { + return nil, ErrWrongInternalVersion + } + + entropy := make([]byte, 16) + copy(entropy[:], plainSeedBytes[3:]) + + return entropy, nil +} + +func init() { + reverseWordMap = make(map[string]int) + for i, v := range defaultWordList { + reverseWordMap[v] = i + } +} + +// defaultWordList is a slice of the current default word list that's used to +// encode the enciphered seed into a human readable set of words. +var defaultWordList = strings.Split(englishWordList, "\n") + +// englishWordList is an English wordlist that's used as part of version 0 of +// the cipherseed scheme. This is the *same* word list that's recommend for use +// with BIP0039. +var englishWordList = `abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo` diff --git a/vault/backend.go b/vault/backend.go new file mode 100644 index 0000000..485e79e --- /dev/null +++ b/vault/backend.go @@ -0,0 +1,666 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/jsonutil" + "github.com/hashicorp/vault/sdk/logical" +) + +const ( + seedLen = 16 // Matches LND usage +) + +type backend struct { + *framework.Backend +} + +type listedAccount struct { + Name string `json:"name"` + AddressType string `json:"address_type"` + XPub string `json:"extended_public_key"` + DerivationPath string `json:"derivation_path"` + ExternalKeyCount int `json:"external_key_count"` + InternalKeyCount int `json:"internal_key_count"` + WatchOnly bool `json:"watch_only"` +} + +func (b *backend) listAccounts(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + rootKey, err := hdkeychain.NewMaster(seed, net) + if err != nil { + return nil, err + } + defer rootKey.Zero() + + acctList := make([]*listedAccount, 0, 260) + + listAccount := func(purpose, coin, act uint32, addrType string, + version []byte) (*listedAccount, error) { + + // Derive purpose. We do these derivations with + // DeriveNonStandard to match btcwallet's (and thus lnd's) + // usage as shown here: + // https://github.com/btcsuite/btcwallet/blob/c314de6995500686c93716037f2279128cc1e9e8/waddrmgr/manager.go#L1459 + purposeKey, err := rootKey.DeriveNonStandard( // nolint:staticcheck + purpose + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return nil, err + } + defer purposeKey.Zero() + + // Derive coin. + coinKey, err := purposeKey.DeriveNonStandard( // nolint:staticcheck + coin + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return nil, err + } + defer coinKey.Zero() + + // Derive account. + actKey, err := coinKey.DeriveNonStandard( // nolint:staticcheck + act + hdkeychain.HardenedKeyStart, + ) + if err != nil { + return nil, err + } + defer actKey.Zero() + + // Get account watch-only pubkey. + xPub, err := actKey.Neuter() + if err != nil { + return nil, err + } + + // Ensure we get the right HDVersion for the account key. + if version != nil { + xPub, err = xPub.CloneWithVersion(version) + if err != nil { + return nil, err + } + } + + strPurpose := fmt.Sprintf("%d", purpose) + strCoin := fmt.Sprintf("%d", coin) + strAct := fmt.Sprintf("%d", act) + + listing := &listedAccount{ + Name: "act:" + strAct, + AddressType: addrType, + XPub: xPub.String(), + DerivationPath: "m/" + strPurpose + "'/" + strCoin + + "'/" + strAct + "'", + } + + if act == 0 { + listing.Name = "default" + } + + return listing, nil + } + + for _, acctInfo := range defaultPurposes { + listing, err := listAccount( + acctInfo.purpose, + 0, + 0, + acctInfo.addrType, + acctInfo.hdVersion[net.HDCoinType][:], + ) + if err != nil { + b.Logger().Error("Failed to derive default account", + "node", strNode, "err", err) + return nil, err + } + + acctList = append(acctList, listing) + } + + for act := uint32(0); act <= MaxAcctID; act++ { + listing, err := listAccount( + Bip0043purpose, + net.HDCoinType, + act, + "WITNESS_PUBKEY_HASH", + nil, + ) + if err != nil { + b.Logger().Error("Failed to derive Lightning account", + "node", strNode, "err", err) + return nil, err + } + + acctList = append(acctList, listing) + } + + resp, err := jsonutil.EncodeJSON(struct { + Accounts []*listedAccount `json:"accounts"` + }{ + Accounts: acctList, + }) + if err != nil { + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "acctList": string(resp), + }, + }, nil +} + +func (b *backend) ecdh(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + peerPubHex := data.Get("peer").(string) + if len(peerPubHex) != 2*btcec.PubKeyBytesLenCompressed { + b.Logger().Error("Peer pubkey is wrong length", + "peer", peerPubHex) + return nil, ErrInvalidPeerPubkey + } + + peerPubBytes, err := hex.DecodeString(peerPubHex) + if err != nil { + b.Logger().Error("Failed to decode peer pubkey hex", + "error", err) + return nil, err + } + + peerPubKey, err := btcec.ParsePubKey(peerPubBytes) + if err != nil { + b.Logger().Error("Failed to parse peer pubkey", + "error", err) + return nil, err + } + + var ( + pubJacobian btcec.JacobianPoint + s btcec.JacobianPoint + ) + peerPubKey.AsJacobian(&pubJacobian) + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + privKey, err := derivePrivKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive privkey", + "node", strNode, "error", err) + return nil, err + } + defer privKey.Zero() + + err = checkRequiredPubKey(privKey, data.Get("pubkey").(string)) + if err != nil { + // We log here as warning because there's no case when we + // should be using ECDH with a mismatching own key. + b.Logger().Warn("Pubkey mismatch", + "node", strNode, "error", err) + return nil, err + } + + ecPrivKey, err := privKey.ECPrivKey() + if err != nil { + b.Logger().Error("Failed to derive valid ECDSA privkey", + "node", strNode, "error", err) + return nil, err + } + defer ecPrivKey.Zero() + + btcec.ScalarMultNonConst(&ecPrivKey.Key, &pubJacobian, &s) + s.ToAffine() + sPubKey := btcec.NewPublicKey(&s.X, &s.Y) + h := sha256.Sum256(sPubKey.SerializeCompressed()) + + return &logical.Response{ + Data: map[string]interface{}{ + "sharedkey": hex.EncodeToString(h[:]), + }, + }, nil +} + +func (b *backend) derivePubKey(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + pubKey, err := derivePubKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive pubkey", + "node", strNode, "error", err) + return nil, err + } + + pubKeyBytes, err := extKeyToPubBytes(pubKey) + if err != nil { + b.Logger().Error("derivePubKey: Failed to get pubkey bytes", + "node", strNode, "error", err) + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "pubkey": hex.EncodeToString(pubKeyBytes), + }, + }, nil +} + +func (b *backend) deriveAndSign(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + tapTweakHex := data.Get("taptweak").(string) + singleTweakHex := data.Get("ln1tweak").(string) + doubleTweakHex := data.Get("ln2tweak").(string) + + numTweaks := int(0) + + if len(singleTweakHex) > 0 { + numTweaks++ + } + if len(doubleTweakHex) > 0 { + numTweaks++ + } + + if numTweaks > 1 { + b.Logger().Error("Both single and double tweak specified") + return nil, ErrTooManyTweaks + } + + strNode := data.Get("node").(string) + + seed, net, err := b.getNode(ctx, req.Storage, strNode) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", strNode, "error", err) + return nil, err + } + defer zero(seed) + + privKey, err := derivePrivKey(seed, net, data.Get("path").([]int)) + if err != nil { + b.Logger().Error("Failed to derive privkey", + "node", strNode, "error", err) + return nil, err + } + defer privKey.Zero() + + err = checkRequiredPubKey(privKey, data.Get("pubkey").(string)) + if err != nil { + // We log here as info because this is expected when signing + // a PSBT. + b.Logger().Info("Pubkey mismatch", + "node", strNode, "error", err) + return nil, err + } + + ecPrivKey, err := privKey.ECPrivKey() + if err != nil { + b.Logger().Error("Failed to derive valid ECDSA privkey", + "node", strNode, "error", err) + return nil, err + } + defer ecPrivKey.Zero() + + signMethod := data.Get("method").(string) + + // Taproot tweak. + var tapTweakBytes []byte + + if len(tapTweakHex) > 0 { + tapTweakBytes, err = hex.DecodeString(tapTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode taptweak hex", + "error", err) + return nil, err + } + } + + if signMethod == "schnorr" { + ecPrivKey = txscript.TweakTaprootPrivKey( + ecPrivKey, + tapTweakBytes, + ) + } + + switch { + // Single commitment tweak as used by SignPsbt. + case len(singleTweakHex) > 0: + singleTweakBytes, err := hex.DecodeString(singleTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode ln1tweak hex", + "error", err) + return nil, err + } + + ecPrivKey = tweakPrivKey( + ecPrivKey, + singleTweakBytes, + ) + + // Double revocation tweak as used by SignPsbt. + case len(doubleTweakHex) > 0: + doubleTweakBytes, err := hex.DecodeString(doubleTweakHex) + if err != nil { + b.Logger().Error("Couldn't decode ln2tweak hex", + "error", err) + return nil, err + } + + doubleTweakKey, _ := btcec.PrivKeyFromBytes(doubleTweakBytes) + ecPrivKey = deriveRevocationPrivKey(ecPrivKey, doubleTweakKey) + } + + digest := data.Get("digest").(string) + if len(digest) != 64 { + b.Logger().Error("Digest is not hex-encoded 32-byte value") + return nil, errors.New("invalid digest") + } + + digestBytes, err := hex.DecodeString(digest) + if err != nil { + b.Logger().Error("Failed to decode digest from hex", + "error", err) + return nil, err + } + + var sigBytes []byte + + // TODO(aakselrod): check derivation paths are sane for the type of + // signature we're requesting. + switch signMethod { + case "ecdsa": + sigBytes = ecdsa.Sign(ecPrivKey, digestBytes).Serialize() + case "ecdsa-compact": + sigBytes, _ = ecdsa.SignCompact(ecPrivKey, digestBytes, true) + case "schnorr": + sig, err := schnorr.Sign(ecPrivKey, digestBytes) + if err != nil { + b.Logger().Error("Failed to sign digest using Schnorr", + "node", strNode, "error", err) + return nil, err + } + + sigBytes = sig.Serialize() + default: + b.Logger().Info("Requested invalid signing method", + "method", signMethod) + return nil, errors.New("invalid signing method") + } + + // We return the pre-tweak pubkey for populating PSBTs and other uses. + pubKeyBytes, err := extKeyToPubBytes(privKey) + if err != nil { + b.Logger().Error("derivePubKey: Failed to get pubkey bytes", + "node", strNode, "error", err) + return nil, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + "signature": hex.EncodeToString(sigBytes), + "pubkey": hex.EncodeToString(pubKeyBytes), + }, + }, nil +} + +func (b *backend) getNode(ctx context.Context, storage logical.Storage, + id string) ([]byte, *chaincfg.Params, error) { + + if len(id) != 2*btcec.PubKeyBytesLenCompressed { + return nil, nil, ErrInvalidNodeID + } + + nodePath := "lnd-nodes/" + id + entry, err := storage.Get(ctx, nodePath) + if err != nil { + return nil, nil, err + } + + if entry == nil { + return nil, nil, ErrNodeNotFound + } + + if len(entry.Value) <= seedLen { + return nil, nil, ErrInvalidSeedFromStorage + } + + net, err := GetNet(string(entry.Value[seedLen:])) + if err != nil { + return nil, nil, err + } + + return entry.Value[:seedLen], net, nil +} + +func (b *backend) listNodes(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + nodes, err := req.Storage.List(ctx, "lnd-nodes/") + if err != nil { + b.Logger().Error("Failed to retrieve the list of nodes", + "error", err) + return nil, err + } + + respData := make(map[string]interface{}) + for _, node := range nodes { + seed, net, err := b.getNode(ctx, req.Storage, node) + if err != nil { + b.Logger().Error("Failed to retrieve node info", + "node", node, "error", err) + return nil, err + } + defer zero(seed) + + netName := net.Name + if netName == "testnet3" { + netName = "testnet" + } + + respData[node] = netName + } + + return &logical.Response{ + Data: respData, + }, nil +} + +func (b *backend) importNode(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNode := data.Get("node").(string) + strNet := data.Get("network").(string) + + seed, err := seedFromSeedAndPassPhrases( + data.Get("seedphrase").(string), + data.Get("passphrase").(string), + ) + if err != nil { + b.Logger().Error("Failed to get seed from seed and "+ + "pass phrases", "error", err) + return nil, err + } + + return b.newNode(ctx, req.Storage, seed, strNet, strNode) +} + +func (b *backend) createNode(ctx context.Context, req *logical.Request, + data *framework.FieldData) (*logical.Response, error) { + + strNet := data.Get("network").(string) + + var seed []byte + defer zero(seed) + + err := hdkeychain.ErrUnusableSeed + for err == hdkeychain.ErrUnusableSeed { + seed, err = hdkeychain.GenerateSeed(seedLen) + } + if err != nil { + b.Logger().Error("Failed to generate new LND seed", + "error", err) + return nil, err + } + + return b.newNode(ctx, req.Storage, seed, strNet, "") +} + +func (b *backend) newNode(ctx context.Context, storage logical.Storage, + seed []byte, strNet, reqKey string) (*logical.Response, error) { + + net, err := GetNet(strNet) + if err != nil { + b.Logger().Error("Failed to parse network", "error", err, + "network", strNet) + return nil, err + } + + nodePubKey, err := derivePubKey(seed, net, []int{ + int(Bip0043purpose + hdkeychain.HardenedKeyStart), + int(net.HDCoinType + hdkeychain.HardenedKeyStart), + int(NodeKeyAcct + hdkeychain.HardenedKeyStart), + 0, + 0, + }) + if err != nil { + b.Logger().Error("Failed to derive node pubkey from LND seed", + "error", err) + return nil, err + } + + pubKeyBytes, err := extKeyToPubBytes(nodePubKey) + if err != nil { + b.Logger().Error("newNode: Failed to get pubkey bytes", + "error", err) + return nil, err + } + + strPubKey := hex.EncodeToString(pubKeyBytes) + + if reqKey != "" && strPubKey != reqKey { + b.Logger().Error("newNode: node pubkey mismatch") + return nil, ErrNodePubkeyMismatch + } + + nodePath := "lnd-nodes/" + strPubKey + + obj, err := storage.Get(ctx, nodePath) + if err != nil { + return nil, err + } + if obj != nil { + b.Logger().Error("newNode: node already exists", + "node", strPubKey) + return nil, ErrNodeAlreadyExists + } + + seed = append(seed, []byte(strNet)...) + err = storage.Put(ctx, &logical.StorageEntry{ + Key: nodePath, + Value: seed, + SealWrap: true, + }) + if err != nil { + b.Logger().Error("Failed to save seed for node", + "node", strPubKey, "error", err) + return nil, err + } + + b.Logger().Info("Wrote new LND seed", "node", strPubKey) + + return &logical.Response{ + Data: map[string]interface{}{ + "node": strPubKey, + }, + }, nil +} + +func GetNet(strNet string) (*chaincfg.Params, error) { + switch strNet { + /*case "mainnet": + return &chaincfg.MainNetParams, nil + */ + case "testnet", "testnet3": + return &chaincfg.TestNet3Params, nil + + case "simnet": + return &chaincfg.SimNetParams, nil + + case "signet": + return &chaincfg.SigNetParams, nil + + case "regtest": + return &chaincfg.RegressionNetParams, nil + + default: + return nil, ErrInvalidNetwork + } +} + +func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, + error) { + + var b backend + b.Backend = &framework.Backend{ + Help: "", + Paths: framework.PathAppend(b.paths()), + PathsSpecial: &logical.Paths{ + SealWrapStorage: []string{ + "lnd-nodes/", + }, + }, + Secrets: []*framework.Secret{}, + BackendType: logical.TypeLogical, + } + + err := b.Setup(ctx, conf) + if err != nil { + return nil, err + } + + return &b, nil +} diff --git a/vault/backend_test.go b/vault/backend_test.go new file mode 100644 index 0000000..b7e331a --- /dev/null +++ b/vault/backend_test.go @@ -0,0 +1,680 @@ +package vault + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "os" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" + filestore "github.com/hashicorp/vault/sdk/physical/file" + "github.com/stretchr/testify/require" +) + +// testContext controls a vault plugin with storage in a temporary directory. +type testContext struct { + // Basic test context info + t *testing.T + cancel context.CancelFunc + + // tmpDir tracks where we created a temp directory to delete at the end. + tmpDir string + + // storage tracks the logical storage object for requests. + storage logical.Storage + + // Plugin back end to test against. + backEnd *backend +} + +// newTestContext creates a new test context from the test environment. +func newTestContext(t *testing.T) *testContext { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + + tmpDir, err := os.MkdirTemp("", "vault-plugin-lndsigner") + require.NoError(t, err) + + logger := hclog.Default() + + // Create storage in a temp directory. When we use in-memory storage, + // some request and response data is passed by reference and gets + // zeroed out inappropriately. This doesn't happen when copies are + // made by the storage backend. + pStorage, err := filestore.NewFileBackend( + map[string]string{"path": tmpDir}, + logger, + ) + require.NoError(t, err) + + storage := logical.NewLogicalStorage(pStorage) + + b, err := Factory(ctx, &logical.BackendConfig{ + StorageView: storage, + Logger: logger, + }) + require.NoError(t, err) + + return &testContext{ + t: t, + cancel: cancel, + tmpDir: tmpDir, + storage: storage, + backEnd: b.(*backend), + } +} + +// Close cancels the test context's inner context and deletes the temporary +// directory. +func (tctx *testContext) Close() { + tctx.t.Helper() + + tctx.cancel() + + require.NoError(tctx.t, os.RemoveAll(tctx.tmpDir)) +} + +// call sends a request to perform an operation to the plugin backend, and +// returns the response. +func (tctx *testContext) call(path *framework.Path, op logical.Operation, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return path.Operations[op].Handler()(context.Background(), + &logical.Request{Storage: tctx.storage}, + &framework.FieldData{ + Schema: path.Fields, + Raw: data, + }, + ) +} + +// update sends an update call to the plugin backend on the specified path, and +// returns the response. +func (tctx *testContext) update(path *framework.Path, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return tctx.call(path, logical.UpdateOperation, data) +} + +// read sends a read call to the plugin backend on the specified path, and +// returns the response. +func (tctx *testContext) read(path *framework.Path, + data map[string]interface{}) (*logical.Response, error) { + + tctx.t.Helper() + + return tctx.call(path, logical.ReadOperation, data) +} + +// ecdh calls the ecdh endpoint on the plugin backend, and returns the shared +// key. +func (tctx *testContext) ecdh(data map[string]interface{}) (*logical.Response, + error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.ecdhPath(), data) +} + +// createNode creates a node on the plugin backend with the specified network, +// and returns the node ID. +func (tctx *testContext) createNode(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.basePath(), data) +} + +// importNode imports a node to the plugin backend given the specified network, +// seed phrase, and optional passphrase, and returns the node ID. +func (tctx *testContext) importNode(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.importPath(), data) +} + +// listAccounts returns a JSON account list compatible with lnd's +// `lncli createwatchonly` command given a node ID. +func (tctx *testContext) listAccounts(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.read(tctx.backEnd.accountsPath(), data) +} + +// listNodes lists all of the nodes stored in the plugin backend's storage and +// the network name for each. +func (tctx *testContext) listNodes() (*logical.Response, error) { + tctx.t.Helper() + + return tctx.read(tctx.backEnd.basePath(), map[string]interface{}{}) +} + +// derivePubkey requests a pubkey given a node ID and derivation path, and +// returns the derived pubkey. +func (tctx *testContext) derivePubkey(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.read(tctx.backEnd.signPath(), data) +} + +// sign requests a signature given a node ID, derivation path, algorithm, +// optional tweaks, and returns the signature and derived pubkey. +func (tctx *testContext) sign(data map[string]interface{}) ( + *logical.Response, error) { + + tctx.t.Helper() + + return tctx.update(tctx.backEnd.signPath(), data) +} + +// TestECDH tests the ECDH endpoint. It's not fully tested because we don't +// have the ability to do deterministic tests without key import. +func TestECDH(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check parsing of peer pubkey. + _, err := tctx.ecdh(map[string]interface{}{ + "peer": "abcdef", + }) + require.ErrorIs(t, err, ErrInvalidPeerPubkey) + + // Check parsing of node pubkey. + _, err = tctx.ecdh(map[string]interface{}{ + "peer": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "node": "abcdef", + }) + require.ErrorIs(t, err, ErrInvalidNodeID) + + // Check that a request for a nonexistent node returns the correct error. + _, err = tctx.ecdh(map[string]interface{}{ + "peer": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "node": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + }) + require.ErrorIs(t, err, ErrNodeNotFound) + + // Create a node for more ECDH checks. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that a request for the wrong pubkey returns the correct error. + _, err = tctx.ecdh(map[string]interface{}{ + "node": createdNode, + "pubkey": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ef", + "peer": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrPubkeyMismatch) + + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Check correct derivation of shared key. + resp, err = tctx.ecdh(map[string]interface{}{ + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "pubkey": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + "peer": "02252bb0fdf7f6e7c055c5419c6fa1c9799cf348b480603b9c0af61dbdea29149e", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["sharedkey"].(string), + "7895c217d4f1a33265c0122ce66dd16bcd0b86976198f1128e6dbaef86a2f327", + ) +} + +// TestListNodes checks that it's possible to list all of the nodes ever +// created in storage. +func TestListNodes(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check that the node list is empty. + resp, err := tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{}, + }) + + // Create a node. + resp, err = tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that our new node is in the list. + resp, err = tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{ + createdNode: "regtest", + }, + }) + + // Import without passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + ) + + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Check that all our nodes are in the list. + resp, err = tctx.listNodes() + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{ + Data: map[string]interface{}{ + createdNode: "regtest", + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf": "regtest", + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6": "regtest", + }, + }) + +} + +// TestDerivePubkey checks that public keys are derived correctly. +func TestDerivePubkey(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Create a node. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check that our new node derives its node pubkey correctly. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp.Data["pubkey"].(string), createdNode) + + // Check for ErrWrongLengthDerivationPath. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{2147484665, 2147483649, 2147483654, 0, 0, 0}, + }) + require.ErrorIs(t, err, ErrWrongLengthDerivationPath) + require.Nil(t, resp) + + // Check for ErrNegativeElement. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{-1, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrNegativeElement) + require.Nil(t, resp) + + // Check for ErrElementOverflow. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{22147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrElementOverflow) + require.Nil(t, resp) + + // Check for ErrElementNotHardened. + resp, err = tctx.derivePubkey(map[string]interface{}{ + "node": createdNode, + "path": []int{1017, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrElementNotHardened) + require.Nil(t, resp) +} + +// TestSign checks that the plugin backend signs digests properly. It's not +// yet fully tested because we don't have deterministic tests without key +// import. +func TestSign(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Create a node. + resp, err := tctx.createNode(map[string]interface{}{ + "network": "regtest", + }) + require.NoError(t, err) + + // Get the new node's pubkey. + createdNode := resp.Data["node"].(string) + require.Equal(t, 66, len(createdNode)) + + // Check for ErrTooManyTweaks. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "ln2tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, ErrTooManyTweaks) + require.Nil(t, resp) + + // Check for invalid hex in ln1tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, hex.InvalidByteError(0x67)) + require.Nil(t, resp) + + // Check for invalid hex in ln2tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": createdNode, + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln2tweak": "g123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.ErrorIs(t, err, hex.InvalidByteError(0x67)) + require.Nil(t, resp) + + // Import without passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }}) + + // Sign ECDSA. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b0220219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign ECDSA ignoring taptweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "taptweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b0220219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign ECDSA with single tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln1tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3045022100ac182be53be9ce5a94565bf21fffd640e56dc10631fbe6f7d75e1ef03f7e23ff022010f917056b002695f33281c6f569de0e2934be966f7d9c36669d93a56530ca9b", + }}) + + // Sign ECDSA with double tweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "ln2tweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "ecdsa", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "3044022067e3a4a3b40592e10dc08e8b585e1b2a00c3f3e906f8d7959642102ebc977d4302202527213f7f795e2d45849c8a147cf39ed8f6246141c6f092f51b0bde53eb3d49", + }}) + + // Sign ECDSA compact. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "ecdsa-compact", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "20d5e9e57012d5bcce055e17a1b467a7b00c9c29e33bcca2aaa23a991452f3d10b219595c988f0e3c3acccb4ccdd856c662a9f462ae02d82243306fb0f316ea872", + }}) + + // Sign Schnorr. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "method": "schnorr", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "71b77d9c8a0badfa7c4eca3fbef5da2a552bf032f56b85fbc5c2f3500498fc20d5ab8505ae9733b1b756da7a5dba41dbe069dd0d86793618829c3077df0cd759", + }}) + + // Sign Schnorr with taptweak. + resp, err = tctx.sign(map[string]interface{}{ + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "digest": "9d3d4b1c81f2554200ccc05635f01c008f1be1fe7164bf39c1dd83a6a1eec7df", + "taptweak": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "method": "schnorr", + "path": []int{2147484665, 2147483649, 2147483654, 0, 0}, + }) + require.NoError(t, err) + require.Equal(t, resp, &logical.Response{Data: map[string]interface{}{ + "pubkey": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + "signature": "e4112ae8f73f1d13a6128ddbde38f8bae00fbe9d6e1c3c330b5856e1587c593d9ed050c5f502ea80ab5bcc1a4ebcd4b3e0bfbbb5312591427d582613982c42a5", + }}) +} + +func TestImportNode(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Check for ErrSeedPhraseWrongLength. + _, err := tctx.importNode(map[string]interface{}{}) + require.ErrorIs(t, err, ErrSeedPhraseWrongLength) + + // Check for ErrSeedPhraseNotBIP39. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "absent weks slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + }) + require.ErrorIs(t, err, ErrSeedPhraseNotBIP39) + + // Check for ErrBadCipherSeedVer. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "walnut absent slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + }) + require.ErrorIs(t, err, ErrBadCipherSeedVer) + + // Check for ErrChecksumMismatch. + _, err = tctx.importNode(map[string]interface{}{ + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall fall", + }) + require.ErrorIs(t, err, ErrChecksumMismatch) + + // Check for ErrInvalidPassphrase. + _, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.ErrorIs(t, err, ErrInvalidPassphrase) + + // Check for ErrNodePubkeyMismatch. + _, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ab", + }) + require.ErrorIs(t, err, ErrNodePubkeyMismatch) + + // Check for ErrInvalidNetwork. + _, err = tctx.importNode(map[string]interface{}{ + "network": "mainnet", // TODO(aakselrod): change this before going live on mainnet + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25ab", + }) + require.ErrorIs(t, err, ErrInvalidNetwork) + + // Import without passphrase. + resp, err := tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "absent walnut slam olive squeeze cluster blame express asthma gym force warfare physical stuff unusual tiny endless patient again sound deny identify fall guard", + "node": "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "023cf344b017a3c91bdb2c9c076da267555f0c0748099418ea1f558a624ced1ac6", + ) + + // Import with passphrase. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Import over an existing node should fail. + resp, err = tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.ErrorIs(t, err, ErrNodeAlreadyExists) + require.Nil(t, resp) +} + +func TestListAccounts(t *testing.T) { + t.Parallel() + + tctx := newTestContext(t) + defer tctx.Close() + + // Import with passphrase. + resp, err := tctx.importNode(map[string]interface{}{ + "network": "regtest", + "seedphrase": "abstract inch live custom just tray hockey enroll upon friend mass author filter desert parrot network finger uniform alley artefact path palace chicken diet", + "passphrase": "weks1234", + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + require.Equal(t, + resp.Data["node"].(string), + "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + ) + + // Get account list. + resp, err = tctx.listAccounts(map[string]interface{}{ + "node": "03c7926302ac72f51ef009dc169561734414b3c6bfd9fb0dc42cac93101c3c25bf", + }) + require.NoError(t, err) + + // Check against expected digest. + acctList, ok := resp.Data["acctList"].(string) + require.True(t, ok) + digest := sha256.Sum256([]byte(acctList)) + digestHex := hex.EncodeToString(digest[:]) + require.Equal(t, + digestHex, + "223b82c397cbccce80c5c5e33c993e332909e093bc5ca3398266f7a5e0f48806", + ) +} diff --git a/vault/errors.go b/vault/errors.go new file mode 100644 index 0000000..47551cb --- /dev/null +++ b/vault/errors.go @@ -0,0 +1,28 @@ +package vault + +import ( + "errors" +) + +var ( + ErrSeedPhraseWrongLength = errors.New("seed phrase must be 24 words") + ErrInvalidPassphrase = errors.New("invalid passphrase") + ErrSeedPhraseNotBIP39 = errors.New("seed phrase must use BIP39 word list") + ErrBadCipherSeedVer = errors.New("cipher seed version not recognized") + ErrWrongLengthChecksum = errors.New("wrong length checksum") + ErrChecksumMismatch = errors.New("checksum mismatch") + ErrWrongInternalVersion = errors.New("wrong internal version") + ErrNodeAlreadyExists = errors.New("node already exists") + ErrNodePubkeyMismatch = errors.New("node pubkey mismatch") + ErrInvalidNetwork = errors.New("invalid network") + ErrInvalidPeerPubkey = errors.New("invalid peer pubkey") + ErrInvalidNodeID = errors.New("invalid node id") + ErrNodeNotFound = errors.New("node not found") + ErrInvalidSeedFromStorage = errors.New("invalid seed from storage") + ErrElementNotHardened = errors.New("derivation path element not hardened") + ErrNegativeElement = errors.New("negative derivation path element") + ErrWrongLengthDerivationPath = errors.New("derivation path not 5 elements") + ErrElementOverflow = errors.New("derivation path element > MaxUint32") + ErrPubkeyMismatch = errors.New("pubkey mismatch") + ErrTooManyTweaks = errors.New("both single and double tweak specified") +) diff --git a/vault/keys.go b/vault/keys.go new file mode 100644 index 0000000..39fc339 --- /dev/null +++ b/vault/keys.go @@ -0,0 +1,277 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "math" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" +) + +const ( + // MaxAcctID is the number of accounts/key families to create on + // initialization. + MaxAcctID = 255 + + Bip0043purpose = 1017 + NodeKeyAcct = 6 +) + +var ( + // defaultPurposes is a list of non-LN(1017) purposes for which we + // should create a m/purpose'/0'/0' account as well as their default + // address types. + defaultPurposes = []struct { + purpose uint32 + addrType string + hdVersion [2][4]byte + }{ + { + purpose: 49, + addrType: "HYBRID_NESTED_WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x9d, 0x7c, 0xb2}, // ypub + [4]byte{0x04, 0x4a, 0x52, 0x62}, // upub + }, + }, + { + purpose: 84, + addrType: "WITNESS_PUBKEY_HASH", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0xb2, 0x47, 0x46}, // zpub + [4]byte{0x04, 0x5f, 0x1c, 0xf6}, // vpub + }, + }, + { + purpose: 86, + addrType: "TAPROOT_PUBKEY", + hdVersion: [2][4]byte{ + [4]byte{0x04, 0x88, 0xb2, 0x1e}, // xpub + [4]byte{0x04, 0x35, 0x87, 0xcf}, // tpub + }, + }, + } +) + +func extKeyToPubBytes(key *hdkeychain.ExtendedKey) ([]byte, error) { + ecPubKey, err := key.ECPubKey() + if err != nil { + return nil, err + } + + return ecPubKey.SerializeCompressed(), nil +} + +func checkRequiredPubKey(derived *hdkeychain.ExtendedKey, + required string) error { + + if required == "" { + return nil + } + + pubKeyBytes, err := extKeyToPubBytes(derived) + if err != nil { + return err + } + + requiredBytes, err := hex.DecodeString(required) + if err != nil { + return err + } + + if !bytes.Equal(requiredBytes, pubKeyBytes) { + return ErrPubkeyMismatch + } + + return nil +} + +func derivePrivKey(seed []byte, net *chaincfg.Params, + derivationPath []int) (*hdkeychain.ExtendedKey, error) { + + if len(derivationPath) != 5 { + return nil, ErrWrongLengthDerivationPath + } + + derPath := make([]uint32, 5) + + for idx, element := range derivationPath { + if element < 0 { + return nil, ErrNegativeElement + } + + if element > math.MaxUint32 { + return nil, ErrElementOverflow + } + + if idx < 3 && element < hdkeychain.HardenedKeyStart { + return nil, ErrElementNotHardened + } + + derPath[idx] = uint32(element) + } + + rootKey, err := hdkeychain.NewMaster(seed, net) + if err != nil { + return nil, err + } + defer rootKey.Zero() + + // Derive purpose. We do these derivations with DeriveNonStandard to + // match btcwallet's (and thus lnd's) usage as shown here: + // https://github.com/btcsuite/btcwallet/blob/c314de6995500686c93716037f2279128cc1e9e8/waddrmgr/manager.go#L1459 + purposeKey, err := rootKey.DeriveNonStandard( // nolint:staticcheck + derPath[0], + ) + if err != nil { + return nil, err + } + defer purposeKey.Zero() + + // Derive coin type. + coinTypeKey, err := purposeKey.DeriveNonStandard( // nolint:staticcheck + derPath[1], + ) + if err != nil { + return nil, err + } + defer coinTypeKey.Zero() + + // Derive account. + accountKey, err := coinTypeKey.DeriveNonStandard( // nolint:staticcheck + derPath[2], + ) + if err != nil { + return nil, err + } + defer accountKey.Zero() + + // Derive branch. + branchKey, err := accountKey.DeriveNonStandard( // nolint:staticcheck + derPath[3], + ) + if err != nil { + return nil, err + } + defer branchKey.Zero() + + // Derive index. + indexKey, err := branchKey.DeriveNonStandard( // nolint:staticcheck + derPath[4], + ) + if err != nil { + return nil, err + } + + return indexKey, nil +} + +func derivePubKey(seed []byte, net *chaincfg.Params, derivationPath []int) ( + *hdkeychain.ExtendedKey, error) { + + privKey, err := derivePrivKey(seed, net, derivationPath) + if err != nil { + return nil, err + } + + return privKey.Neuter() +} + +// zero sets all bytes in the passed slice to zero. This is used to +// explicitly clear private key material from memory. +func zero(b []byte) { + lenb := len(b) + for i := 0; i < lenb; i++ { + b[i] = 0 + } +} + +// tweakPrivKey tweaks the private key of a public base point given a per +// commitment point. The per commitment secret is the revealed revocation +// secret for the commitment state in question. This private key will only need +// to be generated in the case that a channel counter party broadcasts a +// revoked state. Precisely, the following operation is used to derive a +// tweaked private key: +// +// - tweakPriv := basePriv + sha256(commitment || basePub) mod N +// +// Where N is the order of the sub-group. +func tweakPrivKey(basePriv *btcec.PrivateKey, + commitTweak []byte) *btcec.PrivateKey { + + // tweakInt := sha256(commitPoint || basePub) + tweakScalar := new(btcec.ModNScalar) + tweakScalar.SetByteSlice(commitTweak) + + tweakScalar.Add(&basePriv.Key) + + return &btcec.PrivateKey{Key: *tweakScalar} +} + +// singleTweakBytes computes set of bytes we call the single tweak. The purpose +// of the single tweak is to randomize all regular delay and payment base +// points. To do this, we generate a hash that binds the commitment point to +// the pay/delay base point. The end end results is that the basePoint is +// tweaked as follows: +// +// - key = basePoint + sha256(commitPoint || basePoint)*G +func singleTweakBytes(commitPoint, basePoint *btcec.PublicKey) []byte { + h := sha256.New() + h.Write(commitPoint.SerializeCompressed()) + h.Write(basePoint.SerializeCompressed()) + return h.Sum(nil) +} + +// deriveRevocationPrivKey derives the revocation private key given a node's +// commitment private key, and the preimage to a previously seen revocation +// hash. Using this derived private key, a node is able to claim the output +// within the commitment transaction of a node in the case that they broadcast +// a previously revoked commitment transaction. +// +// The private key is derived as follows: +// +// revokePriv := (revokeBasePriv * sha256(revocationBase || commitPoint)) + +// (commitSecret * sha256(commitPoint || revocationBase)) mod N +// +// Where N is the order of the sub-group. +func deriveRevocationPrivKey(revokeBasePriv *btcec.PrivateKey, + commitSecret *btcec.PrivateKey) *btcec.PrivateKey { + + // r = sha256(revokeBasePub || commitPoint) + revokeTweakBytes := singleTweakBytes( + revokeBasePriv.PubKey(), commitSecret.PubKey(), + ) + revokeTweakScalar := new(btcec.ModNScalar) + revokeTweakScalar.SetByteSlice(revokeTweakBytes) + + // c = sha256(commitPoint || revokeBasePub) + commitTweakBytes := singleTweakBytes( + commitSecret.PubKey(), revokeBasePriv.PubKey(), + ) + commitTweakScalar := new(btcec.ModNScalar) + commitTweakScalar.SetByteSlice(commitTweakBytes) + + // Finally to derive the revocation secret key we'll perform the + // following operation: + // + // k = (revocationPriv * r) + (commitSecret * c) mod N + // + // This works since: + // P = (G*a)*b + (G*c)*d + // P = G*(a*b) + G*(c*d) + // P = G*(a*b + c*d) + revokeHalfPriv := revokeTweakScalar.Mul(&revokeBasePriv.Key) + commitHalfPriv := commitTweakScalar.Mul(&commitSecret.Key) + + revocationPriv := revokeHalfPriv.Add(commitHalfPriv) + + return &btcec.PrivateKey{Key: *revocationPriv} +} diff --git a/vault/paths.go b/vault/paths.go new file mode 100644 index 0000000..5610728 --- /dev/null +++ b/vault/paths.go @@ -0,0 +1,241 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package vault + +import ( + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +// TODO(aakselrod): expand text documentation throughout this file where +// fields are available, in order to auto-generate docs. +func wrapOp(f framework.OperationFunc) framework.OperationHandler { + return &framework.PathOperation{ + Callback: f, + } +} + +func (b *backend) basePath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.listNodes), + logical.UpdateOperation: wrapOp(b.createNode), + }, + HelpSynopsis: "Create and list LND nodes", + HelpDescription: ` + +GET - list all node pubkeys and coin types for HD derivations +POST - generate a new node seed and store it indexed by node pubkey + +`, + Fields: map[string]*framework.FieldSchema{ + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: "regtest", + }, + }, + } +} + +func (b *backend) importPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/import/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: wrapOp(b.importNode), + }, + HelpSynopsis: "Import existing LND node into vault", + HelpDescription: ` + +POST - import existing LND node into vault with seedphrase and passphrase + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: node pubkey, " + + "must be 66 hex characters", + Default: "", + }, + "network": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Network, one of " + + "'mainnet', 'testnet', " + + "'simnet', 'signet', or " + + "'regtest'", + Default: "regtest", + }, + "seedphrase": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "seed phrase to import, " + + "use instead of seed", + Default: "", + }, + "passphrase": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: passphrase, " + + "use only with seed phrase", + Default: "", + }, + }, + } +} + +func (b *backend) accountsPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/accounts/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.listAccounts), + }, + HelpSynopsis: "List accounts for import into LND " + + "watch-only node", + HelpDescription: ` + +GET - list all node accounts in JSON format suitable for import into watch- +only LND + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + }, + } +} + +func (b *backend) ecdhPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/ecdh/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: wrapOp(b.ecdh), + }, + HelpSynopsis: "ECDH derived privkey with peer pubkey", + HelpDescription: ` + +POST - ECDH the privkey derived with the submitted path with the specified +peer pubkey + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to do ECDH, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "peer": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "pubkey for ECDH peer, " + + "must be 66 hex characters", + Default: "", + }, + }, + } +} + +func (b *backend) signPath() *framework.Path { + return &framework.Path{ + Pattern: "lnd-nodes/sign/?", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: wrapOp(b.derivePubKey), + logical.UpdateOperation: wrapOp(b.deriveAndSign), + }, + HelpSynopsis: "Derive pubkeys and sign with privkeys", + HelpDescription: ` + +GET - return the pubkey derived with the submitted path +POST - sign a digest with the method specified using the privkey derived with +the submitted path + +`, + Fields: map[string]*framework.FieldSchema{ + "node": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "node pubkey, must be " + + "66 hex characters", + Default: "", + }, + "path": &framework.FieldSchema{ + Type: framework.TypeCommaIntSlice, + Description: "derivation path, with " + + "the first 3 elements " + + "being hardened", + Default: []int{}, + }, + "digest": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "digest to sign, must " + + "be hex-encoded 32 bytes", + Default: "", + }, + "method": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "signing method: " + + "one of: ecdsa, " + + "ecdsa-compact, or schnorr", + Default: "", + }, + "pubkey": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: pubkey for " + + "which to sign, checked " + + "against derived pubkey to " + + "ensure a match", + Default: "", + }, + "taptweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "taproot tweak", + Default: "", + }, + "ln1tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN single commit tweak", + Default: "", + }, + "ln2tweak": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "optional: hex-encoded " + + "LN double revocation tweak", + Default: "", + }, + }, + } +} + +func (b *backend) paths() []*framework.Path { + return []*framework.Path{ + b.basePath(), + b.importPath(), + b.accountsPath(), + b.ecdhPath(), + b.signPath(), + } +} diff --git a/walletkit_server.go b/walletkit_server.go new file mode 100644 index 0000000..83df2ae --- /dev/null +++ b/walletkit_server.go @@ -0,0 +1,86 @@ +// Copyright (C) 2013-2017 The btcsuite developers +// Copyright (C) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers +// Copyright (C) 2022 Bottlepay and The Lightning Network Developers + +package lndsigner + +import ( + "bytes" + "context" + "fmt" + + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/nydig-oss/lndsigner/proto" +) + +// walletKit is a sub-RPC server that exposes a tool kit which allows clients +// to execute common wallet operations. This includes requesting new addresses, +// keys (for contracts!), and publishing transactions. +type walletKit struct { + // Required by the grpc-gateway/v2 library for forward compatibility. + proto.UnimplementedWalletKitServer + + server *rpcServer +} + +// A compile time check to ensure that walletKit fully implements the +// proto.WalletKitServer gRPC service. +var _ proto.WalletKitServer = (*walletKit)(nil) + +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) +// set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lndsignerd was the last signer. +// +// NOTE: This RPC only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (w *walletKit) SignPsbt(_ context.Context, req *proto.SignPsbtRequest) ( + *proto.SignPsbtResponse, error) { + + packet, err := psbt.NewFromRawBytes( + bytes.NewReader(req.FundedPsbt), false, + ) + if err != nil { + signerLog.Debugf("Error parsing PSBT: %v, raw input: %x", err, + req.FundedPsbt) + return nil, fmt.Errorf("error parsing PSBT: %v", err) + } + + // Before we attempt to sign the packet, ensure that every input either + // has a witness UTXO, or a non witness UTXO. + for idx := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Doesn't have either a witness or non witness UTXO so we need + // to exit here as otherwise signing will fail. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + return nil, fmt.Errorf("input (index=%v) doesn't "+ + "specify any UTXO info", idx) + } + } + + // Let the wallet do the heavy lifting. This will sign all inputs that + // we have the UTXO for. If some inputs can't be signed and don't have + // witness data attached, they will just be skipped. + signedInputs, err := w.server.keyRing.SignPsbt(packet) + if err != nil { + return nil, fmt.Errorf("error signing PSBT: %v", err) + } + + // Serialize the signed PSBT in both the packet and wire format. + var signedPsbtBytes bytes.Buffer + err = packet.Serialize(&signedPsbtBytes) + if err != nil { + return nil, fmt.Errorf("error serializing PSBT: %v", err) + } + + return &proto.SignPsbtResponse{ + SignedPsbt: signedPsbtBytes.Bytes(), + SignedInputs: signedInputs, + }, nil +}