Class: BSV::Auth::Certificate

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/auth/certificate.rb

Overview

Identity certificate as per the BRC-52 Wallet interface specification.

A certificate binds identity attributes (fields) to a subject public key, and is signed by a certifier. The binary serialisation format is shared across all BSV SDKs (Go, TypeScript, Python, Ruby) so that certificates produced by one SDK can be verified by another.

All field values are expected to be Base64-encoded encrypted strings. Signing and verification use BRC-42 key derivation:

  • Protocol: [2, ‘certificate signature’]

  • Key ID: “#{type} #{serial_number}”

  • Counterparty on sign: ‘anyone’ (default for create_signature)

  • Counterparty on verify: the certifier’s compressed public key hex

Wallet parameters are duck-typed — any object responding to create_signature, verify_signature, and get_public_key is accepted. No direct dependency on BSV::Wallet::ProtoWallet is introduced here.

See Also:

Direct Known Subclasses

MasterCertificate, VerifiableCertificate

Constant Summary collapse

CERT_SIG_PROTOCOL =
[2, 'certificate signature'].freeze
CERT_FIELD_ENC_PROTOCOL =
[2, 'certificate field encryption'].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type:, serial_number:, subject:, certifier:, revocation_outpoint:, fields:, signature: nil) ⇒ Certificate

Returns a new instance of Certificate.

Parameters:

  • type (String)

    Base64 string (32 bytes decoded)

  • serial_number (String)

    Base64 string (32 bytes decoded)

  • subject (String)

    compressed public key hex

  • certifier (String)

    compressed public key hex

  • revocation_outpoint (String)

    “<txid_hex>.<output_index>”

  • fields (Hash)

    field name strings to value strings

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

    DER-encoded signature hex, or nil



59
60
61
62
63
64
65
66
67
# File 'lib/bsv/auth/certificate.rb', line 59

def initialize(type:, serial_number:, subject:, certifier:, revocation_outpoint:, fields:, signature: nil)
  @type                 = type
  @serial_number        = serial_number
  @subject              = subject
  @certifier            = certifier
  @revocation_outpoint  = revocation_outpoint
  @fields               = fields
  @signature            = signature
end

Instance Attribute Details

#certifierString

Returns compressed public key hex (66 characters).

Returns:

  • (String)

    compressed public key hex (66 characters)



41
42
43
# File 'lib/bsv/auth/certificate.rb', line 41

def certifier
  @certifier
end

#fieldsHash (readonly)

Returns mapping field name strings to value strings.

Returns:

  • (Hash)

    mapping field name strings to value strings



47
48
49
# File 'lib/bsv/auth/certificate.rb', line 47

def fields
  @fields
end

#revocation_outpointString (readonly)

Returns outpoint string “<txid_hex>.<output_index>”.

Returns:

  • (String)

    outpoint string “<txid_hex>.<output_index>”



44
45
46
# File 'lib/bsv/auth/certificate.rb', line 44

def revocation_outpoint
  @revocation_outpoint
end

#serial_numberString (readonly)

Returns Base64 string decoding to 32 bytes.

Returns:

  • (String)

    Base64 string decoding to 32 bytes



35
36
37
# File 'lib/bsv/auth/certificate.rb', line 35

def serial_number
  @serial_number
end

#signatureString?

Returns DER-encoded signature as hex string, or nil if unsigned.

Returns:

  • (String, nil)

    DER-encoded signature as hex string, or nil if unsigned



50
51
52
# File 'lib/bsv/auth/certificate.rb', line 50

def signature
  @signature
end

#subjectString (readonly)

Returns compressed public key hex (66 characters).

Returns:

  • (String)

    compressed public key hex (66 characters)



38
39
40
# File 'lib/bsv/auth/certificate.rb', line 38

def subject
  @subject
end

#typeString (readonly)

Returns Base64 string decoding to 32 bytes.

Returns:

  • (String)

    Base64 string decoding to 32 bytes



32
33
34
# File 'lib/bsv/auth/certificate.rb', line 32

def type
  @type
end

Class Method Details

