Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
pull_request:
branches: [develop]
push:
branches: [develop]
tags: ["v*"]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Restore dependencies
run: dotnet restore "NodeGuard Remote Signer.sln"

- name: Build
run: dotnet build "NodeGuard Remote Signer.sln" --configuration Release --no-restore

- name: Test
run: dotnet test "NodeGuard Remote Signer.sln" --configuration Release --no-build --verbosity normal

build:
name: Build Docker image
needs: [test]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref_name == 'develop' }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./RemoteSigner
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
124 changes: 113 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
# NodeGuard Remote Signer
AWS Lambda function in dotnet to allow the remote signing of transactions in NodeGuard

AWS Lambda function in dotnet to allow the remote signing of transactions in NodeGuard.

## Trusted coordinator signing

The NodeGuard has two modes of signing transactions as as Trusted coordinator (it is mandatory for security reasons, since the FM relies on SIGHASH_NONE inputs from finance managers):

1. Embedded (legacy) signing withing the NodeGuard, less secure but easier to manage
2. Use a remote signing function (AWS Lambda function with public URL) to sign with a AWS KMS encrypted seedphrase that only the function can decrypt. Bear in mind that the AWS KMS symmetric encryption private key is never exposed and it is managed by AWS KMS.

To enable mode #1 set env var as follows `ENABLE_REMOTE_SIGNER = false`, otherwise in case you want mode #2 you need a lambda function deployed with this project with a public function URL and a AWS KMS Symmetric encryption key. Then set up the following env vars as in this example (json-like):

```
"ENABLE_REMOTE_SIGNER": "true",
"AWS_ACCESS_KEY_ID": "********",
"AWS_SECRET_ACCESS_KEY": "********",
"AWS_REGION": "eu-west-1",
"AWS_KMS_KEY_ID": "mrk-cec3e3ef59bc4616a6f44da60bfea0ba",
"AWS_KMS_KEY_ID": "mrk-cec3e3ef59bc4616a6f44da60bfea0ba",
"REMOTE_SIGNER_ENDPOINT": "https://*.lambda-url.eu-west-1.on.aws/"
```

They are detailed as follows:
- AWS_ACCESS_KEY_ID: IAM-based user account id used to auth against AWS lambda

- AWS_ACCESS_KEY_ID: IAM-based user account id used to auth against AWS lambda
- AWS_SECRET_ACCESS_KEY: IAM-based user secret
- AWS_REGION: the region code of the AWS deployed lambda function
- REMOTE_SIGNER_ENDPOINT: AWS Function url endpoint, check https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html
Expand All @@ -25,8 +31,8 @@ They are detailed as follows:

The key aspect when invoking the lambda function is to use the AWS API Gateway format for payloads, check [AWS Function URL invocation basics](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html).


Example input to AWS lambda function:

```json
{
"Version": null,
Expand All @@ -43,7 +49,9 @@ Example input to AWS lambda function:
"StageVariables": null
}
```

Request input body fields:

