Something went wrong. Try again.
DKIM (RFC 6376) signing and verification for Zig 0.16: both canonicalizations, RSA and Ed25519, no I/O or allocation.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Signing a message and then verifying it, which is the only test that//! exercises the two halves against each other.//!//! Every other test in this repository checks one piece against what the RFC//! says it should do. These check that the pieces agree — that the bytes the//! signer hashed are the bytes the verifier hashes, in the same order, with//! the same whitespace. That is the failure mode a unit test cannot find,//! because a signer and a verifier that are wrong in the same way agree//! perfectly with each other.//!//! Which is exactly why these are not enough on their own, and why//! `tests/interop` exists: agreement with *another implementation* is the//! only evidence that this library is right rather than merely consistent.
const std = @import("std");const testing = std.testing;
const dkim = @import("dkim");const rsa = @import("std_crypto_ext").rsa;const keys = @import("keys.zig");
const Ed25519 = std.crypto.sign.Ed25519;
const plain_message = "From: Alice <alice@example.com>\r\n" ++ "To: Bob <bob@example.net>\r\n" ++ "Subject: A test message\r\n" ++ "Date: Fri, 12 Sep 2026 12:00:00 -0500\r\n" ++ "Message-ID: <abc123@example.com>\r\n" ++ "\r\n" ++ "Hello, Bob.\r\n" ++ "\r\n" ++ "This is a message.\r\n";
fn secretKey(der_buf: []u8) !rsa.SecretKey { return rsa.SecretKey.fromPem(der_buf, keys.rsa2048_private);}
/// Signs a message and hands back the signed message: the field, then the/// original, which is exactly how a signer delivers one.fn signMessage( out: []u8, raw: []const u8, options_in: dkim.signing.Options,) ![]const u8 { var field_buf: [dkim.signing.max_field_len]u8 = undefined; const field = try dkim.signing.sign(&field_buf, raw, options_in); if (field.len + raw.len > out.len) return error.BufferTooSmall; @memcpy(out[0..field.len], field); @memcpy(out[field.len..][0..raw.len], raw); return out[0 .. field.len + raw.len];}
/// The DNS record for the 2048-bit test key.const rsa2048_record = "v=DKIM1; k=rsa; p=" ++ keys.rsa2048_public;const rsa1024_record = "v=DKIM1; k=rsa; p=" ++ keys.rsa1024_public;
fn verifyOnly(signed: []const u8, txt: []const u8, options: dkim.verification.Options) !dkim.verification.Verified { var it = dkim.verification.signatures(signed); const signature = try (it.next() orelse return error.NoSignature); const parsed = try dkim.Record.parse(txt); return dkim.verification.verify(signed, signature, parsed, options);}
/// The whole round trip, with whatever options.fn roundTrip( raw: []const u8, sign_options: dkim.signing.Options, verify_options: dkim.verification.Options,) !dkim.verification.Verified { var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, raw, sign_options); return verifyOnly(signed, rsa2048_record, verify_options);}
fn defaultOptions(der_buf: []u8) !dkim.signing.Options { return .{ .domain = "example.com", .selector = "test", .key = .{ .rsa = try secretKey(der_buf) }, .timestamp = 1757000000, };}
test "a signed message verifies" { var der: [rsa.max_secret_key_der]u8 = undefined; const result = try roundTrip(plain_message, try defaultOptions(&der), .{}); try testing.expectEqualStrings("example.com", result.domain); try testing.expectEqual(dkim.Signature.Algorithm.rsa_sha256, result.algorithm); try testing.expect(!result.testing_key); try testing.expect(!result.isPartiallyCovered());}
test "every combination of canonicalization round-trips" { var der: [rsa.max_secret_key_der]u8 = undefined; const both = [_]dkim.canonical.Canonicalization{ .simple, .relaxed }; for (both) |header| { for (both) |body| { var options = try defaultOptions(&der); options.canonicalization = .{ .header = header, .body = body }; _ = roundTrip(plain_message, options, .{}) catch |err| { std.debug.print("failed for c={s}/{s}: {t}\n", .{ @tagName(header), @tagName(body), err, }); return err; }; } }}
test "a message with no body round-trips under both canonicalizations" { // The empty body is where the two algorithms genuinely disagree, so it is // the case most likely to be wrong in exactly one of them. var der: [rsa.max_secret_key_der]u8 = undefined; const empty = "From: a@example.com\r\nSubject: nothing\r\n\r\n"; for ([_]dkim.canonical.Canonicalization{ .simple, .relaxed }) |body| { var options = try defaultOptions(&der); options.canonicalization = .{ .header = .relaxed, .body = body }; _ = try roundTrip(empty, options, .{}); } // ...and with no empty line at all, which is a different message again. for ([_]dkim.canonical.Canonicalization{ .simple, .relaxed }) |body| { var options = try defaultOptions(&der); options.canonicalization = .{ .header = .relaxed, .body = body }; _ = try roundTrip("From: a@example.com\r\n", options, .{}); }}
test "a message with LF endings round-trips" { var der: [rsa.max_secret_key_der]u8 = undefined; const unix = "From: Alice <alice@example.com>\n" ++ "Subject: unix line endings\n" ++ "\n" ++ "body text\n"; _ = try roundTrip(unix, try defaultOptions(&der), .{});}
test "a folded header field round-trips" { var der: [rsa.max_secret_key_der]u8 = undefined; const folded = "From: Alice <alice@example.com>\r\n" ++ "Subject: this subject is long enough that it has\r\n" ++ "\tbeen folded across three lines\r\n" ++ " and here is the third\r\n" ++ "\r\n" ++ "body\r\n"; _ = try roundTrip(folded, try defaultOptions(&der), .{});}
test "changing the body breaks the signature" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der));
var damaged: [16384]u8 = undefined; @memcpy(damaged[0..signed.len], signed); // "Hello, Bob." becomes "Hello, Eve." const at = std.mem.indexOf(u8, damaged[0..signed.len], "Bob.").?; @memcpy(damaged[at..][0..4], "Eve.");
try testing.expectError( error.BodyHashMismatch, verifyOnly(damaged[0..signed.len], rsa2048_record, .{}), );}
test "changing a signed header breaks the signature" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der));
var damaged: [16384]u8 = undefined; @memcpy(damaged[0..signed.len], signed); const at = std.mem.indexOf(u8, damaged[0..signed.len], "Subject: A test").?; @memcpy(damaged[at..][0.."Subject: A best".len], "Subject: A best");
// The body is untouched, so this gets past the body hash and fails at the // signature -- which is the check the two errors distinguish. try testing.expectError( error.SignatureMismatch, verifyOnly(damaged[0..signed.len], rsa2048_record, .{}), );}
test "changing an unsigned header does not break the signature" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); options.headers = &.{ "from", "subject" }; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, options);
// A relay adding a Received field at the top is the ordinary case, and // the entire reason DKIM signs a named list rather than everything. var relayed: [16384]u8 = undefined; const added = "Received: from x.example by y.example; Fri, 12 Sep 2026 12:00:01 -0500\r\n"; @memcpy(relayed[0..added.len], added); @memcpy(relayed[added.len..][0..signed.len], signed); _ = try verifyOnly(relayed[0 .. added.len + signed.len], rsa2048_record, .{});}
test "tampering with the signature's own tags breaks it" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der));
// Moving the signature to another domain, which is the attack the // signature covering its own field exists to stop. var damaged: [16384]u8 = undefined; @memcpy(damaged[0..signed.len], signed); const at = std.mem.indexOf(u8, damaged[0..signed.len], "d=example.com").?; @memcpy(damaged[at..][0.."d=example.org".len], "d=example.org"); try testing.expectError( error.SignatureMismatch, verifyOnly(damaged[0..signed.len], rsa2048_record, .{}), );
// And extending the expiry. var options = try defaultOptions(&der); options.expiration = 1757000100; var buf2: [16384]u8 = undefined; const dated = try signMessage(&buf2, plain_message, options); var damaged2: [16384]u8 = undefined; @memcpy(damaged2[0..dated.len], dated); const x = std.mem.indexOf(u8, damaged2[0..dated.len], "x=1757000100").?; @memcpy(damaged2[x..][0.."x=1957000100".len], "x=1957000100"); try testing.expectError( error.SignatureMismatch, verifyOnly(damaged2[0..dated.len], rsa2048_record, .{}), );}
test "signing the absence of a header stops one being added" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); // Listing `subject` twice when there is one Subject field signs "there is // exactly one Subject here". A second one added in transit is then // covered by the signature and breaks it. options.headers = &.{ "from", "subject", "subject" }; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, options); _ = try verifyOnly(signed, rsa2048_record, .{});
var attacked: [16384]u8 = undefined; const added = "Subject: Please wire the money\r\n"; @memcpy(attacked[0..added.len], added); @memcpy(attacked[added.len..][0..signed.len], signed); try testing.expectError( error.SignatureMismatch, verifyOnly(attacked[0 .. added.len + signed.len], rsa2048_record, .{}), );}
test "the l= tag covers a prefix of the body and says so" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); options.body_length = 13; // "Hello, Bob.\r\n" var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, options);
const result = try verifyOnly(signed, rsa2048_record, .{}); try testing.expect(result.isPartiallyCovered()); try testing.expectEqual(@as(u64, 13), result.covered);
// Appending to the body does not break it, which is the whole hazard. var appended: [16384]u8 = undefined; @memcpy(appended[0..signed.len], signed); const extra = "And an advertisement nobody signed.\r\n"; @memcpy(appended[signed.len..][0..extra.len], extra); const still = try verifyOnly(appended[0 .. signed.len + extra.len], rsa2048_record, .{}); try testing.expect(still.isPartiallyCovered());}
test "an l= larger than the body is refused" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); options.body_length = 1_000_000; var buf: [16384]u8 = undefined; try testing.expectError( error.BodyLengthTooLong, signMessage(&buf, plain_message, options), );
// ...and a signature claiming one is rejected by the verifier too, since // a signer elsewhere may not have checked. options.body_length = 13; const signed = try signMessage(&buf, plain_message, options); var shortened: [16384]u8 = undefined; const cut = std.mem.indexOf(u8, signed, "Hello").? + 5; @memcpy(shortened[0..cut], signed[0..cut]); try testing.expectError( error.BodyLengthTooLong, verifyOnly(shortened[0..cut], rsa2048_record, .{}), );}
test "an expired signature is refused, and only when the clock says so" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); options.timestamp = 1757000000; options.expiration = 1757003600; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, options);
_ = try verifyOnly(signed, rsa2048_record, .{ .now = 1757003600 }); try testing.expectError( error.SignatureExpired, verifyOnly(signed, rsa2048_record, .{ .now = 1757003601 }), ); // With no clock given, expiry is not checked at all. _ = try verifyOnly(signed, rsa2048_record, .{ .now = null });}
test "a revoked key fails the signature" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der)); try testing.expectError( error.KeyRevoked, verifyOnly(signed, "v=DKIM1; k=rsa; p=", .{}), );}
test "a key record that forbids the hash, the service or the key type" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der));
try testing.expectError(error.HashNotAcceptable, verifyOnly( signed, "v=DKIM1; h=sha1; p=" ++ keys.rsa2048_public, .{}, )); try testing.expectError(error.ServiceNotAcceptable, verifyOnly( signed, "v=DKIM1; s=tlsa; p=" ++ keys.rsa2048_public, .{}, )); try testing.expectError(error.KeyTypeMismatch, verifyOnly( signed, "v=DKIM1; k=ed25519; p=" ++ keys.rsa2048_public, .{}, )); // ...and one that permits them explicitly. _ = try verifyOnly(signed, "v=DKIM1; k=rsa; h=sha256; s=email; p=" ++ keys.rsa2048_public, .{});}
test "a testing key verifies and says it is testing" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, try defaultOptions(&der)); const result = try verifyOnly(signed, "v=DKIM1; t=y; p=" ++ keys.rsa2048_public, .{}); try testing.expect(result.testing_key);}
test "t=s requires i= to be the domain exactly" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [16384]u8 = undefined; const strict = "v=DKIM1; t=s; p=" ++ keys.rsa2048_public;
// No i= at all: the flag says nothing about this signature. const plain = try signMessage(&buf, plain_message, try defaultOptions(&der)); _ = try verifyOnly(plain, strict, .{});
var options = try defaultOptions(&der); options.identity = "alice@example.com"; var buf2: [16384]u8 = undefined; const exact = try signMessage(&buf2, plain_message, options); _ = try verifyOnly(exact, strict, .{});
options.identity = "alice@mail.example.com"; var buf3: [16384]u8 = undefined; const sub = try signMessage(&buf3, plain_message, options); _ = try verifyOnly(sub, rsa2048_record, .{}); // fine without the flag try testing.expectError(error.IdentityNotStrict, verifyOnly(sub, strict, .{}));}
test "a key below the minimum size is refused" { var der: [rsa.max_secret_key_der]u8 = undefined; const key = try rsa.SecretKey.fromPem(&der, keys.rsa1024_private); var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, .{ .domain = "example.com", .selector = "test", .key = .{ .rsa = key }, .timestamp = 1757000000, });
// 1024 is the floor RFC 8301 sets, so it passes by default... _ = try verifyOnly(signed, rsa1024_record, .{}); // ...and a verifier asking for 2048 rejects it. try testing.expectError( error.KeyTooSmall, verifyOnly(signed, rsa1024_record, .{ .min_rsa_bits = 2048 }), );}
test "Ed25519 signs and verifies" { // RFC 8463. The key is 32 bytes, the record carries it raw rather than as // DER, and the signature is 64 bytes -- all of which is a different path // through the code from RSA. const seed: [32]u8 = @splat(7); const pair = try Ed25519.KeyPair.generateDeterministic(seed);
var buf: [16384]u8 = undefined; const signed = try signMessage(&buf, plain_message, .{ .domain = "example.com", .selector = "ed", .key = .{ .ed25519 = pair }, .timestamp = 1757000000, }); try testing.expect(std.mem.indexOf(u8, signed, "a=ed25519-sha256") != null);
var public_b64: [64]u8 = undefined; const encoded = std.base64.standard.Encoder.encode(&public_b64, &pair.public_key.toBytes()); var txt: [256]u8 = undefined; const record_txt = try std.fmt.bufPrint(&txt, "v=DKIM1; k=ed25519; p={s}", .{encoded});
const result = try verifyOnly(signed, record_txt, .{}); try testing.expectEqual(dkim.Signature.Algorithm.ed25519_sha256, result.algorithm);
// The wrong key must not verify. const other = try Ed25519.KeyPair.generateDeterministic(@splat(8)); const other_encoded = std.base64.standard.Encoder.encode(&public_b64, &other.public_key.toBytes()); const other_txt = try std.fmt.bufPrint(&txt, "v=DKIM1; k=ed25519; p={s}", .{other_encoded}); try testing.expectError(error.SignatureMismatch, verifyOnly(signed, other_txt, .{}));}
test "rsa-sha1 is refused unless asked for" { // Nothing here can produce one -- `Key.algorithm` only ever says // sha256 -- so this is built by hand, which is also how one arrives in // real life. const signed = "DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d=example.com;\r\n" ++ "\ts=test; bh=2jmj7l5rSw0yVb/vlWAYkK/YBwk=; h=from; b=AAAA\r\n" ++ "From: a@example.com\r\n" ++ "\r\n"; try testing.expectError( error.AlgorithmForbidden, verifyOnly(signed, rsa2048_record, .{}), ); // Allowed, it gets far enough to fail on the signature itself. try testing.expectError( error.SignatureMismatch, verifyOnly(signed, rsa2048_record, .{ .allow_rsa_sha1 = true, .min_rsa_bits = 0 }), );}
test "a signer must sign From" { var der: [rsa.max_secret_key_der]u8 = undefined; var options = try defaultOptions(&der); options.headers = &.{ "to", "subject" }; var buf: [16384]u8 = undefined; try testing.expectError(error.FromNotSigned, signMessage(&buf, plain_message, options));}
test "several signatures on one message are each verifiable" { var der: [rsa.max_secret_key_der]u8 = undefined;
var first_buf: [16384]u8 = undefined; const once = try signMessage(&first_buf, plain_message, try defaultOptions(&der));
var options = try defaultOptions(&der); options.selector = "second"; options.canonicalization = .{ .header = .simple, .body = .simple }; var twice_buf: [16384]u8 = undefined; const twice = try signMessage(&twice_buf, once, options);
var count: usize = 0; var it = dkim.verification.signatures(twice); const parsed = try dkim.Record.parse(rsa2048_record); while (it.next()) |result| { const signature = try result; _ = try dkim.verification.verify(twice, signature, parsed, .{}); count += 1; } try testing.expectEqual(@as(usize, 2), count);}
test "the buffer the field is written into can be too small" { var der: [rsa.max_secret_key_der]u8 = undefined; var tiny: [64]u8 = undefined; try testing.expectError( error.BufferTooSmall, dkim.signing.sign(&tiny, plain_message, try defaultOptions(&der)), );}
test "the produced field folds to something a mail system will not refold" { var der: [rsa.max_secret_key_der]u8 = undefined; var buf: [dkim.signing.max_field_len]u8 = undefined; const field = try dkim.signing.sign(&buf, plain_message, try defaultOptions(&der));
var lines: dkim.message.LineIterator = .init(field); while (lines.next()) |line| { // RFC 5322 recommends 78 including the CRLF, and requires under 998. try testing.expect(line.content.len <= 78); } try testing.expect(std.mem.endsWith(u8, field, "\r\n")); try testing.expect(std.mem.startsWith(u8, field, "DKIM-Signature: "));}
test "a signature under another field name, which is what ARC needs" { // RFC 8617's `ARC-Message-Signature` is a DKIM signature under a // different name and without a version tag. Nothing in the // algorithm looks at the name beyond hashing it, so a round trip // here is what proves that -- the signer hashed the name it wrote // and the verifier hashes the name it reads. var der_buf: [rsa.max_secret_key_der]u8 = undefined; const key = try secretKey(&der_buf);
var out: [dkim.signing.max_field_len]u8 = undefined; const field = try dkim.sign(&out, plain_message, .{ .domain = "example.com", .selector = "test", .key = .{ .rsa = key }, .field_name = "ARC-Message-Signature", .version = false, }); try testing.expect(std.mem.startsWith(u8, field, "ARC-Message-Signature:")); try testing.expect(!std.mem.containsAtLeast(u8, field, 1, "v=1;"));
const signed = try std.fmt.allocPrint(testing.allocator, "{s}{s}", .{ field, plain_message, }); defer testing.allocator.free(signed);
var headers: dkim.message.HeaderIterator = .init(dkim.message.split(signed).headers); const parsed = try dkim.Signature.parseWithOptions( headers.next().?, .{ .require_version = false }, ); const record = try dkim.Record.parse("v=DKIM1; k=rsa; p=" ++ keys.rsa2048_public); const verified = try dkim.verify(signed, parsed, record, .{}); try testing.expectEqualStrings("example.com", verified.domain);}
test "tags a signer does not define, which is what ARC needs" { // An `ARC-Message-Signature` carries an `i=` tag. Inserting one // after signing would invalidate the hash just computed over the // field, so the caller supplies the text and the signature covers // it -- the only order that can work. var der_buf: [rsa.max_secret_key_der]u8 = undefined; const key = try secretKey(&der_buf);
var out: [dkim.signing.max_field_len]u8 = undefined; const field = try dkim.sign(&out, plain_message, .{ .domain = "example.com", .selector = "test", .key = .{ .rsa = key }, .field_name = "ARC-Message-Signature", .version = false, .extra_tags = "i=3;", }); try testing.expect(std.mem.containsAtLeast(u8, field, 1, "i=3;"));
const signed = try std.fmt.allocPrint(testing.allocator, "{s}{s}", .{ field, plain_message }); defer testing.allocator.free(signed);
var headers: dkim.message.HeaderIterator = .init(dkim.message.split(signed).headers); const parsed = try dkim.Signature.parseWithOptions( headers.next().?, .{ .require_version = false, .identity_tag = false }, ); const record = try dkim.Record.parse("v=DKIM1; k=rsa; p=" ++ keys.rsa2048_public); // The tag is inside the signed field, so a verifier that reads it // back gets the same bytes the signer hashed -- which is the whole // claim being tested. _ = try dkim.verify(signed, parsed, record, .{});}
test "signing and verifying a digest directly" { // The two entry points ARC's seal needs: it hashes the ARC header // field values in a fixed order rather than the set an `h=` names, // and then signs and checks the result exactly the way DKIM does. var der_buf: [rsa.max_secret_key_der]u8 = undefined; const key = try secretKey(&der_buf);
const Sha256 = std.crypto.hash.sha2.Sha256; var hasher = Sha256.init(.{}); hasher.update("arc-seal:i=1; cv=none; b="); var made: dkim.digest.Digest = .{ .bytes = @splat(0), .len = Sha256.digest_length }; hasher.final(made.bytes[0..Sha256.digest_length]);
var signature_buf: [1024]u8 = undefined; const b64 = try dkim.signDigest(&signature_buf, .rsa_sha256, made, .{ .rsa = key });
var raw_buf: [512]u8 = undefined; const decoder = std.base64.standard.Decoder; const raw_len = try decoder.calcSizeForSlice(b64); try decoder.decode(raw_buf[0..raw_len], b64);
const record = try dkim.Record.parse("v=DKIM1; k=rsa; p=" ++ keys.rsa2048_public); var key_buf: [2048]u8 = undefined; const key_bytes = try record.decodeKey(&key_buf); try dkim.verifyDigest(.rsa_sha256, made, raw_buf[0..raw_len], key_bytes, .{});
// And a digest of something else does not check out, so the test // above is about the signature rather than about the call returning. var other = made; other.bytes[0] ^= 0xff; try testing.expectError( error.SignatureMismatch, dkim.verifyDigest(.rsa_sha256, other, raw_buf[0..raw_len], key_bytes, .{}), );}