Class: Protocol::ZMTP::Mechanism::Blake3

Inherits:
Object
  • Object
show all
Defined in:
lib/omq/blake3zmq/mechanism.rb

Overview

BLAKE3ZMQ security mechanism.

Provides X25519 key exchange, ChaCha20-BLAKE3 AEAD encryption, and BLAKE3 transcript hashing for ZMTP 3.1 connections.

Crypto-backend-agnostic: pass any module that provides the required interface via the crypto: parameter.

The crypto backend must provide:

backend::PrivateKey.generate / .new(bytes)
  #public_key -> PublicKey, #to_s -> 32 bytes, #diffie_hellman(pub) -> 32 bytes
backend::PublicKey.new(bytes)
  #to_s -> 32 bytes
backend::Cipher.new(key)
  #encrypt(nonce, plaintext, aad:) -> ciphertext+tag
  #decrypt(nonce, ciphertext+tag, aad:) -> plaintext
backend::Stream.new(key, nonce)
  #encrypt(plaintext, aad:) -> ciphertext+tag
  #decrypt(ciphertext+tag, aad:) -> plaintext
backend::Hash.digest(input) -> 32 bytes
backend::Hash.derive_key(context, material) -> 32 bytes
backend::Hash.derive_key(context, material, size: n) -> n bytes
backend.random_bytes(n) -> n bytes
backend::CryptoError (exception class)
backend::TAG_SIZE = 32

Constant Summary collapse

MECHANISM_NAME =
"BLAKE3"
PROTOCOL_ID =
"BLAKE3ZMQ-1.0"
TAG_SIZE =
32
KEY_SIZE =
32
NONCE_SIZE =
24

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(public_key: nil, secret_key: nil, server_key: nil, crypto: OMQ::Blake3ZMQ::Crypto, as_server: false, authenticator: nil) ⇒ Blake3

Initializes a new BLAKE3 mechanism instance.

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) (defaults to: OMQ::Blake3ZMQ::Crypto)

    crypto backend module

  • as_server (Boolean) (defaults to: false)

    whether this instance acts as a server

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

    optional authenticator for server mode



74
75
76
77
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
# File 'lib/omq/blake3zmq/mechanism.rb', line 74

