bsv-wallet 0.3.2 → 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
|
|
@@ -598,6 +598,7 @@ module BSV
|
|
|
598
598
|
txid = tx.txid_hex
|
|
599
599
|
status = args.dig(:options, :no_send) ? 'nosend' : 'completed'
|
|
600
600
|
|
|
601
|
+
@storage.store_transaction(txid, tx.to_hex)
|
|
601
602
|
store_action(tx, args, status: status)
|
|
602
603
|
store_tracked_outputs(txid, tx, args[:outputs])
|
|
603
604
|
|
|
@@ -802,7 +803,7 @@ module BSV
|
|
|
802
803
|
end
|
|
803
804
|
|
|
804
805
|
def acquire_via_direct(args)
|
|
805
|
-
{
|
|
806
|
+
cert = {
|
|
806
807
|
type: args[:type],
|
|
807
808
|
subject: @key_deriver.identity_key,
|
|
808
809
|
serial_number: args[:serial_number],
|
|
@@ -812,6 +813,15 @@ module BSV
|
|
|
812
813
|
fields: args[:fields],
|
|
813
814
|
keyring: args[:keyring_for_subject]
|
|
814
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
|
|
815
825
|
end
|
|
816
826
|
|
|
817
827
|
def acquire_via_issuance(args)
|
|
@@ -832,7 +842,7 @@ module BSV
|
|
|
832
842
|
|
|
833
843
|
body = JSON.parse(response.body)
|
|
834
844
|
|
|
835
|
-
{
|
|
845
|
+
cert = {
|
|
836
846
|
type: body['type'] || args[:type],
|
|
837
847
|
subject: @key_deriver.identity_key,
|
|
838
848
|
serial_number: body['serialNumber'],
|
|
@@ -842,6 +852,14 @@ module BSV
|
|
|
842
852
|
fields: body['fields'] || args[:fields],
|
|
843
853
|
keyring: body['keyringForSubject']
|
|
844
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
|
|
845
863
|
rescue JSON::ParserError
|
|
846
864
|
raise WalletError, 'Certificate issuance failed: invalid JSON response'
|
|
847
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
|