authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-07-14 17:49:38 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-07-14 17:49:38 -07:00
log02e70c5de8f1002a6193fbe47ef6733546db8c75
tree1132369a0a29098e22a94a3a311a0073b0a9216c
parentc79ce4fada829f3afeebd2a3b55e46a2f8c98592
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

Signature: get pgp verification working too


1 files changed, 316 insertions(+), 20 deletions(-)

git.zig+316-20
......@@ -2167,9 +2167,12 @@ pub const Signature = union(enum) {
21672167 packet_length: u32,
21682168 version: u8,
21692169 type_id: Pgp.Type,
2170 pk_algorithm: Pgp.PublicKeyAlgorithm,
2170 material: Material,
21712171 hash_algorithm: Pgp.HashAlgorithm,
2172 keyid: [16]u8,
2173 creation_time: time.DateTime,
21722174 signed_hash_value_prefix: [4]u8,
2175 pubkey: ?PubKey,
21732176 valid: ?bool,
21742177
21752178 /// https://datatracker.ietf.org/doc/html/rfc9580#name-signature-types
......@@ -2245,6 +2248,77 @@ pub const Signature = union(enum) {
22452248 };
22462249 }
22472250 };
2251
2252 pub const SubType = enum(u8) {
2253 signature_creation_time = 2,
2254 signature_expiration_time = 3,
2255 exportable_certification = 4,
2256 trust_signature = 5,
2257 revocable_expression = 6,
2258 revocable = 7,
2259 key_expiration_time = 9,
2260 preferred_v1_seipd_ciphers = 11,
2261 issuer_key_id = 16,
2262 notation_data = 20,
2263 preferred_hash_algorithms = 21,
2264 preferred_compression_algorithms = 22,
2265 key_server_preferences = 23,
2266 preferred_key_server = 24,
2267 primary_user_id = 25,
2268 policy_uri = 26,
2269 key_flags = 27,
2270 signer_user_id = 28,
2271 revocation_reason = 29,
2272 features = 30,
2273 signature_target = 31,
2274 embedded_signature = 32,
2275 issuer_fingerprint = 33,
2276 intended_recipient_fingerprint = 35,
2277 preferred_aead_ciphersuites = 39,
2278 _,
2279 };
2280
2281 pub const Material = union(PublicKeyAlgorithm) {
2282 reserved: void,
2283 rsa_encrypt_or_sign: []const u8,
2284 rsa_encrypt: void,
2285 rsa_sign: void,
2286 elgamal_encrypt: void,
2287 dsa: void,
2288 ecdh: void,
2289 ecdsa: void,
2290 x25519: void,
2291 x448: void,
2292 ed25519: void,
2293 ed448: void,
2294 };
2295
2296 const PubKey = struct {
2297 version: u8,
2298 creation_time: time.DateTime,
2299 days_valid: u16,
2300 material: PubKey.Material,
2301
2302 const Material = union(Pgp.PublicKeyAlgorithm) {
2303 reserved: void,
2304 rsa_encrypt_or_sign: RSA,
2305 rsa_encrypt: void,
2306 rsa_sign: void,
2307 elgamal_encrypt: void,
2308 dsa: void,
2309 ecdh: void,
2310 ecdsa: void,
2311 x25519: void,
2312 x448: void,
2313 ed25519: void,
2314 ed448: void,
2315
2316 const RSA = struct {
2317 n: []const u8,
2318 e: []const u8,
2319 };
2320 };
2321 };
22482322 };
22492323
22502324 pub const Ssh = struct {
......@@ -2264,57 +2338,159 @@ pub const Signature = union(enum) {
22642338 return .none;
22652339 }
22662340 if (std.mem.startsWith(u8, pem_sig, "-----BEGIN PGP SIGNATURE-----\n \n ") and (std.mem.endsWith(u8, pem_sig, "\n -----END PGP SIGNATURE-----\n ") or std.mem.endsWith(u8, pem_sig, "\n -----END PGP SIGNATURE-----"))) {
2267 const pembody = pem_sig[33 .. std.mem.indexOf(u8, pem_sig, "\n =") orelse std.mem.indexOf(u8, pem_sig, "\n -----END").?];
2341 const pembody = pem_sig[33..std.mem.indexOf(u8, pem_sig, "\n -----END").?];
22682342 const sigcontent = pembody[0..std.mem.lastIndexOf(u8, pembody, "\n ").?];
22692343 var fixed: nio.FixedBufferStream([]const u8) = .init(sigcontent);
22702344 var skip = nio.SkipReader(void).from(&fixed, "\n ");
22712345 var b64r = nio.Base64Reader(void).from(&skip);
22722346
2347 var message = extras.ManyArrayList(u8).init(allocator);
2348 defer message.deinit();
2349 try message.appendSlice(try message.add(), content_plus_gpgsig);
2350 message.lengths.items.len = 0;
2351 {
2352 var iter = std.mem.splitScalar(u8, content_plus_gpgsig, '\n');
2353 while (iter.next()) |line| {
2354 try message.lengths.append(allocator, line.len + 1);
2355 }
2356 var skipping = false;
2357 var i: usize = 0;
2358 while (i < message.lengths.items.len) : (i += 1) {
2359 if (!skipping and std.mem.startsWith(u8, message.items(i), "gpgsig ")) {
2360 skipping = true;
2361 message.remove(i);
2362 i -= 1;
2363 continue;
2364 }
2365 if (!skipping) {
2366 continue;
2367 }
2368 if (skipping and std.mem.startsWith(u8, message.items(i), " ")) {
2369 message.remove(i);
2370 i -= 1;
2371 continue;
2372 }
2373 break;
2374 }
2375 }
2376
2377 var signed_data: nio.AllocatingWriter = .init(allocator);
2378 defer signed_data.deinit();
2379 try signed_data.writeAll(message.list.items);
2380
22732381 const packet_type: packed struct { id: u6, format: u1, reserved: u1 } = @bitCast(try b64r.readByte());
22742382 if (packet_type.reserved != 1) return .unrecognized;
22752383 if (packet_type.format != 1) return .unrecognized; // legacy non-OpenPGP format
22762384 if (packet_type.id != 2) return .unrecognized; // not a signature
22772385
2278 var packet_len_partial = false;
2279 const packet_len: u32 = blk: {
2280 const oct1: u32 = try b64r.readByte();
2281 if (oct1 <= 191) break :blk oct1;
2282 if (oct1 >= 224 and oct1 < 255) packet_len_partial = true;
2283 if (oct1 >= 224 and oct1 < 255) break :blk @as(u32, 1) << @intCast(oct1 & 0x1F);
2284 const oct2: u32 = try b64r.readByte();
2285 if (oct1 <= 223) break :blk ((oct1 - 192) << 8) + (oct2) + 192;
2286 const oct3: u32 = try b64r.readByte();
2287 const oct4: u32 = try b64r.readByte();
2288 const oct5: u32 = try b64r.readByte();
2289 std.debug.assert(oct1 == 255);
2290 break :blk (oct2 << 24) | (oct3 << 16) | (oct4 << 8) | oct5;
2291 };
2386 _, const packet_len = pgp_read_packet_len(&b64r) catch return .unrecognized;
22922387
22932388 const sig_version = try b64r.readByte();
2389 try signed_data.writeAll(&.{sig_version});
22942390 var type_id: Pgp.Type = .reserved;
22952391 var pk_algo: Pgp.PublicKeyAlgorithm = .reserved;
22962392 var hash_algo: Pgp.HashAlgorithm = .reserved;
2393 var creation_time: [4]u8 = @splat(0);
2394 var keyid: [8]u8 = @splat(0);
22972395 var signed_hash_value_prefix: [2]u8 = @splat(0);
22982396
2397 if (sig_version == 3) blk: {
2398 const hashed_len = try b64r.readByte();
2399 if (hashed_len != 5) break :blk;
2400 type_id = @enumFromInt(try b64r.readByte());
2401 creation_time = try b64r.readArray(4);
2402 keyid = try b64r.readArray(8);
2403 pk_algo = @enumFromInt(try b64r.readByte());
2404 hash_algo = @enumFromInt(try b64r.readByte());
2405 signed_hash_value_prefix = try b64r.readArray(2);
2406 }
22992407 if (sig_version == 4) {
23002408 type_id = @enumFromInt(try b64r.readByte());
23012409 pk_algo = @enumFromInt(try b64r.readByte());
23022410 hash_algo = @enumFromInt(try b64r.readByte());
2411 try signed_data.writeAll(&.{ @intFromEnum(type_id), @intFromEnum(pk_algo), @intFromEnum(hash_algo) });
23032412 const subpacket_len_hashed = try b64r.readInt(u16, .big);
2304 try b64r.skipBytes(subpacket_len_hashed, .{}); // TODO read this
2413 try signed_data.writeInt(u16, subpacket_len_hashed, .big);
2414 const subpacket_bytes = try b64r.readAlloc(allocator, subpacket_len_hashed);
2415 try signed_data.writeAll(subpacket_bytes);
2416 try signed_data.writeAll(&.{0x04});
2417 try signed_data.writeAll(&.{0xFF});
2418 try signed_data.writeInt(u32, @truncate(signed_data.items.len - 2 - message.list.items.len), .big);
2419 {
2420 var lr = nio.FixedBufferStream([]u8).init(subpacket_bytes);
2421 while (lr.rest().len > 0) {
2422 const subpacket_len = try pgp_read_subpacket_len(&lr);
2423 const subpacket_typeid: Pgp.SubType = @enumFromInt(try lr.readByte() & 127);
2424 if (subpacket_typeid == .signature_creation_time and subpacket_len == 1 + 4) {
2425 creation_time = try lr.readArray(4);
2426 continue;
2427 }
2428 if (subpacket_typeid == .issuer_key_id and subpacket_len == 1 + 8) {
2429 keyid = try lr.readArray(8);
2430 continue;
2431 }
2432 try lr.skipBytes(subpacket_len - 1, .{});
2433 }
2434 }
23052435 const subpacket_len_unhashed = try b64r.readInt(u16, .big);
2306 try b64r.skipBytes(subpacket_len_unhashed, .{}); // TODO read this
2436 {
2437 try b64r.skipBytes(subpacket_len_unhashed, .{}); // TODO read this
2438 }
23072439 signed_hash_value_prefix = try b64r.readArray(2);
23082440 }
2441 if (sig_version == 6) {
2442 type_id = @enumFromInt(try b64r.readByte());
2443 pk_algo = @enumFromInt(try b64r.readByte());
2444 hash_algo = @enumFromInt(try b64r.readByte());
2445 const subpacket_len_hashed = try b64r.readInt(u32, .big);
2446 {
2447 try b64r.skipBytes(subpacket_len_hashed, .{}); // TODO read this
2448 }
2449 const subpacket_len_unhashed = try b64r.readInt(u32, .big);
2450 {
2451 try b64r.skipBytes(subpacket_len_unhashed, .{}); // TODO read this
2452 }
2453 signed_hash_value_prefix = try b64r.readArray(2);
2454 }
2455
2456 const material = try pgp_read_signature_material(&b64r, allocator, pk_algo);
2457
2458 var valid: ?bool = null;
2459 _ = &valid;
2460 var pubkey_t: ?Pgp.PubKey = null;
2461
2462 // PGP keys are not stored in-band, need to fetch them and cache them
2463 // TODO check https://keys.openpgp.org
2464 // TODO check https://keyserver.ubuntu.com
2465 // TODO check manually uploaded keys in database
2466 if (known_pgp_keys.get(&extras.to_HEX(keyid))) |pubkey_bytes| blk: {
2467 const ns = std.crypto.Certificate.rsa;
2468 var pubkey_fbs: nio.FixedBufferStream([]const u8) = .init(pubkey_bytes);
2469 pubkey_t = pgp_parse_pubkey(&pubkey_fbs, allocator) catch break :blk;
2470 if (pubkey_t.?.material != pk_algo) {
2471 valid = false;
2472 break :blk;
2473 }
2474 switch (pubkey_t.?.material) {
2475 .rsa_encrypt_or_sign => |*m| {
2476 const pubkey = ns.PublicKey.fromBytes(m.e, m.n) catch break :blk;
2477 valid = if (pgp_rsa_verify(m.n.len, material.rsa_encrypt_or_sign, signed_data.items, pubkey, hash_algo)) true else |err| if (err == error.unrecognized) null else false;
2478 },
2479 else => {},
2480 }
2481 }
23092482
23102483 return .{ .pgp = .{
23112484 .packet_length = packet_len,
23122485 .version = sig_version,
23132486 .type_id = type_id,
2314 .pk_algorithm = pk_algo,
2487 .material = material,
23152488 .hash_algorithm = hash_algo,
2489 .keyid = extras.to_HEX(keyid),
2490 .creation_time = .initUnix(std.mem.readInt(u32, &creation_time, .big)),
23162491 .signed_hash_value_prefix = extras.to_hex(signed_hash_value_prefix),
2317 .valid = null,
2492 .pubkey = pubkey_t,
2493 .valid = valid,
23182494 } };
23192495 }
23202496 if (std.mem.startsWith(u8, pem_sig, "-----BEGIN SSH SIGNATURE-----\n") and std.mem.endsWith(u8, pem_sig, "\n -----END SSH SIGNATURE-----")) {
......@@ -2429,6 +2605,126 @@ pub const Signature = union(enum) {
24292605 }
24302606 return .unrecognized;
24312607 }
2608
2609 fn pgp_read_packet_len(r: anytype) !struct { bool, u32 } {
2610 const oct1: u32 = try r.readByte();
2611 if (oct1 <= 191) return .{ false, oct1 };
2612 if (oct1 >= 224 and oct1 < 255) return .{ true, @as(u32, 1) << @intCast(oct1 & 0x1F) };
2613 const oct2: u32 = try r.readByte();
2614 if (oct1 <= 223) return .{ false, ((oct1 - 192) << 8) + (oct2) + 192 };
2615 const oct3: u32 = try r.readByte();
2616 const oct4: u32 = try r.readByte();
2617 const oct5: u32 = try r.readByte();
2618 std.debug.assert(oct1 == 255);
2619 return .{ false, (oct2 << 24) | (oct3 << 16) | (oct4 << 8) | oct5 };
2620 }
2621
2622 fn pgp_read_subpacket_len(r: anytype) !u32 {
2623 const oct1: u32 = try r.readByte();
2624 if (oct1 < 192) return oct1;
2625 const oct2: u32 = try r.readByte();
2626 if (oct1 < 255) return ((oct1 - 192) << 8) + (oct2) + 192;
2627 const oct3: u32 = try r.readByte();
2628 const oct4: u32 = try r.readByte();
2629 const oct5: u32 = try r.readByte();
2630 return (oct2 << 24) | (oct3 << 16) | (oct4 << 8) | oct5;
2631 }
2632
2633 fn pgp_parse_pubkey(r: anytype, allocator: std.mem.Allocator) !Pgp.PubKey {
2634 const packet_type: packed struct { id: u6, format: u1, reserved: u1 } = @bitCast(try r.readByte());
2635 if (packet_type.reserved != 1) return error.unrecognized;
2636 if (packet_type.format != 1) return error.unrecognized; // legacy non-OpenPGP format
2637 if (packet_type.id != 6) return error.unrecognized; // not a public key
2638 _, const packet_len = pgp_read_packet_len(r) catch return error.unrecognized;
2639 var lr = nio.LimitedReader(void).from(r, packet_len);
2640 const key_version = try lr.readByte();
2641 if (key_version == 3) {
2642 // TODO
2643 }
2644 if (key_version == 4) {
2645 const creation_time = try lr.readArray(4);
2646 const pk_algo: Pgp.PublicKeyAlgorithm = @enumFromInt(try lr.readByte());
2647 const material = try pgp_parse_pubkey_material(r, allocator, pk_algo);
2648 return .{
2649 .version = key_version,
2650 .creation_time = .initUnix(std.mem.readInt(u32, &creation_time, .big)),
2651 .days_valid = 0,
2652 .material = material,
2653 };
2654 }
2655 if (key_version == 6) {
2656 // TODO
2657 }
2658 return .{
2659 .version = key_version,
2660 .creation_time = .initUnix(0),
2661 .days_valid = 0,
2662 .material = .reserved,
2663 };
2664 }
2665
2666 fn pgp_parse_pubkey_material(r: anytype, allocator: std.mem.Allocator, pk_algo: Pgp.PublicKeyAlgorithm) !Pgp.PubKey.Material {
2667 switch (pk_algo) {
2668 .rsa_encrypt_or_sign => {
2669 const n_len = try r.readInt(u16, .big);
2670 const n = try r.readAlloc(allocator, (n_len + 7) / 8);
2671 const e_len = try r.readInt(u16, .big);
2672 const e = try r.readAlloc(allocator, (e_len + 7) / 8);
2673 return .{ .rsa_encrypt_or_sign = .{ .n = n, .e = e } };
2674 },
2675 inline else => |t| return @unionInit(Pgp.PubKey.Material, @tagName(t), {}),
2676 _ => return .reserved,
2677 }
2678 }
2679
2680 fn pgp_read_signature_material(r: anytype, allocator: std.mem.Allocator, pk_algo: Pgp.PublicKeyAlgorithm) !Pgp.Material {
2681 switch (pk_algo) {
2682 .rsa_encrypt_or_sign => {
2683 const len = try r.readInt(u16, .big);
2684 const bytes = try r.readAlloc(allocator, (len + 7) / 8);
2685 return .{ .rsa_encrypt_or_sign = bytes };
2686 },
2687 inline else => |t| return @unionInit(Pgp.Material, @tagName(t), {}),
2688 _ => return .reserved,
2689 }
2690 }
2691
2692 // https://datatracker.ietf.org/doc/html/rfc9580#name-rsa
2693 // An implementation SHOULD NOT encrypt, sign, or verify using RSA keys of a size less than 3072 bits.
2694 // An implementation that decrypts a message using an RSA secret key of a size less than 3072 bits SHOULD generate a deprecation warning that the key is too weak for modern use.
2695 fn pgp_rsa_verify(modulus_len_r: usize, sig: []const u8, message: []const u8, pubkey: std.crypto.Certificate.rsa.PublicKey, hash_algorithm: Pgp.HashAlgorithm) !void {
2696 return switch (modulus_len_r) {
2697 inline 384, 512 => |modulus_len| pgp_rsa_verify_inner(modulus_len, sig, message, pubkey, hash_algorithm),
2698 else => error.unrecognized,
2699 };
2700 }
2701
2702 fn pgp_rsa_verify_inner(comptime modulus_len: usize, sig: []const u8, message: []const u8, pubkey: std.crypto.Certificate.rsa.PublicKey, hash_algorithm: Pgp.HashAlgorithm) !void {
2703 const ns = std.crypto.Certificate.rsa;
2704 const signature = ns.PKCS1v1_5Signature.fromBytes(modulus_len, sig);
2705 return switch (hash_algorithm) {
2706 .reserved => error.unrecognized,
2707 .md5 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.Md5),
2708 .sha1 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.Sha1),
2709 .ripemd160 => error.unrecognized,
2710 .sha2_224 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha224),
2711 .sha2_256 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha256),
2712 .sha2_384 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha384),
2713 .sha2_512 => ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha2.Sha512),
2714 .sha3_256 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha3.Sha3_256),
2715 .sha3_512 => error.unrecognized, //ns.PKCS1v1_5Signature.verify(modulus_len, signature, message, pubkey, std.crypto.hash.sha3.Sha3_512),
2716 _ => error.unrecognized,
2717 };
2718 }
2719
2720 // curl https://keys.openpgp.org/vks/v1/by-keyid/<KEYID> | head -n -2 | tail +4 | tr -d '\n' | base64 -d | xxd -p
2721 pub const known_pgp_keys: std.StaticStringMap([]const u8) = blk: {
2722 @setEvalBranchQuota(std.math.maxInt(u32));
2723 break :blk .initComptime(.{
2724 // GitHub <noreply@github.com>
2725 .{ "B5690EEEBB952194", &extras.from_hex("c6c14d0465a6c576011000b237ee2f88540ce904282abd00d96d2b19d3286d3270399b28d903e97b06f206cb8bdeb356e4e987ad6c170213ca36a508c8ecddabc3284b1a134eea8b92ca1a862c442ff5f69f284e07147d1a4633fe823d4779d870b45a150a1226855f3adeb5a2990b5336d601e8696e04fc10a967f09b0436ad3ed270e55908b487643da069aa960573b24c26559f0b7b2bf8320ee137ab30c99ee6a197e49290f9f94ed9d9756eac2ce44927e0e336f694c44b268b3678201cef8f440ac5f4b7b3817ed17e62a8232178b15ec9b646b52abd4e3b3a43ad65e021bc061e9db0b7a6bf4585ae4dc6805411afb5d4c2bd5ba39c461c2fc55094acc511e854d26720bffd5a574d3b6b4653aaa54e7018c6cfdf3a67f607aa3970a5f2b17bfb58003c6fe8f901504c1b1512bbb08b8256a20890b90c8ec0022515effb76f6b15991a9bffc96dbe2f782e2fbc5a1ca1551ac6b28658ad410e576be3f589280a4970f245e09b5a8b5fa807a6a4fc92236e4b1e1dd271723a3607474ab481685a23c007e0c5b3d3110e9a748b0a1c24287eb9cbacd6b750e7860c688e38ad1a615de38840c6028b9ab6435fdaaf39128b23d5ff65682a801f760b243edd7129ded620a12b9a914c1c8cbc8c9e483d0d6cd9d5305386a57c92c96b4a6f695da6ef00581409b09b3d166601716006040da9a7e2316829faac120722c51515137c39564ab04b01916cb0011010001") },
2726 });
2727 };
24322728};
24332729
24342730pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize {