Class: BSV::Wallet::KeyDeriver

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/wallet/key_deriver.rb

Overview

BRC-42/43 key derivation facade.

Wraps one or two root private keys (everyday + optional privileged) and provides key derivation per the BRC-42 spec. Invoice numbers follow the BRC-43 format: “security_level-protocol_name-key_id”.

Examples:

kd = KeyDeriver.new(private_key: BSV::Primitives::PrivateKey.generate)
kd.identity_key  #=> "02abc...def" (66-char hex)
pub = kd.derive_public_key(protocol_id: [1, "my protocol"], key_id: "1", counterparty: "self")

Constant Summary collapse

ANYONE_PRIVATE_KEY =
BSV::Primitives::PrivateKey.new(OpenSSL::BN.new(1))
ANYONE_PUBLIC_KEY =
ANYONE_PRIVATE_KEY.public_key

Instance Method Summary collapse

Constructor Details

#initialize(private_key:, privileged_key: nil) ⇒ KeyDeriver

Returns a new instance of KeyDeriver.

Parameters:

  • private_key (BSV::Primitives::PrivateKey)

    everyday root key

  • privileged_key (BSV::Primitives::PrivateKey, nil) (defaults to: nil)

    optional privileged root key



21
22
23
24
# File 'lib/bsv/wallet/key_deriver.rb', line 21

def initialize(private_key:, privileged_key: nil)
  @root_key = private_key
  @privileged_key = privileged_key
end

Instance Method Details

#create_hmac(data:, protocol_id:, key_id:, counterparty:, privileged: false) ⇒ String

Compute an HMAC-SHA-256 over data using an ECDH-derived symmetric key.

Derives the symmetric key for the given derivation parameters and returns the HMAC-SHA-256 of the data keyed with that symmetric key.

Parameters:

  • data (String)

    binary data to authenticate

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (String)

    32-byte HMAC



133
134
135
136
137
138
139
# File 'lib/bsv/wallet/key_deriver.rb', line 133

def create_hmac(data:, protocol_id:, key_id:, counterparty:, privileged: false)
  sym_key = derive_symmetric_key(
    protocol_id: protocol_id, key_id: key_id,
    counterparty: counterparty, privileged: privileged
  )
  BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, data)
end

#create_signature(protocol_id:, key_id:, counterparty:, data: nil, hash_to_directly_sign: nil, privileged: false) ⇒ BSV::Primitives::Signature

Sign data using ECDSA with a derived private key.

Either data or hash_to_directly_sign must be provided. When data is given, it is SHA-256 hashed before signing. When hash_to_directly_sign is given, it is used as-is (must be 32 bytes).

Parameters:

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

    raw data to hash and sign

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

    pre-computed 32-byte hash to sign directly

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (BSV::Primitives::Signature)

    the ECDSA signature



215
216
217
218
219
220
221
# File 'lib/bsv/wallet/key_deriver.rb', line 215

def create_signature(protocol_id:, key_id:, counterparty:, data: nil, hash_to_directly_sign: nil,
                     privileged: false)
  hash = resolve_hash(data, hash_to_directly_sign)
  private_key = derive_private_key(protocol_id: protocol_id, key_id: key_id,
                                   counterparty: counterparty, privileged: privileged)
  private_key.sign(hash)
end

#decrypt(ciphertext:, protocol_id:, key_id:, counterparty:, privileged: false) ⇒ String

Decrypt ciphertext using AES-256-GCM with an ECDH-derived symmetric key.

Derives the same symmetric key used for encryption and decrypts.

Parameters:

  • ciphertext (String)

    binary data to decrypt (IV + ciphertext + auth tag)

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (String)

    decrypted binary plaintext

Raises:

  • (OpenSSL::Cipher::CipherError)

    if authentication fails



114
115
116
117
118
119
120
# File 'lib/bsv/wallet/key_deriver.rb', line 114

