From 18e92e7b4fc9437eb1840d4a7fb8f3d2d69047aa Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 5 Sep 2024 11:04:19 +0900 Subject: [PATCH] Improve signature parsing --- NBitcoin.Tests/BIP322Tests.cs | 126 ++++++++++++----------- NBitcoin/BIP322/BIP322Signature.cs | 159 +++++++++++++++++++++++++++++ NBitcoin/{BIP322.cs => BIP322o.cs} | 125 ++++++++++------------- NBitcoin/CompactSignature.cs | 22 +--- NBitcoin/Crypto/Hashes.cs | 2 + 5 files changed, 279 insertions(+), 155 deletions(-) create mode 100644 NBitcoin/BIP322/BIP322Signature.cs rename NBitcoin/{BIP322.cs => BIP322o.cs} (68%) diff --git a/NBitcoin.Tests/BIP322Tests.cs b/NBitcoin.Tests/BIP322Tests.cs index d33945fa13..27c0510a3b 100644 --- a/NBitcoin.Tests/BIP322Tests.cs +++ b/NBitcoin.Tests/BIP322Tests.cs @@ -4,6 +4,7 @@ namespace NBitcoin.Tests { + [Trait("UnitTest", "UnitTest")] public class BIP322Tests { //from https://github.com/ACken2/bip322-js/tree/main/test && https://github.com/bitcoin/bitcoin/pull/24058/files#diff-2bd57d7fbec4bb262834d155c304ebe15d26f73fea87c75ff273df3529a15510 @@ -18,16 +19,16 @@ public async Task CanHandleLegacyBIP322Message() var addressWrongRegtest = BitcoinAddress.Create("mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", Network.RegTest); var message = "This is an example of a signed message."; var messageWrong = ""; - var signature = "H9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk="; - Assert.True(await BIP322.Verify(message, address, signature)); - Assert.True(await BIP322.Verify(message, addressTestnet, signature)); - Assert.True(await BIP322.Verify(message, addressRegtest, signature)); - Assert.False(await BIP322.Verify(messageWrong, address, signature)); - Assert.False(await BIP322.Verify(messageWrong, addressTestnet, signature)); - Assert.False(await BIP322.Verify(messageWrong, addressRegtest, signature)); - Assert.False(await BIP322.Verify(message, addressWrong, signature)); - Assert.False(await BIP322.Verify(message, addressWrongTestnet, signature)); - Assert.False(await BIP322.Verify(message, addressWrongRegtest, signature)); + var signature = BIP322.BIP322Signature.Parse("H9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk=", address.Network); + Assert.True(await BIP322o.Verify(message, address, signature)); + Assert.True(await BIP322o.Verify(message, addressTestnet, signature)); + Assert.True(await BIP322o.Verify(message, addressRegtest, signature)); + Assert.False(await BIP322o.Verify(messageWrong, address, signature)); + Assert.False(await BIP322o.Verify(messageWrong, addressTestnet, signature)); + Assert.False(await BIP322o.Verify(messageWrong, addressRegtest, signature)); + Assert.False(await BIP322o.Verify(message, addressWrong, signature)); + Assert.False(await BIP322o.Verify(message, addressWrongTestnet, signature)); + Assert.False(await BIP322o.Verify(message, addressWrongRegtest, signature)); var privateKey = new BitcoinSecret("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k", Network.Main) @@ -36,15 +37,15 @@ public async Task CanHandleLegacyBIP322Message() addressTestnet = privateKey.GetAddress(ScriptPubKeyType.Legacy, Network.TestNet); addressRegtest = privateKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest); message = "Hello World"; - signature = await BIP322.SignEncoded(address, message, BIP322.SignatureType.Legacy, privateKey); + signature = await BIP322o.SignEncoded(address, message, BIP322o.SignatureType.Legacy, privateKey); var signatureTestnet = - await BIP322.SignEncoded(addressTestnet, message, BIP322.SignatureType.Legacy, privateKey); + await BIP322o.SignEncoded(addressTestnet, message, BIP322o.SignatureType.Legacy, privateKey); var signatureRegtest = - await BIP322.SignEncoded(addressRegtest, message, BIP322.SignatureType.Legacy, privateKey); + await BIP322o.SignEncoded(addressRegtest, message, BIP322o.SignatureType.Legacy, privateKey); - Assert.True(await BIP322.Verify(message, address, signature)); - Assert.True(await BIP322.Verify(message, addressTestnet, signatureTestnet)); - Assert.True(await BIP322.Verify(message, addressRegtest, signatureRegtest)); + Assert.True(await BIP322o.Verify(message, address, signature)); + Assert.True(await BIP322o.Verify(message, addressTestnet, signatureTestnet)); + Assert.True(await BIP322o.Verify(message, addressRegtest, signatureRegtest)); } [Fact] @@ -63,11 +64,11 @@ public async Task CanSign() var addressRegtest = BitcoinAddress.Create("2N8zi3ydDsMnVkqaUUe5Xav6SZqhyqEduap", Network.RegTest); var message = "Hello World"; - var signature = await BIP322.SignEncoded(address, message, BIP322.SignatureType.Simple, privateKey); + var signature = await BIP322o.SignEncoded(address, message, BIP322o.SignatureType.Simple, privateKey); var signatureTestnet = - await BIP322.SignEncoded(addressTestnet, message, BIP322.SignatureType.Simple, privateKeyTestnet); + await BIP322o.SignEncoded(addressTestnet, message, BIP322o.SignatureType.Simple, privateKeyTestnet); var signatureRegtest = - await BIP322.SignEncoded(addressRegtest, message, BIP322.SignatureType.Simple, privateKeyTestnet); + await BIP322o.SignEncoded(addressRegtest, message, BIP322o.SignatureType.Simple, privateKeyTestnet); Assert.Equal(signatureTestnet, signature); Assert.Equal(signatureRegtest, signature); @@ -88,37 +89,37 @@ public async Task CanSign() var message2of3 = "This will be a 2-of-3 multisig BIP 322 signed message"; await Assert.ThrowsAsync(async () => { - await BIP322.SignEncoded(p2shAddress, message2of3, BIP322.SignatureType.Simple, redeem, null,k2, k3); + await BIP322o.SignEncoded(p2shAddress, message2of3, BIP322o.SignatureType.Simple, redeem, null,k2, k3); }); var p2sh_signature2of3_k2_k3 = - await BIP322.SignEncoded(p2shAddress, message2of3, BIP322.SignatureType.Full, redeem, null,k2, k3); + await BIP322o.SignEncoded(p2shAddress, message2of3, BIP322o.SignatureType.Full, redeem, null,k2, k3); var p2sh_signature2of3_k1_k3 = - await BIP322.SignEncoded(p2shAddress, message2of3, BIP322.SignatureType.Full, redeem, null,k1, k3); + await BIP322o.SignEncoded(p2shAddress, message2of3, BIP322o.SignatureType.Full, redeem, null,k1, k3); await Assert.ThrowsAsync(async () => { - await BIP322.SignEncoded(p2shAddress, message2of3, BIP322.SignatureType.Full, redeem, null,k1); + await BIP322o.SignEncoded(p2shAddress, message2of3, BIP322o.SignatureType.Full, redeem, null,k1); }); - Assert.True(await BIP322.Verify(message2of3, p2shAddress, p2sh_signature2of3_k2_k3)); - Assert.True(await BIP322.Verify(message2of3, p2shAddress, p2sh_signature2of3_k1_k3)); + Assert.True(await BIP322o.Verify(message2of3, p2shAddress, p2sh_signature2of3_k2_k3)); + Assert.True(await BIP322o.Verify(message2of3, p2shAddress, p2sh_signature2of3_k1_k3)); var p2wsh_signature2of3_k2_k3 = - await BIP322.SignEncoded(p2wshAddress, message2of3, BIP322.SignatureType.Simple, redeem, null,k2, k3); + await BIP322o.SignEncoded(p2wshAddress, message2of3, BIP322o.SignatureType.Simple, redeem, null,k2, k3); var p2wsh_signature2of3_k1_k3 = - await BIP322.SignEncoded(p2wshAddress, message2of3, BIP322.SignatureType.Simple, redeem, null,k1, k3); + await BIP322o.SignEncoded(p2wshAddress, message2of3, BIP322o.SignatureType.Simple, redeem, null,k1, k3); await Assert.ThrowsAsync(async () => { - await BIP322.SignEncoded(p2wshAddress, message2of3, BIP322.SignatureType.Simple, redeem, null,k1); + await BIP322o.SignEncoded(p2wshAddress, message2of3, BIP322o.SignatureType.Simple, redeem, null,k1); }); - Assert.True(await BIP322.Verify(message2of3, p2wshAddress, p2wsh_signature2of3_k2_k3)); - Assert.True(await BIP322.Verify(message2of3, p2wshAddress, p2wsh_signature2of3_k1_k3)); + Assert.True(await BIP322o.Verify(message2of3, p2wshAddress, p2wsh_signature2of3_k2_k3)); + Assert.True(await BIP322o.Verify(message2of3, p2wshAddress, p2wsh_signature2of3_k1_k3)); } [Fact] public async Task CanVerifyBIP322Message() { - var emptyStringHash = BIP322.CreateMessageHash(""); - var helloWorldHash = BIP322.CreateMessageHash("Hello World"); + var emptyStringHash = BIP322o.CreateMessageHash(""); + var helloWorldHash = BIP322o.CreateMessageHash("Hello World"); Assert.Equal(new uint256("c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1"), emptyStringHash); @@ -133,16 +134,16 @@ public async Task CanVerifyBIP322Message() var emptyStringToSpendTx = - BIP322.CreateToSpendTransaction(Network.Main, emptyStringHash, segwitAddress.ScriptPubKey); + BIP322o.CreateToSpendTransaction(Network.Main, emptyStringHash, segwitAddress.ScriptPubKey); var helloWorldToSpendTx = - BIP322.CreateToSpendTransaction(Network.Main, helloWorldHash, segwitAddress.ScriptPubKey); + BIP322o.CreateToSpendTransaction(Network.Main, helloWorldHash, segwitAddress.ScriptPubKey); Assert.Equal("c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7", emptyStringToSpendTx.GetHash().ToString()); Assert.Equal("b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b", helloWorldToSpendTx.GetHash().ToString()); - var emptyStringToSignTx = BIP322.CreateToSignTransaction(Network.Main, emptyStringToSpendTx.GetHash()); - var helloWorldToSignTx = BIP322.CreateToSignTransaction(Network.Main, helloWorldToSpendTx.GetHash()); + var emptyStringToSignTx = BIP322o.CreateToSignTransaction(Network.Main, emptyStringToSpendTx.GetHash()); + var helloWorldToSignTx = BIP322o.CreateToSignTransaction(Network.Main, helloWorldToSpendTx.GetHash()); Assert.Equal("1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6", emptyStringToSignTx.GetHash().ToString()); @@ -150,35 +151,36 @@ public async Task CanVerifyBIP322Message() helloWorldToSignTx.GetHash().ToString()); var simpleHelloWorldSignature = await - BIP322.SignEncoded(segwitAddress, "Hello World", BIP322.SignatureType.Simple, key); + BIP322o.SignEncoded(segwitAddress, "Hello World", BIP322o.SignatureType.Simple, key); var simpleEmptySignature = await - BIP322.SignEncoded(segwitAddress, "", BIP322.SignatureType.Simple, key); + BIP322o.SignEncoded(segwitAddress, "", BIP322o.SignatureType.Simple, key); var fullHelloWorldSignature = await - BIP322.SignEncoded(segwitAddress, "Hello World", BIP322.SignatureType.Full, key); - var fullEmptySignature = await BIP322.SignEncoded(segwitAddress, "", BIP322.SignatureType.Full, key); + BIP322o.SignEncoded(segwitAddress, "Hello World", BIP322o.SignatureType.Full, key); + var fullEmptySignature = await BIP322o.SignEncoded(segwitAddress, "", BIP322o.SignatureType.Full, key); - Assert.True(await BIP322.Verify("Hello World", segwitAddress, simpleHelloWorldSignature)); - Assert.True(await BIP322.Verify("", segwitAddress, simpleEmptySignature)); - Assert.True(await BIP322.Verify("Hello World", segwitAddress, fullHelloWorldSignature)); - Assert.True(await BIP322.Verify("", segwitAddress, fullEmptySignature)); + Assert.True(await BIP322o.Verify("Hello World", segwitAddress, simpleHelloWorldSignature)); + Assert.True(await BIP322o.Verify("", segwitAddress, simpleEmptySignature)); + Assert.True(await BIP322o.Verify("Hello World", segwitAddress, fullHelloWorldSignature)); + Assert.True(await BIP322o.Verify("", segwitAddress, fullEmptySignature)); - Assert.False(await BIP322.Verify("nuhuh", segwitAddress, simpleHelloWorldSignature)); - Assert.False(await BIP322.Verify("nuhuh", segwitAddress, simpleEmptySignature)); - Assert.False(await BIP322.Verify("nuhuh", segwitAddress, fullHelloWorldSignature)); - Assert.False(await BIP322.Verify("nuhuh", segwitAddress, fullEmptySignature)); + Assert.False(await BIP322o.Verify("nuhuh", segwitAddress, simpleHelloWorldSignature)); + Assert.False(await BIP322o.Verify("nuhuh", segwitAddress, simpleEmptySignature)); + Assert.False(await BIP322o.Verify("nuhuh", segwitAddress, fullHelloWorldSignature)); + Assert.False(await BIP322o.Verify("nuhuh", segwitAddress, fullEmptySignature)); - - Assert.True(await BIP322.Verify("", segwitAddress, - "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=")); - - Assert.True(await BIP322.Verify("", segwitAddress, - "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy")); - Assert.True(await BIP322.Verify("Hello World", segwitAddress, - "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=")); - Assert.True(await BIP322.Verify("Hello World", segwitAddress, - "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy")); + foreach (var t in new[] + { + ("", "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="), + ("", "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy"), + ("Hello World", "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI="), + ("Hello World", "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy") + }) + { + (var message, var sig) = t; + Assert.True(await BIP322o.Verify(message, segwitAddress, sig)); + } // // 2-of-3 p2sh multisig BIP322 signature (created with the buidl-python library) // // Keys are defined as (HDRootWIF, bip322_path) @@ -192,7 +194,7 @@ public async Task CanVerifyBIP322Message() // "AAAAAAHNcfHaNfl8f/+ZC2gTr8aF+0KgppYjKM94egaNm/u1ZAAAAAD8AEcwRAIhAJ6hdj61vLDP+aFa30qUZQmrbBfE0kiOObYvt5nqPSxsAh9IrOKFwflfPRUcQ/5e0REkdFHVP2GGdUsMgDet+sNlAUcwRAIgH3eW/VyFDoXvCasd8qxgwj5NDVo0weXvM6qyGXLCR5YCIEwjbEV6fS6RWP6QsKOcMwvlGr1/SgdCC6pW4eH87/YgAUxpUiECKJfGy28imLcuAeNBLHCNv3NRP5jnJwFDNRXCYNY/vJ4hAv1RQtaZs7+vKqQeWl2rb/jd/gMxkEjUnjZdDGPDZkMLIQL65cH2X5O7LujjTLDL2l8Pxy0Y2UUR99u1qCfjdz7dklOuAAAAAAEAAAAAAAAAAAFqAAAAAA==", // "This will be a p2sh 2-of-3 multisig BIP 322 signed message"), // MessageVerificationResult::OK); - Assert.True(await BIP322.Verify("This will be a p2sh 2-of-3 multisig BIP 322 signed message", + Assert.True(await BIP322o.Verify("This will be a p2sh 2-of-3 multisig BIP 322 signed message", BitcoinAddress.Create("3LnYoUkFrhyYP3V7rq3mhpwALz1XbCY9Uq", Network.Main), "AAAAAAHNcfHaNfl8f/+ZC2gTr8aF+0KgppYjKM94egaNm/u1ZAAAAAD8AEcwRAIhAJ6hdj61vLDP+aFa30qUZQmrbBfE0kiOObYvt5nqPSxsAh9IrOKFwflfPRUcQ/5e0REkdFHVP2GGdUsMgDet+sNlAUcwRAIgH3eW/VyFDoXvCasd8qxgwj5NDVo0weXvM6qyGXLCR5YCIEwjbEV6fS6RWP6QsKOcMwvlGr1/SgdCC6pW4eH87/YgAUxpUiECKJfGy28imLcuAeNBLHCNv3NRP5jnJwFDNRXCYNY/vJ4hAv1RQtaZs7+vKqQeWl2rb/jd/gMxkEjUnjZdDGPDZkMLIQL65cH2X5O7LujjTLDL2l8Pxy0Y2UUR99u1qCfjdz7dklOuAAAAAAEAAAAAAAAAAAFqAAAAAA==")); @@ -206,7 +208,7 @@ public async Task CanVerifyBIP322Message() // "bc1qlqtuzpmazp2xmcutlwv0qvggdvem8vahkc333usey4gskug8nutsz53msw", "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=", // "This will be a p2wsh 3-of-3 multisig BIP 322 signed message"), // MessageVerificationResult::OK); - Assert.True(await BIP322.Verify("This will be a p2wsh 3-of-3 multisig BIP 322 signed message", + Assert.True(await BIP322o.Verify("This will be a p2wsh 3-of-3 multisig BIP 322 signed message", BitcoinAddress.Create("bc1qlqtuzpmazp2xmcutlwv0qvggdvem8vahkc333usey4gskug8nutsz53msw", Network.Main), "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=")); @@ -219,7 +221,7 @@ public async Task CanVerifyBIP322Message() // "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", // "Hello World"), // MessageVerificationResult::OK); - Assert.True(await BIP322.Verify("Hello World", + Assert.True(await BIP322o.Verify("Hello World", BitcoinAddress.Create("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", Network.Main), "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==")); #endif @@ -254,7 +256,7 @@ public async Task CanDoProofOfFunds() var message = "I own these coins"; - var signature = await BIP322.SignEncoded(addr, message, BIP322.SignatureType.Full, null, coins, key, key2); + var signature = await BIP322o.SignEncoded(addr, message, BIP322o.SignatureType.Full, null, coins, key, key2); diff --git a/NBitcoin/BIP322/BIP322Signature.cs b/NBitcoin/BIP322/BIP322Signature.cs new file mode 100644 index 0000000000..eaefb51566 --- /dev/null +++ b/NBitcoin/BIP322/BIP322Signature.cs @@ -0,0 +1,159 @@ +#if HAS_SPAN +#nullable enable +using NBitcoin.DataEncoders; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NBitcoin.BIP322 +{ + public abstract class BIP322Signature + { + public Network Network { get; } + + protected BIP322Signature(Network network) + { + if (network is null) + throw new ArgumentNullException(nameof(network)); + Network = network; + } + public static BIP322Signature Parse(string str, Network network) + { + if (TryParse(str, network, out var r)) + return r; + throw new FormatException("Parsing error for expected BIP322 signature"); + } + public static bool TryParse(string str, Network network, [MaybeNullWhen(false)] out BIP322Signature result) + { + result = null; + if (str is null) + throw new ArgumentNullException(nameof(str)); + byte[] bytes; + try + { + bytes = Encoders.Base64.DecodeData(str); + } + catch (FormatException) + { + return false; + } + return TryCreate(bytes, network, out result); + } + public static bool TryCreate(byte[] bytes, Network network, [MaybeNullWhen(false)] out BIP322Signature result) + { + result = null; + if (bytes is null) + throw new ArgumentNullException(nameof(bytes)); + if (network is null) + throw new ArgumentNullException(nameof(network)); + if (bytes.Length == 65 && bytes[0] >= 27) + { + int recid = (bytes[0] - 27) & 3; + bool compressed = ((bytes[0] - 27) & 4) != 0; + result = new Legacy(compressed, new CompactSignature(recid, bytes[1..]), network); + } + else if (bytes.Length > 0 && bytes[0] == 0) + { + Transaction tx; + try + { + tx = Transaction.Load(bytes, network); + } + catch + { + return false; + } + if (tx.Inputs.Count != 1 || tx.Outputs.Count != 1) + return false; + if (!Full.IsValid(tx)) + return false; + result = new Full(tx, network); + } + else + { + WitScript witScript; + try + { + witScript = new WitScript(bytes); + } + catch + { + return false; + } + result = new Simple(witScript, network); + } + return true; + } + + public class Legacy : BIP322Signature + { + internal Legacy(bool compressed, CompactSignature compactSignature, Network network) : base(network) + { + if (compactSignature is null) + throw new ArgumentNullException(nameof(compactSignature)); + Compressed = compressed; + CompactSignature = compactSignature; + } + + public bool Compressed { get; } + public CompactSignature CompactSignature { get; } + + public override byte[] ToBytes() + { + var b = new byte[65]; + b[0] = (byte)(27 + CompactSignature.RecoveryId + (Compressed ? 4 : 0)); + Array.Copy(CompactSignature.Signature, 0, b, 1, 64); + return b; + } + } + public class Simple : BIP322Signature + { + public Simple(WitScript witnessScript, Network network) : base(network) + { + if (witnessScript is null) + throw new ArgumentNullException(nameof(witnessScript)); + this.WitnessScript = witnessScript; + } + public WitScript WitnessScript { get; } + public override byte[] ToBytes() + { + return WitnessScript.ToBytes(); + } + } + public class Full : BIP322Signature + { + public Full(Transaction signedTransaction, Network network) : base(network) + { + if (signedTransaction is null) + throw new ArgumentNullException(nameof(signedTransaction)); + if (!IsValid(signedTransaction)) + throw new ArgumentException("This isn't a valid BIP0322 to_sign transaction", nameof(signedTransaction)); + SignedTransaction = signedTransaction; + } + + public Transaction SignedTransaction { get; } + static Script OpReturn = new Script("OP_RETURN"); + internal static bool IsValid(Transaction tx) => + tx.Inputs[0].Sequence == 0 && + tx.Outputs[0].Value == Money.Zero && + tx.Outputs[0].ScriptPubKey == OpReturn; + + public override byte[] ToBytes() + { + return SignedTransaction.ToBytes(); + } + } + + public abstract byte[] ToBytes(); + public string ToBase64() => Encoders.Base64.EncodeData(ToBytes()); + + public override bool Equals(object? obj) => obj is BIP322Signature o && ToBase64().Equals(o.ToBase64()); + public static bool operator ==(BIP322Signature? a, BIP322Signature? b) => a is null ? b is null : a.Equals(b); + public static bool operator !=(BIP322Signature? a, BIP322Signature? b) => !(a == b); + public override int GetHashCode() => ToBase64().GetHashCode(); + } +} +#endif diff --git a/NBitcoin/BIP322.cs b/NBitcoin/BIP322o.cs similarity index 68% rename from NBitcoin/BIP322.cs rename to NBitcoin/BIP322o.cs index 7d6b152944..d62db91c64 100644 --- a/NBitcoin/BIP322.cs +++ b/NBitcoin/BIP322o.cs @@ -1,16 +1,18 @@ -#nullable enable +#if HAS_SPAN +#nullable enable using System; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using NBitcoin.BIP322; using NBitcoin.Crypto; using NBitcoin.DataEncoders; using NBitcoin.Protocol; namespace NBitcoin { - public static class BIP322 + public static class BIP322o { public enum SignatureType { @@ -53,7 +55,7 @@ public static uint256 CreateMessageHash(byte[] message, MessageType type) return new uint256(Hashes.SHA256(tagBytes.Concat(message).ToArray()), false); } - public static Transaction CreateToSpendTransaction(Network network, uint256 messageHash, Script scriptPubKey) + internal static Transaction CreateToSpendTransaction(Network network, uint256 messageHash, Script scriptPubKey) { var tx = network.CreateTransaction(); tx.Version = 0; @@ -67,7 +69,7 @@ public static Transaction CreateToSpendTransaction(Network network, uint256 mess return tx; } - public static Transaction CreateToSignTransaction(Network network, uint256 toSpendTxId, + internal static Transaction CreateToSignTransaction(Network network, uint256 toSpendTxId, WitScript? messageSignature = null, uint version = 0, uint lockTime = 0, uint sequence = 0, ScriptCoin[]? additionalInputs = null) { @@ -93,11 +95,11 @@ public static Transaction CreateToSignTransaction(Network network, uint256 toSpe tx.Outputs.Add(new TxOut(Money.Zero, new Script(OpcodeType.OP_RETURN))); return tx; } - public static async Task SignEncoded(BitcoinAddress address, string message, SignatureType type, - Script? redeemScript = null, ICoin[]? additionalCoins = null, params Key[] keys) => Encoders.Base64.EncodeData(await Sign(address, Encoding.UTF8.GetBytes(message), type, keys, redeemScript, additionalCoins)); - public static async Task SignEncoded(BitcoinAddress address, string message, SignatureType type, - params Key[] keys) => Encoders.Base64.EncodeData(await Sign(address, Encoding.UTF8.GetBytes(message), type, keys)); - public static async Task Sign(BitcoinAddress address, byte[] message, SignatureType type, + public static async Task SignEncoded(BitcoinAddress address, string message, SignatureType type, + Script? redeemScript = null, ICoin[]? additionalCoins = null, params Key[] keys) => await Sign(address, message, type, keys, redeemScript, additionalCoins); + public static async Task SignEncoded(BitcoinAddress address, string message, SignatureType type, + params Key[] keys) => await Sign(address, message, type, keys); + public static async Task Sign(BitcoinAddress address, string message, SignatureType type, Key[] keys, Script? redeemScript = null, ICoin[]? additionalCoins = null) { // if(address.ScriptPubKey.IsScriptType(ScriptType.P2SH) && type == SignatureType.Simple) @@ -125,11 +127,7 @@ public static async Task Sign(BitcoinAddress address, byte[] message, Si { throw new InvalidOperationException("Invalid signature."); } - - return new[] - { - (byte) sig.RecoveryId - }.Concat(sig.Signature).ToArray(); + return new BIP322.BIP322Signature.Legacy(key.IsCompressed, sig, address.Network); } } @@ -149,11 +147,13 @@ public static async Task Sign(BitcoinAddress address, byte[] message, Si toSignTx.Sign(keys.Select(key=> key.GetBitcoinSecret(address.Network)), coins); - var result = type == SignatureType.Simple ? toSignTx.Inputs[0].WitScript.ToBytes() : toSignTx.ToBytes(); - if (result.Length == 0) + BIP322.BIP322Signature result = + type == SignatureType.Simple ? new BIP322.BIP322Signature.Simple(toSignTx.Inputs[0].WitScript, address.Network) + : new BIP322.BIP322Signature.Full(toSignTx, address.Network); + if (result is BIP322.BIP322Signature.Simple { WitnessScript : { PushCount: 0 } } || + result is BIP322.BIP322Signature.Full full && full.SignedTransaction.Inputs[0].WitScript.PushCount == 0 && full.SignedTransaction.Inputs[0].ScriptSig.Length == 0) { - if(type == SignatureType.Simple && address.ScriptPubKey.IsScriptType(ScriptType.P2SH)) - throw new InvalidOperationException("Failed to sign the message. Did you forget to provide a redeem script?"); + throw new InvalidOperationException("Failed to sign the message. Did you forget to provide a redeem script?"); } @@ -164,34 +164,22 @@ public static async Task Sign(BitcoinAddress address, byte[] message, Si return result; } - - - private static CompactSignature ParseCompactSignature(byte[] signature) + public static Task Verify(string message, BitcoinAddress address, string signature, + Func>? proofOfFundsLookup = null) { - if (signature.Length != 65) - { - throw new ArgumentException("Compact signature must be 65 bytes long.", nameof(signature)); - } - - var recoveryId = signature[0]; - var sig = new byte[64]; - Buffer.BlockCopy(signature, 1, sig, 0, 64); - return new CompactSignature(recoveryId, sig); + var sig = BIP322Signature.Parse(signature, address.Network); + return Verify(message, address, sig, proofOfFundsLookup); } - public static async Task Verify(string message, BitcoinAddress address, string signature, - Func>? proofOfFundsLookup = null) => await Verify(Encoding.UTF8.GetBytes(message), - address, Encoders.Base64.DecodeData(signature), proofOfFundsLookup); - - public static async Task Verify(byte[] message, BitcoinAddress address, byte[] signature, + public static async Task Verify(string message, BitcoinAddress address, BIP322.BIP322Signature signature, Func>? proofOfFundsLookup = null) { - try + var messageBytes = Encoding.UTF8.GetBytes(message); + if (signature is BIP322.BIP322Signature.Simple { WitnessScript: var script }) { - var script = new WitScript(signature); if (script.PushCount < 2 && !address.ScriptPubKey.IsScriptType(ScriptType.Taproot)) { - throw new InvalidOperationException("Invalid signature."); + return false; } var toSpend = CreateToSpendTransaction(address.Network, CreateMessageHash(message, MessageType.BIP322), @@ -200,15 +188,15 @@ public static async Task Verify(byte[] message, BitcoinAddress address, by ScriptEvaluationContext evalContext = new ScriptEvaluationContext() { ScriptVerify = ScriptVerify.Const_ScriptCode - | ScriptVerify.LowS - | ScriptVerify.StrictEnc - | ScriptVerify.NullFail - | ScriptVerify.MinimalData - | ScriptVerify.CleanStack - | ScriptVerify.P2SH - | ScriptVerify.Witness - | ScriptVerify.Taproot - | ScriptVerify.MinimalIf + | ScriptVerify.LowS + | ScriptVerify.StrictEnc + | ScriptVerify.NullFail + | ScriptVerify.MinimalData + | ScriptVerify.CleanStack + | ScriptVerify.P2SH + | ScriptVerify.Witness + | ScriptVerify.Taproot + | ScriptVerify.MinimalIf }; if (address.ScriptPubKey.IsScriptType(ScriptType.P2SH)) @@ -218,41 +206,32 @@ public static async Task Verify(byte[] message, BitcoinAddress address, by toSign.Inputs[0].ScriptSig = PayToScriptHashTemplate.Instance.GenerateScriptSig(new Op[0], withScriptParams.Hash.ScriptPubKey); } // Create a checker for the signature - TransactionChecker checker = address.ScriptPubKey.IsScriptType(ScriptType.Taproot) ? new TransactionChecker(toSign, 0, toSpend.Outputs[0], new TaprootReadyPrecomputedTransactionData(toSign,toSpend.Outputs.ToArray())) : new TransactionChecker(toSign, 0, toSpend.Outputs[0]); - return evalContext.VerifyScript(toSign.Inputs[0].ScriptSig , script, address.ScriptPubKey, checker); + TransactionChecker checker = address.ScriptPubKey.IsScriptType(ScriptType.Taproot) ? new TransactionChecker(toSign, 0, toSpend.Outputs[0], new TaprootReadyPrecomputedTransactionData(toSign, toSpend.Outputs.ToArray())) : new TransactionChecker(toSign, 0, toSpend.Outputs[0]); + return evalContext.VerifyScript(toSign.Inputs[0].ScriptSig, script, address.ScriptPubKey, checker); } - catch (Exception) + else if (signature is BIP322.BIP322Signature.Legacy { CompactSignature : var sig}) { - Transaction toSign; try { - toSign = Transaction.Load(signature, address.Network); - } - catch - { - try + if (!address.ScriptPubKey.IsScriptType(ScriptType.P2PKH)) { - if (!address.ScriptPubKey.IsScriptType(ScriptType.P2PKH)) - { - return false; - } - - var sig = ParseCompactSignature(signature); - var hash = CreateMessageHash(message, MessageType.Legacy); - var k = sig.RecoverPubKey(hash); - if (k.GetAddress(ScriptPubKeyType.Legacy, address.Network) != address) - { - return false; - } - - return ECDSASignature.TryParseFromCompact(sig.Signature, out var ecSig) && k.Verify(hash, ecSig); + return false; } - catch + var hash = CreateMessageHash(message, MessageType.Legacy); + var k = sig.RecoverPubKey(hash); + if (k.GetAddress(ScriptPubKeyType.Legacy, address.Network) != address) { return false; } + return ECDSASignature.TryParseFromCompact(sig.Signature, out var ecSig) && k.Verify(hash, ecSig); } - + catch + { + return false; + } + } + else if (signature is BIP322.BIP322Signature.Full { SignedTransaction: var toSign }) + { var toSpend = CreateToSpendTransaction(address.Network, CreateMessageHash(message, MessageType.BIP322), address.ScriptPubKey); if (toSign!.Inputs[0].PrevOut.Hash != toSpend.GetHash()) @@ -280,6 +259,8 @@ public static async Task Verify(byte[] message, BitcoinAddress address, by return toSign.CreateValidator(toSpend.Outputs.ToArray()).ValidateInputs().All(x => x.Error is null); } + return false; } } } +#endif diff --git a/NBitcoin/CompactSignature.cs b/NBitcoin/CompactSignature.cs index cbb133c797..aa51cfc320 100644 --- a/NBitcoin/CompactSignature.cs +++ b/NBitcoin/CompactSignature.cs @@ -9,32 +9,12 @@ namespace NBitcoin { public class CompactSignature { - public CompactSignature(int header, byte[] sig64) + public CompactSignature(int recoveryId, byte[] sig64) { if (sig64 is null) throw new ArgumentNullException(nameof(sig64)); if (sig64.Length is not 64) throw new ArgumentException("sig64 should be 64 bytes", nameof(sig64)); - - var recoveryId = header; - if(recoveryId>= 39) // this is a bech32 signature - { - recoveryId -= 12; - } // this is a segwit p2sh signature - else if (recoveryId >= 35) - { - recoveryId -= 8; - } // this is a compressed key signature - else if (recoveryId >= 31) - { - recoveryId -= 4; - } - - if (!IsValidRecId(recoveryId)) - { - recoveryId = recoveryId - 27; - } - if (!IsValidRecId(recoveryId)) throw new ArgumentOutOfRangeException(nameof(recoveryId), $"recoveryId should be recoveryId >= 0 && recoveryId < 4 but was {recoveryId}"); diff --git a/NBitcoin/Crypto/Hashes.cs b/NBitcoin/Crypto/Hashes.cs index 77d9bc589b..6c1a7d3e49 100644 --- a/NBitcoin/Crypto/Hashes.cs +++ b/NBitcoin/Crypto/Hashes.cs @@ -36,6 +36,8 @@ public static uint256 DoubleSHA256(byte[] data, int offset, int count) } #endregion + public static byte[] DoubleSHA256RawBytes(byte[] data) => DoubleSHA256RawBytes(data, 0, data.Length); + public static byte[] DoubleSHA256RawBytes(byte[] data, int offset, int count) { #if NONATIVEHASH