feat(audit): hash chain + HMAC ratchet + Merkle batches + Ed25519 signing

Three layers of integrity:
- Hash chain (SHA-256 chained) catches silent edits
- HMAC ratchet rotates keys every N entries with HKDF-style derivation
  and zeroizes old key bytes from memory
- Merkle batch sealing builds tree over content_hashes, signs root
  with Ed25519 (FFI to libcrypto EVP_PKEY_ED25519 directly since stdlib
  lacks the high-level OpenSSL::PKey wrapper)

AuditLog.append is mutex-guarded for serial writes; verify_chain detects
both single-row tampering and structural inconsistency. BatchSealer's
pack_message format (BE start_seq || BE end_seq || merkle_root) is shared
between signer and verifier so external auditors can validate offline.
This commit is contained in:
CarterPerez-dev 2026-04-29 00:45:54 -04:00
parent 4723716e08
commit fe5e9cd07b
12 changed files with 704 additions and 0 deletions

View File

@ -0,0 +1,62 @@
# ===================
# ©AngelaMos | 2026
# audit_log_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/audit_log"
require "../../../src/cre/persistence/sqlite/sqlite_persistence"
describe CRE::Audit::AuditLog do
it "appends and reads back entries with valid hash chain" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
log.append("rotation.completed", "system", nil, {"x" => "y"})
log.append("policy.violation", "system", nil, {"a" => "b"})
persist.audit.latest_seq.should eq 2_i64
log.verify_chain.should be_true
ensure
persist.try(&.close)
end
it "verify_chain detects when a row is tampered in DB directly" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
log.append("a", "s", nil, {"k" => "v"})
log.append("b", "s", nil, {"k" => "v2"})
persist.db.exec("UPDATE audit_events SET payload = ? WHERE seq = 2", %({"event_type":"b","actor":"s","target_id":null,"payload":{"k":"BAD"}}))
log.verify_chain.should be_false
ensure
persist.try(&.close)
end
it "verify_chain returns true on empty log" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
log.verify_chain.should be_true
ensure
persist.try(&.close)
end
it "ratchets version after configured threshold" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, ratchet_every: 2)
log.append("a", "s", nil, {"i" => "1"})
log.append("a", "s", nil, {"i" => "2"})
log.ratchet_version.should eq 1
log.append("a", "s", nil, {"i" => "3"})
log.ratchet_version.should eq 2
ensure
persist.try(&.close)
end
end

View File

@ -0,0 +1,73 @@
# ===================
# ©AngelaMos | 2026
# batch_sealer_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/audit_log"
require "../../../src/cre/audit/batch_sealer"
require "../../../src/cre/audit/signing"
require "../../../src/cre/persistence/sqlite/sqlite_persistence"
describe CRE::Audit::BatchSealer do
it "seals pending audit events into a signed Merkle batch" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
log.append("a", "s", nil, {"i" => "1"})
log.append("b", "s", nil, {"i" => "2"})
log.append("c", "s", nil, {"i" => "3"})
kp = CRE::Audit::Signing::Ed25519Keypair.generate(version: 1)
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
sealer = CRE::Audit::BatchSealer.new(persist, signer)
batch = sealer.seal_pending.not_nil!
batch.start_seq.should eq 1_i64
batch.end_seq.should eq 3_i64
batch.signing_key_version.should eq 1
batch.merkle_root.size.should eq 32
batch.signature.size.should eq 64
verifier = CRE::Audit::Signing::Ed25519Verifier.new(kp.public_key)
msg = CRE::Audit::BatchSealer.pack_message(batch.start_seq, batch.end_seq, batch.merkle_root)
verifier.verify(msg, batch.signature).should be_true
persist.audit.last_sealed_seq.should eq 3_i64
ensure
persist.try(&.close)
end
it "returns nil when nothing pending" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
kp = CRE::Audit::Signing::Ed25519Keypair.generate
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
sealer = CRE::Audit::BatchSealer.new(persist, signer)
sealer.seal_pending.should be_nil
ensure
persist.try(&.close)
end
it "subsequent seal covers only new events" do
persist = CRE::Persistence::Sqlite::SqlitePersistence.new(":memory:")
persist.migrate!
log = CRE::Audit::AuditLog.new(persist, Bytes.new(32, 0_u8), 1, 1024)
kp = CRE::Audit::Signing::Ed25519Keypair.generate
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
sealer = CRE::Audit::BatchSealer.new(persist, signer)
log.append("a", "s", nil, {"i" => "1"})
log.append("b", "s", nil, {"i" => "2"})
sealer.seal_pending.not_nil!.end_seq.should eq 2_i64
log.append("c", "s", nil, {"i" => "3"})
second = sealer.seal_pending.not_nil!
second.start_seq.should eq 3_i64
second.end_seq.should eq 3_i64
ensure
persist.try(&.close)
end
end