def decrypt(ciphertext:, protocol_id:, key_id:, counterparty:, privileged: false)
  sym_key = derive_symmetric_key(
    protocol_id: protocol_id, key_id: key_id,
    counterparty: counterparty, privileged: privileged
  )
  sym_key.decrypt(ciphertext)
end

#derive_private_key(protocol_id:, key_id:, counterparty:, privileged: false) ⇒ BSV::Primitives::PrivateKey

Derive a child private key using BRC-42.

Parameters:

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (BSV::Primitives::PrivateKey)

    derived child private key



76
77
78
79
80
81
82
# File 'lib/bsv/wallet/key_deriver.rb', line 76

def derive_private_key(protocol_id:, key_id:, counterparty:, privileged: false)
  key = select_key(privileged)
  invoice = compute_invoice_number(protocol_id, key_id)
  counterparty_pub = resolve_counterparty(counterparty)

  key.derive_child(counterparty_pub, invoice)
end

#derive_public_key(protocol_id:, key_id:, counterparty:, for_self: false, privileged: false) ⇒ String

Derive a child public key using BRC-42.

In normal mode (for_self: false), derives OUR child public key that the counterparty could also compute if they knew our public key. Equivalent to derive_private_key(…).public_key.

In for_self mode, derives the COUNTERPARTY’S child public key —the key they would have derived using their private key and our public key.

Parameters:

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • for_self (Boolean) (defaults to: false)

    reverse derivation direction

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (String)

    compressed public key bytes (33 bytes, binary)



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/bsv/wallet/key_deriver.rb', line 55

def derive_public_key(protocol_id:, key_id:, counterparty:, for_self: false, privileged: false)
  key = select_key(privileged)
  invoice = compute_invoice_number(protocol_id, key_id)
  counterparty_pub = resolve_counterparty(counterparty)

  if for_self
    # Counterparty's child public key, derived using our private key
    counterparty_pub.derive_child(key, invoice).compressed
  else
    # Our child public key — same as derive_private_key.public_key
    key.derive_child(counterparty_pub, invoice).public_key.compressed
  end
end

#derive_revelation_keyring(certificate:, fields_to_reveal:, verifier:, privileged: false) ⇒ Hash{String => String}

Derive a revelation keyring for a verifier from a certificate’s master keyring.

For each field in fields_to_reveal, decrypts the encrypted field key from the certificate’s keyring (using the certifier as counterparty per BRC-52), then re-encrypts it for the specified verifier.

Parameters:

  • certificate (Hash)

    certificate hash with :type, :serial_number, :certifier, :keyring

  • fields_to_reveal (Array<String>)

    field names whose keys to reveal

  • verifier (String)

    verifier’s public key (hex string)

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (Hash{String => String})

    field names mapped to re-encrypted keys

Raises:

  • (BSV::Wallet::Error)

    if a field is not in the keyring or keyring is missing



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/bsv/wallet/key_deriver.rb', line 153

def derive_revelation_keyring(certificate:, fields_to_reveal:, verifier:, privileged: false)
  return {} if fields_to_reveal.nil? || fields_to_reveal.empty?

  keyring = certificate[:keyring]
  raise BSV::Wallet::Error, 'certificate has no keyring' if keyring.nil? || keyring.empty?

  cert_type = certificate[:type]
  serial = certificate[:serial_number]
  certifier = certificate[:certifier]

  # BRC-52: master keyring was encrypted with protocol "authrite certificate field encryption {type}"
  decrypt_protocol = [2, "authrite certificate field encryption #{cert_type}"]

  # BRC-52: revelation keyring uses protocol "authrite certificate field encryption"
  encrypt_protocol = [2, 'authrite certificate field encryption']

  verifier_hex = normalize_pubkey_to_hex(verifier)

  fields_to_reveal.each_with_object({}) do |field_name, result|
    field_key_name = field_name.to_s
    encrypted_key = keyring[field_key_name]

    unless encrypted_key
      raise BSV::Wallet::Error,
            "field '#{field_key_name}' not found in certificate keyring"
    end

    key_id = "#{serial} #{field_key_name}"

    # Decrypt the field key (certifier encrypted it for us)
    field_key = decrypt(
      ciphertext: encrypted_key,
      protocol_id: decrypt_protocol,
      key_id: key_id,
      counterparty: certifier,
      privileged: privileged
    )

    # Re-encrypt the field key for the verifier
    result[field_key_name] = encrypt(
      plaintext: field_key,
      protocol_id: encrypt_protocol,
      key_id: key_id,
      counterparty: verifier_hex,
      privileged: privileged
    )
  end
