Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
55 changes: 55 additions & 0 deletions src/NetMQ.Tests/CurveTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
Expand Down Expand Up @@ -40,6 +41,60 @@ 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}");

// 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
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]
Expand Down
5 changes: 5 additions & 0 deletions src/NetMQ/Core/Mechanisms/CurveServerMechanism.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ 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).
// 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
m_box = new Curve25519XSalsa20Poly1305(m_cnSecretKey, m_cnClientKey);

Expand Down
22 changes: 22 additions & 0 deletions src/NetMQ/Core/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -302,6 +304,12 @@ public byte IdentitySize {
/// </summary>
public byte[] CurveServerKey { get; set; }

/// <summary>
/// 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.
/// </summary>
public HashSet<byte[]> CurveAllowedClients { get; } = new HashSet<byte[]>(new ByteArrayEqualityComparer());

/// <summary>
/// If remote peer receives a PING message and doesn't receive another
/// message within the ttl value, it should close the connection
Expand Down Expand Up @@ -529,6 +537,17 @@ public void SetSocketOption(ZmqSocketOption option, object? optionValue)
break;
}

case ZmqSocketOption.CurveAllowedClients:
{
var key = Get<byte[]>();
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)
Expand Down Expand Up @@ -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);
}
Expand Down
8 changes: 7 additions & 1 deletion src/NetMQ/Core/ZmqSocketOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

/// <summary>
/// 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.
/// </summary>
CurveAllowedClients = 61

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I don't think we can just add this here. These are standardised across ZeroMQ libraries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. CurveAllowedClients has been removed from ZmqSocketOption entirely. It is now a NetMQ-specific internal property on SocketBase (backed directly by m_options.CurveAllowedClients), and SocketOptions/ThreadSafeSocketOptions expose it by accessing that property directly without routing through the ZMQ option infrastructure. (commit 2f44dd2)

}
}
12 changes: 12 additions & 0 deletions src/NetMQ/SocketOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

using NetMQ.Core;
Expand Down Expand Up @@ -475,6 +476,17 @@ public NetMQCertificate CurveServerCertificate
{
set => m_socket.SetSocketOption(ZmqSocketOption.CurveServerKey, value.PublicKey);
}

/// <summary>
/// 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:
/// <code>
/// server.Options.CurveAllowedClients.Add(clientCertificate.PublicKey);
/// </code>
/// </summary>
public HashSet<byte[]> CurveAllowedClients =>
m_socket.GetSocketOptionX<HashSet<byte[]>>(ZmqSocketOption.CurveAllowedClients)!;

/// <summary>
/// If remote peer receives a PING message and doesn't receive another
Expand Down
12 changes: 12 additions & 0 deletions src/NetMQ/ThreadSafeSocketOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NetMQ.Core;

Expand Down Expand Up @@ -368,6 +369,17 @@ public NetMQCertificate CurveServerCertificate
{
set => SetSocketOption(ZmqSocketOption.CurveServerKey, value.PublicKey);
}

/// <summary>
/// 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:
/// <code>
/// server.Options.CurveAllowedClients.Add(clientCertificate.PublicKey);
/// </code>
/// </summary>
public HashSet<byte[]> CurveAllowedClients =>
GetSocketOptionX<HashSet<byte[]>>(ZmqSocketOption.CurveAllowedClients)!;

/// <summary>
/// If remote peer receives a PING message and doesn't receive another
Expand Down