View File

@ -0,0 +1,63 @@
# ===================
# ©AngelaMos | 2026
# hash_chain_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/hash_chain"
describe CRE::Audit::HashChain do
it "first hash is 32 zero bytes" do
g = CRE::Audit::HashChain.genesis
g.size.should eq 32
g.all? { |b| b == 0_u8 }.should be_true
end
it "next_hash is deterministic" do
prev = CRE::Audit::HashChain.genesis
payload = "hello".to_slice
a = CRE::Audit::HashChain.next_hash(prev, payload)
b = CRE::Audit::HashChain.next_hash(prev, payload)
a.should eq b
a.size.should eq 32
end
it "different prev produces different hash" do
payload = "x".to_slice
a = CRE::Audit::HashChain.next_hash(Bytes.new(32, 0_u8), payload)
b = CRE::Audit::HashChain.next_hash(Bytes.new(32, 1_u8), payload)
a.should_not eq b
end
it "different payload produces different hash" do
prev = CRE::Audit::HashChain.genesis
a = CRE::Audit::HashChain.next_hash(prev, "a".to_slice)
b = CRE::Audit::HashChain.next_hash(prev, "b".to_slice)
a.should_not eq b
end
it "verify_chain detects tampering" do
pairs = [] of {Bytes, Bytes}
h = CRE::Audit::HashChain.genesis
payloads = ["a", "b", "c", "d"].map(&.to_slice)
payloads.each do |p|
next_h = CRE::Audit::HashChain.next_hash(h, p)
pairs << {h, next_h}
h = next_h
end
CRE::Audit::HashChain.verify(pairs, payloads).should be_true
tampered = payloads.dup
tampered[2] = "BAD".to_slice
CRE::Audit::HashChain.verify(pairs, tampered).should be_false
end
it "verify_chain returns true for empty input" do
CRE::Audit::HashChain.verify([] of {Bytes, Bytes}, [] of Bytes).should be_true
end
it "verify_chain returns false on mismatched array sizes" do
CRE::Audit::HashChain.verify([] of {Bytes, Bytes}, ["x".to_slice]).should be_false
end
end

View File

@ -0,0 +1,43 @@
# ===================
# ©AngelaMos | 2026
# hmac_ratchet_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/hmac_ratchet"
describe CRE::Audit::HmacRatchet do
it "produces 32-byte HMAC" do
r = CRE::Audit::HmacRatchet.new(Bytes.new(32, 0_u8), version: 1, ratchet_every: 1024)
h = r.sign("payload".to_slice)
h.size.should eq 32
r.version.should eq 1
end
it "rotates after N entries" do
r = CRE::Audit::HmacRatchet.new(Bytes.new(32, 0_u8), version: 1, ratchet_every: 3)
3.times { r.sign("x".to_slice) }
r.version.should eq 1 # not yet rotated
r.sign("x".to_slice)
r.version.should eq 2 # rotated on the 4th call
end
it "different keys produce different HMACs" do
a = CRE::Audit::HmacRatchet.new(Bytes.new(32, 0_u8), 1, 1024).sign("x".to_slice)
b = CRE::Audit::HmacRatchet.new(Bytes.new(32, 1_u8), 1, 1024).sign("x".to_slice)
a.should_not eq b
end
it "rejects keys of incorrect size" do
expect_raises(ArgumentError) do
CRE::Audit::HmacRatchet.new(Bytes.new(16), 1, 1024)
end
end
it "verify (static) round-trips" do
key = Bytes.new(32, 0xff_u8)
payload = "p".to_slice
h = OpenSSL::HMAC.digest(:sha256, key, payload)
CRE::Audit::HmacRatchet.verify(payload, h, key).should be_true
end
end

