bsv-wallet 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f3c7d9f919b64229f0c2c9c99a05fbd58850803af74861157f884ade90f7af6
|
|
4
|
+
data.tar.gz: c655fbf296f6a4a3ed554681b8ab673c8dd3d108d37e6832cbf48814b3ed2169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87e2b8653d787445a281fb78393e380857799852908487cbef82af26a3e1e04f16df5f67ce295ed4162742527d7ebc5d1380b4670f8a949b78d4c44524e6ab66
|
|
7
|
+
data.tar.gz: 9d288d1787f7448278eb95de1e96e41384e0c06141c755812549b9bc693c0269b8361cf31982f6b38472be3418ef575e7f373bde11d3ef0892743041c14dab10
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
# BRC-52 identity certificate signature verification.
|
|
8
|
+
#
|
|
9
|
+
# Certificates carry a signature from the certifier over a canonical
|
|
10
|
+
# binary serialisation of their fields (excluding the signature itself).
|
|
11
|
+
# This module builds that canonical serialisation and delegates
|
|
12
|
+
# verification to a {ProtoWallet}-compatible verifier.
|
|
13
|
+
#
|
|
14
|
+
# Every field is included in the preimage in this order:
|
|
15
|
+
#
|
|
16
|
+
# - +type+ (base64 → 32 bytes)
|
|
17
|
+
# - +serial_number+ (base64 → 32 bytes)
|
|
18
|
+
# - +subject+ (hex → 33-byte compressed pubkey)
|
|
19
|
+
# - +certifier+ (hex → 33-byte compressed pubkey)
|
|
20
|
+
# - +revocation_outpoint+: txid hex (32 bytes) + output index VarInt
|
|
21
|
+
# - +fields+: VarInt count, then for each field (sorted
|
|
22
|
+
# lexicographically by name): VarInt name length + UTF-8 name bytes
|
|
23
|
+
# + VarInt value length + UTF-8 value bytes
|
|
24
|
+
#
|
|
25
|
+
# Signing uses BRC-42 key derivation with:
|
|
26
|
+
#
|
|
27
|
+
# - protocol ID: +[2, 'certificate signature']+
|
|
28
|
+
# - key ID: +"\#{type} \#{serial_number}"+
|
|
29
|
+
# - counterparty on sign: +'anyone'+ (default of
|
|
30
|
+
# +ProtoWallet#create_signature+ in TS — Ruby consumers should pass
|
|
31
|
+
# it explicitly since Ruby defaults to +'self'+)
|
|
32
|
+
# - counterparty on verify: the certifier's public key hex
|
|
33
|
+
#
|
|
34
|
+
# @see https://hub.bsvblockchain.org/brc/wallet/0052 BRC-52
|
|
35
|
+
module CertificateSignature
|
|
36
|
+
PROTOCOL_ID = [2, 'certificate signature'].freeze
|
|
37
|
+
|
|
38
|
+
# Error raised when a certificate's signature cannot be verified.
|
|
39
|
+
class InvalidError < InvalidSignatureError
|
|
40
|
+
def initialize(detail)
|
|
41
|
+
super("certificate signature verification failed: #{detail}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# Verify a certificate's certifier signature.
|
|
48
|
+
#
|
|
49
|
+
# Raises {InvalidError} if the signature is missing, malformed, or
|
|
50
|
+
# does not match the expected certifier.
|
|
51
|
+
#
|
|
52
|
+
# @param cert [Hash] certificate fields. Required keys:
|
|
53
|
+
# +:type+, +:serial_number+, +:subject+, +:certifier+,
|
|
54
|
+
# +:revocation_outpoint+, +:fields+, +:signature+
|
|
55
|
+
# @param verifier [#verify_signature] optional verifier; defaults to
|
|
56
|
+
# a fresh +ProtoWallet.new('anyone')+
|
|
57
|
+
# @return [true] when the signature verifies
|
|
58
|
+
# @raise [InvalidError] otherwise
|
|
59
|
+
def verify!(cert, verifier: ProtoWallet.new('anyone'))
|
|
60
|
+
signature_hex = cert[:signature]
|
|
61
|
+
raise InvalidError, 'signature is missing' if signature_hex.nil? || signature_hex.empty?
|
|
62
|
+
|
|
63
|
+
preimage = serialise_preimage(cert)
|
|
64
|
+
sig_bytes = hex_to_bytes(signature_hex)
|
|
65
|
+
|
|
66
|
+
verifier.verify_signature({
|
|
67
|
+
data: preimage.unpack('C*'),
|
|
68
|
+
signature: sig_bytes,
|
|
69
|
+
protocol_id: PROTOCOL_ID,
|
|
70
|
+
key_id: "#{cert[:type]} #{cert[:serial_number]}",
|
|
71
|
+
counterparty: cert[:certifier]
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
true
|
|
75
|
+
rescue InvalidSignatureError => e
|
|
76
|
+
raise if e.is_a?(InvalidError)
|
|
77
|
+
|
|
78
|
+
raise InvalidError, e.message
|
|
79
|
+
rescue ArgumentError, EncodingError => e
|
|
80
|
+
# EncodingError covers Encoding::InvalidByteSequenceError and
|
|
81
|
+
# Encoding::UndefinedConversionError, which `encode_fields`
|
|
82
|
+
# raises for non-UTF-8 field names or values. Callers of
|
|
83
|
+
# `acquire_certificate` expect `InvalidError` on bad cert input
|
|
84
|
+
# — leaking EncodingError would break that contract.
|
|
85
|
+
raise InvalidError, e.message
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build the BRC-52 canonical preimage for signing or verifying.
|
|
89
|
+
#
|
|
90
|
+
# @param cert [Hash] certificate fields (see {.verify!})
|
|
91
|
+
# @return [String] binary string suitable for +sha256+ (via
|
|
92
|
+
# {ProtoWallet#verify_signature})
|
|
93
|
+
def serialise_preimage(cert)
|
|
94
|
+
buf = String.new(encoding: Encoding::ASCII_8BIT)
|
|
95
|
+
buf << decode_base64_exact(cert[:type], 32, 'type')
|
|
96
|
+
buf << decode_base64_exact(cert[:serial_number], 32, 'serial_number')
|
|
97
|
+
buf << decode_hex_exact(cert[:subject], 33, 'subject')
|
|
98
|
+
buf << decode_hex_exact(cert[:certifier], 33, 'certifier')
|
|
99
|
+
|
|
100
|
+
buf << encode_revocation_outpoint(cert[:revocation_outpoint])
|
|
101
|
+
buf << encode_fields(cert[:fields])
|
|
102
|
+
buf
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def encode_revocation_outpoint(outpoint)
|
|
109
|
+
raise ArgumentError, 'revocation_outpoint is missing' if outpoint.nil? || outpoint.empty?
|
|
110
|
+
|
|
111
|
+
txid_hex, output_index_str = outpoint.to_s.split('.', 2)
|
|
112
|
+
raise ArgumentError, "revocation_outpoint #{outpoint.inspect} missing '.<output_index>'" if output_index_str.nil?
|
|
113
|
+
|
|
114
|
+
unless output_index_str.match?(/\A\d+\z/)
|
|
115
|
+
raise ArgumentError, "revocation_outpoint output index must be a non-negative integer, got #{output_index_str.inspect}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
buf = String.new(encoding: Encoding::ASCII_8BIT)
|
|
119
|
+
buf << decode_hex_exact(txid_hex, 32, 'revocation_outpoint txid')
|
|
120
|
+
buf << BSV::Transaction::VarInt.encode(output_index_str.to_i)
|
|
121
|
+
buf
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def encode_fields(fields)
|
|
125
|
+
raise ArgumentError, 'fields must be a Hash' unless fields.is_a?(Hash)
|
|
126
|
+
|
|
127
|
+
normalised = normalise_field_keys(fields)
|
|
128
|
+
|
|
129
|
+
buf = String.new(encoding: Encoding::ASCII_8BIT)
|
|
130
|
+
sorted_names = normalised.keys.sort
|
|
131
|
+
buf << BSV::Transaction::VarInt.encode(sorted_names.length)
|
|
132
|
+
|
|
133
|
+
sorted_names.each do |name|
|
|
134
|
+
name_bytes = name.encode('UTF-8').b
|
|
135
|
+
value_bytes = normalised[name].to_s.encode('UTF-8').b
|
|
136
|
+
|
|
137
|
+
buf << BSV::Transaction::VarInt.encode(name_bytes.bytesize)
|
|
138
|
+
buf << name_bytes
|
|
139
|
+
buf << BSV::Transaction::VarInt.encode(value_bytes.bytesize)
|
|
140
|
+
buf << value_bytes
|
|
141
|
+
end
|
|
142
|
+
buf
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Normalise Hash keys to strings and reject post-normalisation
|
|
146
|
+
# duplicates (e.g. both `:email` and `'email'`). Without this,
|
|
147
|
+
# `fields.keys.map(&:to_s)` silently produces duplicate entries
|
|
148
|
+
# with ambiguous value ordering, which makes the BRC-52 preimage
|
|
149
|
+
# non-deterministic.
|
|
150
|
+
def normalise_field_keys(fields)
|
|
151
|
+
normalised = {}
|
|
152
|
+
fields.each do |key, value|
|
|
153
|
+
str_key = key.to_s
|
|
154
|
+
if normalised.key?(str_key)
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
"duplicate field name #{str_key.inspect} " \
|
|
157
|
+
'(once as string, once as symbol)'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
normalised[str_key] = value
|
|
161
|
+
end
|
|
162
|
+
normalised
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def decode_base64_exact(value, expected_length, field_name)
|
|
166
|
+
raise ArgumentError, "#{field_name} is missing" if value.nil? || value.empty?
|
|
167
|
+
|
|
168
|
+
# strict_decode64 (vs permissive decode64) rejects whitespace,
|
|
169
|
+
# non-base64 characters, and non-canonical padding. The rest
|
|
170
|
+
# of bsv-wallet (e.g. the wire serialiser) uses strict mode,
|
|
171
|
+
# and the BRC-52 preimage must be unambiguous — a cert with
|
|
172
|
+
# whitespace-injected type/serial_number would decode to the
|
|
173
|
+
# right length but produce a different canonical form than
|
|
174
|
+
# the same data re-submitted cleanly.
|
|
175
|
+
bytes = begin
|
|
176
|
+
Base64.strict_decode64(value)
|
|
177
|
+
rescue ArgumentError => e
|
|
178
|
+
raise ArgumentError, "#{field_name} is not valid base64: #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
if bytes.bytesize != expected_length
|
|
182
|
+
raise ArgumentError,
|
|
183
|
+
"#{field_name} must decode to #{expected_length} bytes, got #{bytes.bytesize}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
bytes.b
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def decode_hex_exact(value, expected_length, field_name)
|
|
190
|
+
raise ArgumentError, "#{field_name} is missing" if value.nil? || value.empty?
|
|
191
|
+
raise ArgumentError, "#{field_name} must be a hex string" unless value.match?(/\A\h+\z/)
|
|
192
|
+
raise ArgumentError, "#{field_name} hex length must be even" unless value.length.even?
|
|
193
|
+
|
|
194
|
+
bytes = [value].pack('H*')
|
|
195
|
+
if bytes.bytesize != expected_length
|
|
196
|
+
raise ArgumentError,
|
|
197
|
+
"#{field_name} must decode to #{expected_length} bytes, got #{bytes.bytesize}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
bytes.b
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def hex_to_bytes(hex)
|
|
204
|
+
raise ArgumentError, 'signature must be a hex string' unless hex.is_a?(String) && hex.match?(/\A\h+\z/)
|
|
205
|
+
raise ArgumentError, 'signature hex length must be even' unless hex.length.even?
|
|
206
|
+
|
|
207
|
+
[hex].pack('H*').unpack('C*')
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -803,7 +803,7 @@ module BSV
|
|
|
803
803
|
end
|
|
804
804
|
|
|
805
805
|
def acquire_via_direct(args)
|
|
806
|
-
{
|
|
806
|
+
cert = {
|
|
807
807
|
type: args[:type],
|
|
808
808
|
subject: @key_deriver.identity_key,
|
|
809
809
|
serial_number: args[:serial_number],
|
|
@@ -813,6 +813,15 @@ module BSV
|
|
|
813
813
|
fields: args[:fields],
|
|
814
814
|
keyring: args[:keyring_for_subject]
|
|
815
815
|
}
|
|
816
|
+
|
|
817
|
+
# BRC-52: verify the certifier's signature against the canonical
|
|
818
|
+
# serialisation before persisting. Fixes F8.15 from the 2026-04-08
|
|
819
|
+
# cross-SDK compliance review (#305) — prior to this check, callers
|
|
820
|
+
# could pass any value in `args[:signature]` and it would be stored
|
|
821
|
+
# as if certifier-authentic.
|
|
822
|
+
CertificateSignature.verify!(cert)
|
|
823
|
+
|
|
824
|
+
cert
|
|
816
825
|
end
|
|
817
826
|
|
|
818
827
|
def acquire_via_issuance(args)
|
|
@@ -833,7 +842,7 @@ module BSV
|
|
|
833
842
|
|
|
834
843
|
body = JSON.parse(response.body)
|
|
835
844
|
|
|
836
|
-
{
|
|
845
|
+
cert = {
|
|
837
846
|
type: body['type'] || args[:type],
|
|
838
847
|
subject: @key_deriver.identity_key,
|
|
839
848
|
serial_number: body['serialNumber'],
|
|
@@ -843,6 +852,14 @@ module BSV
|
|
|
843
852
|
fields: body['fields'] || args[:fields],
|
|
844
853
|
keyring: body['keyringForSubject']
|
|
845
854
|
}
|
|
855
|
+
|
|
856
|
+
# BRC-52: the certifier's HTTP response is untrusted network data;
|
|
857
|
+
# verify the signature before persisting. Closes the F8.15 class of
|
|
858
|
+
# bug on the issuance path too (the finding's "Same issue as F8.15"
|
|
859
|
+
# note from F8.16).
|
|
860
|
+
CertificateSignature.verify!(cert)
|
|
861
|
+
|
|
862
|
+
cert
|
|
846
863
|
rescue JSON::ParserError
|
|
847
864
|
raise WalletError, 'Certificate issuance failed: invalid JSON response'
|
|
848
865
|
end
|
data/lib/bsv/wallet_interface.rb
CHANGED
|
@@ -20,6 +20,7 @@ module BSV
|
|
|
20
20
|
autoload :NullChainProvider, 'bsv/wallet_interface/null_chain_provider'
|
|
21
21
|
autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
|
|
22
22
|
autoload :Wire, 'bsv/wallet_interface/wire'
|
|
23
|
+
autoload :CertificateSignature, 'bsv/wallet_interface/certificate_signature'
|
|
23
24
|
|
|
24
25
|
# Error classes
|
|
25
26
|
autoload :WalletError, 'bsv/wallet_interface/errors/wallet_error'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-wallet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -27,16 +27,22 @@ dependencies:
|
|
|
27
27
|
name: bsv-sdk
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.8.2
|
|
33
|
+
- - "<"
|
|
31
34
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0
|
|
35
|
+
version: '1.0'
|
|
33
36
|
type: :runtime
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
39
|
requirements:
|
|
37
|
-
- - "
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 0.8.2
|
|
43
|
+
- - "<"
|
|
38
44
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0
|
|
45
|
+
version: '1.0'
|
|
40
46
|
description: Implements the BRC-100 standard wallet-to-application interface for the
|
|
41
47
|
BSV Blockchain.
|
|
42
48
|
executables: []
|
|
@@ -46,6 +52,7 @@ files:
|
|
|
46
52
|
- LICENSE
|
|
47
53
|
- lib/bsv-wallet.rb
|
|
48
54
|
- lib/bsv/wallet_interface.rb
|
|
55
|
+
- lib/bsv/wallet_interface/certificate_signature.rb
|
|
49
56
|
- lib/bsv/wallet_interface/chain_provider.rb
|
|
50
57
|
- lib/bsv/wallet_interface/errors/invalid_hmac_error.rb
|
|
51
58
|
- lib/bsv/wallet_interface/errors/invalid_parameter_error.rb
|