Module: ForceDream::Verify

Defined in:
lib/force_dream/verify.rb

Overview

Trustlessly verifies a ForceDream proof's Ed25519 signature entirely client-side. ForceDream is never asked whether the proof is valid -- the math decides, locally.

Uses Ruby's standard-library OpenSSL::PKey for Ed25519 -- no external gem needed. Confirmed directly, live, before writing any client logic here: OpenSSL::PKey.read parses the real SPKI PEM the API returns directly (no manual byte-offset extraction needed, unlike PHP's sodium or Swift's CryptoKit, both of which need raw key bytes only); a real generate/sign/verify/tamper-detection round-trip was run and confirmed correct before relying on this.

Class Method Summary collapse

Class Method Details

.build_signable(proof) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/force_dream/verify.rb', line 25

def build_signable(proof)
  has_ext = !proof['external_cost_hash'].nil?

  base = {
    'task_id' => text_or_nil(proof['task_id']),
    'agent_id' => text_or_nil(proof['agent_id']),
    'input_hash' => text_or_nil(proof['input_hash']),
    'output_hash' => text_or_nil(proof['output_hash']),
    'cost_pence' => number_or_zero(proof['cost_pence']),
    'budget_pence' => number_or_zero(proof['budget_pence']),
    'started_at' => number_or_zero(proof['started_at']),
    'completed_at' => string_value(proof['completed_at'])
  }

  if has_ext
    base['external_cost_hash'] = string_value(proof['external_cost_hash'])
    base['retrieved_count'] = number_or_zero(proof['retrieved_count'] || 0)
    # Model binding: the server records which provider and model actually served
    # the execution and binds them into the signed payload. Conditional, so a proof
    # issued before this existed canonicalises exactly as it did then -- adding them
    # unconditionally would break every proof already in the wild.
    n = 10
    unless proof['inference_provider'].nil?
      base['inference_provider'] = string_value(proof['inference_provider'])
      n += 1
    end
    unless proof['inference_model'].nil?
      base['inference_model'] = string_value(proof['inference_model'])
      n += 1
    end
    [base, n]
  else
    [base, 8]
  end
end

.number_or_zero(v) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/force_dream/verify.rb', line 65

def number_or_zero(v)
  case v
  when Numeric then v.to_f
  when String then v.to_f
  else 0.0
  end
end

.string_value(v) ⇒ Object



73
74
75
76
77
78
# File 'lib/force_dream/verify.rb', line 73

def string_value(v)
  return v if v.is_a?(String)
  return Canonical.js_number(v.to_f) if v.is_a?(Numeric)

  ''
end

.text_or_nil(v) ⇒ Object



61
62
63
# File 'lib/force_dream/verify.rb', line 61

def text_or_nil(v)
  v.is_a?(String) ? v : nil
end

.verify_merkle_inclusion(leaf_hash, siblings, expected_root) ⇒ Object

Exact replica of the server's verifyMerkleInclusion. Each sibling carries its own position, so ordering is never derived from leaf_index. Hashing is over concatenated HEX STRINGS, not raw bytes -- matching the server exactly. Empty siblings means the root is the leaf digest unchanged (the batch_size == 1 case, which is every real proof the platform has emitted to date).



85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/force_dream/verify.rb', line 85

def verify_merkle_inclusion(leaf_hash, siblings, expected_root)
  current = leaf_hash
  siblings.each do |step|
    sibling_hash = step['hash']
    return false unless sibling_hash.is_a?(String)

    current = if step['position'] == 'right'
                Canonical.sha256_hex(current + sibling_hash)
              else
                Canonical.sha256_hex(sibling_hash + current)
              end
  end
  current == expected_root
end

.verify_proof(api_base:, task_id: nil, proof: nil) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
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
# File 'lib/force_dream/verify.rb', line 100

def verify_proof(api_base:, task_id: nil, proof: nil)
  if proof.nil?
    raise ArgumentError, 'Provide task_id or proof' if task_id.nil?

    data = Http.get("#{api_base}/v1/workforce/proof/#{CGI.escape(task_id)}/public")
    raise 'proof_not_found' unless data['proof']

    proof = data['proof']
  end

  key_data = Http.get("#{api_base}/v1/workforce/proof/public-key")
  key_id = key_data['key_id']
  pem = key_data['public_key_pem'] || ''

  verifying_key = begin
    OpenSSL::PKey.read(pem)
  rescue StandardError
    nil
  end

  signable, field_count = build_signable(proof)
  digest_hex = Canonical.sha256_hex(Canonical.wf_canonical(signable))

  algorithm = proof['algorithm']
  verified = false

  if verifying_key && proof['signature'] &&
     (algorithm.nil? || algorithm == 'Ed25519' || algorithm == 'Ed25519-batched')
    begin
      sig_bytes = Base64.decode64(proof['signature'])

      if algorithm == 'Ed25519-batched'
        # A batched proof is only as strong as this real double-check: the digest
        # must genuinely be a leaf of the claimed root, verified BEFORE the
        # signature is trusted. The signature is over the ROOT, not the digest.
        root = proof['merkle_root']
        inclusion = proof['inclusion_proof']
        siblings = inclusion.is_a?(Hash) ? inclusion['siblings'] : nil

        if root.is_a?(String) && !root.empty? && siblings.is_a?(Array) &&
           verify_merkle_inclusion(digest_hex, siblings, root)
          root_bytes = [root].pack('H*')
          verified = verifying_key.verify(nil, sig_bytes, root_bytes)
        end
      else
        digest_bytes = [digest_hex].pack('H*')
        verified = verifying_key.verify(nil, sig_bytes, digest_bytes)
      end
    rescue StandardError
      verified = false
    end
  end

  VerifyResult.new(
    verified: verified,
    task_id: proof['task_id'],
    key_id: key_id,
    algorithm: algorithm || 'Ed25519',
    fields_signed: field_count,
    trustless: true,
    message: verified ? \
      'Signature mathematically verified. This proof was signed by ForceDream and has not been altered.' : \
      'Signature verification FAILED. The proof was altered or not signed by ForceDream.'
  )
end