View File

@ -0,0 +1,38 @@
# ===================
# ©AngelaMos | 2026
# merkle_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/merkle"
describe CRE::Audit::Merkle do
it "single leaf root equals the leaf" do
leaf = "x".to_slice
CRE::Audit::Merkle.root([leaf]).should eq leaf
end
it "balanced tree root is deterministic" do
leaves = ["a", "b", "c", "d"].map(&.to_slice)
r1 = CRE::Audit::Merkle.root(leaves)
r2 = CRE::Audit::Merkle.root(leaves)
r1.should eq r2
r1.size.should eq 32
end
it "different leaves produce different roots" do
a = CRE::Audit::Merkle.root(["a", "b"].map(&.to_slice))
b = CRE::Audit::Merkle.root(["a", "c"].map(&.to_slice))
a.should_not eq b
end
it "odd leaf count is supported (last is promoted)" do
leaves = ["a", "b", "c"].map(&.to_slice)
r = CRE::Audit::Merkle.root(leaves)
r.size.should eq 32
end
it "raises on empty input" do
expect_raises(ArgumentError) { CRE::Audit::Merkle.root([] of Bytes) }
end
end

View File

@ -0,0 +1,55 @@
# ===================
# ©AngelaMos | 2026
# signing_spec.cr
# ===================
require "../../spec_helper"
require "../../../src/cre/audit/signing"
describe CRE::Audit::Signing do
it "generates a keypair" do
kp = CRE::Audit::Signing::Ed25519Keypair.generate(version: 1)
kp.private_key.size.should eq 32
kp.public_key.size.should eq 32
kp.version.should eq 1
end
it "signs and verifies a message" do
kp = CRE::Audit::Signing::Ed25519Keypair.generate
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
verifier = CRE::Audit::Signing::Ed25519Verifier.new(kp.public_key)
msg = "audit batch root".to_slice
sig = signer.sign(msg)
sig.size.should eq 64
verifier.verify(msg, sig).should be_true
end
it "rejects tampered message" do
kp = CRE::Audit::Signing::Ed25519Keypair.generate
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
verifier = CRE::Audit::Signing::Ed25519Verifier.new(kp.public_key)
msg = "original".to_slice
sig = signer.sign(msg)
verifier.verify("tampered".to_slice, sig).should be_false
end
it "rejects tampered signature" do
kp = CRE::Audit::Signing::Ed25519Keypair.generate
signer = CRE::Audit::Signing::Ed25519Signer.from_keypair(kp)
verifier = CRE::Audit::Signing::Ed25519Verifier.new(kp.public_key)
msg = "x".to_slice
sig = signer.sign(msg)
sig[0] ^= 0x01_u8
verifier.verify(msg, sig).should be_false
end
it "two keypairs produce different keys" do
a = CRE::Audit::Signing::Ed25519Keypair.generate
b = CRE::Audit::Signing::Ed25519Keypair.generate
a.private_key.should_not eq b.private_key
a.public_key.should_not eq b.public_key
end
end

View File