def initialize(public_key: nil, secret_key: nil, server_key: nil, crypto: OMQ::Blake3ZMQ::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_bytes(KEY_SIZE)
  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

  @send_stream = nil
  @recv_stream = nil
end

Class Method Details

.client(server_key:, crypto: OMQ::Blake3ZMQ::Crypto, public_key: nil, secret_key: nil) ⇒ Blake3

Creates a BLAKE3 client mechanism.

Parameters:

  • server_key (String)

    32 bytes (server permanent public key)

  • crypto (Module) (defaults to: OMQ::Blake3ZMQ::Crypto)

    crypto backend

  • 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:



61
62
63
# File 'lib/omq/blake3zmq/mechanism.rb', line 61

def self.client(server_key:, crypto: OMQ::Blake3ZMQ::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: OMQ::Blake3ZMQ::Crypto, authenticator: nil) ⇒ Blake3

Creates a BLAKE3 server mechanism.

Parameters:

  • public_key (String)

    32 bytes

  • secret_key (String)

    32 bytes

  • crypto (Module) (defaults to: OMQ::Blake3ZMQ::Crypto)

    crypto backend

  • 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:



49
50
51
# File 'lib/omq/blake3zmq/mechanism.rb', line 49

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

Instance Method Details

#decrypt(frame) ⇒ Codec::Frame

Decrypts an encrypted ZMTP frame.

The AAD is reconstructed from the exact wire bytes the codec parsed: the flags byte (with LONG bit if the frame was long) and the length encoding that followed it.

Parameters:

  • frame (Codec::Frame)

    encrypted frame with body, more?, and command? attributes

Returns:

  • (Codec::Frame)

    decrypted frame

Raises:

  • (Error)

    if decryption fails



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/omq/blake3zmq/mechanism.rb', line 202

def decrypt(frame)
  flags  = 0
  flags |= 0x01 if frame.more?
  flags |= 0x04 if frame.command?

  frame_size = frame.body.bytesize
  if frame_size > 255
    wire_flags = (flags | 0x02).chr
    length_bytes = [frame_size].pack("Q>")
  else
    wire_flags = flags.chr
    length_bytes = frame_size.chr
  end
  aad = wire_flags + length_bytes

  begin
    pt = @recv_stream.decrypt(frame.body, aad: aad)
  rescue @crypto::CryptoError
    raise Error, "decryption failed"
  end

  Codec::Frame.new(pt, more: frame.more?, command: frame.command?)
end

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

Encrypts a ZMTP frame body for transmission.

The AEAD AAD per RFC §10.3 is ‘flags_byte || length_bytes` —every wire byte that is not itself encrypted. This binds the full wire envelope (including the LONG bit and the size field) so any single-bit modification fails verification.

Parameters:

  • body (String)

    plaintext frame body

  • more (Boolean) (defaults to: false)

    whether the MORE flag is set

  • command (Boolean) (defaults to: false)

    whether this is a command frame

Returns:

  • (String)

    wire-encoded encrypted frame (header + ciphertext)



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/omq/blake3zmq/mechanism.rb', line 169

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

  # ChaCha20-BLAKE3 ciphertext is plaintext + 32-byte tag.
  frame_size = body.bytesize + TAG_SIZE

  if frame_size > 255
    wire_flags = (flags | 0x02).chr
    length_bytes = [frame_size].pack("Q>")
  else
    wire_flags = flags.chr
    length_bytes = frame_size.chr
  end
  aad = wire_flags + length_bytes

  ct = @send_stream.encrypt(body, aad: aad)

  wire = String.new(encoding: Encoding::BINARY, capacity: aad.bytesize + frame_size)
  wire << aad << ct
end

#encrypted?Boolean

Whether this mechanism encrypts traffic.

Returns:

  • (Boolean)

    always true



121
122
123
# File 'lib/omq/blake3zmq/mechanism.rb', line 121

def encrypted?
  true
end

#handshake!(io, as_server:, socket_type:, identity:, metadata: nil) ⇒ Hash

Performs the BLAKE3ZMQ handshake over the given IO.

Delegates to the client or server handshake depending on role.

Parameters:

  • io (#write, #read_exactly)

    transport IO

  • as_server (Boolean)

    ignored (role is set at construction)

  • socket_type (String)

    ZMTP socket type name

  • identity (String)

    socket identity

  • metadata (Hash{String => String}, nil) (defaults to: nil)

    extra READY properties

Returns:

  • (Hash)

    peer metadata including :peer_socket_type, :peer_identity, :peer_properties



149
150
151
152
153
154
155
# File 'lib/omq/blake3zmq/mechanism.rb', line 149

def handshake!(io, as_server:, socket_type:, identity:, metadata: nil)
  if @as_server
    server_handshake!(io, socket_type:, identity:, metadata:)
  else
    client_handshake!(io, socket_type:, identity:, metadata:)
  end
end

#initialize_dup(source) ⇒ Object

Resets stream state when duplicating the mechanism.

Parameters:

  • source (Blake3)

    the original mechanism being duplicated



111
112
113
114
115
# File 'lib/omq/blake3zmq/mechanism.rb', line 111

def initialize_dup(source)
  super
  @send_stream = nil
  @recv_stream = nil
end

#maintenanceHash?

Returns a maintenance task that rotates the server cookie key.

Returns:

  • (Hash, nil)

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



129
130
131
132
133
134
135
136
# File 'lib/omq/blake3zmq/mechanism.rb', line 129

def maintenance
  return unless @as_server

  {
    interval: 60,
    task:     -> { @cookie_key = @crypto.random_bytes(KEY_SIZE) }
  }.freeze
end