| ... | @@ -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; |
| 571 | | 588 | 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, |
| 2100 | | 2132 | |
| 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 | }; |
| 2107 | | 2143 | |
| 2108 | pub const UserAndAt = struct { | 2144 | pub 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, |
| 2120 | | 2157 | |
| 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 | }; |
| 2126 | | 2167 | |
| 2127 | pub const Ref = struct { | 2168 | pub 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 | }; |
| 2132 | | 2173 | |
| | 2174 | pub 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 | |
| 2133 | pub fn findFirstUnset(set: std.bit_set.DynamicBitSetUnmanaged, after: usize) ?usize { | 2304 | pub 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; |