@ -0,0 +1,71 @@
# ===================
# ©AngelaMos | 2026
# audit_log.cr
# ===================
require "json"
require "uuid"
require "./hash_chain"
require "./hmac_ratchet"
require "../persistence/persistence"
require "../persistence/repos"
module CRE::Audit
class AuditLog
@ratchet : HmacRatchet
@mutex : Mutex
def initialize(@persistence : Persistence::Persistence, initial_hmac_key : Bytes, @hmac_version : Int32, @ratchet_every : Int32)
@ratchet = HmacRatchet.new(initial_hmac_key, @hmac_version, @ratchet_every)
@mutex = Mutex.new
end
def append(event_type : String, actor : String, target_id : UUID?, payload : Hash) : Persistence::AuditEntry
@mutex.synchronize do
prev = @persistence.audit.latest_hash
canonical = canonical_json(event_type, actor, target_id, payload)
content_hash = HashChain.next_hash(prev, canonical.to_slice)
hmac = @ratchet.sign(content_hash)
entry = Persistence::AuditEntry.new(
seq: 0_i64,
event_id: UUID.random,
occurred_at: Time.utc,
event_type: event_type,
actor: actor,
target_id: target_id,
payload: canonical,
prev_hash: prev,
content_hash: content_hash,
hmac: hmac,
hmac_key_version: @ratchet.version,
)
@persistence.audit.append(entry)
entry
end
end
def verify_chain : Bool
latest = @persistence.audit.latest_seq
return true if latest == 0
entries = @persistence.audit.range(1_i64, latest)
return false if entries.size != latest
pairs = entries.map { |e| {e.prev_hash, e.content_hash} }
payloads = entries.map(&.payload).map(&.to_slice)
HashChain.verify(pairs, payloads)
end
def ratchet_version : Int32
@ratchet.version
end
private def canonical_json(event_type, actor, target_id, payload) : String
{
event_type: event_type,
actor: actor,
target_id: target_id.try(&.to_s),
payload: payload,
}.to_json
end
end
end

View File

@ -0,0 +1,61 @@
# ===================
# ©AngelaMos | 2026
# batch_sealer.cr
# ===================
require "uuid"
require "./merkle"
require "./signing"
require "../persistence/persistence"
require "../persistence/repos"
module CRE::Audit
class BatchSealer
def initialize(
@persistence : Persistence::Persistence,
@signer : Signing::Ed25519Signer,
)
end
def seal_pending : Persistence::AuditBatch?
latest = @persistence.audit.latest_seq
last_end = @persistence.audit.last_sealed_seq
return nil if latest <= last_end
start_seq = last_end + 1
end_seq = latest
entries = @persistence.audit.range(start_seq, end_seq)
return nil if entries.empty?
leaves = entries.map(&.content_hash)
root = Merkle.root(leaves)
msg = pack_message(start_seq, end_seq, root)
sig = @signer.sign(msg)
batch = Persistence::AuditBatch.new(
id: UUID.random,
start_seq: start_seq,
end_seq: end_seq,
merkle_root: root,
signature: sig,
signing_key_version: @signer.version,
sealed_at: Time.utc,
)
@persistence.audit.insert_batch(batch)
batch
end
def self.pack_message(start_seq : Int64, end_seq : Int64, root : Bytes) : Bytes
io = IO::Memory.new
io.write_bytes(start_seq, IO::ByteFormat::BigEndian)
io.write_bytes(end_seq, IO::ByteFormat::BigEndian)
io.write(root)
io.to_slice
end
private def pack_message(start_seq : Int64, end_seq : Int64, root : Bytes) : Bytes
BatchSealer.pack_message(start_seq, end_seq, root)
end
end
end

View File

@ -0,0 +1,34 @@
# ===================
# ©AngelaMos | 2026
# hash_chain.cr
# ===================
require "openssl/digest"
require "../crypto/random"
module CRE::Audit
module HashChain
GENESIS_SIZE = 32
def self.genesis : Bytes
Bytes.new(GENESIS_SIZE, 0_u8)
end
def self.next_hash(prev_hash : Bytes, payload : Bytes) : Bytes
d = OpenSSL::Digest.new("SHA256")
d.update(prev_hash)
d.update(payload)
d.final
end
def self.verify(pairs : Array({Bytes, Bytes}), payloads : Array(Bytes)) : Bool
return false unless pairs.size == payloads.size
pairs.each_with_index do |entry, i|
prev, current = entry
expected = next_hash(prev, payloads[i])
return false unless CRE::Crypto::Random.constant_time_equal?(expected, current)
end
true
end
end
end

View File

