-
Notifications
You must be signed in to change notification settings - Fork 0
Seedphrase encryption tool via KMS #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| using System.Text.Json; | ||
| using NBitcoin; | ||
|
|
||
| namespace RemoteSigner.SeedCeremony; | ||
|
|
||
| /// <summary> | ||
| /// Public identifiers derived from a mnemonic during a seed ceremony | ||
| /// </summary> | ||
| /// <param name="MasterFingerprint">8 lowercase hex chars, the MF_ env var suffix</param> | ||
| /// <param name="AccountXpub">The xpub at the account derivation path (NodeGuard's InternalWallets.XPUB)</param> | ||
| /// <param name="EnvName">The Lambda env var name, MF_{MasterFingerprint}</param> | ||
| public sealed record CeremonyResult(string MasterFingerprint, string AccountXpub, string EnvName); | ||
|
|
||
| /// <summary> | ||
| /// Pure derivation and output-assembly logic of the seed ceremony, kept free of console/AWS I/O so | ||
| /// it can be unit tested. Everything here must stay call-for-call compatible with the lambda | ||
| /// (Function.SignPSBT fingerprint handling) and with NodeGuard's InternalWallet.GetXPUB | ||
| /// </summary> | ||
| public static class Ceremony | ||
| { | ||
| /// <summary> | ||
| /// Generates a fresh 24-word english mnemonic without BIP39 passphrase (all consumers derive | ||
| /// with Mnemonic.DeriveExtKey() and no passphrase) | ||
| /// </summary> | ||
| public static Mnemonic GenerateMnemonic() | ||
| { | ||
| return new Mnemonic(Wordlist.English, WordCount.TwentyFour); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Derives the public identifiers NodeGuard and the remote signer need from a mnemonic. The | ||
| /// fingerprint mirrors Function.SignPSBT (extKey.GetWif(network).GetPublicKey().GetHDFingerPrint()) | ||
| /// and the account xpub mirrors NodeGuard's InternalWallet.GetXPUB (master derived at the | ||
| /// account path, neutered) | ||
| /// </summary> | ||
| /// <param name="mnemonic"></param> | ||
| /// <param name="network"></param> | ||
| /// <param name="accountPath">Account-level derivation path, e.g. m/48'/1'</param> | ||
| public static CeremonyResult Derive(Mnemonic mnemonic, Network network, KeyPath accountPath) | ||
| { | ||
| var masterKey = mnemonic.DeriveExtKey().GetWif(network); | ||
|
|
||
| var masterFingerprint = masterKey.GetPublicKey().GetHDFingerPrint().ToString(); | ||
|
|
||
| var accountXpub = masterKey.Derive(accountPath).Neuter().ToWif(); | ||
|
|
||
| return new CeremonyResult(masterFingerprint, accountXpub, $"MF_{masterFingerprint}"); | ||
| } | ||
|
|
||
| //Relaxed escaping keeps base64 plus signs literal instead of the default encoder's u002B | ||
| //unicode escapes; JSON parsers decode both identically | ||
| private static readonly JsonSerializerOptions EnvValueSerializerOptions = new() | ||
| { | ||
| Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Builds the MF_* env var value by serializing the lambda's own SignPSBTConfig DTO, so the | ||
| /// JSON shape/casing can never drift from what the lambda deserializes | ||
| /// </summary> | ||
| /// <param name="encryptedSeedphraseBase64"></param> | ||
| /// <param name="kmsKeyId"></param> | ||
| public static string BuildEnvValue(string encryptedSeedphraseBase64, string kmsKeyId) | ||
| { | ||
| var config = new SignPSBTConfig | ||
| { | ||
| EncryptedSeedphrase = encryptedSeedphraseBase64, | ||
| AwsKmsKeyId = kmsKeyId | ||
| }; | ||
|
|
||
| return JsonSerializer.Serialize(config, EnvValueSerializerOptions); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks that a manifest is internally consistent with the (already decrypted) mnemonic it | ||
| /// was produced from: env var name, master fingerprint and account xpub must all re-derive | ||
| /// identically. Throws with a specific message on the first mismatch | ||
| /// </summary> | ||
| /// <param name="manifest"></param> | ||
| /// <param name="mnemonic"></param> | ||
| public static void VerifyManifest(CeremonyManifest manifest, Mnemonic mnemonic) | ||
| { | ||
| var result = Derive(mnemonic, Function.ParseNetwork(manifest.Network), KeyPath.Parse(manifest.DerivationPath)); | ||
|
|
||
| if (!string.Equals(manifest.MasterFingerprint, result.MasterFingerprint, StringComparison.Ordinal)) | ||
| throw new ArgumentException( | ||
| $"Master fingerprint mismatch: the manifest says {manifest.MasterFingerprint} but the decrypted seed derives {result.MasterFingerprint}"); | ||
|
|
||
| if (!string.Equals(manifest.EnvName, result.EnvName, StringComparison.Ordinal)) | ||
| throw new ArgumentException( | ||
| $"Env var name mismatch: the manifest says {manifest.EnvName} but the decrypted seed derives {result.EnvName}"); | ||
|
|
||
| if (!string.Equals(manifest.AccountXpub, result.AccountXpub, StringComparison.Ordinal)) | ||
| throw new ArgumentException( | ||
| $"Account xpub mismatch at {manifest.DerivationPath} on {manifest.Network}: the manifest says {manifest.AccountXpub} but the decrypted seed derives {result.AccountXpub}"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| using System.Text.Encodings.Web; | ||
| using System.Text.Json; | ||
|
|
||
| namespace RemoteSigner.SeedCeremony; | ||
|
|
||
| /// <summary> | ||
| /// The public output of a seed ceremony: everything an operator needs to configure the lambda | ||
| /// (EnvName/EnvValue) and NodeGuard (MasterFingerprint/AccountXpub). Contains ciphertext and | ||
| /// public key material only — never the mnemonic | ||
| /// </summary> | ||
| public sealed class CeremonyManifest | ||
| { | ||
| public required string EnvName { get; init; } | ||
|
|
||
| /// <summary>Byte-exact MF_* env var value (serialized SignPSBTConfig)</summary> | ||
| public required string EnvValue { get; init; } | ||
|
|
||
| public required string MasterFingerprint { get; init; } | ||
|
|
||
| public required string AccountXpub { get; init; } | ||
|
|
||
| public required string DerivationPath { get; init; } | ||
|
|
||
| public required string Network { get; init; } | ||
|
|
||
| public required string CreatedAtUtc { get; init; } | ||
|
|
||
| //Relaxed escaping keeps the manifest human-readable instead of the default encoder's | ||
| //unicode escapes (u0022 for quotes, u002B for plus). Every JSON parser decodes both | ||
| //encodings identically; this is presentation only | ||
| private static readonly JsonSerializerOptions SerializerOptions = new() | ||
| { | ||
| WriteIndented = true, | ||
| Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping | ||
| }; | ||
|
|
||
| public string ToJson() | ||
| { | ||
| return JsonSerializer.Serialize(this, SerializerOptions); | ||
| } | ||
|
|
||
| public static CeremonyManifest FromJson(string json) | ||
| { | ||
| return JsonSerializer.Deserialize<CeremonyManifest>(json) | ||
| ?? throw new ArgumentException("The manifest could not be deserialized", nameof(json)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using NBitcoin; | ||
|
|
||
| namespace RemoteSigner.SeedCeremony.Commands; | ||
|
|
||
| /// <summary> | ||
| /// Shared tail of the generate/encrypt commands: derive the public identifiers, KMS-encrypt the | ||
| /// mnemonic through the lambda's own EncryptSeedphrase, assemble the manifest and emit it | ||
| /// </summary> | ||
| public static class EncryptAndEmit | ||
| { | ||
| public const string DefaultDerivationPath = "m/48'/1'"; | ||
|
|
||
| public static async Task<int> Run(Mnemonic mnemonic, Options options) | ||
| { | ||
| var kmsKeyId = options.Require("--kms-key-id"); | ||
| var networkArg = options.Require("--network"); | ||
| var derivationPath = options.GetOrDefault("--derivation-path", DefaultDerivationPath); | ||
| var outputFormat = options.GetOrDefault("--output", "text"); | ||
| var outPath = options.Get("--out"); | ||
|
|
||
| if (outputFormat is not ("text" or "json")) | ||
| throw new UsageException($"--output must be 'text' or 'json', got '{outputFormat}'"); | ||
|
|
||
| var network = Function.ParseNetwork(networkArg); | ||
| var accountPath = KeyPath.Parse(derivationPath); | ||
|
|
||
| var derived = Ceremony.Derive(mnemonic, network, accountPath); | ||
|
|
||
| var kmsClient = options.CreateKmsClient(); | ||
| var encryptedSeedphrase = await new Function().EncryptSeedphrase(mnemonic.ToString(), kmsKeyId, kmsClient); | ||
|
|
||
| var manifest = new CeremonyManifest | ||
| { | ||
| EnvName = derived.EnvName, | ||
| EnvValue = Ceremony.BuildEnvValue(encryptedSeedphrase, kmsKeyId), | ||
| MasterFingerprint = derived.MasterFingerprint, | ||
| AccountXpub = derived.AccountXpub, | ||
| DerivationPath = derivationPath, | ||
| Network = networkArg.ToLowerInvariant(), | ||
| CreatedAtUtc = DateTime.UtcNow.ToString("O") | ||
| }; | ||
|
|
||
| if (outPath != null) | ||
| { | ||
| await File.WriteAllTextAsync(outPath, manifest.ToJson()); | ||
|
Jossec101 marked this conversation as resolved.
|
||
| Console.Error.WriteLine($"Manifest written to {outPath}"); | ||
| } | ||
|
|
||
| if (outputFormat == "json") | ||
| { | ||
| Console.WriteLine(manifest.ToJson()); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"Env var name : {manifest.EnvName}"); | ||
| Console.WriteLine($"Master fingerprint : {manifest.MasterFingerprint}"); | ||
| Console.WriteLine($"Account xpub : {manifest.AccountXpub}"); | ||
| Console.WriteLine($"Derivation path : {manifest.DerivationPath}"); | ||
| Console.WriteLine($"Network : {manifest.Network}"); | ||
| Console.WriteLine(outPath != null | ||
| ? "Env var value : (in the manifest file, keep it for the lambda env merge)" | ||
| : $"Env var value : {manifest.EnvValue}"); | ||
| Console.Error.WriteLine(); | ||
| Console.Error.WriteLine("Next steps: run 'seed-ceremony verify --in <manifest>' as preflight, merge the env"); | ||
| Console.Error.WriteLine("var into the lambda configuration (snapshot -> jq merge -> apply, see README), and"); | ||
| Console.Error.WriteLine("insert the fingerprint + xpub into NodeGuard."); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.