Class: BSV::Auth::Certificate
- Inherits:
-
Object
- Object
- BSV::Auth::Certificate
- 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.
Direct Known Subclasses
Constant Summary collapse
- CERT_SIG_PROTOCOL =
[2, 'certificate signature'].freeze
- CERT_FIELD_ENC_PROTOCOL =
[2, 'certificate field encryption'].freeze
Instance Attribute Summary collapse
-
#certifier ⇒ String
Compressed public key hex (66 characters).
-
#fields ⇒ Hash
readonly
Mapping field name strings to value strings.
-
#revocation_outpoint ⇒ String
readonly
Outpoint string “<txid_hex>.<output_index>”.
-
#serial_number ⇒ String
readonly
Base64 string decoding to 32 bytes.
-
#signature ⇒ String?
DER-encoded signature as hex string, or nil if unsigned.
-
#subject ⇒ String
readonly
Compressed public key hex (66 characters).
-
#type ⇒ String
readonly
Base64 string decoding to 32 bytes.
Class Method Summary collapse
-
.certificate_field_encryption_details(field_name, serial_number = nil) ⇒ Hash
Returns the protocol ID and key ID for certificate field encryption.
-
.from_binary(data) ⇒ Certificate
Deserialise a certificate from its binary format.
-
.from_hash(hash) ⇒ Certificate
Construct a Certificate from a plain Hash.
Instance Method Summary collapse
-
#initialize(type:, serial_number:, subject:, certifier:, revocation_outpoint:, fields:, signature: nil) ⇒ Certificate
constructor
A new instance of Certificate.
-
#sign(certifier_wallet) ⇒ Object
Sign the certificate using the provided certifier wallet.
-
#to_binary(include_signature: true) ⇒ String
Serialise the certificate into its binary format.
-
#to_h ⇒ Hash
Return the certificate as a plain Hash with snake_case keys.
-
#verify(verifier_wallet = nil) ⇒ Boolean
Verify the certificate’s signature.
Constructor Details
#initialize(type:, serial_number:, subject:, certifier:, revocation_outpoint:, fields:, signature: nil) ⇒ Certificate
Returns a new instance of Certificate.
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
#certifier ⇒ String
Returns compressed public key hex (66 characters).
41 42 43 |
# File 'lib/bsv/auth/certificate.rb', line 41 def certifier @certifier end |
#fields ⇒ Hash (readonly)
Returns mapping field name strings to value strings.
47 48 49 |
# File 'lib/bsv/auth/certificate.rb', line 47 def fields @fields end |
#revocation_outpoint ⇒ String (readonly)
Returns outpoint string “<txid_hex>.<output_index>”.
44 45 46 |
# File 'lib/bsv/auth/certificate.rb', line 44 def revocation_outpoint @revocation_outpoint end |
#serial_number ⇒ String (readonly)
Returns Base64 string decoding to 32 bytes.
35 36 37 |
# File 'lib/bsv/auth/certificate.rb', line 35 def serial_number @serial_number end |
#signature ⇒ String?
Returns 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 |
#subject ⇒ String (readonly)
Returns compressed public key hex (66 characters).
38 39 40 |
# File 'lib/bsv/auth/certificate.rb', line 38 def subject @subject end |
#type ⇒ String (readonly)
Returns 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.
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.
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.
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.
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.
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_h ⇒ Hash
Return the certificate as a plain Hash with snake_case keys.
JSON serialisation is simply cert.to_h.to_json.
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.
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 |