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...@@ -135,11 +135,14 @@ pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const
135 .parents = undefined,135 .parents = undefined,
136 .author = undefined,136 .author = undefined,
137 .committer = undefined,137 .committer = undefined,
138 .gpgsig = "",
138 .message = undefined,139 .message = undefined,
139 };140 };
140 var parents = std.array_list.Managed(CommitId).init(alloc);141 var parents = std.array_list.Managed(CommitId).init(alloc);
141 errdefer parents.deinit();142 errdefer parents.deinit();
143 var f_start: usize = 0;
142 while (true) {144 while (true) {
145 const line_start = iter.index.?;
143 const line = iter.next() orelse break;146 const line = iter.next() orelse break;
144 if (line.len == 0) break;147 if (line.len == 0) break;
145 const space = std.mem.indexOfScalar(u8, line, ' ').?;148 const space = std.mem.indexOfScalar(u8, line, ' ').?;
...@@ -149,6 +152,19 @@ pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const...@@ -149,6 +152,19 @@ pub fn parseCommit(alloc: std.mem.Allocator, commitfile: string, mailmap: *const
149 if (std.mem.eql(u8, k, "author")) result.author = try parseCommitUserAndAt(line[space + 1 ..]);152 if (std.mem.eql(u8, k, "author")) result.author = try parseCommitUserAndAt(line[space + 1 ..]);
150 if (std.mem.eql(u8, k, "committer")) result.committer = try parseCommitUserAndAt(line[space + 1 ..]);153 if (std.mem.eql(u8, k, "committer")) result.committer = try parseCommitUserAndAt(line[space + 1 ..]);
151 if (std.mem.eql(u8, k, "parent")) try parents.append(.{ .id = line[space + 1 ..][0..40] });154 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 }
152 }168 }
153 result.parents = try parents.toOwnedSlice();169 result.parents = try parents.toOwnedSlice();
154 result.author.email = mailmap.get(result.author.email) orelse result.author.email;170 result.author.email = mailmap.get(result.author.email) orelse result.author.email;
...@@ -559,6 +575,7 @@ pub fn parseTag(tagfile: string) !Tag {...@@ -559,6 +575,7 @@ pub fn parseTag(tagfile: string) !Tag {
559 .object = undefined,575 .object = undefined,
560 .type = undefined,576 .type = undefined,
561 .tagger = null,577 .tagger = null,
578 .gpgsig = "",
562 .message = undefined,579 .message = undefined,
563 };580 };
564 const object = extras.trimPrefixEnsure(iter.next().?, "object ").?;581 const object = extras.trimPrefixEnsure(iter.next().?, "object ").?;
...@@ -568,13 +585,27 @@ pub fn parseTag(tagfile: string) !Tag {...@@ -568,13 +585,27 @@ pub fn parseTag(tagfile: string) !Tag {
568 result.type = std.meta.stringToEnum(RefType, ty).?;585 result.type = std.meta.stringToEnum(RefType, ty).?;
569 const tag = extras.trimPrefixEnsure(iter.next().?, "tag ").?;586 const tag = extras.trimPrefixEnsure(iter.next().?, "tag ").?;
570 _ = tag;587 _ = tag;
571588 var f_start: usize = 0;
572 while (true) {589 while (true) {
590 const line_start = iter.index.?;
573 const line = iter.next() orelse break;591 const line = iter.next() orelse break;
574 if (line.len == 0) break;592 if (line.len == 0) break;
575 const space = std.mem.indexOfScalar(u8, line, ' ').?;593 const space = std.mem.indexOfScalar(u8, line, ' ').?;
576 const k = line[0..space];594 const k = line[0..space];
577 if (std.mem.eql(u8, k, "tagger")) result.tagger = try parseCommitUserAndAt(line[space + 1 ..]);595 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 }
578 }609 }
579 result.message = iter.rest();610 result.message = iter.rest();
580 return result;611 return result;
...@@ -2096,6 +2127,7 @@ pub const Commit = struct {...@@ -2096,6 +2127,7 @@ pub const Commit = struct {
2096 parents: []const CommitId,2127 parents: []const CommitId,
2097 author: UserAndAt,2128 author: UserAndAt,
2098 committer: UserAndAt,2129 committer: UserAndAt,
2130 gpgsig: []const u8,
2099 message: string,2131 message: string,
21002132
2101 pub fn destroy(t: *Commit, r: *Repository) void {2133 pub fn destroy(t: *Commit, r: *Repository) void {
...@@ -2103,6 +2135,10 @@ pub const Commit = struct {...@@ -2103,6 +2135,10 @@ pub const Commit = struct {
2103 r.gpa.free(t.raw);2135 r.gpa.free(t.raw);
2104 r.gpa.destroy(t);2136 r.gpa.destroy(t);
2105 }2137 }
2138
2139 pub fn signature(t: *const Commit, allocator: std.mem.Allocator) !Signature {
2140 return .from(allocator, t.gpgsig, t.raw);
2141 }
2106};2142};
21072143
2108pub const UserAndAt = struct {2144pub const UserAndAt = struct {
...@@ -2116,12 +2152,17 @@ pub const Tag = struct {...@@ -2116,12 +2152,17 @@ pub const Tag = struct {
2116 object: Id,2152 object: Id,
2117 type: RefType,2153 type: RefType,
2118 tagger: ?UserAndAt,2154 tagger: ?UserAndAt,
2155 gpgsig: []const u8,
2119 message: string,2156 message: string,
21202157
2121 pub fn destroy(t: *Tag, r: *Repository) void {2158 pub fn destroy(t: *Tag, r: *Repository) void {
2122 r.gpa.free(t.raw);2159 r.gpa.free(t.raw);
2123 r.gpa.destroy(t);2160 r.gpa.destroy(t);
2124 }2161 }
2162
2163 pub fn signature(t: *const Tag, allocator: std.mem.Allocator) !Signature {
2164 return .from(allocator, t.gpgsig, t.raw);
2165 }
2125};2166};
21262167
2127pub const Ref = struct {2168pub const Ref = struct {
...@@ -2130,6 +2171,136 @@ pub const Ref = struct {...@@ -2130,6 +2171,136 @@ pub const Ref = struct {
2130 commit: ?Id,2171 commit: ?Id,
2131};2172};
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
2133pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize {2304pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize {
2134 const MaskInt = std.bit_set.DynamicBitSetUnmanaged.MaskInt;2305 const MaskInt = std.bit_set.DynamicBitSetUnmanaged.MaskInt;
2135 if (after >= set.bit_length) return null;2306 if (after >= set.bit_length) return null;