- Psbt: The base64-encoded PSBT to sign
- EnforcedSighash: used to enforce a SIGHASH mode signing for all the inputs, 1 means SIGHASH_ALL, check NBitcoin enums to match other sighash types.
- Network: Bitcoin network, this is case-insensitive as long the values are either one of the following `Mainnet, Regtest, Testnet`
Expand All @@ -59,21 +67,26 @@ Example output from the lambda function:
```

Request output body fields:

- Psbt: The base64-encoded signed PSBT

### Encrypted Seedphrase generation
Right now, the easiest way to encrypt a wallet seedphrase (AKA Mnemomnic) is to use the function `EncryptSeedphrase` in the `Function.cs` class in the Remote signer by invoking a unit test to generate an encrypted seedphrase which is in the `FunctionTest.cs` named `GenerateEncryptedSeedTest`. Take into account that you must use [AWS SDK Credentials for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to call AWS KMS.

Right now, the easiest way to encrypt a wallet seedphrase (AKA Mnemomnic) is to use the function `EncryptSeedphrase` in the `Function.cs` class in the Remote signer by invoking a unit test to generate an encrypted seedphrase which is in the `FunctionTest.cs` named `GenerateEncryptedSeedTest`. Take into account that you must use [AWS SDK Credentials for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to call AWS KMS.

### Setting the function main config

The lambda function uses environment variables as a key-value dictionary for configuration of the different wallets that can be used to sign, the dictionary keys are the master fingerprints of the different wallets while the value of the keys are the configuration of the lambda function.

The environment variable key must start with a prefix as `MF_{Master Fingerprint}` (e.g. MF_ed0210c8)

The configuration has two fields:
* EncryptedSeedphrase: The encrypted seedphrase as explained [above](#encrypted-seedphrase-generation)
* AwsKmsKeyId: Symmetric key generated by AWS KMS which decrypts the seedphrase

- EncryptedSeedphrase: The encrypted seedphrase as explained above
- AwsKmsKeyId: Symmetric key generated by AWS KMS which decrypts the seedphrase

Example (json-like structure of key-value, `ed0210c8` is the master fingerprint of the wallet):

```json
{
"MF_ed0210c8": {
Expand All @@ -82,14 +95,103 @@ Example (json-like structure of key-value, `ed0210c8` is the master fingerprint
}}
```

## CI pipeline

The project uses GitHub Actions for continuous integration. The workflow is defined in `.github/workflows/ci.yml` and triggers on three events:

### Pull requests to develop

When a pull request targets the `develop` branch, the pipeline:

1. Runs the .NET test suite (restore, build, test)
2. Builds the Docker image (no push)

This validates that both the code and the Docker build are correct before merging.

### Push to develop

When code is merged into `develop`, the pipeline:

1. Runs the .NET test suite
2. Builds the Docker image
3. Pushes the image to GHCR with two tags:
- `develop` (branch name)
- `latest`

### Tag push (releases)

When a tag matching `v*` is pushed (e.g. `v1.2.3`), the pipeline:

1. Runs the .NET test suite
2. Builds the Docker image
3. Pushes the image to GHCR with the tag name as the Docker tag (e.g. `v1.2.3`)

### Workflow diagram

```
Pull request (develop) Push to develop Push tag v*
| | |
run tests run tests run tests
| | |
build image build image build image
| | |
(no push) push to GHCR push to GHCR
tags: latest, develop tag: <semver>
```

## Using the image

### Pull from GHCR

```bash
docker pull ghcr.io/elenpay/nodeguard-remote-signer:latest
docker pull ghcr.io/elenpay/nodeguard-remote-signer:develop
docker pull ghcr.io/elenpay/nodeguard-remote-signer:v1.2.3
```

### Push to ECR for Lambda deployment

The image is consumed by an AWS Lambda function. To deploy it to your own AWS account, pull from GHCR, tag for ECR, and push.

Replace the account ID and region with your own values.

```bash
# Authenticate with your ECR registry
aws ecr get-login-password --region <region> | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com

# Create the repository if it does not exist
aws ecr create-repository --repository-name nodeguardremotesigner --region <region>

# Pull from GHCR
docker pull ghcr.io/elenpay/nodeguard-remote-signer:latest

# Tag for ECR
docker tag ghcr.io/elenpay/nodeguard-remote-signer:latest \
<account-id>.dkr.ecr.<region>.amazonaws.com/nodeguardremotesigner:latest

# Push to ECR
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/nodeguardremotesigner:latest

# Update the Lambda function
aws lambda update-function-code \
--function-name arn:aws:lambda:<region>:<account-id>:function:SignPSBT-<env> \
--image-uri <account-id>.dkr.ecr.<region>.amazonaws.com/nodeguardremotesigner:latest \
--publish
```

Replace `<account-id>`, `<region>`, and `<env>` (stg or prod) with your actual values.

## Deploying the lambda function

### Requirements
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html)
* [JUST](https://github.com/casey/just)
* Access to the AWS account where the lambda function will be deployed

- AWS CLI
- Access to the AWS account where the lambda function will be deployed

### Deploy

```bash
just deploy
# Pull the image from GHCR, tag for ECR, push, and update the Lambda function
# See instructions above for the full sequence
```
6 changes: 3 additions & 3 deletions RemoteSigner/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ RUN dotnet build "RemoteSigner.csproj" --configuration Release --output /app/bui

FROM build AS publish
RUN dotnet publish "RemoteSigner.csproj" \
--configuration Release \
--configuration Release \
--runtime linux-x64 \
--self-contained false \
--self-contained false \
--output /app/publish \
-p:PublishReadyToRun=true
-p:PublishReadyToRun=true

FROM base AS final
WORKDIR /var/task
Expand Down
Loading