.certificate_field_encryption_details(field_name, serial_number = nil) ⇒ Hash

Returns the protocol ID and key ID for certificate field encryption.

When serial_number is provided (for verifier keyring creation) the key ID is “#{serial_number} #{field_name}”. Without a serial number (for master keyring creation) the key ID is just the field_name.

Parameters:

  • field_name (String)

    name of the certificate field

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

    certificate serial number (Base64)

Returns:

  • (Hash)

    { protocol_id:, key_id: }



226
227
228
229
# File 'lib/bsv/auth/certificate.rb', line 226

def self.certificate_field_encryption_details(field_name, serial_number = nil)
  key_id = serial_number ? "#{serial_number} #{field_name}" : field_name
  { protocol_id: CERT_FIELD_ENC_PROTOCOL, key_id: key_id }
end

.from_binary(data) ⇒ Certificate

Deserialise a certificate from its binary format.

When a signature is present in the trailing bytes, it is parsed via Primitives::Signature.from_der to ensure strict DER normalisation before being re-serialised as hex.

Parameters:

  • data (String)

    binary string

Returns:

Raises:

  • (ArgumentError)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/bsv/auth/certificate.rb', line 112

def self.from_binary(data)
  data = data.b
  raise ArgumentError, "certificate binary too short (#{data.bytesize} bytes, minimum 163)" if data.bytesize < 163

  pos = 0

  type_bytes = data.byteslice(pos, 32)
  pos += 32
  serial_bytes = data.byteslice(pos, 32)
  pos += 32
  subject_bytes = data.byteslice(pos, 33)
  pos += 33
  certifier_bytes = data.byteslice(pos, 33)
  pos += 33

  txid_bytes = data.byteslice(pos, 32)
  pos += 32
  output_index, vi_len = BSV::Transaction::VarInt.decode(data, pos)
  pos += vi_len

  num_fields, vi_len = BSV::Transaction::VarInt.decode(data, pos)
  pos += vi_len
  fields = {}
  num_fields.times do
    name_len, vi_len = BSV::Transaction::VarInt.decode(data, pos)
    pos += vi_len
    name = data.byteslice(pos, name_len).force_encoding('UTF-8')
    pos += name_len

    value_len, vi_len = BSV::Transaction::VarInt.decode(data, pos)
    pos += vi_len
    value = data.byteslice(pos, value_len).force_encoding('UTF-8')
    pos += value_len

    fields[name] = value
  end

  signature = nil
  if pos < data.bytesize
    sig_bytes = data.byteslice(pos, data.bytesize - pos)
    parsed    = BSV::Primitives::Signature.from_der(sig_bytes)
    signature = parsed.to_hex
  end

  new(
    type: Base64.strict_encode64(type_bytes),
    serial_number: Base64.strict_encode64(serial_bytes),
    subject: subject_bytes.unpack1('H*'),
    certifier: certifier_bytes.unpack1('H*'),
    revocation_outpoint: "#{txid_bytes.unpack1('H*')}.#{output_index}",
    fields: fields,
    signature: signature
  )
end

.from_hash(hash) ⇒ Certificate

Construct a Certificate from a plain Hash.

Accepts both snake_case and camelCase key variants for each field so that wire-format hashes can be passed in directly.

Parameters:

  • hash (Hash)

    certificate data with snake_case or camelCase keys

Returns:



238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/bsv/auth/certificate.rb', line 238

def self.from_hash(hash)
  h = normalise_hash_keys(hash)
  new(
    type: h['type'],
    serial_number: h['serial_number'],
    subject: h['subject'],
    certifier: h['certifier'],
    revocation_outpoint: h['revocation_outpoint'],
    fields: h['fields'] || {},
    signature: h['signature']
  )
end

Instance Method Details

#sign(certifier_wallet) ⇒ Object

Sign the certificate using the provided certifier wallet.

The certifier field is updated to the wallet’s identity key before signing. Raises if the certificate is already signed.

