Class: Protocol::ZMTP::Mechanism::Curve

Inherits:
Object
  • Object
show all
Defined in:
lib/protocol/zmtp/mechanism/curve.rb

Overview

CurveZMQ security mechanism (RFC 26).

Provides Curve25519-XSalsa20-Poly1305 encryption and authentication for ZMTP 3.1 connections.

Crypto-backend-agnostic: pass any module that provides the NaCl API (RbNaCl or Nuckle) via the crypto: parameter.

The crypto backend must provide:

backend::PrivateKey.new(bytes) / .generate
backend::PublicKey.new(bytes)
backend::Box.new(peer_pub, my_secret)  → #encrypt(nonce, pt) / #decrypt(nonce, ct)
backend::SecretBox.new(key)             → #encrypt(nonce, pt) / #decrypt(nonce, ct)
backend::Random.random_bytes(n)
backend::Util.verify32(a, b) / .verify64(a, b)
backend::CryptoError (exception class)

Constant Summary collapse

MECHANISM_NAME =
"CURVE"
NONCE_PREFIX_HELLO =

Nonce prefixes.

"CurveZMQHELLO---"
NONCE_PREFIX_WELCOME =
"WELCOME-"
NONCE_PREFIX_INITIATE =
"CurveZMQINITIATE"
NONCE_PREFIX_READY =
"CurveZMQREADY---"
NONCE_PREFIX_MESSAGE_C =
"CurveZMQMESSAGEC"
NONCE_PREFIX_MESSAGE_S =
"CurveZMQMESSAGES"
NONCE_PREFIX_VOUCH =
"VOUCH---"
"COOKIE--"
BOX_OVERHEAD =
16
MAX_NONCE =
(2**64) - 1
MESSAGE_PREFIX =
"\x07MESSAGE".b.freeze
MESSAGE_PREFIX_SIZE =
MESSAGE_PREFIX.bytesize

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(public_key: nil, secret_key: nil, server_key: nil, crypto:, as_server: false, authenticator: nil) ⇒ Curve

Returns a new instance of Curve.

