From 569f68968f7fb2b7062ee193312c4328c8d61bcc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 23:01:38 +0000
Subject: [PATCH 1/5] Initial plan
From de2fb707a5bef8525164dbd09a393faa5596986f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 23:07:25 +0000
Subject: [PATCH 2/5] Add CurveAllowedClients feature to enable server-side
client public key whitelist
Agent-Logs-Url: https://github.com/zeromq/netmq/sessions/f166bdbe-9fb4-4148-ad85-7eb065157838
Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com>
---
src/NetMQ.Tests/CurveTests.cs | 53 +++++++++++++++++++
.../Core/Mechanisms/CurveServerMechanism.cs | 4 ++
src/NetMQ/Core/Options.cs | 22 ++++++++
src/NetMQ/Core/ZmqSocketOption.cs | 8 ++-
src/NetMQ/SocketOptions.cs | 12 +++++
src/NetMQ/ThreadSafeSocketOptions.cs | 12 +++++
6 files changed, 110 insertions(+), 1 deletion(-)
diff --git a/src/NetMQ.Tests/CurveTests.cs b/src/NetMQ.Tests/CurveTests.cs
index f693aec3..e363333a 100644
--- a/src/NetMQ.Tests/CurveTests.cs
+++ b/src/NetMQ.Tests/CurveTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
@@ -40,6 +41,58 @@ public void CurveTest()
}
+
+ [Fact]
+ public void CurveAllowedClientsTest_AllowedClientCanConnect()
+ {
+ var serverPair = new NetMQCertificate();
+ using var server = new DealerSocket();
+ server.Options.CurveServer = true;
+ server.Options.CurveCertificate = serverPair;
+
+ var allowedClientPair = new NetMQCertificate();
+ // Only allow this specific client
+ server.Options.CurveAllowedClients.Add(allowedClientPair.PublicKey);
+
+ int port = server.BindRandomPort("tcp://127.0.0.1");
+
+ using var client = new DealerSocket();
+ client.Options.CurveServerKey = serverPair.PublicKey;
+ client.Options.CurveCertificate = allowedClientPair;
+ client.Connect($"tcp://127.0.0.1:{port}");
+
+ client.SendFrame("Hello");
+ var hello = server.ReceiveFrameString();
+ Assert.Equal("Hello", hello);
+ }
+
+ [Fact]
+ public void CurveAllowedClientsTest_BlockedClientCannotConnect()
+ {
+ var serverPair = new NetMQCertificate();
+ using var server = new DealerSocket();
+ server.Options.CurveServer = true;
+ server.Options.CurveCertificate = serverPair;
+
+ var allowedClientPair = new NetMQCertificate();
+ // Only allow this specific client
+ server.Options.CurveAllowedClients.Add(allowedClientPair.PublicKey);
+
+ int port = server.BindRandomPort("tcp://127.0.0.1");
+
+ // Connect with a different (not allowed) client
+ var blockedClientPair = new NetMQCertificate();
+ using var blockedClient = new DealerSocket();
+ blockedClient.Options.CurveServerKey = serverPair.PublicKey;
+ blockedClient.Options.CurveCertificate = blockedClientPair;
+ blockedClient.Connect($"tcp://127.0.0.1:{port}");
+
+ blockedClient.TrySendFrame(TimeSpan.FromMilliseconds(100), "ShouldBeBlocked");
+
+ // Server should not receive anything from the blocked client
+ var received = server.TryReceiveFrameString(TimeSpan.FromMilliseconds(500), out var message);
+ Assert.False(received, "Blocked client should not have been able to send a message");
+ }
#if NETFRAMEWORK
[Fact]
diff --git a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
index 92aaf5e0..1371fb05 100644
--- a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
+++ b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
@@ -267,6 +267,10 @@ PushMsgResult ProcessInitiate(ref Msg msg)
if (!SpanUtility.Equals(vouchPlaintext.Slice(0, 32), m_cnClientKey))
return PushMsgResult.Error;
+ // Verify client long-term public key is in the allowed clients list (if configured)
+ if (Options.CurveAllowedClients.Count > 0 && !Options.CurveAllowedClients.Contains(clientKey.ToArray()))
+ return PushMsgResult.Error;
+
// Create the session box
m_box = new Curve25519XSalsa20Poly1305(m_cnSecretKey, m_cnClientKey);
diff --git a/src/NetMQ/Core/Options.cs b/src/NetMQ/Core/Options.cs
index 5cac3c3d..79985187 100644
--- a/src/NetMQ/Core/Options.cs
+++ b/src/NetMQ/Core/Options.cs
@@ -21,7 +21,9 @@ You should have received a copy of the GNU Lesser General Public License
*/
using System;
+using System.Collections.Generic;
using System.Text;
+using NetMQ.Core.Utils;
namespace NetMQ.Core
{
@@ -302,6 +304,12 @@ public byte IdentitySize {
///
public byte[] CurveServerKey { get; set; }
+ ///
+ /// The set of allowed client long-term public keys for a CURVE server socket.
+ /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
+ ///
+ public HashSet CurveAllowedClients { get; } = new HashSet(new ByteArrayEqualityComparer());
+
///
/// If remote peer receives a PING message and doesn't receive another
/// message within the ttl value, it should close the connection
@@ -529,6 +537,17 @@ public void SetSocketOption(ZmqSocketOption option, object? optionValue)
break;
}
+ case ZmqSocketOption.CurveAllowedClients:
+ {
+ var key = Get();
+ if (key.Length != 32)
+ throw new InvalidException("Curve key size must be 32 bytes");
+ var keyCopy = new byte[32];
+ Buffer.BlockCopy(key, 0, keyCopy, 0, 32);
+ CurveAllowedClients.Add(keyCopy);
+ break;
+ }
+
case ZmqSocketOption.HelloMessage:
{
if (optionValue == null)
@@ -700,6 +719,9 @@ public void SetSocketOption(ZmqSocketOption option, object? optionValue)
case ZmqSocketOption.CurveServerKey:
return CurveServerKey;
+ case ZmqSocketOption.CurveAllowedClients:
+ return CurveAllowedClients;
+
default:
throw new InvalidException("GetSocketOption called with invalid ZmqSocketOption of " + option);
}
diff --git a/src/NetMQ/Core/ZmqSocketOption.cs b/src/NetMQ/Core/ZmqSocketOption.cs
index 43b8aab4..bc47b8c3 100644
--- a/src/NetMQ/Core/ZmqSocketOption.cs
+++ b/src/NetMQ/Core/ZmqSocketOption.cs
@@ -360,6 +360,12 @@ internal enum ZmqSocketOption
PgmMaxTransportServiceDataUnitLength = 1001,
Correlate = 58, // in zmq it's 52 and 53, but I guess it does not matter so long as it is unique.
- Relaxed = 59
+ Relaxed = 59,
+
+ ///
+ /// The set of allowed client public keys for a CURVE server socket.
+ /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
+ ///
+ CurveAllowedClients = 61
}
}
\ No newline at end of file
diff --git a/src/NetMQ/SocketOptions.cs b/src/NetMQ/SocketOptions.cs
index de520e68..f26310ae 100644
--- a/src/NetMQ/SocketOptions.cs
+++ b/src/NetMQ/SocketOptions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NetMQ.Core;
@@ -475,6 +476,17 @@ public NetMQCertificate CurveServerCertificate
{
set => m_socket.SetSocketOption(ZmqSocketOption.CurveServerKey, value.PublicKey);
}
+
+ ///
+ /// Gets the set of allowed client public keys for a CURVE server socket.
+ /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
+ /// Add public keys to restrict which clients can connect:
+ ///
+ /// server.Options.CurveAllowedClients.Add(clientCertificate.PublicKey);
+ ///
+ ///
+ public HashSet CurveAllowedClients =>
+ m_socket.GetSocketOptionX>(ZmqSocketOption.CurveAllowedClients)!;
///
/// If remote peer receives a PING message and doesn't receive another
diff --git a/src/NetMQ/ThreadSafeSocketOptions.cs b/src/NetMQ/ThreadSafeSocketOptions.cs
index fcfd8568..dea11ed1 100644
--- a/src/NetMQ/ThreadSafeSocketOptions.cs
+++ b/src/NetMQ/ThreadSafeSocketOptions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NetMQ.Core;
@@ -368,6 +369,17 @@ public NetMQCertificate CurveServerCertificate
{
set => SetSocketOption(ZmqSocketOption.CurveServerKey, value.PublicKey);
}
+
+ ///
+ /// Gets the set of allowed client public keys for a CURVE server socket.
+ /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
+ /// Add public keys to restrict which clients can connect:
+ ///
+ /// server.Options.CurveAllowedClients.Add(clientCertificate.PublicKey);
+ ///
+ ///
+ public HashSet CurveAllowedClients =>
+ GetSocketOptionX>(ZmqSocketOption.CurveAllowedClients)!;
///
/// If remote peer receives a PING message and doesn't receive another
From 8bdb954c39b7dd41c9ab49d2db113654eac36f19 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 23:10:02 +0000
Subject: [PATCH 3/5] Address code review: use pre-allocated byte[] for client
key to avoid ToArray() allocation
Agent-Logs-Url: https://github.com/zeromq/netmq/sessions/f166bdbe-9fb4-4148-ad85-7eb065157838
Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com>
---
src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
index 1371fb05..04b6a613 100644
--- a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
+++ b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
@@ -253,7 +253,8 @@ PushMsgResult ProcessInitiate(ref Msg msg)
Span vouchNonce = stackalloc byte[Curve25519XSalsa20Poly1305.NonceLength];
Span vouchPlaintext = stackalloc byte[64];
Span vouchBox = new Span(initiatePlaintext, 48, 80);
- var clientKey = new Span(initiatePlaintext, 0, 32);
+ var clientKey = new byte[32];
+ Buffer.BlockCopy(initiatePlaintext, 0, clientKey, 0, 32);
VouchNoncePrefix.CopyTo(vouchNonce);
new Span(initiatePlaintext, 32, 16).CopyTo(vouchNonce.Slice(8));
@@ -268,7 +269,7 @@ PushMsgResult ProcessInitiate(ref Msg msg)
return PushMsgResult.Error;
// Verify client long-term public key is in the allowed clients list (if configured)
- if (Options.CurveAllowedClients.Count > 0 && !Options.CurveAllowedClients.Contains(clientKey.ToArray()))
+ if (Options.CurveAllowedClients.Count > 0 && !Options.CurveAllowedClients.Contains(clientKey))
return PushMsgResult.Error;
// Create the session box
From 6e39e1b6d1a1d5a682ea4aeef82bbf9c1b073acf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 1 May 2026 23:12:32 +0000
Subject: [PATCH 4/5] Revert to Span for clientKey; only allocate byte[] when
whitelist is non-empty
Agent-Logs-Url: https://github.com/zeromq/netmq/sessions/f166bdbe-9fb4-4148-ad85-7eb065157838
Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com>
---
src/NetMQ.Tests/CurveTests.cs | 2 ++
src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs | 8 ++++----
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/NetMQ.Tests/CurveTests.cs b/src/NetMQ.Tests/CurveTests.cs
index e363333a..9f2345ec 100644
--- a/src/NetMQ.Tests/CurveTests.cs
+++ b/src/NetMQ.Tests/CurveTests.cs
@@ -87,6 +87,8 @@ public void CurveAllowedClientsTest_BlockedClientCannotConnect()
blockedClient.Options.CurveCertificate = blockedClientPair;
blockedClient.Connect($"tcp://127.0.0.1:{port}");
+ // The send may or may not succeed at the transport level (the handshake failure
+ // happens asynchronously), but the server must never deliver the message.
blockedClient.TrySendFrame(TimeSpan.FromMilliseconds(100), "ShouldBeBlocked");
// Server should not receive anything from the blocked client
diff --git a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
index 04b6a613..2f5da823 100644
--- a/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
+++ b/src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
@@ -253,8 +253,7 @@ PushMsgResult ProcessInitiate(ref Msg msg)
Span vouchNonce = stackalloc byte[Curve25519XSalsa20Poly1305.NonceLength];
Span vouchPlaintext = stackalloc byte[64];
Span vouchBox = new Span(initiatePlaintext, 48, 80);
- var clientKey = new byte[32];
- Buffer.BlockCopy(initiatePlaintext, 0, clientKey, 0, 32);
+ var clientKey = new Span(initiatePlaintext, 0, 32);
VouchNoncePrefix.CopyTo(vouchNonce);
new Span(initiatePlaintext, 32, 16).CopyTo(vouchNonce.Slice(8));
@@ -268,8 +267,9 @@ PushMsgResult ProcessInitiate(ref Msg msg)
if (!SpanUtility.Equals(vouchPlaintext.Slice(0, 32), m_cnClientKey))
return PushMsgResult.Error;
- // Verify client long-term public key is in the allowed clients list (if configured)
- if (Options.CurveAllowedClients.Count > 0 && !Options.CurveAllowedClients.Contains(clientKey))
+ // Verify client long-term public key is in the allowed clients list (if configured).
+ // The byte[] is only allocated when the whitelist is non-empty.
+ if (Options.CurveAllowedClients.Count > 0 && !Options.CurveAllowedClients.Contains(clientKey.ToArray()))
return PushMsgResult.Error;
// Create the session box
From 2f44dd22a7a92be7f0f18eddee83c326fd1f23b3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 May 2026 13:35:47 +0000
Subject: [PATCH 5/5] Remove CurveAllowedClients from ZmqSocketOption; expose
directly via SocketBase
Agent-Logs-Url: https://github.com/zeromq/netmq/sessions/4c5608bd-8fa2-4db2-af6d-711ce7c90b0e
Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com>
---
src/NetMQ/Core/Options.cs | 14 --------------
src/NetMQ/Core/SocketBase.cs | 6 ++++++
src/NetMQ/Core/ZmqSocketOption.cs | 8 +-------
src/NetMQ/SocketOptions.cs | 2 +-
src/NetMQ/ThreadSafeSocketOptions.cs | 2 +-
5 files changed, 9 insertions(+), 23 deletions(-)
diff --git a/src/NetMQ/Core/Options.cs b/src/NetMQ/Core/Options.cs
index 79985187..514dba0b 100644
--- a/src/NetMQ/Core/Options.cs
+++ b/src/NetMQ/Core/Options.cs
@@ -537,17 +537,6 @@ public void SetSocketOption(ZmqSocketOption option, object? optionValue)
break;
}
- case ZmqSocketOption.CurveAllowedClients:
- {
- var key = Get();
- if (key.Length != 32)
- throw new InvalidException("Curve key size must be 32 bytes");
- var keyCopy = new byte[32];
- Buffer.BlockCopy(key, 0, keyCopy, 0, 32);
- CurveAllowedClients.Add(keyCopy);
- break;
- }
-
case ZmqSocketOption.HelloMessage:
{
if (optionValue == null)
@@ -719,9 +708,6 @@ public void SetSocketOption(ZmqSocketOption option, object? optionValue)
case ZmqSocketOption.CurveServerKey:
return CurveServerKey;
- case ZmqSocketOption.CurveAllowedClients:
- return CurveAllowedClients;
-
default:
throw new InvalidException("GetSocketOption called with invalid ZmqSocketOption of " + option);
}
diff --git a/src/NetMQ/Core/SocketBase.cs b/src/NetMQ/Core/SocketBase.cs
index 6bc1db50..f739b054 100644
--- a/src/NetMQ/Core/SocketBase.cs
+++ b/src/NetMQ/Core/SocketBase.cs
@@ -480,6 +480,12 @@ public int GetSocketOption(ZmqSocketOption option)
}
}
+ ///
+ /// Gets the set of allowed client long-term public keys for a CURVE server socket.
+ /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
+ ///
+ internal System.Collections.Generic.HashSet CurveAllowedClients => m_options.CurveAllowedClients;
+
///
/// Join the dish socket to a group
///
diff --git a/src/NetMQ/Core/ZmqSocketOption.cs b/src/NetMQ/Core/ZmqSocketOption.cs
index bc47b8c3..43b8aab4 100644
--- a/src/NetMQ/Core/ZmqSocketOption.cs
+++ b/src/NetMQ/Core/ZmqSocketOption.cs
@@ -360,12 +360,6 @@ internal enum ZmqSocketOption
PgmMaxTransportServiceDataUnitLength = 1001,
Correlate = 58, // in zmq it's 52 and 53, but I guess it does not matter so long as it is unique.
- Relaxed = 59,
-
- ///
- /// The set of allowed client public keys for a CURVE server socket.
- /// When non-empty, only clients whose long-term public key is present in this set will be allowed to connect.
- ///
- CurveAllowedClients = 61
+ Relaxed = 59
}
}
\ No newline at end of file
diff --git a/src/NetMQ/SocketOptions.cs b/src/NetMQ/SocketOptions.cs
index f26310ae..2dea116b 100644
--- a/src/NetMQ/SocketOptions.cs
+++ b/src/NetMQ/SocketOptions.cs
@@ -486,7 +486,7 @@ public NetMQCertificate CurveServerCertificate
///
///
public HashSet CurveAllowedClients =>
- m_socket.GetSocketOptionX>(ZmqSocketOption.CurveAllowedClients)!;
+ m_socket.SocketHandle.CurveAllowedClients;
///
/// If remote peer receives a PING message and doesn't receive another
diff --git a/src/NetMQ/ThreadSafeSocketOptions.cs b/src/NetMQ/ThreadSafeSocketOptions.cs
index dea11ed1..3f061811 100644
--- a/src/NetMQ/ThreadSafeSocketOptions.cs
+++ b/src/NetMQ/ThreadSafeSocketOptions.cs
@@ -379,7 +379,7 @@ public NetMQCertificate CurveServerCertificate
///
///
public HashSet CurveAllowedClients =>
- GetSocketOptionX>(ZmqSocketOption.CurveAllowedClients)!;
+ m_socket.CurveAllowedClients;
///
/// If remote peer receives a PING message and doesn't receive another