---
title: "Ruby signature verification"
description: "Run the complete Ruby Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases."
slug: "/docs/v1/security/verify-ed25519/ruby"
section: "Signature examples"
order: 211
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"]
draft: false
---
Verify KDPika payload authenticity in Ruby without losing the server's signed numeric tokens.

## What you will learn
- Build and run the complete Ruby verifier.
- Reject altered, ambiguous and untrusted envelopes.
- Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license).

## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Install Ruby 2.6+; libsodium 1.0.20; standard Ruby Fiddle/JSON.
- Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it.
- Open a terminal in the extracted security directory; all paths below are relative to it.
- Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier.

## Install and build
```bash
ruby --version
# Install libsodium 1.0.20 with your OS package manager.
# Set SODIUM_LIBRARY to its full library path if it is not in the loader path.
```
Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support.

Install libsodium through the operating system's package tooling; it is a client development dependency, not a requirement on KDPika shared hosting. Windows needs a matching libsodium library and compiler include/link paths. Linux uses its own library/package paths. macOS Homebrew may require PKG_CONFIG_PATH pointing at libsodium's lib/pkgconfig directory. Native builds were executed on macOS only.

## Run the positive and negative cases
```bash
ruby ruby/verify.rb vectors/positive.key.json vectors/positive.envelope.json
ruby ruby/verify.rb vectors/tampered.key.json vectors/tampered.envelope.json
ruby ruby/verify.rb vectors/expired.key.json vectors/expired.envelope.json
python3 selftest.py --command ruby ruby/verify.rb
```
Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches.

Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk).

## Complete source
```ruby
# KDPika signature authenticity only. Ruby 2.6+, native libsodium 1.0.20.
# Run: ruby verify.rb key.json envelope.json. Output signature_valid=true/false.
# Set SODIUM_LIBRARY to the installed library path if needed. Trust the key separately.
require 'json'
require 'strscan'
require 'base64'
require 'digest'
require 'fiddle/import'
module Sodium
  extend Fiddle::Importer
  dlload ENV.fetch('SODIUM_LIBRARY', RUBY_PLATFORM.include?('darwin') ? 'libsodium.dylib' : 'libsodium.so')
  extern 'int sodium_init()'
  extern 'int crypto_sign_verify_detached(void*, void*, unsigned long long, void*)'
end
NumberToken = Struct.new(:raw)
class RawJson
  def initialize(raw)
    raise 'Invalid JSON size or UTF-8' if raw.bytesize > 1_048_576 || !raw.force_encoding('UTF-8').valid_encoding?
    @scanner = StringScanner.new(raw)
  end
  def string
    token = @scanner.scan(/"(?:[^"\\\x00-\x1f]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/)
    raise 'Invalid JSON string' unless token
    JSON.parse(token)
  end
  def value(depth)
    raise 'JSON exceeds 64 levels' if depth > 64
    @scanner.skip(/[ \t\r\n]*/)
    return string if @scanner.peek(1) == '"'
    if ['{', '['].include?(@scanner.peek(1))
      object = @scanner.getch == '{'; ending = object ? '}' : ']'; result = object ? {} : []
      @scanner.skip(/[ \t\r\n]*/)
      return result if @scanner.scan(Regexp.new(Regexp.escape(ending)))
      loop do
        if object
          @scanner.skip(/[ \t\r\n]*/); key = string; @scanner.skip(/[ \t\r\n]*/)
          raise 'Duplicate or invalid property' if result.key?(key) || !@scanner.scan(/:/)
          result[key] = value(depth + 1)
        else
          result << value(depth + 1)
        end
        @scanner.skip(/[ \t\r\n]*/); next_char = @scanner.getch
        return result if next_char == ending
        raise 'Invalid collection' unless next_char == ','
      end
    end
    {'true' => true, 'false' => false, 'null' => nil}.each do |token, result|
      return result if @scanner.scan(Regexp.new(token))
    end
    number = @scanner.scan(/-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/)
    raise 'Invalid JSON value' unless number
    NumberToken.new(number)
  end
  def parse
    result = value(0); @scanner.skip(/[ \t\r\n]*/)
    raise 'Trailing JSON' unless @scanner.eos?
    result
  end
end
def quote(value)
  raise 'Invalid Unicode' unless value.valid_encoding?
  JSON.generate(value).gsub("\u2028", '\\u2028').gsub("\u2029", '\\u2029')
end
def canonical(value)
  case value
  when NumberToken then value.raw
  when Hash then '{' + value.keys.sort_by(&:b).map { |k| quote(k) + ':' + canonical(value[k]) }.join(',') + '}'
  when Array then '[' + value.map { |child| canonical(child) }.join(',') + ']'
  when String then quote(value)
  when TrueClass then 'true'
  when FalseClass then 'false'
  when NilClass then 'null'
  else raise 'Unsupported value'
  end
end
def decode(value, size)
  raise 'Invalid base64url' unless value.is_a?(String) && /\A[A-Za-z0-9_-]+\z/.match?(value)
  raw = Base64.urlsafe_decode64(value)
  raise 'Invalid base64url length' unless raw.bytesize == size && Base64.urlsafe_encode64(raw, padding: false) == value
  raw
end
def verify_envelope(trusted_key_raw, envelope_raw)
  key = RawJson.new(trusted_key_raw).parse; env = RawJson.new(envelope_raw).parse
  public_key = decode(key.fetch('public_key'), 32)
  id = 'ed25519_' + Digest::SHA256.hexdigest(public_key)[0, 16]
  return false unless key['algorithm'] == 'Ed25519' && key['encoding'] == 'base64url' && key['key_id'] == id &&
                      env['algorithm'] == 'Ed25519' && env['key_id'] == id && env['payload'].is_a?(Hash)
  message = canonical(env['payload']).b; signature = decode(env.fetch('signature'), 64)
  Sodium.sodium_init >= 0 && Sodium.crypto_sign_verify_detached(signature, message, message.bytesize, public_key) == 0
rescue StandardError
  false
end
if $PROGRAM_NAME == __FILE__
  begin
    valid = ARGV.length == 2 && verify_envelope(File.binread(ARGV[0]), File.binread(ARGV[1]))
  rescue StandardError
    valid = false
  end
  puts "signature_valid=#{valid}"; exit(valid ? 0 : 1)
end
```

## Expected result
All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values.

## Common mistakes
- Taking the verification key from the same untrusted message being checked.
- Treating a successful signature as proof of active access, correct installation or recent payment.
- Parsing and reserializing numbers before passing the envelope into this sample.

## Security notes
The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature).

After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs.

## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build |
| Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification |
| Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer |
| Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths |

## Next steps
Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses).

## Related
- [Verification language index](/docs/v1/security/verify-ed25519)
- [Secure token storage](/docs/v1/security/secure-token-storage)