Parameters:

  • public_key (String, nil) (defaults to: nil)

    32-byte permanent public key

  • secret_key (String, nil) (defaults to: nil)

    32-byte permanent secret key

  • server_key (String, nil) (defaults to: nil)

    32-byte server permanent public key (client only)

  • crypto (Module)

    NaCl-compatible crypto backend

  • as_server (Boolean) (defaults to: false)

    whether this side acts as the CURVE server

  • authenticator (#call, nil) (defaults to: nil)

    optional server-side authenticator



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 78

def initialize(public_key: nil, secret_key: nil, server_key: nil, crypto:, as_server: false, authenticator: nil)
  @crypto        = crypto
  @as_server     = as_server
  @authenticator = authenticator

  if as_server
    validate_key!(public_key, "public_key")
    validate_key!(secret_key, "secret_key")
    @permanent_public = crypto::PublicKey.new(public_key.b)
    @permanent_secret = crypto::PrivateKey.new(secret_key.b)
    @cookie_key = crypto::Random.random_bytes(32)
  else
    validate_key!(server_key, "server_key")
    @server_public = crypto::PublicKey.new(server_key.b)
    if public_key && secret_key
      validate_key!(public_key, "public_key")
      validate_key!(secret_key, "secret_key")
      @permanent_public = crypto::PublicKey.new(public_key.b)
      @permanent_secret = crypto::PrivateKey.new(secret_key.b)
    else
      @permanent_secret = crypto::PrivateKey.generate
      @permanent_public = @permanent_secret.public_key
    end
  end

  @session_box      = nil
  @send_nonce       = 0
  @recv_nonce       = -1
  @metadata = nil
end

Instance Attribute Details

#metadataObject

Extra READY/INITIATE properties merged in by extensions (e.g. ZMTP-Zstd’s X-Compression). nil = none.



28
29
30
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 28

def 
  @metadata
end

Class Method Details

.client(server_key:, crypto:, public_key: nil, secret_key: nil) ⇒ Curve

Creates a CURVE client mechanism.

Parameters:

  • server_key (String)

    32 bytes (server permanent public key)

  • crypto (Module)

    NaCl-compatible backend (RbNaCl or Nuckle)

  • public_key (String, nil) (defaults to: nil)

    32 bytes (or nil for auto-generated ephemeral identity)

  • secret_key (String, nil) (defaults to: nil)

    32 bytes (or nil for auto-generated ephemeral identity)

Returns:



67
68
69
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 67

def self.client(server_key:, crypto:, public_key: nil, secret_key: nil)
  new(public_key:, secret_key:, server_key:, crypto:, as_server: false)
end

.server(public_key:, secret_key:, crypto:, authenticator: nil) ⇒ Curve

Creates a CURVE server mechanism.

Parameters:

  • public_key (String)

    32 bytes

  • secret_key (String)

    32 bytes

  • crypto (Module)

    NaCl-compatible backend (RbNaCl or Nuckle)

  • authenticator (#call, nil) (defaults to: nil)

    called with a PeerInfo during authentication; must return truthy to allow the connection. When nil, any client with a valid vouch is accepted.

Returns:



55
56
57
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 55

def self.server(public_key:, secret_key:, crypto:, authenticator: nil)
  new(public_key:, secret_key:, crypto:, as_server: true, authenticator:)
end

Instance Method Details

#decrypt(frame) ⇒ Codec::Frame

Decrypts a CURVE MESSAGE command frame back into a plaintext frame.

Parameters:

  • frame (Codec::Frame)

    an encrypted MESSAGE command frame

Returns:

  • (Codec::Frame)

    the decrypted frame with restored flags

Raises:

  • (Error)

    on decryption failure or nonce violation



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 194

def decrypt(frame)
  body = frame.body
  unless body.start_with?(MESSAGE_PREFIX)
    raise Error, "expected MESSAGE command"
  end

  data = body.byteslice(MESSAGE_PREFIX_SIZE..)
  raise Error, "MESSAGE too short" if data.bytesize < 8 + BOX_OVERHEAD

  short_nonce = data.byteslice(0, 8)
  ciphertext  = data.byteslice(8..)

  nonce_value = short_nonce.unpack1("Q>")
  unless nonce_value > @recv_nonce
    raise Error, "MESSAGE nonce not strictly incrementing"
  end
  @recv_nonce = nonce_value

  @recv_nonce_buf[16, 8] = short_nonce
  begin
    plaintext = @session_box.decrypt(@recv_nonce_buf, ciphertext)
  rescue @crypto::CryptoError
    raise Error, "MESSAGE decryption failed"
  end

  flags = plaintext.getbyte(0)
  body  = plaintext.byteslice(1..) || "".b
  Codec::Frame.new(body, more: (flags & 0x01) != 0, command: (flags & 0x04) != 0)
end

#encrypt(body, more: false, command: false) ⇒ String

Encrypts a frame body into a CURVE MESSAGE command on the wire.

Parameters:

  • body (String)

    plaintext frame body

  • more (Boolean) (defaults to: false)

    whether more frames follow in this message

  • command (Boolean) (defaults to: false)

    whether this is a command frame

Returns:

  • (String)

    binary wire bytes ready for writing



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 161

def encrypt(body, more: false, command: false)
  flags = 0
  flags |= 0x01 if more
  flags |= 0x04 if command

  plaintext = String.new(encoding: Encoding::BINARY, capacity: 1 + body.bytesize)
  plaintext << flags << body

  nonce       = make_send_nonce
  ciphertext  = @session_box.encrypt(nonce, plaintext)
  short_nonce = nonce.byteslice(16, 8)

  msg_body_size = 16 + ciphertext.bytesize
  if msg_body_size > 255
    wire = String.new(encoding: Encoding::BINARY, capacity: 9 + msg_body_size)
    wire << "\x02" << [msg_body_size].pack("Q>")
  else
    wire = String.new(encoding: Encoding::BINARY, capacity: 2 + msg_body_size)
    wire << "\x00" << msg_body_size
  end
  wire << "\x07MESSAGE" << short_nonce << ciphertext
end

#encrypted?Boolean

Returns true – CURVE always encrypts frames.

Returns:

  • (Boolean)

    true – CURVE always encrypts frames



125
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 125

def encrypted? = true

#handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "") ⇒ Hash

Performs the full CurveZMQ handshake (HELLO/WELCOME/INITIATE/READY).

Parameters:

  • io (#read_exactly, #write, #flush)

    transport IO

  • as_server (Boolean)

    ignored – uses the value from #initialize

  • socket_type (String)

    our socket type name

  • identity (String)

    our identity

  • qos (Integer) (defaults to: 0)

    QoS level

  • qos_hash (String) (defaults to: "")

    supported hash algorithms

Returns:

  • (Hash)

    { peer_socket_type:, peer_identity:, peer_qos:, peer_qos_hash: }

Raises:

  • (Error)

    on handshake failure



146
147
148
149
150
151
152
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 146

def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "")
  if @as_server
    server_handshake!(io, socket_type:, identity:, qos:, qos_hash:)
  else
    client_handshake!(io, socket_type:, identity:, qos:, qos_hash:)
  end
end

#initialize_dup(source) ⇒ void

This method returns an undefined value.

Resets session state when duplicating (e.g. for a new connection).

Parameters:

  • source (Curve)

    the original instance being duplicated



114
115
116
117
118
119
120
121
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 114

def initialize_dup(source)
  super
  @session_box    = nil
  @send_nonce     = 0
  @recv_nonce     = -1
  @send_nonce_buf = nil
  @recv_nonce_buf = nil
end

#maintenanceHash?

Returns a periodic maintenance task for rotating the cookie key (server only).

Returns:

  • (Hash, nil)

    a hash with :interval (seconds) and :task (Proc), or nil for clients



130
131
132
133
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 130

def maintenance
  return unless @as_server
  { interval: 60, task: -> { @cookie_key = @crypto::Random.random_bytes(32) } }.freeze
end