Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ frost-ed25519 = { git = "https://github.com/ZcashFoundation/frost", branch = "ma
frost-p256 = { git = "https://github.com/ZcashFoundation/frost", branch = "main" }
frost-ristretto255 = { git = "https://github.com/ZcashFoundation/frost", branch = "main" }
frost-ed448 = { git = "https://github.com/ZcashFoundation/frost", branch = "main" }
dkls23-secp256k1 = { git = "https://github.com/0xCarbon/DKLs23", branch = "main", features = ["serde"] }
dkls23-secp256r1 = { git = "https://github.com/0xCarbon/DKLs23", branch = "main", features = ["serde"] }
dkls23-secp256k1 = { git = "https://github.com/Lunar-Rails/DKLs23", branch = "upstream/scalar-tweaks", features = ["serde"] }
dkls23-secp256r1 = { git = "https://github.com/Lunar-Rails/DKLs23", branch = "upstream/scalar-tweaks", features = ["serde"] }
zeroize = { version = "1", features = ["derive"] }
slotmap = "1"
k256 = "=0.14.0-rc.8"
Expand Down
6 changes: 2 additions & 4 deletions examples/tokio-tss/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ impl EncryptedRouter {
if let Some(target) = message.to {
let (send_channel, _) = self.peer_channels.get_mut(&target).ok_or_else(|| {
TssError::Protocol(format!(
"party {} has no encrypted channel for peer {}",
self_id, target
"party {self_id} has no encrypted channel for peer {target}"
))
})?;

Expand Down Expand Up @@ -211,8 +210,7 @@ pub fn create_encrypted_routers(

if left_shared != right_shared {
return Err(TssError::Encryption(format!(
"mismatched shared secret for parties {} and {}",
left, right
"mismatched shared secret for parties {left} and {right}"
)));
}

Expand Down
31 changes: 31 additions & 0 deletions libtss-dotnet/src/Libtss/KeyShareHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,37 @@ public unsafe byte[] GroupVerifyingKey
}
}

/// <summary>
/// The 32-byte BIP-32 chain code paired with <see cref="GroupVerifyingKey"/>.
/// </summary>
/// <remarks>
/// <para>
/// Only the BIP-32-capable ciphersuites carry a chain code:
/// <see cref="Ciphersuite.Secp256k1ECDSA"/>, <see cref="Ciphersuite.Secp256r1ECDSA"/>
/// and <see cref="Ciphersuite.Secp256k1Taproot"/>. Every other suite throws a
/// <see cref="TssException"/> with <see cref="TssStatus.ProtoMismatch"/>.
/// </para>
/// <para>
/// Pair with <see cref="GroupVerifyingKey"/> to build a BIP-32 extended public key (xpub)
/// whose off-MPC child derivation matches <see cref="DeriveOperations.DeriveChild"/>.
/// </para>
/// </remarks>
public unsafe byte[] ChainCode
{
get
{
ThrowIfDisposed();
var buf = new NativeTssBuffer();
int status;
fixed (TssBuffer* p = &buf.Raw)
{
status = Native.tss_handle_chain_code(_handle, p);
}
TssException.ThrowIfError(status);
return buf.ToArray();
}
}

/// <summary>
/// The public verification key for this specific share as raw bytes.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions libtss-dotnet/src/Libtss/Native.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ public static extern int tss_refresh_next(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_group_key(ulong handle, TssBuffer* @out);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_chain_code(ulong handle, TssBuffer* @out);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_handle_pubkey_package(ulong handle, TssBuffer* @out);

Expand Down Expand Up @@ -285,6 +288,20 @@ public static extern int tss_derive_path(
[MarshalAs(UnmanagedType.LPUTF8Str)] string path,
ulong* out_handle);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_tweak_add(
ulong key_share,
byte* tweak,
nuint tweak_len,
ulong* out_handle);

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern int tss_tweak_mul(
ulong key_share,
byte* factor,
nuint factor_len,
ulong* out_handle);

// --- Init ---

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
Expand Down
100 changes: 100 additions & 0 deletions libtss-dotnet/src/Libtss/TweakOperations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
namespace Libtss;

/// <summary>
/// Scalar tweaks for DKLs23 threshold ECDSA key shares
/// (<see cref="Ciphersuite.Secp256k1ECDSA"/> and <see cref="Ciphersuite.Secp256r1ECDSA"/>).
/// Lightning derives per-commitment keys from caller-supplied scalars rather than BIP-32
/// indices: LND's single tweak is <c>k' = k + t (mod n)</c> and its revocation ("double")
/// tweak composes <c>k' = k * a (mod n)</c> with an additive step.
/// </summary>
/// <remarks>
/// <para>
/// Each operation returns a new key share whose secret share, public key, BIP-32 derivation
/// data and address are moved by the given map, together with a public key package whose
/// group key and every verifying share are moved by the same map. Because Lagrange
/// coefficients sum to one, a threshold set of tweaked shares signs under the tweaked group
/// key; every party must apply the same tweak before a signing session.
/// </para>
/// <para>
/// A tweak is not a BIP-32 edge: the derivation metadata still describes the un-tweaked
/// ancestor and the chain code is unchanged. For BIP-341 Taproot tweaking of FROST shares
/// see <see cref="FrostOperations.TweakKeyShare"/>.
/// </para>
/// </remarks>
public static class TweakOperations
{
private const int ScalarLength = 32;

/// <summary>
/// Returns a new key share whose secret is <c>k + tweak (mod n)</c>.
/// The original key share remains valid.
/// </summary>
/// <param name="keyShare">The key share to tweak.</param>
/// <param name="tweak">
/// A 32-byte big-endian scalar, reduced mod n (LND's per-commitment "single tweak"
/// convention). A zero tweak is a no-op.
/// </param>
/// <returns>A new key share handle for the tweaked share.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="keyShare"/> or <paramref name="tweak"/> is null.
/// </exception>
/// <exception cref="ArgumentException"><paramref name="tweak"/> is not 32 bytes.</exception>
/// <exception cref="TssException">
/// <see cref="TssStatus.ProtoMismatch"/> when the key share is not a DKLs23 share;
/// <see cref="TssStatus.Tweak"/> when the tweak produces the identity public key.
/// </exception>
public static unsafe KeyShareHandle TweakAdd(KeyShareHandle keyShare, byte[] tweak)
{
ArgumentNullException.ThrowIfNull(keyShare);
ValidateScalar(tweak, nameof(tweak));

ulong outHandle;
int status;
fixed (byte* p = tweak)
{
status = Native.tss_tweak_add(keyShare.Handle, p, (nuint)tweak.Length, &outHandle);
}
TssException.ThrowIfError(status);
return new KeyShareHandle(outHandle);
}

/// <summary>
/// Returns a new key share whose secret is <c>k * factor (mod n)</c>.
/// The original key share remains valid.
/// </summary>
/// <param name="keyShare">The key share to tweak.</param>
/// <param name="factor">
/// A 32-byte big-endian scalar, reduced mod n; the building block of LND's revocation
/// ("double") tweak. A factor of one is a no-op.
/// </param>
/// <returns>A new key share handle for the tweaked share.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="keyShare"/> or <paramref name="factor"/> is null.
/// </exception>
/// <exception cref="ArgumentException"><paramref name="factor"/> is not 32 bytes.</exception>
/// <exception cref="TssException">
/// <see cref="TssStatus.ProtoMismatch"/> when the key share is not a DKLs23 share;
/// <see cref="TssStatus.Tweak"/> when the factor reduces to zero.
/// </exception>
public static unsafe KeyShareHandle TweakMul(KeyShareHandle keyShare, byte[] factor)
{
ArgumentNullException.ThrowIfNull(keyShare);
ValidateScalar(factor, nameof(factor));

ulong outHandle;
int status;
fixed (byte* p = factor)
{
status = Native.tss_tweak_mul(keyShare.Handle, p, (nuint)factor.Length, &outHandle);
}
TssException.ThrowIfError(status);
return new KeyShareHandle(outHandle);
}

private static void ValidateScalar(byte[] scalar, string paramName)
{
ArgumentNullException.ThrowIfNull(scalar, paramName);
if (scalar.Length != ScalarLength)
throw new ArgumentException($"Must be exactly {ScalarLength} bytes.", paramName);
}
}
63 changes: 63 additions & 0 deletions libtss-dotnet/tests/Libtss.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,69 @@ public void SplitKey_ZerosInputSecretKey()
Assert.All(secretKey, b => Assert.Equal(0, b));
}

[Fact]
public void ChainCode_Taproot_IsSha256OfGroupKey()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Secp256k1Taproot, maxSigners: 3, minSigners: 2);
try
{
byte[] chainCode = handles[0].ChainCode;
Assert.Equal(32, chainCode.Length);
Assert.Equal(SHA256.HashData(handles[0].GroupVerifyingKey), chainCode);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void ChainCode_ChangesAfterDeriveChild()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Secp256k1Taproot, maxSigners: 3, minSigners: 2);
try
{
using var child = DeriveOperations.DeriveChild(handles[0], childNumber: 7);
byte[] childChainCode = child.ChainCode;
Assert.Equal(32, childChainCode.Length);
Assert.NotEqual(handles[0].ChainCode, childChainCode);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void ChainCode_UnsupportedSuite_Throws()
{
RequireNativeLibrary();
Tss.Init();

var (handles, _) = FrostOperations.GenerateDealer(
Ciphersuite.Ed25519, maxSigners: 3, minSigners: 2);
try
{
var ex = Assert.Throws<TssException>(() => handles[0].ChainCode);
Assert.Equal(TssStatus.ProtoMismatch, ex.Code);
}
finally
{
foreach (var h in handles)
h.Dispose();
}
}

[Fact]
public void WipeBytes_ZerosArray()
{
Expand Down
Loading
Loading