Save the fingerprint of the root of LedgerWallet, and use it. Simplify HardwareWallet

This commit is contained in:
nicolas.dorier
2019-05-10 01:05:37 +09:00
parent e504163bc7
commit 01e5b319d1
6 changed files with 73 additions and 34 deletions

View File

@@ -43,6 +43,14 @@ namespace BTCPayServer.Controllers
return View(vm); 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] [HttpGet]
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")] [Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
public async Task<IActionResult> AddDerivationSchemeLedger( public async Task<IActionResult> AddDerivationSchemeLedger(
@@ -73,7 +81,18 @@ namespace BTCPayServer.Controllers
var k = KeyPath.Parse(keyPath); var k = KeyPath.Parse(keyPath);
if (k.Indexes.Length == 0) if (k.Indexes.Length == 0)
throw new FormatException("Invalid key path"); 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; result = getxpubResult;
} }
} }
@@ -87,7 +106,7 @@ namespace BTCPayServer.Controllers
if (result != null) if (result != null)
{ {
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); 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<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
} }
} }
@@ -185,6 +204,8 @@ namespace BTCPayServer.Controllers
{ {
strategy = newStrategy; strategy = newStrategy;
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); 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(); vm.DerivationScheme = strategy.AccountDerivation.ToString();
} }
} }

View File

@@ -560,9 +560,9 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation); 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, // Some deployment does not have the AccountKeyPath set, let's fix this...
if (derivationSettings.AccountKeyPath == null || !await hw.CanSign(network, strategy, derivationSettings.AccountKeyPath, normalOperationTimeout.Token)) 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 // 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); var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
@@ -572,7 +572,34 @@ namespace BTCPayServer.Controllers
storeData.SetSupportedPaymentMethod(derivationSettings); storeData.SetSupportedPaymentMethod(derivationSettings);
await Repository.UpdateStore(storeData); 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); var psbt = await CreatePSBT(network, derivationSettings, model, normalOperationTimeout.Token);
signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); signTimeout.CancelAfter(TimeSpan.FromMinutes(5));

View File

@@ -26,6 +26,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public string KeyPath { get; set; } public string KeyPath { get; set; }
public string RootFingerprint { get; set; }
[Display(Name = "Hint address")] [Display(Name = "Hint address")]
public string HintAddress { get; set; } public string HintAddress { get; set; }
public bool Confirmation { get; set; } public bool Confirmation { get; set; }
@@ -37,5 +38,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Coldcard Wallet File")] [Display(Name = "Coldcard Wallet File")]
public IFormFile ColdcardPublicFile{ get; set; } public IFormFile ColdcardPublicFile{ get; set; }
public string Config { get; set; } public string Config { get; set; }
public string Source { get; set; }
} }
} }

View File

@@ -73,6 +73,9 @@ namespace BTCPayServer.Services
return _Ledger; return _Ledger;
} }
} }
public string Device => "Ledger wallet";
WebSocketTransport _Transport = null; WebSocketTransport _Transport = null;
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
{ {
@@ -89,19 +92,11 @@ namespace BTCPayServer.Services
return new LedgerTestResult() { Success = true }; return new LedgerTestResult() { Success = true };
} }
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) public async Task<BitcoinExtPubKey> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
{ {
if (network == null) if (network == null)
throw new ArgumentNullException(nameof(network)); throw new ArgumentNullException(nameof(network));
return await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
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 };
} }
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
@@ -118,8 +113,12 @@ namespace BTCPayServer.Services
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet) if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}."); 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 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, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); 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; return extpubkey;
} }
catch (FormatException) catch (FormatException)
@@ -128,12 +127,6 @@ namespace BTCPayServer.Services
} }
} }
public async Task<bool> 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<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
{ {
List<KeyPath> derivations = new List<KeyPath>(); List<KeyPath> derivations = new List<KeyPath>();
@@ -218,11 +211,4 @@ namespace BTCPayServer.Services
public bool Success { get; set; } public bool Success { get; set; }
public string Error { 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; }
}
} }

View File

@@ -50,6 +50,8 @@
</div> </div>
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" /> <input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
<input id="KeyPath" asp-for="KeyPath" type="hidden" /> <input id="KeyPath" asp-for="KeyPath" type="hidden" />
<input id="Source" asp-for="Source" type="hidden" />
<input id="RootFingerprint" asp-for="RootFingerprint" type="hidden" />
<input id="Config" asp-for="Config" type="hidden" /> <input id="Config" asp-for="Config" type="hidden" />
<div class="form-group"> <div class="form-group">
<label asp-for="DerivationScheme"></label> <label asp-for="DerivationScheme"></label>
@@ -66,7 +68,6 @@
</p> </p>
<div id="ledger-info" class="form-text text-muted display-when-ledger-connected"> <div id="ledger-info" class="form-text text-muted display-when-ledger-connected">
<span>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.</span> <span>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.</span>
</div> </div>
<div class="d-flex"> <div class="d-flex">
<button type="button" class="btn btn-primary mr-2 " data-toggle="modal" data-target="#coldcardimport"> <button type="button" class="btn btn-primary mr-2 " data-toggle="modal" data-target="#coldcardimport">
@@ -84,10 +85,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<span>BTCPay format memo</span> <span>BTCPay format memo</span>
@@ -140,6 +139,8 @@
</div> </div>
<input type="hidden" asp-for="Confirmation" /> <input type="hidden" asp-for="Confirmation" />
<input id="KeyPath" asp-for="KeyPath" type="hidden" /> <input id="KeyPath" asp-for="KeyPath" type="hidden" />
<input id="Source" asp-for="Source" type="hidden" />
<input id="RootFingerprint" asp-for="RootFingerprint" type="hidden" />
<input type="hidden" asp-for="DerivationScheme" /> <input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="Enabled" /> <input type="hidden" asp-for="Enabled" />
<input id="Config" asp-for="Config" type="hidden" /> <input id="Config" asp-for="Config" type="hidden" />

View File

@@ -48,7 +48,9 @@
showFeedback("ledger-info"); showFeedback("ledger-info");
$("#DerivationScheme").val(result.extPubKey); $("#DerivationScheme").val(result.derivationScheme);
$("#RootFingerprint").val(result.rootFingerprint);
$("#Source").val(result.source);
$("#DerivationSchemeFormat").val("BTCPay"); $("#DerivationSchemeFormat").val("BTCPay");
$("#KeyPath").val(keypath); $("#KeyPath").val(keypath);
}) })