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