@ -0,0 +1,52 @@
# ===================
# ©AngelaMos | 2026
# hmac_ratchet.cr
# ===================
require "openssl/hmac"
require "openssl/digest"
module CRE::Audit
class HmacRatchet
getter version : Int32
@key : Bytes
@counter : Int32
def initialize(initial_key : Bytes, @version : Int32, @ratchet_every : Int32)
raise ArgumentError.new("key must be 32 bytes") unless initial_key.size == 32
@key = initial_key.dup
@counter = 0
end
def sign(payload : Bytes) : Bytes
maybe_rotate
h = OpenSSL::HMAC.digest(:sha256, @key, payload)
@counter += 1
h
end
def self.verify(payload : Bytes, expected : Bytes, key : Bytes) : Bool
h = OpenSSL::HMAC.digest(:sha256, key, payload)
CRE::Crypto::Random.constant_time_equal?(h, expected)
end
def current_key : Bytes
@key.dup
end
private def maybe_rotate : Nil
return unless @counter >= @ratchet_every
d = OpenSSL::Digest.new("SHA256")
d.update(@key)
d.update("ratchet-v#{@version + 1}".to_slice)
new_key = d.final
@key.size.times { |i| @key[i] = 0_u8 }
@key = new_key
@version += 1
@counter = 0
end
end
end

View File

@ -0,0 +1,36 @@
# ===================
# ©AngelaMos | 2026
# merkle.cr
# ===================
require "openssl/digest"
module CRE::Audit
module Merkle
def self.root(leaves : Array(Bytes)) : Bytes
raise ArgumentError.new("empty merkle tree") if leaves.empty?
level = leaves.dup
while level.size > 1
next_level = [] of Bytes
i = 0
while i < level.size
if i + 1 < level.size
next_level << combine(level[i], level[i + 1])
else
next_level << level[i]
end
i += 2
end
level = next_level
end
level[0]
end
private def self.combine(a : Bytes, b : Bytes) : Bytes
d = OpenSSL::Digest.new("SHA256")
d.update(a)
d.update(b)
d.final
end
end
end

View File

