diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 748d053b2..65005a172 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -43,6 +43,14 @@ namespace BTCPayServer.Controllers return View(vm); } + class GetXPubs + { + public BitcoinExtPubKey ExtPubKey { get; set; } + public DerivationStrategyBase DerivationScheme { get; set; } + public HDFingerprint RootFingerprint { get; set; } + public string Source { get; set; } + } + [HttpGet] [Route("{storeId}/derivations/{cryptoCode}/ledger/ws")] public async Task AddDerivationSchemeLedger( @@ -73,7 +81,18 @@ namespace BTCPayServer.Controllers var k = KeyPath.Parse(keyPath); if (k.Indexes.Length == 0) throw new FormatException("Invalid key path"); - var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token); + + var getxpubResult = new GetXPubs(); + getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token); + var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit; + var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions() + { + P2SH = segwit, + Legacy = !segwit + }); + getxpubResult.DerivationScheme = derivation; + getxpubResult.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).ExtPubKey.PubKey.GetHDFingerPrint(); + getxpubResult.Source = hw.Device; result = getxpubResult; } } @@ -87,7 +106,7 @@ namespace BTCPayServer.Controllers if (result != null) { UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); - var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings)); + var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings)); await webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); } } @@ -185,6 +204,8 @@ namespace BTCPayServer.Controllers { strategy = newStrategy; strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); + strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint)); + strategy.Source = vm.Source; vm.DerivationScheme = strategy.AccountDerivation.ToString(); } } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 296fba583..1f66d0a1b 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -560,9 +560,9 @@ namespace BTCPayServer.Controllers var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation); - // Some deployment have the wallet root key path saved in the store blob - // If it does, we only have to make 1 call to the hw to check if it can sign the given strategy, - if (derivationSettings.AccountKeyPath == null || !await hw.CanSign(network, strategy, derivationSettings.AccountKeyPath, normalOperationTimeout.Token)) + + // Some deployment does not have the AccountKeyPath set, let's fix this... + if (derivationSettings.AccountKeyPath == null) { // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token); @@ -572,7 +572,34 @@ namespace BTCPayServer.Controllers storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } + // If it has the AccountKeyPath, let's check if we opened the right ledger + else + { + // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, + // but some deployment does not have it, so let's use AccountKeyPath instead + if (derivationSettings.RootFingerprint == null) + { + + var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token); + if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey())) + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + // We have the root fingerprint, we can check the root from it + else + { + var actualPubKey = await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token); + if (actualPubKey.GetPublicKey().GetHDFingerPrint() != derivationSettings.RootFingerprint.Value) + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + } + // Some deployment does not have the RootFingerprint set, let's fix this... + if (derivationSettings.RootFingerprint == null) + { + derivationSettings.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetPublicKey().GetHDFingerPrint(); + storeData.SetSupportedPaymentMethod(derivationSettings); + await Repository.UpdateStore(storeData); + } var psbt = await CreatePSBT(network, derivationSettings, model, normalOperationTimeout.Token); signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index c047bd296..3a484fed3 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Models.StoreViewModels public string CryptoCode { get; set; } public string KeyPath { get; set; } + public string RootFingerprint { get; set; } [Display(Name = "Hint address")] public string HintAddress { get; set; } public bool Confirmation { get; set; } @@ -37,5 +38,6 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Coldcard Wallet File")] public IFormFile ColdcardPublicFile{ get; set; } public string Config { get; set; } + public string Source { get; set; } } } diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index 535ccc1ac..87131c9ca 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -73,6 +73,9 @@ namespace BTCPayServer.Services return _Ledger; } } + + public string Device => "Ledger wallet"; + WebSocketTransport _Transport = null; public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) { @@ -89,19 +92,11 @@ namespace BTCPayServer.Services return new LedgerTestResult() { Success = true }; } - public async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) + public async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) { if (network == null) throw new ArgumentNullException(nameof(network)); - - var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit; - var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation); - var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions() - { - P2SH = segwit, - Legacy = !segwit - }); - return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath }; + return await GetExtPubKey(Ledger, network, keyPath, false, cancellation); } private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) @@ -118,8 +113,12 @@ namespace BTCPayServer.Services if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); } - var fingerprint = onlyChaincode ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); - var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); + var parentFP = onlyChaincode || account.Indexes.Length == 0 ? default : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().GetHDFingerPrint(); + var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), + pubKey.ChainCode, + (byte)account.Indexes.Length, + parentFP, + account.Indexes.Length == 0 ? 0 : account.Indexes.Last()).GetWif(network.NBitcoinNetwork); return extpubkey; } catch (FormatException) @@ -128,12 +127,6 @@ namespace BTCPayServer.Services } } - public async Task CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation) - { - var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation); - return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey; - } - public async Task FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) { List derivations = new List(); @@ -218,11 +211,4 @@ namespace BTCPayServer.Services public bool Success { get; set; } public string Error { get; set; } } - - public class GetXPubResult - { - public string ExtPubKey { get; set; } - [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] - public KeyPath KeyPath { get; set; } - } } diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 278d5cdbf..18e0c1bee 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -50,6 +50,8 @@ + +
@@ -66,7 +68,6 @@

A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate. -
- -
BTCPay format memo @@ -140,6 +139,8 @@
+ + diff --git a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js index 9bafe5656..2fa6034d1 100644 --- a/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js +++ b/BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js @@ -48,7 +48,9 @@ showFeedback("ledger-info"); - $("#DerivationScheme").val(result.extPubKey); + $("#DerivationScheme").val(result.derivationScheme); + $("#RootFingerprint").val(result.rootFingerprint); + $("#Source").val(result.source); $("#DerivationSchemeFormat").val("BTCPay"); $("#KeyPath").val(keypath); })