end

#encrypt(plaintext:, protocol_id:, key_id:, counterparty:, privileged: false) ⇒ String

Encrypt plaintext using AES-256-GCM with an ECDH-derived symmetric key.

Derives child keys for both parties, computes the ECDH shared secret, and encrypts using AES-256-GCM.

Parameters:

  • plaintext (String)

    binary data to encrypt

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (String)

    binary ciphertext (IV + ciphertext + auth tag)



95
96
97
98
99
100
101
# File 'lib/bsv/wallet/key_deriver.rb', line 95

def encrypt(plaintext:, protocol_id:, key_id:, counterparty:, privileged: false)
  sym_key = derive_symmetric_key(
    protocol_id: protocol_id, key_id: key_id,
    counterparty: counterparty, privileged: privileged
  )
  sym_key.encrypt(plaintext)
end

#identity_keyString

Returns the compressed public key hex of the everyday key.

Returns:

  • (String)

    66-character hex-encoded compressed public key



35
36
37
# File 'lib/bsv/wallet/key_deriver.rb', line 35

def identity_key
  @identity_key ||= @root_key.public_key.to_hex
end

#reveal_counterparty_linkage(counterparty:, verifier:, privileged: false) ⇒ Hash

Reveal the ECDH shared secret between this wallet and a counterparty, encrypted for a verifier with a Schnorr proof (BRC-69 Method 1).

Parameters:

  • counterparty (String)

    hex public key (not ‘self’ or ‘anyone’)

  • verifier (String)

    hex public key of the verifier

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (Hash)

    revelation result with encrypted linkage and proof



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/bsv/wallet/key_deriver.rb', line 254

def reveal_counterparty_linkage(counterparty:, verifier:, privileged: false)
  validate_linkage_counterparty!(counterparty)
  key = select_key(privileged)
  counterparty_pub = resolve_counterparty(counterparty)

  # Compute the ECDH shared secret (the linkage being revealed)
  shared_secret = key.derive_shared_secret(counterparty_pub)
  linkage = shared_secret.compressed

  revelation_time = Time.now.utc.iso8601

  # Encrypt the linkage for the verifier
  encrypted_linkage = encrypt(
    plaintext: linkage,
    protocol_id: [2, 'counterparty linkage revelation'],
    key_id: revelation_time,
    counterparty: verifier,
    privileged: privileged
  )

  # Generate Schnorr proof of the shared secret
  proof = BSV::Primitives::Schnorr.generate_proof(
    key, key.public_key, counterparty_pub, shared_secret
  )

  # Serialize the proof: R (33 bytes) + S' (33 bytes) + z (32 bytes, zero-padded)
  z_bytes = proof.z.to_s(2)
  z_bytes = ("\x00".b * (32 - z_bytes.length)) + z_bytes if z_bytes.length < 32
  proof_bin = proof.r.compressed + proof.s_prime.compressed + z_bytes

  # Encrypt the proof for the verifier
  encrypted_proof = encrypt(
    plaintext: proof_bin,
    protocol_id: [2, 'counterparty linkage revelation'],
    key_id: revelation_time,
    counterparty: verifier,
    privileged: privileged
  )

  {
    prover: identity_key,
    verifier: verifier,
    counterparty: counterparty,
    revelation_time: revelation_time,
    encrypted_linkage: encrypted_linkage,
    encrypted_linkage_proof: encrypted_proof
  }
end

#reveal_specific_linkage(counterparty:, verifier:, protocol_id:, key_id:, privileged: false) ⇒ Hash

Reveal the specific key offset for a particular derived key, encrypted for a verifier (BRC-69 Method 2).

Parameters:

  • counterparty (String)

    hex public key (not ‘self’ or ‘anyone’)

  • verifier (String)

    hex public key of the verifier

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (Hash)

    revelation result with encrypted linkage and proof_type 0



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/bsv/wallet/key_deriver.rb', line 312

def reveal_specific_linkage(counterparty:, verifier:, protocol_id:, key_id:, privileged: false)
  validate_linkage_counterparty!(counterparty)
  key = select_key(privileged)
  counterparty_pub = resolve_counterparty(counterparty)

  # Compute the specific key offset (HMAC of shared secret with invoice number)
  shared_secret = key.derive_shared_secret(counterparty_pub)
  invoice = compute_invoice_number(protocol_id, key_id)
  linkage = BSV::Primitives::Digest.hmac_sha256(shared_secret.compressed, invoice.encode('UTF-8'))

  derived_protocol = "specific linkage revelation #{protocol_id[0]} #{protocol_id[1]}"

  # Encrypt the linkage for the verifier
  encrypted_linkage = encrypt(
    plaintext: linkage,
    protocol_id: [2, derived_protocol],
    key_id: key_id,
    counterparty: verifier,
    privileged: privileged
  )

  # Encrypt proof_type 0 for the verifier
  encrypted_proof = encrypt(
    plaintext: "\x00".b,
    protocol_id: [2, derived_protocol],
    key_id: key_id,
    counterparty: verifier,
    privileged: privileged
  )

  {
    prover: identity_key,
    verifier: verifier,
    counterparty: counterparty,
    protocol_id: protocol_id,
    key_id: key_id,
    encrypted_linkage: encrypted_linkage,
    encrypted_linkage_proof: encrypted_proof,
    proof_type: 0
  }
end

#root_private_keyObject

The root (everyday) private key, for signing UTXOs paid directly to the identity address (no BRC-42/43 derivation).



28
29
30
# File 'lib/bsv/wallet/key_deriver.rb', line 28

def root_private_key
  @root_key
end

#verify_signature(signature:, protocol_id:, key_id:, counterparty:, data: nil, hash_to_directly_verify: nil, for_self: false, privileged: false) ⇒ Boolean

Verify an ECDSA signature against data using a derived public key.

Either data or hash_to_directly_verify must be provided. When data is given, it is SHA-256 hashed before verification.

Parameters:

  • signature (BSV::Primitives::Signature)

    the signature to verify

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

    raw data that was signed

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

    pre-computed 32-byte hash

  • protocol_id (Array<Integer, String>)
    security_level, protocol_name
  • key_id (String)

    key identifier

  • counterparty (String)

    “self”, “anyone”, or hex public key

  • for_self (Boolean) (defaults to: false)

    reverse derivation direction for verification

  • privileged (Boolean) (defaults to: false)

    use privileged keyring

Returns:

  • (Boolean)

    true if the signature is valid



237
238
239
240
241
242
243
244
245
# File 'lib/bsv/wallet/key_deriver.rb', line 237

def verify_signature(signature:, protocol_id:, key_id:, counterparty:, data: nil, hash_to_directly_verify: nil,
                     for_self: false, privileged: false)
  hash = resolve_hash(data, hash_to_directly_verify)
  pub_bytes = derive_public_key(protocol_id: protocol_id, key_id: key_id,
                                counterparty: counterparty, for_self: for_self,
                                privileged: privileged)
  public_key = BSV::Primitives::PublicKey.from_bytes(pub_bytes)
  public_key.verify(hash, signature)
end