Parameters:

  • certifier_wallet (#create_signature, #get_public_key)

    certifier wallet

Raises:

  • (ArgumentError)

    if the certificate already has a signature



203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/bsv/auth/certificate.rb', line 203

def sign(certifier_wallet)
  raise ArgumentError, "certificate has already been signed: #{@signature}" if @signature && !@signature.empty?

  @certifier = certifier_wallet.get_public_key({ identity_key: true })[:public_key]

  preimage = to_binary(include_signature: false)
  result   = certifier_wallet.create_signature({
                                                 data: preimage.unpack('C*'),
                                                 protocol_id: CERT_SIG_PROTOCOL,
                                                 key_id: "#{@type} #{@serial_number}"
                                               })
  @signature = result[:signature].pack('C*').unpack1('H*')
end

#to_binary(include_signature: true) ⇒ String

Serialise the certificate into its binary format.

The binary format is byte-compatible with all other BSV SDK implementations. Fields are sorted lexicographically by name.

Parameters:

  • include_signature (Boolean) (defaults to: true)

    whether to append the signature bytes

Returns:

  • (String)

    binary string



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
# File 'lib/bsv/auth/certificate.rb', line 76

def to_binary(include_signature: true)
  buf = String.new(encoding: Encoding::ASCII_8BIT)

  buf << Base64.strict_decode64(@type)
  buf << Base64.strict_decode64(@serial_number)
  buf << [@subject].pack('H*')
  buf << [@certifier].pack('H*')

  txid_hex, output_index_str = @revocation_outpoint.to_s.split('.', 2)
  buf << [txid_hex].pack('H*')
  buf << BSV::Transaction::VarInt.encode(output_index_str.to_i)

  sorted_names = @fields.keys.sort
  buf << BSV::Transaction::VarInt.encode(sorted_names.length)
  sorted_names.each do |name|
    name_bytes  = name.to_s.encode('UTF-8').b
    value_bytes = @fields[name].to_s.encode('UTF-8').b
    buf << BSV::Transaction::VarInt.encode(name_bytes.bytesize)
    buf << name_bytes
    buf << BSV::Transaction::VarInt.encode(value_bytes.bytesize)
    buf << value_bytes
  end

  buf << [@signature].pack('H*') if include_signature && @signature && !@signature.empty?

  buf
end

#to_hHash

Return the certificate as a plain Hash with snake_case keys.

JSON serialisation is simply cert.to_h.to_json.

Returns:

  • (Hash)


256
257
258
259
260
261
262
263
264
265
266
# File 'lib/bsv/auth/certificate.rb', line 256

def to_h
  {
    'type' => @type,
    'serial_number' => @serial_number,
    'subject' => @subject,
    'certifier' => @certifier,
    'revocation_outpoint' => @revocation_outpoint,
    'fields' => @fields.dup,
    'signature' => @signature
  }
end

#verify(verifier_wallet = nil) ⇒ Boolean

Verify the certificate’s signature.

Uses a fresh ‘anyone’ ProtoWallet as the verifier, which matches the TS SDK behaviour. If no signature is present, raises ArgumentError.

Parameters:

  • verifier_wallet (#verify_signature, nil) (defaults to: nil)

    wallet to verify with; defaults to BSV::Wallet::ProtoWallet.new(‘anyone’)

Returns:

  • (Boolean)

    true if the signature is valid

Raises:

  • (ArgumentError)

    if the certificate has no signature



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/bsv/auth/certificate.rb', line 176

def verify(verifier_wallet = nil)
  raise ArgumentError, 'certificate has no signature to verify' if @signature.nil? || @signature.empty?

  verifier_wallet ||= BSV::Wallet::ProtoWallet.new('anyone')
  preimage  = to_binary(include_signature: false)
  sig_bytes = [@signature].pack('H*').unpack('C*')

  result = verifier_wallet.verify_signature({
                                              data: preimage.unpack('C*'),
                                              signature: sig_bytes,
                                              protocol_id: CERT_SIG_PROTOCOL,
                                              key_id: "#{@type} #{@serial_number}",
                                              counterparty: @certifier
                                            })

  result.is_a?(Hash) && result[:valid] == true
rescue BSV::Wallet::InvalidSignatureError
  false
end