@ -0,0 +1,116 @@
# ===================
# ©AngelaMos | 2026
# signing.cr
# ===================
require "openssl"
require "openssl/lib_crypto"
lib LibCrypto
type EVP_PKEY = Void*
fun evp_pkey_new_raw_private_key_cre = EVP_PKEY_new_raw_private_key(type : LibC::Int, e : Void*, key : UInt8*, keylen : LibC::SizeT) : EVP_PKEY
fun evp_pkey_new_raw_public_key_cre = EVP_PKEY_new_raw_public_key(type : LibC::Int, e : Void*, key : UInt8*, keylen : LibC::SizeT) : EVP_PKEY
fun evp_pkey_get_raw_public_key_cre = EVP_PKEY_get_raw_public_key(pkey : EVP_PKEY, pub : UInt8*, len : LibC::SizeT*) : LibC::Int
fun evp_pkey_free_cre = EVP_PKEY_free(pkey : EVP_PKEY) : Void
fun evp_digestsigninit_cre = EVP_DigestSignInit(ctx : EVP_MD_CTX, pctx : Void*, type : EVP_MD, e : Void*, pkey : EVP_PKEY) : LibC::Int
fun evp_digestsign_cre = EVP_DigestSign(ctx : EVP_MD_CTX, sigret : UInt8*, siglen : LibC::SizeT*, tbs : UInt8*, tbslen : LibC::SizeT) : LibC::Int
fun evp_digestverifyinit_cre = EVP_DigestVerifyInit(ctx : EVP_MD_CTX, pctx : Void*, type : EVP_MD, e : Void*, pkey : EVP_PKEY) : LibC::Int
fun evp_digestverify_cre = EVP_DigestVerify(ctx : EVP_MD_CTX, sig : UInt8*, siglen : LibC::SizeT, tbs : UInt8*, tbslen : LibC::SizeT) : LibC::Int
end
module CRE::Audit::Signing
NID_ED25519 = 1087
ED25519_KEY_SIZE = 32
ED25519_SIG_SIZE = 64
ED25519_PUBKEY_SIZE = 32
class Error < OpenSSL::Error; end
class Ed25519Keypair
getter version : Int32
getter private_key : Bytes
getter public_key : Bytes
def initialize(@private_key : Bytes, @public_key : Bytes, @version : Int32)
raise ArgumentError.new("private key must be 32 bytes") unless @private_key.size == ED25519_KEY_SIZE
raise ArgumentError.new("public key must be 32 bytes") unless @public_key.size == ED25519_PUBKEY_SIZE
end
def self.generate(version : Int32 = 1) : Ed25519Keypair
private_key = ::Random::Secure.random_bytes(ED25519_KEY_SIZE)
pkey = LibCrypto.evp_pkey_new_raw_private_key_cre(NID_ED25519, Pointer(Void).null, private_key.to_unsafe, ED25519_KEY_SIZE.to_u64)
raise Error.new("EVP_PKEY_new_raw_private_key failed") if pkey.null?
begin
pubkey_buf = Bytes.new(ED25519_PUBKEY_SIZE)
len = ED25519_PUBKEY_SIZE.to_u64
rc = LibCrypto.evp_pkey_get_raw_public_key_cre(pkey, pubkey_buf.to_unsafe, pointerof(len))
raise Error.new("EVP_PKEY_get_raw_public_key failed (rc=#{rc})") unless rc == 1
Ed25519Keypair.new(private_key, pubkey_buf, version)
ensure
LibCrypto.evp_pkey_free_cre(pkey)
end
end
end
class Ed25519Signer
getter version : Int32
def initialize(@private_key : Bytes, @version : Int32)
raise ArgumentError.new("private key must be 32 bytes") unless @private_key.size == ED25519_KEY_SIZE
end
def self.from_keypair(kp : Ed25519Keypair) : Ed25519Signer
Ed25519Signer.new(kp.private_key, kp.version)
end
def sign(message : Bytes) : Bytes
pkey = LibCrypto.evp_pkey_new_raw_private_key_cre(NID_ED25519, Pointer(Void).null, @private_key.to_unsafe, ED25519_KEY_SIZE.to_u64)
raise Error.new("EVP_PKEY_new_raw_private_key failed") if pkey.null?
ctx = LibCrypto.evp_md_ctx_new
raise Error.new("EVP_MD_CTX_new failed") if ctx.null?
begin
rc = LibCrypto.evp_digestsigninit_cre(ctx, Pointer(Void).null, Pointer(Void).null.as(LibCrypto::EVP_MD), Pointer(Void).null, pkey)
raise Error.new("EVP_DigestSignInit failed (rc=#{rc})") unless rc == 1
siglen = ED25519_SIG_SIZE.to_u64
sig = Bytes.new(ED25519_SIG_SIZE)
rc = LibCrypto.evp_digestsign_cre(ctx, sig.to_unsafe, pointerof(siglen), message.to_unsafe, message.size.to_u64)
raise Error.new("EVP_DigestSign failed (rc=#{rc})") unless rc == 1
sig
ensure
LibCrypto.evp_md_ctx_free(ctx)
LibCrypto.evp_pkey_free_cre(pkey)
end
end
end
class Ed25519Verifier
def initialize(@public_key : Bytes)
raise ArgumentError.new("public key must be 32 bytes") unless @public_key.size == ED25519_PUBKEY_SIZE
end
def verify(message : Bytes, signature : Bytes) : Bool
return false unless signature.size == ED25519_SIG_SIZE
pkey = LibCrypto.evp_pkey_new_raw_public_key_cre(NID_ED25519, Pointer(Void).null, @public_key.to_unsafe, ED25519_PUBKEY_SIZE.to_u64)
return false if pkey.null?
ctx = LibCrypto.evp_md_ctx_new
if ctx.null?
LibCrypto.evp_pkey_free_cre(pkey)
return false
end
begin
rc = LibCrypto.evp_digestverifyinit_cre(ctx, Pointer(Void).null, Pointer(Void).null.as(LibCrypto::EVP_MD), Pointer(Void).null, pkey)
return false unless rc == 1
rc = LibCrypto.evp_digestverify_cre(ctx, signature.to_unsafe, signature.size.to_u64, message.to_unsafe, message.size.to_u64)
rc == 1
ensure
LibCrypto.evp_md_ctx_free(ctx)
LibCrypto.evp_pkey_free_cre(pkey)
end
end
end
end