authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-07-10 19:58:04 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-07-10 19:58:04 -07:00
logc1d5247b88cad7ae7b229aaf56c8257ce95c0bf7
tree0e453f7580c3d69a7d569308960d39adb23d2afe
parentd90af788fd4349337a89bb92f58e47fded6baabc
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

recognize 'gpgsign' and verify signed objects


1 files changed, 172 insertions(+), 1 deletions(-)

git.zig+172-1
......@@ -135,11 +135,14 @@ pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const
135135 .parents = undefined,
136136 .author = undefined,
137137 .committer = undefined,
138 .gpgsig = "",
138139 .message = undefined,
139140 };
140141 var parents = std.array_list.Managed(CommitId).init(alloc);
141142 errdefer parents.deinit();
143 var f_start: usize = 0;
142144 while (true) {
145 const line_start = iter.index.?;
143146 const line = iter.next() orelse break;
144147 if (line.len == 0) break;
145148 const space = std.mem.indexOfScalar(u8, line, ' ').?;
......@@ -149,6 +152,19 @@ pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const
149152 if (std.mem.eql(u8, k, "author")) result.author = try parseCommitUserAndAt(line[space + 1 ..]);
150153 if (std.mem.eql(u8, k, "committer")) result.committer = try parseCommitUserAndAt(line[space + 1 ..]);
151154 if (std.mem.eql(u8, k, "parent")) try parents.append(.{ .id = line[space + 1 ..][0..40] });
155 if (std.mem.eql(u8, k, "gpgsig")) {
156 f_start = line_start + 7;
157 _ = iter.next().?;
158 while (true) {
159 const line2_start = iter.index.?;
160 const line2 = iter.next() orelse break;
161 if (line2.len == 0 or line2[0] != ' ') {
162 iter.index = line2_start;
163 break;
164 }
165 }
166 result.gpgsig = commitfile[f_start .. iter.index.? - 1];
167 }
152168 }
153169 result.parents = try parents.toOwnedSlice();
154170 result.author.email = mailmap.get(result.author.email) orelse result.author.email;
......@@ -559,6 +575,7 @@ pub fn parseTag(tagfile: string) !Tag {
559575 .object = undefined,
560576 .type = undefined,
561577 .tagger = null,
578 .gpgsig = "",
562579 .message = undefined,
563580 };
564581 const object = extras.trimPrefixEnsure(iter.next().?, "object ").?;
......@@ -568,13 +585,27 @@ pub fn parseTag(tagfile: string) !Tag {
568585 result.type = std.meta.stringToEnum(RefType, ty).?;
569586 const tag = extras.trimPrefixEnsure(iter.next().?, "tag ").?;
570587 _ = tag;
571
588 var f_start: usize = 0;
572589 while (true) {
590 const line_start = iter.index.?;
573591 const line = iter.next() orelse break;
574592 if (line.len == 0) break;
575593 const space = std.mem.indexOfScalar(u8, line, ' ').?;
576594 const k = line[0..space];
577595 if (std.mem.eql(u8, k, "tagger")) result.tagger = try parseCommitUserAndAt(line[space + 1 ..]);
596 if (std.mem.eql(u8, k, "gpgsig")) {
597 f_start = line_start + 7;
598 _ = iter.next().?;
599 while (true) {
600 const line2_start = iter.index.?;
601 const line2 = iter.next() orelse break;
602 if (line2.len == 0 or line2[0] != ' ') {
603 iter.index = line2_start;
604 break;
605 }
606 }
607 result.gpgsig = tagfile[f_start .. iter.index.? - 1];
608 }
578609 }
579610 result.message = iter.rest();
580611 return result;
......@@ -2096,6 +2127,7 @@ pub const Commit = struct {
20962127 parents: []const CommitId,
20972128 author: UserAndAt,
20982129 committer: UserAndAt,
2130 gpgsig: []const u8,
20992131 message: string,
21002132
21012133 pub fn destroy(t: *Commit, r: *Repository) void {
......@@ -2103,6 +2135,10 @@ pub const Commit = struct {
21032135 r.gpa.free(t.raw);
21042136 r.gpa.destroy(t);
21052137 }
2138
2139 pub fn signature(t: *const Commit, allocator: std.mem.Allocator) !Signature {
2140 return .from(allocator, t.gpgsig, t.raw);
2141 }
21062142};
21072143
21082144pub const UserAndAt = struct {
......@@ -2116,12 +2152,17 @@ pub const Tag = struct {
21162152 object: Id,
21172153 type: RefType,
21182154 tagger: ?UserAndAt,
2155 gpgsig: []const u8,
21192156 message: string,
21202157
21212158 pub fn destroy(t: *Tag, r: *Repository) void {
21222159 r.gpa.free(t.raw);
21232160 r.gpa.destroy(t);
21242161 }
2162
2163 pub fn signature(t: *const Tag, allocator: std.mem.Allocator) !Signature {
2164 return .from(allocator, t.gpgsig, t.raw);
2165 }
21252166};
21262167
21272168pub const Ref = struct {
......@@ -2130,6 +2171,136 @@ pub const Ref = struct {
21302171 commit: ?Id,
21312172};
21322173
2174pub const Signature = union(enum) {
2175 none,
2176 unrecognized,
2177 ssh: Ssh,
2178
2179 pub const Ssh = struct {
2180 publickey: []const u8,
2181 hash_algorithm: []const u8,
2182 signature: []const u8,
2183 valid: ?bool,
2184 };
2185
2186 // https://www.ietf.org/archive/id/draft-josefsson-sshsig-format-03.html
2187 // https://datatracker.ietf.org/doc/html/rfc4251#section-5
2188 // https://pkg.go.dev/golang.org/x/crypto/ssh#pkg-constants
2189 pub fn from(allocator: std.mem.Allocator, pem_sig: []const u8, content_plus_gpgsig: []const u8) !Signature {
2190 if (pem_sig.len == 0) {
2191 return .none;
2192 }
2193 if (std.mem.startsWith(u8, pem_sig, "-----BEGIN SSH SIGNATURE-----\n") and std.mem.endsWith(u8, pem_sig, "\n -----END SSH SIGNATURE-----")) {
2194 const sigcontent = pem_sig[30 .. pem_sig.len - 29];
2195 var fixed: nio.FixedBufferStream([]const u8) = .init(sigcontent);
2196 var skip = nio.SkipReader(void).from(&fixed, "\n ");
2197 var b64r = nio.Base64Reader(void).from(&skip);
2198 if (!std.mem.eql(u8, &try b64r.readArray(6), "SSHSIG")) return .unrecognized;
2199 const sigversion = try b64r.readInt(u32, .big);
2200 if (sigversion != 1) return .unrecognized;
2201 const publickey = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2202 const namespace = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2203 const reserved = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2204 const hash_algorithm = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2205 const signature = try b64r.readAlloc(allocator, try b64r.readInt(u32, .big));
2206 if (!std.mem.eql(u8, namespace, "git")) return .unrecognized;
2207 if (reserved.len > 0) return .unrecognized;
2208
2209 var message = extras.ManyArrayList(u8).init(allocator);
2210 defer message.deinit();
2211 try message.appendSlice(try message.add(), content_plus_gpgsig);
2212 message.lengths.items.len = 0;
2213 {
2214 var iter = std.mem.splitScalar(u8, content_plus_gpgsig, '\n');
2215 while (iter.next()) |line| {
2216 try message.lengths.append(allocator, line.len + 1);
2217 }
2218 var skipping = false;
2219 var i: usize = 0;
2220 while (i < message.lengths.items.len) : (i += 1) {
2221 if (!skipping and std.mem.startsWith(u8, message.items(i), "gpgsig ")) {
2222 skipping = true;
2223 message.remove(i);
2224 i -= 1;
2225 continue;
2226 }
2227 if (!skipping) {
2228 continue;
2229 }
2230 if (skipping and std.mem.startsWith(u8, message.items(i), " ")) {
2231 message.remove(i);
2232 i -= 1;
2233 continue;
2234 }
2235 break;
2236 }
2237 }
2238
2239 var signed_data: nio.AllocatingWriter = .init(allocator);
2240 defer signed_data.deinit();
2241 try signed_data.writeAll("SSHSIG");
2242 try signed_data.writeInt(u32, @intCast(namespace.len), .big);
2243 try signed_data.writeAll(namespace);
2244 try signed_data.writeInt(u32, @intCast(reserved.len), .big);
2245 try signed_data.writeAll(reserved);
2246 try signed_data.writeInt(u32, @intCast(hash_algorithm.len), .big);
2247 try signed_data.writeAll(hash_algorithm);
2248 if (std.mem.eql(u8, hash_algorithm, "sha256")) {
2249 const H = std.crypto.hash.sha2.Sha256;
2250 try signed_data.writeInt(u32, H.digest_length, .big);
2251 try signed_data.writeAll(&extras.hashBytes(H, message.list.items));
2252 }
2253 if (std.mem.eql(u8, hash_algorithm, "sha512")) {
2254 const H = std.crypto.hash.sha2.Sha512;
2255 try signed_data.writeInt(u32, H.digest_length, .big);
2256 try signed_data.writeAll(&extras.hashBytes(H, message.list.items));
2257 }
2258
2259 var valid: ?bool = null;
2260 var sigfixed: nio.FixedBufferStream([]const u8) = .init(signature);
2261 const sigformat = try sigfixed.readAlloc(allocator, try sigfixed.readInt(u32, .big));
2262 defer allocator.free(sigformat);
2263 var pkfixed: nio.FixedBufferStream([]const u8) = .init(publickey);
2264 const pkformat = try pkfixed.readAlloc(allocator, try pkfixed.readInt(u32, .big));
2265 defer allocator.free(pkformat);
2266
2267 if (std.mem.eql(u8, sigformat, "ssh-rsa")) {
2268 // intentionally skipped
2269 }
2270 if (std.mem.eql(u8, sigformat, "ssh-ed25519")) blk: {
2271 const ed = std.crypto.sign.Ed25519;
2272 const pk_len = pkfixed.readInt(u32, .big) catch break :blk;
2273 if (pk_len != ed.PublicKey.encoded_length) break :blk;
2274 const pk_bytes = pkfixed.readArray(ed.PublicKey.encoded_length) catch break :blk;
2275 const pk = ed.PublicKey.fromBytes(pk_bytes) catch break :blk;
2276 const sig_len = sigfixed.readInt(u32, .big) catch break :blk;
2277 if (sig_len != ed.Signature.encoded_length) break :blk;
2278 const sig_bytes = sigfixed.readArray(ed.Signature.encoded_length) catch break :blk;
2279 const sig = ed.Signature.fromBytes(sig_bytes);
2280 valid = if (sig.verifyStrict(signed_data.items, pk)) true else |_| false;
2281 }
2282 // ssh-dss (dsa)
2283 // ecdsa-sha2-nistp256
2284 // sk-ecdsa-sha2-nistp256@openssh.com
2285 // ecdsa-sha2-nistp384
2286 // ecdsa-sha2-nistp521
2287 // sk-ssh-ed25519@openssh.com
2288 // rsa-sha2-256
2289 // rsa-sha2-512
2290
2291 return .{
2292 .ssh = .{
2293 .publickey = publickey,
2294 .hash_algorithm = hash_algorithm,
2295 .signature = signature,
2296 .valid = valid,
2297 },
2298 };
2299 }
2300 return .unrecognized;
2301 }
2302};
2303
21332304pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize {
21342305 const MaskInt = std.bit_set.DynamicBitSetUnmanaged.MaskInt;
21352306 if (after >= set.bit_length) return null;