]
[--profile ] [--region ]
Preflight gate: KMS-decrypt the manifest's env value and check that the
fingerprint, env var name and account xpub all re-derive identically.
+ With --spark-account, also print the seed's Spark identity at that account.
Exit codes: 0 ok, 1 verification/derivation failure, 2 usage error, 3 AWS/KMS error.
AWS credentials come from the default chain (env vars, profiles, SSO) — never from flags.
@@ -57,7 +58,7 @@ The mnemonic is never accepted as a command-line argument.
"encrypt" => await EncryptCommand.Run(Options.Parse(commandArgs,
"--kms-key-id", "--network", "--derivation-path", "--seed-file", "--out", "--output", "--profile", "--region")),
"verify" => await VerifyCommand.Run(Options.Parse(commandArgs,
- "--in", "--xpub", "--profile", "--region")),
+ "--in", "--xpub", "--spark-account", "--profile", "--region")),
_ => throw new UsageException($"Unknown command '{command}'")
};
}
diff --git a/RemoteSigner.Tests/SeedCeremonyTest.cs b/RemoteSigner.Tests/SeedCeremonyTest.cs
index 2cceabc..fb16121 100644
--- a/RemoteSigner.Tests/SeedCeremonyTest.cs
+++ b/RemoteSigner.Tests/SeedCeremonyTest.cs
@@ -4,6 +4,7 @@
using Amazon.KeyManagementService.Model;
using FluentAssertions;
using NBitcoin;
+using NSpark.Signer;
using RemoteSigner.SeedCeremony;
using Xunit;
@@ -17,6 +18,17 @@ public class SeedCeremonyTest
private const string DevMnemonic =
"middle teach digital prefer fiscal theory syrup enter crash muffin easily anxiety ill barely eagle swim volume consider dynamic unaware deputy middle into physical";
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public async Task SparkIdentityPublicKey_IsTheIdentityTheSignerServes(int account)
+ {
+ var served = await SparkSigner.FromMnemonic(DevMnemonic, account).GetIdentityPublicKeyAsync();
+
+ Ceremony.SparkIdentityPublicKey(new Mnemonic(DevMnemonic), account).Should()
+ .Be(Convert.ToHexString(served).ToLowerInvariant());
+ }
+
[Theory]
[InlineData("regtest", "tpub")]
[InlineData("testnet", "tpub")]
diff --git a/RemoteSigner/aws-lambda-tools-defaults.json b/RemoteSigner/aws-lambda-tools-defaults.json
index c905ac7..1194254 100644
--- a/RemoteSigner/aws-lambda-tools-defaults.json
+++ b/RemoteSigner/aws-lambda-tools-defaults.json
@@ -10,7 +10,7 @@
"region" : "eu-central-1",
"configuration" : "Release",
"package-type" : "Image",
- "function-memory-size" : 256,
+ "function-memory-size" : 512,
"function-timeout" : 30,
"image-command" : "RemoteSigner::RemoteSigner.Function::FunctionHandler",
"docker-host-build-output-dir" : "./bin/Release/lambda-publish",
@@ -21,7 +21,6 @@
"function-subnets" : "",
"function-security-groups" : "",
"tracing-mode" : "PassThrough",
- "environment-variables" : "\"MF_ed0210c8\"=\"{\"EncryptedSeedphrase\":\"AQICAHheBtxW\\u002B2iTBvvhvmRXaxaScHh6up1/VWCRSMlopexrdwE1C/ylXBL5pmjJ3P/UG7XnAAABBzCCAQMGCSqGSIb3DQEHBqCB9TCB8gIBADCB7AYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxPlkxPX65p7aRcXykCARCAgb4En2Bb/nWQ6m4i3JDP\\u002BKGjaGDAVF4LR6\\u002B2Ljl7orp6pfZbCCxK6e89OBpJWi7elQM670vD/SWkYSZ9MUWUshU8n7NyBJZuZgBhtaH6j6yDhgHtBv7cwJngv0d72QEaTrH2YqLCVuoddEKEpB13ezfkf56230QD134kcJze4fITQGA6sXxQ0x\\u002BWjKOeYltpB\\u002BShk4\\u002BkaNja42ZM0MMjyrMOmQtXCkgdoTUVi6twiqU\\u002Bqr8mQEEq0aNdZzlLCI/v\",\"AwsKmsKeyId\":\"mrk-cec3e3ef59bc4616a6f44da60bfea0ba\"}\"",
"dockerfile" : "Dockerfile",
"docker-build-working-dir" : "..",
"image-tag" : "nodeguardremotesigner:latest"
diff --git a/justfile b/justfile
index e7049fb..ebbf696 100755
--- a/justfile
+++ b/justfile
@@ -23,6 +23,11 @@ deploy-no-cli env='stg': build-docker-image push-docker-image
# Update the AWS Lambda function code without using Gum
aws lambda update-function-code --no-paginate --function-name arn:aws:lambda:eu-central-1:839166930136:function:SignPSBT-{{env}} --image-uri 839166930136.dkr.ecr.eu-central-1.amazonaws.com/nodeguardremotesigner:latest --publish
+# One-off: give the function the memory its Spark routes need (FROST signing, NSpark libraries)
+set-memory env='stg' size='512':
+ aws lambda update-function-configuration --no-paginate --function-name arn:aws:lambda:eu-central-1:839166930136:function:SignPSBT-{{env}} --memory-size {{size}}
+ aws lambda wait function-updated-v2 --function-name arn:aws:lambda:eu-central-1:839166930136:function:SignPSBT-{{env}}
+
# Seed ceremony: generate a NEW 24-word seed, KMS-encrypt it and write the manifest (interactive terminal required)
ceremony-generate kms_key_id network='mainnet' out='manifest.json':
dotnet run --project RemoteSigner.SeedCeremony -- generate --kms-key-id {{kms_key_id}} --network {{network}} --out {{out}}
@@ -50,4 +55,10 @@ ceremony-test-local endpoint='http://localhost:4566':
| dotnet run --project RemoteSigner.SeedCeremony -- encrypt --kms-key-id "$KEY_ID" --network regtest --out "$MANIFEST"
dotnet run --project RemoteSigner.SeedCeremony -- verify --in "$MANIFEST"
grep -q '"MF_ed0210c8"' "$MANIFEST"
- echo "Local emulator ceremony smoke test OK (fingerprint ed0210c8 round-tripped)"
\ No newline at end of file
+ echo "Local emulator ceremony smoke test OK (fingerprint ed0210c8 round-tripped)"
+
+# Smoke-test the real image locally, never touching AWS: the Lambda runtime interface emulator with a
+# throwaway floci KMS and the PUBLIC dev vector (ed0210c8) as the Spark seed. Checks /spark/info
+# serves the dev Spark identity and / still signs the golden PSBT byte-for-byte.
+spark-smoke-local floci='floci/floci:1.5.25':
+ scripts/spark-smoke-local.sh {{floci}}
diff --git a/scripts/spark-smoke-local.sh b/scripts/spark-smoke-local.sh
new file mode 100755
index 0000000..a3089ff
--- /dev/null
+++ b/scripts/spark-smoke-local.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# Smoke-tests the real signer image locally, never touching AWS: the Lambda runtime interface
+# emulator (built into the AWS base image) runs it against a throwaway floci KMS, with the PUBLIC
+# dev test vector (fingerprint ed0210c8) as the Spark seed. Checks both routes:
+# /spark/info returns the dev vector's Spark identity (the one ceremony verify prints), and
+# refuses any other wallet;
+# / still signs the golden PSBT byte-for-byte.
+# Emulator ciphertexts are throwaway by design - never reuse them.
+# Usage: scripts/spark-smoke-local.sh [floci-image] (needs docker, aws CLI, jq, dotnet)
+set -euo pipefail
+
+FLOCI_IMAGE="${1:-floci/floci:1.5.25}"
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+NAME="nodeguard-signer-smoke-$$"
+IMAGE="nodeguard-remote-signer:spark-smoke"
+DEV_MNEMONIC="middle teach digital prefer fiscal theory syrup enter crash muffin easily anxiety ill barely eagle swim volume consider dynamic unaware deputy middle into physical"
+DEV_IDENTITY="032986ec06431b7b80a4171836174ac10c07e1cb1dff4af93082ee5f3ebca64a0a"
+WORK=$(mktemp -d)
+
+cleanup() {
+ docker rm -f "$NAME-kms" "$NAME-signer" >/dev/null 2>&1 || true
+ docker network rm "$NAME" >/dev/null 2>&1 || true
+ rm -rf "$WORK"
+}
+trap cleanup EXIT
+
+fail() {
+ echo "SMOKE FAILED: $1" >&2
+ docker logs "$NAME-signer" 2>&1 | tail -30 >&2 || true
+ exit 1
+}
+
+echo "Building the signer image..."
+git -C "$ROOT" submodule update --init vendor/nspark
+docker build -q --platform linux/amd64 -f "$ROOT/RemoteSigner/Dockerfile" -t "$IMAGE" "$ROOT" >/dev/null
+
+echo "Starting a throwaway KMS ($FLOCI_IMAGE)..."
+docker network create "$NAME" >/dev/null
+docker run -d --name "$NAME-kms" --network "$NAME" -p 127.0.0.1::4566 "$FLOCI_IMAGE" >/dev/null
+export AWS_ENDPOINT_URL="http://$(docker port "$NAME-kms" 4566/tcp | head -1)"
+export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_REGION=eu-central-1
+for _ in $(seq 30); do aws kms list-keys >/dev/null 2>&1 && break; sleep 1; done
+KEY_ID=$(aws kms create-key --query KeyMetadata.KeyId --output text)
+
+echo "Encrypting the dev seed with the ceremony tool..."
+echo "$DEV_MNEMONIC" | dotnet run --project "$ROOT/RemoteSigner.SeedCeremony" -- \
+ encrypt --kms-key-id "$KEY_ID" --network regtest --out "$WORK/manifest.json" >/dev/null
+
+{
+ echo "AWS_ENDPOINT_URL=http://$NAME-kms:4566"
+ echo "AWS_ACCESS_KEY_ID=test"
+ echo "AWS_SECRET_ACCESS_KEY=test"
+ echo "AWS_REGION=eu-central-1"
+ echo "MF_ed0210c8=$(jq -r .EnvValue "$WORK/manifest.json")"
+ echo "SPARK_SEED_FINGERPRINT=ed0210c8"
+ echo "SPARK_ACCOUNT=0"
+} > "$WORK/signer.env"
+
+echo "Starting the signer under the Lambda runtime interface emulator..."
+docker run -d --platform linux/amd64 --name "$NAME-signer" --network "$NAME" --env-file "$WORK/signer.env" \
+ -p 127.0.0.1::8080 "$IMAGE" RemoteSigner::RemoteSigner.Function::FunctionHandler >/dev/null
+RIE="http://$(docker port "$NAME-signer" 8080/tcp | head -1)/2015-03-31/functions/function/invocations"
+
+# A Function URL event (payload format 2.0) for a POST of $2 to path $1
+invoke() {
+ jq -n --arg path "$1" --arg body "$2" \
+ '{version: "2.0", rawPath: $path, body: $body, isBase64Encoded: false,
+ requestContext: {http: {method: "POST", path: $path}}}' |
+ curl -s --fail --retry 30 --retry-all-errors --retry-delay 1 -X POST "$RIE" -d @-
+}
+
+INFO=$(invoke /spark/info '{"wallet":"ed0210c8"}') || fail "the signer did not answer"
+[ "$(jq -r .statusCode <<<"$INFO")" = 200 ] || fail "/spark/info: $INFO"
+IDENTITY=$(jq -r '.body | fromjson | .result.identityPublicKey' <<<"$INFO")
+[ "$IDENTITY" = "$DEV_IDENTITY" ] || fail "/spark/info returned identity $IDENTITY, expected $DEV_IDENTITY"
+echo "OK /spark/info -> Spark identity $IDENTITY"
+
+CEREMONY=$(dotnet run --project "$ROOT/RemoteSigner.SeedCeremony" -- verify --in "$WORK/manifest.json" --spark-account 0)
+grep -q "$DEV_IDENTITY" <<<"$CEREMONY" || fail "ceremony verify printed another Spark identity: $CEREMONY"
+echo "OK ceremony verify --spark-account 0 -> the same identity"
+
+OTHER=$(invoke /spark/info '{"wallet":"1fcce4de"}') || fail "the signer did not answer"
+[ "$(jq -r .statusCode <<<"$OTHER")" = 403 ] || fail "/spark/info for another wallet was not refused: $OTHER"
+echo "OK /spark/info for another wallet -> 403"
+
+GOLDEN=$(jq -c '.[] | select(.name == "p2wsh-2of3-all")' "$ROOT/RemoteSigner.Tests/Fixtures/golden-psbt-signatures.json")
+SIGNED=$(invoke / "$(jq -c '{Psbt: .psbt, EnforcedSighash: 1, Network: .network}' <<<"$GOLDEN")") ||
+ fail "the signer did not answer"
+[ "$(jq -r .statusCode <<<"$SIGNED")" = 200 ] || fail "PSBT signing: $SIGNED"
+[ "$(jq -r '.body | fromjson | .Psbt' <<<"$SIGNED")" = "$(jq -r .expected <<<"$GOLDEN")" ] ||
+ fail "the signed PSBT differs from the golden output"
+echo "OK / -> golden PSBT signed byte-for-byte"
+
+echo "Local Spark smoke test OK"