Module: Invoance::AuditVerify
- Defined in:
- lib/invoance/audit_verify.rb
Overview
Offline, client-side signature verification for audit events.
Reconstructs the canonical signed bytes from an event returned by the API and checks the Ed25519 signature using stdlib OpenSSL (no external dependency, matching this SDK's zero-dependency design).
Trust note: by default this verifies against the key embedded in the event (event), which proves the payload is internally consistent with that key. For a real tamper guarantee, pass public_key = the tenant's registered key (the server pins it and never trusts the row's key).
Ed25519 requires Ruby's OpenSSL to support Ed25519 (Ruby >= 3.0 with a
modern OpenSSL). On older stacks the verify degrades to an invalid result
with a descriptive reason rather than crashing. If you need to support
such stacks, the ed25519 gem is a drop-in alternative — but this SDK
stays dependency-free.
Constant Summary collapse
- SPKI_ED25519_PREFIX =
DER SPKI prefix for a raw 32-byte Ed25519 public key (RFC 8410).
["302a300506032b6570032100"].pack("H*").freeze
Class Method Summary collapse
-
.ed25519_verify(message, signature, pubkey) ⇒ Boolean
Ed25519-verify a raw message with a raw 32-byte public key.
-
.hex_to_bytes(hex) ⇒ Object
private
Hex-decode a hex string into a binary String.
- .result(valid, reason, payload_hash, key_source) ⇒ Object private
- .stringify(hash) ⇒ Object private
-
.verify_audit_event(event, public_key: nil) ⇒ Hash
Verify one audit event's signature offline.
Class Method Details
.ed25519_verify(message, signature, pubkey) ⇒ Boolean
Ed25519-verify a raw message with a raw 32-byte public key. Returns true/false; never raises (OpenSSL errors → false).
39 40 41 42 43 44 45 46 |
# File 'lib/invoance/audit_verify.rb', line 39 def ed25519_verify(, signature, pubkey) der = SPKI_ED25519_PREFIX + pubkey.b pkey = OpenSSL::PKey.read(der) # nil digest for Ed25519 (single-shot, no separate hash). pkey.verify(nil, signature.b, .b) rescue StandardError, OpenSSL::OpenSSLError false end |
.hex_to_bytes(hex) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Hex-decode a hex string into a binary String.
50 51 52 |
# File 'lib/invoance/audit_verify.rb', line 50 def hex_to_bytes(hex) [hex].pack("H*") end |
.result(valid, reason, payload_hash, key_source) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
105 106 107 108 109 110 111 112 |
# File 'lib/invoance/audit_verify.rb', line 105 def result(valid, reason, payload_hash, key_source) { "valid" => valid, "reason" => reason, "payload_hash" => payload_hash, "key_source" => key_source } end |
.stringify(hash) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
115 116 117 118 119 |
# File 'lib/invoance/audit_verify.rb', line 115 def stringify(hash) return {} unless hash.is_a?(Hash) hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v } end |
.verify_audit_event(event, public_key: nil) ⇒ Hash
Verify one audit event's signature offline.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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/invoance/audit_verify.rb', line 59 def verify_audit_event(event, public_key: nil) key_source = public_key.nil? ? "event" : "pinned" e = stringify(event) signed_input = { "org_id" => e["org_id"], "event_id" => (e["id"].nil? ? e["event_id"] : e["id"]), "seq" => e["seq"], "ingested_at" => e["ingested_at"], "action" => e["action"], "occurred_at" => e["occurred_at"], "actor" => e["actor"], "targets" => e["targets"] } signed_input["context"] = e["context"] unless e["context"].nil? signed_input["metadata"] = e["metadata"] unless e["metadata"].nil? begin canonical = AuditCanonical.canonical_audit_bytes(signed_input) rescue StandardError return result(false, "canonicalization_failed", "", key_source) end recomputed = AuditCanonical.payload_hash_hex(canonical) if !e["payload_hash"].nil? && e["payload_hash"] != recomputed return result(false, "payload_hash_mismatch", recomputed, key_source) end key = public_key.nil? ? e["signing_public_key"] : public_key return result(false, "no_public_key", recomputed, key_source) if key.nil? || key == "" sig = e["signature"] return result(false, "no_signature", recomputed, key_source) if sig.nil? || sig == "" sig_bytes = sig.is_a?(String) ? hex_to_bytes(sig) : sig key_bytes = key.is_a?(String) ? hex_to_bytes(key) : key ok = ed25519_verify(canonical, sig_bytes, key_bytes) if ok result(true, nil, recomputed, key_source) else result(false, "signature_invalid", recomputed, key_source) end end |