| author | |
| committer | |
| log | 9233f771c736879cd42d07b1aa1cacf29e99ef64 |
| tree | 44c76fc620846636adf963830e204646386fb399 |
| parent | 194ac2fd5bc3580e12ededb3b51df3e3cfa7a9d6 |
24 files changed, 5099 insertions(+), 0 deletions(-)
.gitmodules+6| ... | @@ -7,3 +7,9 @@ | ... | @@ -7,3 +7,9 @@ |
| 7 | [submodule "libs/zig-ansi"] | 7 | [submodule "libs/zig-ansi"] |
| 8 | 	path = libs/zig-ansi | 8 | 	path = libs/zig-ansi |
| 9 | 	url = https://github.com/nektro/zig-ansi | 9 | 	url = https://github.com/nektro/zig-ansi |
| 10 | [submodule "libs/zuri"] | ||
| 11 | 	path = libs/zuri | ||
| 12 | 	url = https://github.com/Vexu/zuri | ||
| 13 | [submodule "libs/iguanatls"] | ||
| 14 | 	path = libs/iguanatls | ||
| 15 | 	url = https://github.com/alexnask/iguanaTLS |
build.zig+2| ... | @@ -38,6 +38,8 @@ pub fn build(b: *Builder) void { | ... | @@ -38,6 +38,8 @@ pub fn build(b: *Builder) void { |
| 38 | 38 | ||
| 39 | exe.addPackagePath("known-folders", "./libs/zig-known-folders/known-folders.zig"); | 39 | exe.addPackagePath("known-folders", "./libs/zig-known-folders/known-folders.zig"); |
| 40 | exe.addPackagePath("ansi", "./libs/zig-ansi/src/lib.zig"); | 40 | exe.addPackagePath("ansi", "./libs/zig-ansi/src/lib.zig"); |
| 41 | exe.addPackagePath("zuri", "./libs/zuri/src/zuri.zig"); | ||
| 42 | exe.addPackagePath("iguanatls", "./libs/iguanatls/src/main.zig"); | ||
| 41 | 43 | ||
| 42 | exe.install(); | 44 | exe.install(); |
| 43 | 45 |
libs/iguanatls/.gitattributes created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | *.zig text=auto eol=lf | ||
libs/iguanatls/.gitignore created+3| ... | @@ -0,0 +1,3 @@ | ||
| 1 | /zig-cache | ||
| 2 | deps.zig | ||
| 3 | gyro.lock | ||
libs/iguanatls/.gitmoduleslibs/iguanatls/LICENSE created+21| ... | @@ -0,0 +1,21 @@ | ||
| 1 | MIT License | ||
| 2 | |||
| 3 | Copyright (c) 2020 Alexandros Naskos | ||
| 4 | |||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | of this software and associated documentation files (the "Software"), to deal | ||
| 7 | in the Software without restriction, including without limitation the rights | ||
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | copies of the Software, and to permit persons to whom the Software is | ||
| 10 | furnished to do so, subject to the following conditions: | ||
| 11 | |||
| 12 | The above copyright notice and this permission notice shall be included in all | ||
| 13 | copies or substantial portions of the Software. | ||
| 14 | |||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | SOFTWARE. | ||
libs/iguanatls/build.zig created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | const Builder = @import("std").build.Builder; | ||
| 2 | |||
| 3 | pub fn build(b: *Builder) void { | ||
| 4 | const mode = b.standardReleaseOptions(); | ||
| 5 | const lib = b.addStaticLibrary("iguanaTLS", "src/main.zig"); | ||
| 6 | lib.setBuildMode(mode); | ||
| 7 | lib.install(); | ||
| 8 | |||
| 9 | var main_tests = b.addTest("src/main.zig"); | ||
| 10 | main_tests.setBuildMode(mode); | ||
| 11 | |||
| 12 | const test_step = b.step("test", "Run library tests"); | ||
| 13 | test_step.dependOn(&main_tests.step); | ||
| 14 | } | ||
libs/iguanatls/gyro.zzz created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | pkgs: | ||
| 2 | iguanaTLS: | ||
| 3 | version: 0.0.0 | ||
| 4 | author: alexnask | ||
| 5 | description: "Minimal, experimental TLS 1.2 implementation in Zig" | ||
| 6 | license: MIT | ||
| 7 | source_url: "https://github.com/alexnask/iguanaTLS" | ||
| 8 | |||
| 9 | files: | ||
| 10 | build.zig | ||
| 11 | README.md | ||
| 12 | LICENSE | ||
| 13 | src/*.zig | ||
| 14 | test/* | ||
libs/iguanatls/src/asn1.zig created+622| ... | @@ -0,0 +1,622 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const BigInt = std.math.big.int.Const; | ||
| 3 | const mem = std.mem; | ||
| 4 | const Allocator = mem.Allocator; | ||
| 5 | const ArenaAllocator = std.heap.ArenaAllocator; | ||
| 6 | |||
| 7 | // zig fmt: off | ||
| 8 | pub const Tag = enum(u8) { | ||
| 9 | bool = 0x01, | ||
| 10 | int = 0x02, | ||
| 11 | bit_string = 0x03, | ||
| 12 | octet_string = 0x04, | ||
| 13 | @"null" = 0x05, | ||
| 14 | object_identifier = 0x06, | ||
| 15 | utf8_string = 0x0c, | ||
| 16 | printable_string = 0x13, | ||
| 17 | ia5_string = 0x16, | ||
| 18 | utc_time = 0x17, | ||
| 19 | bmp_string = 0x1e, | ||
| 20 | sequence = 0x30, | ||
| 21 | set = 0x31, | ||
| 22 | // Bogus value | ||
| 23 | context_specific = 0xff, | ||
| 24 | }; | ||
| 25 | // zig fmt: on | ||
| 26 | |||
| 27 | pub const ObjectIdentifier = struct { | ||
| 28 | data: [16]u32, | ||
| 29 | len: u8, | ||
| 30 | }; | ||
| 31 | |||
| 32 | pub const BitString = struct { | ||
| 33 | data: []const u8, | ||
| 34 | bit_len: usize, | ||
| 35 | }; | ||
| 36 | |||
| 37 | pub const Value = union(Tag) { | ||
| 38 | bool: bool, | ||
| 39 | int: BigInt, | ||
| 40 | bit_string: BitString, | ||
| 41 | octet_string: []const u8, | ||
| 42 | @"null", | ||
| 43 | // @TODO Make this []u32, owned? | ||
| 44 | object_identifier: ObjectIdentifier, | ||
| 45 | utf8_string: []const u8, | ||
| 46 | printable_string: []const u8, | ||
| 47 | ia5_string: []const u8, | ||
| 48 | utc_time: []const u8, | ||
| 49 | bmp_string: []const u16, | ||
| 50 | sequence: []const @This(), | ||
| 51 | set: []const @This(), | ||
| 52 | context_specific: struct { | ||
| 53 | child: *const Value, | ||
| 54 | number: u8, | ||
| 55 | }, | ||
| 56 | |||
| 57 | pub fn deinit(self: @This(), alloc: *Allocator) void { | ||
| 58 | switch (self) { | ||
| 59 | .int => |i| alloc.free(i.limbs), | ||
| 60 | .bit_string => |bs| alloc.free(bs.data), | ||
| 61 | .octet_string, | ||
| 62 | .utf8_string, | ||
| 63 | .printable_string, | ||
| 64 | .ia5_string, | ||
| 65 | .utc_time, | ||
| 66 | => |s| alloc.free(s), | ||
| 67 | .bmp_string => |s| alloc.free(s), | ||
| 68 | .sequence, .set => |s| { | ||
| 69 | for (s) |c| { | ||
| 70 | c.deinit(alloc); | ||
| 71 | } | ||
| 72 | alloc.free(s); | ||
| 73 | }, | ||
| 74 | .context_specific => |cs| { | ||
| 75 | cs.child.deinit(alloc); | ||
| 76 | alloc.destroy(cs.child); | ||
| 77 | }, | ||
| 78 | else => {}, | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | fn formatInternal( | ||
| 83 | self: Value, | ||
| 84 | comptime fmt: []const u8, | ||
| 85 | options: std.fmt.FormatOptions, | ||
| 86 | indents: usize, | ||
| 87 | writer: anytype, | ||
| 88 | ) @TypeOf(writer).Error!void { | ||
| 89 | try writer.writeByteNTimes(' ', indents); | ||
| 90 | switch (self) { | ||
| 91 | .bool => |b| try writer.print("BOOLEAN {}\n", .{b}), | ||
| 92 | .int => |i| { | ||
| 93 | try writer.writeAll("INTEGER "); | ||
| 94 | try i.format(fmt, options, writer); | ||
| 95 | try writer.writeByte('\n'); | ||
| 96 | }, | ||
| 97 | .bit_string => |bs| { | ||
| 98 | try writer.print("BIT STRING ({} bits) ", .{bs.bit_len}); | ||
| 99 | const bits_to_show = std.math.min(8 * 3, bs.bit_len); | ||
| 100 | const bytes = std.math.divCeil(usize, bits_to_show, 8) catch unreachable; | ||
| 101 | |||
| 102 | var bit_idx: usize = 0; | ||
| 103 | var byte_idx: usize = 0; | ||
| 104 | while (byte_idx < bytes) : (byte_idx += 1) { | ||
| 105 | const byte = bs.data[byte_idx]; | ||
| 106 | var cur_bit_idx: u3 = 0; | ||
| 107 | while (bit_idx < bits_to_show) { | ||
| 108 | const mask = @as(u8, 0x80) >> cur_bit_idx; | ||
| 109 | try writer.print("{}", .{@boolToInt(byte & mask == mask)}); | ||
| 110 | cur_bit_idx += 1; | ||
| 111 | bit_idx += 1; | ||
| 112 | if (cur_bit_idx == 7) | ||
| 113 | break; | ||
| 114 | } | ||
| 115 | } | ||
| 116 | if (bits_to_show != bs.bit_len) | ||
| 117 | try writer.writeAll("..."); | ||
| 118 | try writer.writeByte('\n'); | ||
| 119 | }, | ||
| 120 | .octet_string => |s| try writer.print("OCTET STRING ({} bytes) {X}\n", .{ s.len, s }), | ||
| 121 | .@"null" => try writer.writeAll("NULL\n"), | ||
| 122 | .object_identifier => |oid| { | ||
| 123 | try writer.writeAll("OBJECT IDENTIFIER "); | ||
| 124 | var i: u8 = 0; | ||
| 125 | while (i < oid.len) : (i += 1) { | ||
| 126 | if (i != 0) try writer.writeByte('.'); | ||
| 127 | try writer.print("{}", .{oid.data[i]}); | ||
| 128 | } | ||
| 129 | try writer.writeByte('\n'); | ||
| 130 | }, | ||
| 131 | .utf8_string => |s| try writer.print("UTF8 STRING ({} bytes) {}\n", .{ s.len, s }), | ||
| 132 | .printable_string => |s| try writer.print("PRINTABLE STRING ({} bytes) {}\n", .{ s.len, s }), | ||
| 133 | .ia5_string => |s| try writer.print("IA5 STRING ({} bytes) {}\n", .{ s.len, s }), | ||
| 134 | .utc_time => |s| try writer.print("UTC TIME {}\n", .{s}), | ||
| 135 | .bmp_string => |s| try writer.print("BMP STRING ({} words) {}\n", .{ | ||
| 136 | s.len, | ||
| 137 | @ptrCast([*]const u16, s.ptr)[0 .. s.len * 2], | ||
| 138 | }), | ||
| 139 | .sequence => |children| { | ||
| 140 | try writer.print("SEQUENCE ({} elems)\n", .{children.len}); | ||
| 141 | for (children) |child| try child.formatInternal(fmt, options, indents + 2, writer); | ||
| 142 | }, | ||
| 143 | .set => |children| { | ||
| 144 | try writer.print("SET ({} elems)\n", .{children.len}); | ||
| 145 | for (children) |child| try child.formatInternal(fmt, options, indents + 2, writer); | ||
| 146 | }, | ||
| 147 | .context_specific => |cs| { | ||
| 148 | try writer.print("[{}]\n", .{cs.number}); | ||
| 149 | try cs.child.formatInternal(fmt, options, indents + 2, writer); | ||
| 150 | }, | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | pub fn format(self: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { | ||
| 155 | try self.formatInternal(fmt, options, 0, writer); | ||
| 156 | } | ||
| 157 | }; | ||
| 158 | |||
| 159 | /// Distinguished encoding rules | ||
| 160 | pub const der = struct { | ||
| 161 | pub fn DecodeError(comptime Reader: type) type { | ||
| 162 | return Reader.Error || error{ | ||
| 163 | OutOfMemory, | ||
| 164 | EndOfStream, | ||
| 165 | InvalidLength, | ||
| 166 | InvalidTag, | ||
| 167 | InvalidContainerLength, | ||
| 168 | DoesNotMatchSchema, | ||
| 169 | }; | ||
| 170 | } | ||
| 171 | |||
| 172 | fn DERReaderState(comptime Reader: type) type { | ||
| 173 | return struct { | ||
| 174 | der_reader: Reader, | ||
| 175 | length: usize, | ||
| 176 | idx: usize = 0, | ||
| 177 | }; | ||
| 178 | } | ||
| 179 | |||
| 180 | fn DERReader(comptime Reader: type) type { | ||
| 181 | const S = struct { | ||
| 182 | pub fn read(state: *DERReaderState(Reader), buffer: []u8) DecodeError(Reader)!usize { | ||
| 183 | const out_bytes = std.math.min(buffer.len, state.length - state.idx); | ||
| 184 | const res = try state.der_reader.readAll(buffer[0..out_bytes]); | ||
| 185 | state.idx += res; | ||
| 186 | return res; | ||
| 187 | } | ||
| 188 | }; | ||
| 189 | |||
| 190 | return std.io.Reader(*DERReaderState(Reader), DecodeError(Reader), S.read); | ||
| 191 | } | ||
| 192 | |||
| 193 | pub fn parse_schema( | ||
| 194 | schema: anytype, | ||
| 195 | captures: anytype, | ||
| 196 | der_reader: anytype, | ||
| 197 | ) !void { | ||
| 198 | const res = try parse_schema_tag_len_internal(null, null, schema, captures, der_reader); | ||
| 199 | if (res != null) return error.DoesNotMatchSchema; | ||
| 200 | } | ||
| 201 | |||
| 202 | pub fn parse_schema_tag_len( | ||
| 203 | existing_tag_byte: ?u8, | ||
| 204 | existing_length: ?usize, | ||
| 205 | schema: anytype, | ||
| 206 | captures: anytype, | ||
| 207 | der_reader: anytype, | ||
| 208 | ) !void { | ||
| 209 | const res = try parse_schema_tag_len_internal( | ||
| 210 | existing_tag_byte, | ||
| 211 | existing_length, | ||
| 212 | schema, | ||
| 213 | captures, | ||
| 214 | der_reader, | ||
| 215 | ); | ||
| 216 | if (res != null) return error.DoesNotMatchSchema; | ||
| 217 | } | ||
| 218 | |||
| 219 | const TagLength = struct { | ||
| 220 | tag: u8, | ||
| 221 | length: usize, | ||
| 222 | }; | ||
| 223 | |||
| 224 | pub fn parse_schema_tag_len_internal( | ||
| 225 | existing_tag_byte: ?u8, | ||
| 226 | existing_length: ?usize, | ||
| 227 | schema: anytype, | ||
| 228 | captures: anytype, | ||
| 229 | der_reader: anytype, | ||
| 230 | ) !?TagLength { | ||
| 231 | const Reader = @TypeOf(der_reader); | ||
| 232 | |||
| 233 | const isEnumLit = comptime std.meta.trait.is(.EnumLiteral); | ||
| 234 | comptime var tag_idx = 0; | ||
| 235 | |||
| 236 | const has_capture = comptime isEnumLit(@TypeOf(schema[tag_idx])) and schema[tag_idx] == .capture; | ||
| 237 | if (has_capture) tag_idx += 2; | ||
| 238 | |||
| 239 | const is_optional = comptime isEnumLit(@TypeOf(schema[tag_idx])) and schema[tag_idx] == .optional; | ||
| 240 | if (is_optional) tag_idx += 1; | ||
| 241 | |||
| 242 | const tag_literal = schema[tag_idx]; | ||
| 243 | comptime std.debug.assert(isEnumLit(@TypeOf(tag_literal))); | ||
| 244 | |||
| 245 | const tag_byte = existing_tag_byte orelse (der_reader.readByte() catch |err| switch (err) { | ||
| 246 | error.EndOfStream => |e| return if (is_optional) null else error.EndOfStream, | ||
| 247 | else => |e| return e, | ||
| 248 | }); | ||
| 249 | |||
| 250 | const length = existing_length orelse try parse_length(der_reader); | ||
| 251 | if (tag_literal == .sequence_of) { | ||
| 252 | if (tag_byte != @enumToInt(Tag.sequence)) { | ||
| 253 | if (is_optional) return TagLength{ .tag = tag_byte, .length = length }; | ||
| 254 | return error.InvalidTag; | ||
| 255 | } | ||
| 256 | |||
| 257 | var curr_tag_length: ?TagLength = null; | ||
| 258 | const sub_schema = schema[tag_idx + 1]; | ||
| 259 | while (true) { | ||
| 260 | if (curr_tag_length == null) { | ||
| 261 | curr_tag_length = .{ | ||
| 262 | .tag = der_reader.readByte() catch |err| switch (err) { | ||
| 263 | error.EndOfStream => { | ||
| 264 | curr_tag_length = null; | ||
| 265 | break; | ||
| 266 | }, | ||
| 267 | else => |e| return e, | ||
| 268 | }, | ||
| 269 | .length = try parse_length(der_reader), | ||
| 270 | }; | ||
| 271 | } | ||
| 272 | |||
| 273 | curr_tag_length = parse_schema_tag_len_internal( | ||
| 274 | curr_tag_length.?.tag, | ||
| 275 | curr_tag_length.?.length, | ||
| 276 | sub_schema, | ||
| 277 | captures, | ||
| 278 | der_reader, | ||
| 279 | ) catch |err| switch (err) { | ||
| 280 | error.DoesNotMatchSchema => break, | ||
| 281 | else => |e| return e, | ||
| 282 | }; | ||
| 283 | } | ||
| 284 | return curr_tag_length; | ||
| 285 | } else if (tag_literal == .any) { | ||
| 286 | if (!has_capture) { | ||
| 287 | try der_reader.skipBytes(length, .{}); | ||
| 288 | return null; | ||
| 289 | } | ||
| 290 | |||
| 291 | var reader_state = DERReaderState(Reader){ | ||
| 292 | .der_reader = der_reader, | ||
| 293 | .idx = 0, | ||
| 294 | .length = length, | ||
| 295 | }; | ||
| 296 | var reader = DERReader(@TypeOf(der_reader)){ .context = &reader_state }; | ||
| 297 | const capture_context = captures[schema[1] * 2]; | ||
| 298 | const capture_action = captures[schema[1] * 2 + 1]; | ||
| 299 | try capture_action(capture_context, tag_byte, length, reader); | ||
| 300 | |||
| 301 | // Skip remaining bytes | ||
| 302 | try der_reader.skipBytes(reader_state.length - reader_state.idx, .{}); | ||
| 303 | return null; | ||
| 304 | } else if (tag_literal == .context_specific) { | ||
| 305 | const cs_number = schema[tag_idx + 1]; | ||
| 306 | if (tag_byte & 0xC0 == 0x80 and tag_byte - 0xa0 == cs_number) { | ||
| 307 | if (!has_capture) { | ||
| 308 | if (schema.len > tag_idx + 2) { | ||
| 309 | return try parse_schema_tag_len_internal(null, null, schema[tag_idx + 2], captures, der_reader); | ||
| 310 | } | ||
| 311 | |||
| 312 | try der_reader.skipBytes(length, .{}); | ||
| 313 | return null; | ||
| 314 | } | ||
| 315 | |||
| 316 | var reader_state = DERReaderState(Reader){ | ||
| 317 | .der_reader = der_reader, | ||
| 318 | .idx = 0, | ||
| 319 | .length = length, | ||
| 320 | }; | ||
| 321 | var reader = DERReader(Reader){ .context = &reader_state }; | ||
| 322 | const capture_context = captures[schema[1] * 2]; | ||
| 323 | const capture_action = captures[schema[1] * 2 + 1]; | ||
| 324 | try capture_action(capture_context, tag_byte, length, reader); | ||
| 325 | |||
| 326 | // Skip remaining bytes | ||
| 327 | try der_reader.skipBytes(reader_state.length - reader_state.idx, .{}); | ||
| 328 | return null; | ||
| 329 | } else if (is_optional) | ||
| 330 | return TagLength{ .tag = tag_byte, .length = length } | ||
| 331 | else | ||
| 332 | return error.DoesNotMatchSchema; | ||
| 333 | } | ||
| 334 | |||
| 335 | const schema_tag: Tag = tag_literal; | ||
| 336 | const actual_tag = std.meta.intToEnum(Tag, tag_byte) catch return error.InvalidTag; | ||
| 337 | if (actual_tag != schema_tag) { | ||
| 338 | if (is_optional) return tag_byte; | ||
| 339 | return error.DoesNotMatchSchema; | ||
| 340 | } | ||
| 341 | |||
| 342 | const single_seq = schema_tag == .sequence and schema.len == 1; | ||
| 343 | if ((!has_capture and schema_tag != .sequence) or (!has_capture and single_seq)) { | ||
| 344 | try der_reader.skipBytes(length, .{}); | ||
| 345 | return null; | ||
| 346 | } | ||
| 347 | |||
| 348 | if (has_capture) { | ||
| 349 | var reader_state = DERReaderState(Reader){ | ||
| 350 | .der_reader = der_reader, | ||
| 351 | .idx = 0, | ||
| 352 | .length = length, | ||
| 353 | }; | ||
| 354 | var reader = DERReader(Reader){ .context = &reader_state }; | ||
| 355 | const capture_context = captures[schema[1] * 2]; | ||
| 356 | const capture_action = captures[schema[1] * 2 + 1]; | ||
| 357 | try capture_action(capture_context, tag_byte, length, reader); | ||
| 358 | |||
| 359 | // Skip remaining bytes | ||
| 360 | try der_reader.skipBytes(reader_state.length - reader_state.idx, .{}); | ||
| 361 | return null; | ||
| 362 | } | ||
| 363 | |||
| 364 | var cur_tag_length: ?TagLength = null; | ||
| 365 | const sub_schemas = schema[tag_idx + 1]; | ||
| 366 | comptime var i = 0; | ||
| 367 | inline while (i < sub_schemas.len) : (i += 1) { | ||
| 368 | const curr_tag = if (cur_tag_length) |tl| tl.tag else null; | ||
| 369 | const curr_length = if (cur_tag_length) |tl| tl.length else null; | ||
| 370 | cur_tag_length = try parse_schema_tag_len_internal(curr_tag, curr_length, sub_schemas[i], captures, der_reader); | ||
| 371 | } | ||
| 372 | return cur_tag_length; | ||
| 373 | } | ||
| 374 | |||
| 375 | pub const EncodedLength = struct { | ||
| 376 | data: [@sizeOf(usize) + 1]u8, | ||
| 377 | len: usize, | ||
| 378 | |||
| 379 | pub fn slice(self: @This()) []const u8 { | ||
| 380 | if (self.len == 1) return self.data[0..1]; | ||
| 381 | return self.data[0 .. 1 + self.len]; | ||
| 382 | } | ||
| 383 | }; | ||
| 384 | |||
| 385 | pub fn encode_length(length: usize) EncodedLength { | ||
| 386 | var enc = EncodedLength{ .data = undefined, .len = 0 }; | ||
| 387 | if (length < 128) { | ||
| 388 | enc.data[0] = @truncate(u8, length); | ||
| 389 | enc.len = 1; | ||
| 390 | } else { | ||
| 391 | const bytes_needed = @intCast(u8, std.math.divCeil( | ||
| 392 | usize, | ||
| 393 | std.math.log2_int_ceil(usize, length), | ||
| 394 | 8, | ||
| 395 | ) catch unreachable); | ||
| 396 | enc.data[0] = bytes_needed | 0x80; | ||
| 397 | mem.copy( | ||
| 398 | u8, | ||
| 399 | enc.data[1 .. bytes_needed + 1], | ||
| 400 | mem.asBytes(&length)[0..bytes_needed], | ||
| 401 | ); | ||
| 402 | if (std.builtin.endian != .Big) { | ||
| 403 | mem.reverse(u8, enc.data[1 .. bytes_needed + 1]); | ||
| 404 | } | ||
| 405 | enc.len = bytes_needed; | ||
| 406 | } | ||
| 407 | return enc; | ||
| 408 | } | ||
| 409 | |||
| 410 | fn parse_int_internal(alloc: *Allocator, bytes_read: *usize, der_reader: anytype) !BigInt { | ||
| 411 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 412 | const first_byte = try der_reader.readByte(); | ||
| 413 | if (first_byte == 0x0 and length > 1) { | ||
| 414 | // Positive number with highest bit set to 1 in the rest. | ||
| 415 | const limb_count = std.math.divCeil(usize, length - 1, @sizeOf(usize)) catch unreachable; | ||
| 416 | const limbs = try alloc.alloc(usize, limb_count); | ||
| 417 | std.mem.set(usize, limbs, 0); | ||
| 418 | errdefer alloc.free(limbs); | ||
| 419 | |||
| 420 | var limb_ptr = @ptrCast([*]u8, limbs.ptr); | ||
| 421 | try der_reader.readNoEof(limb_ptr[0 .. length - 1]); | ||
| 422 | // We always reverse because the standard library big int expects little endian. | ||
| 423 | mem.reverse(u8, limb_ptr[0 .. length - 1]); | ||
| 424 | |||
| 425 | bytes_read.* += length; | ||
| 426 | return BigInt{ .limbs = limbs, .positive = true }; | ||
| 427 | } | ||
| 428 | std.debug.assert(length != 0); | ||
| 429 | // Write first_byte | ||
| 430 | // Twos complement | ||
| 431 | const limb_count = std.math.divCeil(usize, length, @sizeOf(usize)) catch unreachable; | ||
| 432 | const limbs = try alloc.alloc(usize, limb_count); | ||
| 433 | std.mem.set(usize, limbs, 0); | ||
| 434 | errdefer alloc.free(limbs); | ||
| 435 | |||
| 436 | var limb_ptr = @ptrCast([*]u8, limbs.ptr); | ||
| 437 | limb_ptr[0] = first_byte & ~@as(u8, 0x80); | ||
| 438 | try der_reader.readNoEof(limb_ptr[1..length]); | ||
| 439 | |||
| 440 | // We always reverse because the standard library big int expects little endian. | ||
| 441 | mem.reverse(u8, limb_ptr[0..length]); | ||
| 442 | bytes_read.* += length; | ||
| 443 | return BigInt{ .limbs = limbs, .positive = (first_byte & 0x80) == 0x00 }; | ||
| 444 | } | ||
| 445 | |||
| 446 | pub fn parse_int(alloc: *Allocator, der_reader: anytype) !BigInt { | ||
| 447 | var bytes: usize = undefined; | ||
| 448 | return try parse_int_internal(alloc, &bytes, der_reader); | ||
| 449 | } | ||
| 450 | |||
| 451 | pub fn parse_length(der_reader: anytype) !usize { | ||
| 452 | var bytes: usize = 0; | ||
| 453 | return try parse_length_internal(&bytes, der_reader); | ||
| 454 | } | ||
| 455 | |||
| 456 | fn parse_length_internal(bytes_read: *usize, der_reader: anytype) !usize { | ||
| 457 | const first_byte = try der_reader.readByte(); | ||
| 458 | bytes_read.* += 1; | ||
| 459 | if (first_byte & 0x80 == 0x00) { | ||
| 460 | // 1 byte value | ||
| 461 | return first_byte; | ||
| 462 | } | ||
| 463 | const length = @truncate(u7, first_byte); | ||
| 464 | if (length > @sizeOf(usize)) | ||
| 465 | @panic("DER length does not fit in usize"); | ||
| 466 | |||
| 467 | var res_buf = std.mem.zeroes([@sizeOf(usize)]u8); | ||
| 468 | try der_reader.readNoEof(res_buf[0..length]); | ||
| 469 | bytes_read.* += length; | ||
| 470 | |||
| 471 | if (std.builtin.endian != .Big) { | ||
| 472 | mem.reverse(u8, res_buf[0..length]); | ||
| 473 | } | ||
| 474 | return mem.bytesToValue(usize, &res_buf); | ||
| 475 | } | ||
| 476 | |||
| 477 | fn parse_value_with_tag_byte( | ||
| 478 | tag_byte: u8, | ||
| 479 | alloc: *Allocator, | ||
| 480 | bytes_read: *usize, | ||
| 481 | der_reader: anytype, | ||
| 482 | ) DecodeError(@TypeOf(der_reader))!Value { | ||
| 483 | const tag = std.meta.intToEnum(Tag, tag_byte) catch { | ||
| 484 | // tag starts with '0b10...', this is the context specific class. | ||
| 485 | if (tag_byte & 0xC0 == 0x80) { | ||
| 486 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 487 | var cur_read_bytes: usize = 0; | ||
| 488 | var child = try alloc.create(Value); | ||
| 489 | errdefer alloc.destroy(child); | ||
| 490 | |||
| 491 | child.* = try parse_value_internal(alloc, &cur_read_bytes, der_reader); | ||
| 492 | if (cur_read_bytes != length) | ||
| 493 | return error.InvalidContainerLength; | ||
| 494 | bytes_read.* += length; | ||
| 495 | return Value{ .context_specific = .{ .child = child, .number = tag_byte - 0xa0 } }; | ||
| 496 | } | ||
| 497 | |||
| 498 | return error.InvalidTag; | ||
| 499 | }; | ||
| 500 | switch (tag) { | ||
| 501 | .bool => { | ||
| 502 | if ((try der_reader.readByte()) != 0x1) | ||
| 503 | return error.InvalidLength; | ||
| 504 | defer bytes_read.* += 2; | ||
| 505 | return Value{ .bool = (try der_reader.readByte()) != 0x0 }; | ||
| 506 | }, | ||
| 507 | .int => return Value{ .int = try parse_int_internal(alloc, bytes_read, der_reader) }, | ||
| 508 | .bit_string => { | ||
| 509 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 510 | const unused_bits = try der_reader.readByte(); | ||
| 511 | std.debug.assert(unused_bits < 8); | ||
| 512 | const bit_count = (length - 1) * 8 - unused_bits; | ||
| 513 | const bit_memory = try alloc.alloc(u8, std.math.divCeil(usize, bit_count, 8) catch unreachable); | ||
| 514 | errdefer alloc.free(bit_memory); | ||
| 515 | try der_reader.readNoEof(bit_memory[0 .. length - 1]); | ||
| 516 | |||
| 517 | bytes_read.* += length; | ||
| 518 | return Value{ .bit_string = .{ .data = bit_memory, .bit_len = bit_count } }; | ||
| 519 | }, | ||
| 520 | .octet_string, .utf8_string, .printable_string, .utc_time, .ia5_string => { | ||
| 521 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 522 | const str_mem = try alloc.alloc(u8, length); | ||
| 523 | try der_reader.readNoEof(str_mem); | ||
| 524 | bytes_read.* += length; | ||
| 525 | return @as(Value, switch (tag) { | ||
| 526 | .octet_string => .{ .octet_string = str_mem }, | ||
| 527 | .utf8_string => .{ .utf8_string = str_mem }, | ||
| 528 | .printable_string => .{ .printable_string = str_mem }, | ||
| 529 | .utc_time => .{ .utc_time = str_mem }, | ||
| 530 | .ia5_string => .{ .ia5_string = str_mem }, | ||
| 531 | else => unreachable, | ||
| 532 | }); | ||
| 533 | }, | ||
| 534 | .@"null" => { | ||
| 535 | std.debug.assert((try parse_length_internal(bytes_read, der_reader)) == 0x00); | ||
| 536 | return .@"null"; | ||
| 537 | }, | ||
| 538 | .object_identifier => { | ||
| 539 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 540 | const first_byte = try der_reader.readByte(); | ||
| 541 | var ret = Value{ .object_identifier = .{ .data = undefined, .len = 0 } }; | ||
| 542 | ret.object_identifier.data[0] = first_byte / 40; | ||
| 543 | ret.object_identifier.data[1] = first_byte % 40; | ||
| 544 | |||
| 545 | var out_idx: u8 = 2; | ||
| 546 | var i: usize = 0; | ||
| 547 | while (i < length - 1) { | ||
| 548 | var current_value: u32 = 0; | ||
| 549 | var current_byte = try der_reader.readByte(); | ||
| 550 | i += 1; | ||
| 551 | while (current_byte & 0x80 == 0x80) : (i += 1) { | ||
| 552 | // Increase the base of the previous bytes | ||
| 553 | current_value *= 128; | ||
| 554 | // Add the current byte in base 128 | ||
| 555 | current_value += @as(u32, current_byte & ~@as(u8, 0x80)) * 128; | ||
| 556 | current_byte = try der_reader.readByte(); | ||
| 557 | } else { | ||
| 558 | current_value += current_byte; | ||
| 559 | } | ||
| 560 | ret.object_identifier.data[out_idx] = current_value; | ||
| 561 | out_idx += 1; | ||
| 562 | } | ||
| 563 | ret.object_identifier.len = out_idx; | ||
| 564 | std.debug.assert(out_idx <= 16); | ||
| 565 | bytes_read.* += length; | ||
| 566 | return ret; | ||
| 567 | }, | ||
| 568 | .bmp_string => { | ||
| 569 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 570 | const str_mem = try alloc.alloc(u16, @divExact(length, 2)); | ||
| 571 | errdefer alloc.free(str_mem); | ||
| 572 | |||
| 573 | for (str_mem) |*wide_char| { | ||
| 574 | wide_char.* = try der_reader.readIntBig(u16); | ||
| 575 | } | ||
| 576 | bytes_read.* += length; | ||
| 577 | return Value{ .bmp_string = str_mem }; | ||
| 578 | }, | ||
| 579 | .sequence, .set => { | ||
| 580 | const length = try parse_length_internal(bytes_read, der_reader); | ||
| 581 | var cur_read_bytes: usize = 0; | ||
| 582 | var arr = std.ArrayList(Value).init(alloc); | ||
| 583 | errdefer arr.deinit(); | ||
| 584 | |||
| 585 | while (cur_read_bytes < length) { | ||
| 586 | (try arr.addOne()).* = try parse_value_internal(alloc, &cur_read_bytes, der_reader); | ||
| 587 | } | ||
| 588 | if (cur_read_bytes != length) | ||
| 589 | return error.InvalidContainerLength; | ||
| 590 | bytes_read.* += length; | ||
| 591 | |||
| 592 | return @as(Value, switch (tag) { | ||
| 593 | .sequence => .{ .sequence = arr.toOwnedSlice() }, | ||
| 594 | .set => .{ .set = arr.toOwnedSlice() }, | ||
| 595 | else => unreachable, | ||
| 596 | }); | ||
| 597 | }, | ||
| 598 | .context_specific => unreachable, | ||
| 599 | } | ||
| 600 | } | ||
| 601 | |||
| 602 | fn parse_value_internal(alloc: *Allocator, bytes_read: *usize, der_reader: anytype) DecodeError(@TypeOf(der_reader))!Value { | ||
| 603 | const tag_byte = try der_reader.readByte(); | ||
| 604 | bytes_read.* += 1; | ||
| 605 | return try parse_value_with_tag_byte(tag_byte, alloc, bytes_read, der_reader); | ||
| 606 | } | ||
| 607 | |||
| 608 | pub fn parse_value(alloc: *Allocator, der_reader: anytype) DecodeError(@TypeOf(der_reader))!Value { | ||
| 609 | var read: usize = 0; | ||
| 610 | return try parse_value_internal(alloc, &read, der_reader); | ||
| 611 | } | ||
| 612 | }; | ||
| 613 | |||
| 614 | test "der.parse_value" { | ||
| 615 | const github_der = @embedFile("../test/github.der"); | ||
| 616 | var fbs = std.io.fixedBufferStream(github_der); | ||
| 617 | |||
| 618 | var arena = ArenaAllocator.init(std.testing.allocator); | ||
| 619 | defer arena.deinit(); | ||
| 620 | |||
| 621 | _ = try der.parse_value(&arena.allocator, fbs.reader()); | ||
| 622 | } | ||
libs/iguanatls/src/ciphersuites.zig created+534| ... | @@ -0,0 +1,534 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const mem = std.mem; | ||
| 3 | |||
| 4 | usingnamespace @import("crypto.zig"); | ||
| 5 | const Chacha20Poly1305 = std.crypto.aead.chacha_poly.ChaCha20Poly1305; | ||
| 6 | const Aes128Gcm = std.crypto.aead.aes_gcm.Aes128Gcm; | ||
| 7 | const record_length = @import("main.zig").record_length; | ||
| 8 | |||
| 9 | pub const suites = struct { | ||
| 10 | pub const ECDHE_RSA_Chacha20_Poly1305 = struct { | ||
| 11 | pub const name = "ECDHE-RSA-CHACHA20-POLY1305"; | ||
| 12 | pub const tag = 0xCCA8; | ||
| 13 | pub const key_exchange = .ecdhe; | ||
| 14 | pub const hash = .sha256; | ||
| 15 | |||
| 16 | pub const Keys = struct { | ||
| 17 | client_key: [32]u8, | ||
| 18 | server_key: [32]u8, | ||
| 19 | client_iv: [12]u8, | ||
| 20 | server_iv: [12]u8, | ||
| 21 | }; | ||
| 22 | |||
| 23 | pub const State = union(enum) { | ||
| 24 | none, | ||
| 25 | in_record: struct { | ||
| 26 | left: usize, | ||
| 27 | context: ChaCha20Stream.BlockVec, | ||
| 28 | idx: usize, | ||
| 29 | buf: [64]u8, | ||
| 30 | }, | ||
| 31 | }; | ||
| 32 | pub const default_state: State = .none; | ||
| 33 | |||
| 34 | pub fn raw_write( | ||
| 35 | comptime buffer_size: usize, | ||
| 36 | rand: *std.rand.Random, | ||
| 37 | key_data: anytype, | ||
| 38 | writer: anytype, | ||
| 39 | prefix: [3]u8, | ||
| 40 | seq: u64, | ||
| 41 | buffer: []const u8, | ||
| 42 | ) !void { | ||
| 43 | std.debug.assert(buffer.len <= buffer_size); | ||
| 44 | try writer.writeAll(&prefix); | ||
| 45 | try writer.writeIntBig(u16, @intCast(u16, buffer.len + 16)); | ||
| 46 | |||
| 47 | var additional_data: [13]u8 = undefined; | ||
| 48 | mem.writeIntBig(u64, additional_data[0..8], seq); | ||
| 49 | additional_data[8..11].* = prefix; | ||
| 50 | mem.writeIntBig(u16, additional_data[11..13], @intCast(u16, buffer.len)); | ||
| 51 | |||
| 52 | var encrypted_data: [buffer_size]u8 = undefined; | ||
| 53 | var tag_data: [16]u8 = undefined; | ||
| 54 | |||
| 55 | var nonce: [12]u8 = ([1]u8{0} ** 4) ++ ([1]u8{undefined} ** 8); | ||
| 56 | mem.writeIntBig(u64, nonce[4..12], seq); | ||
| 57 | for (nonce) |*n, i| { | ||
| 58 | n.* ^= key_data.client_iv(@This())[i]; | ||
| 59 | } | ||
| 60 | |||
| 61 | Chacha20Poly1305.encrypt( | ||
| 62 | encrypted_data[0..buffer.len], | ||
| 63 | &tag_data, | ||
| 64 | buffer, | ||
| 65 | &additional_data, | ||
| 66 | nonce, | ||
| 67 | key_data.client_key(@This()).*, | ||
| 68 | ); | ||
| 69 | try writer.writeAll(encrypted_data[0..buffer.len]); | ||
| 70 | try writer.writeAll(&tag_data); | ||
| 71 | } | ||
| 72 | |||
| 73 | pub fn check_verify_message( | ||
| 74 | key_data: anytype, | ||
| 75 | length: usize, | ||
| 76 | reader: anytype, | ||
| 77 | verify_message: [16]u8, | ||
| 78 | ) !bool { | ||
| 79 | if (length != 32) | ||
| 80 | return false; | ||
| 81 | |||
| 82 | var msg_in: [32]u8 = undefined; | ||
| 83 | try reader.readNoEof(&msg_in); | ||
| 84 | |||
| 85 | const additional_data: [13]u8 = ([1]u8{0} ** 8) ++ [5]u8{ 0x16, 0x03, 0x03, 0x00, 0x10 }; | ||
| 86 | var decrypted: [16]u8 = undefined; | ||
| 87 | Chacha20Poly1305.decrypt( | ||
| 88 | &decrypted, | ||
| 89 | msg_in[0..16], | ||
| 90 | msg_in[16..].*, | ||
| 91 | &additional_data, | ||
| 92 | key_data.server_iv(@This()).*, | ||
| 93 | key_data.server_key(@This()).*, | ||
| 94 | ) catch return false; | ||
| 95 | |||
| 96 | return mem.eql(u8, &decrypted, &verify_message); | ||
| 97 | } | ||
| 98 | |||
| 99 | pub fn read( | ||
| 100 | comptime buf_size: usize, | ||
| 101 | state: *State, | ||
| 102 | key_data: anytype, | ||
| 103 | reader: anytype, | ||
| 104 | server_seq: *u64, | ||
| 105 | buffer: []u8, | ||
| 106 | ) !usize { | ||
| 107 | switch (state.*) { | ||
| 108 | .none => { | ||
| 109 | const len = (record_length(0x17, reader) catch |err| switch (err) { | ||
| 110 | error.EndOfStream => return 0, | ||
| 111 | else => |e| return e, | ||
| 112 | }) - 16; | ||
| 113 | |||
| 114 | const curr_bytes = std.math.min(std.math.min(len, buf_size), buffer.len); | ||
| 115 | |||
| 116 | var nonce: [12]u8 = ([1]u8{0} ** 4) ++ ([1]u8{undefined} ** 8); | ||
| 117 | mem.writeIntBig(u64, nonce[4..12], server_seq.*); | ||
| 118 | for (nonce) |*n, i| { | ||
| 119 | n.* ^= key_data.server_iv(@This())[i]; | ||
| 120 | } | ||
| 121 | |||
| 122 | // Partially decrypt the data. | ||
| 123 | var encrypted: [buf_size]u8 = undefined; | ||
| 124 | const actually_read = try reader.read(encrypted[0..curr_bytes]); | ||
| 125 | |||
| 126 | var c: [4]u32 = undefined; | ||
| 127 | c[0] = 1; | ||
| 128 | c[1] = mem.readIntLittle(u32, nonce[0..4]); | ||
| 129 | c[2] = mem.readIntLittle(u32, nonce[4..8]); | ||
| 130 | c[3] = mem.readIntLittle(u32, nonce[8..12]); | ||
| 131 | const server_key = keyToWords(key_data.server_key(@This()).*); | ||
| 132 | var context = ChaCha20Stream.initContext(server_key, c); | ||
| 133 | var idx: usize = 0; | ||
| 134 | var buf: [64]u8 = undefined; | ||
| 135 | ChaCha20Stream.chacha20Xor( | ||
| 136 | buffer[0..actually_read], | ||
| 137 | encrypted[0..actually_read], | ||
| 138 | server_key, | ||
| 139 | &context, | ||
| 140 | &idx, | ||
| 141 | &buf, | ||
| 142 | ); | ||
| 143 | if (actually_read < len) { | ||
| 144 | state.* = .{ | ||
| 145 | .in_record = .{ | ||
| 146 | .left = len - actually_read, | ||
| 147 | .context = context, | ||
| 148 | .idx = idx, | ||
| 149 | .buf = buf, | ||
| 150 | }, | ||
| 151 | }; | ||
| 152 | } else { | ||
| 153 | // @TODO Verify Poly1305. | ||
| 154 | reader.skipBytes(16, .{}) catch |err| switch (err) { | ||
| 155 | error.EndOfStream => return 0, | ||
| 156 | else => |e| return e, | ||
| 157 | }; | ||
| 158 | server_seq.* += 1; | ||
| 159 | } | ||
| 160 | return actually_read; | ||
| 161 | }, | ||
| 162 | .in_record => |*record_info| { | ||
| 163 | const curr_bytes = std.math.min(std.math.min(buf_size, buffer.len), record_info.left); | ||
| 164 | // Partially decrypt the data. | ||
| 165 | var encrypted: [buf_size]u8 = undefined; | ||
| 166 | const actually_read = try reader.read(encrypted[0..curr_bytes]); | ||
| 167 | ChaCha20Stream.chacha20Xor( | ||
| 168 | buffer[0..actually_read], | ||
| 169 | encrypted[0..actually_read], | ||
| 170 | keyToWords(key_data.server_key(@This()).*), | ||
| 171 | &record_info.context, | ||
| 172 | &record_info.idx, | ||
| 173 | &record_info.buf, | ||
| 174 | ); | ||
| 175 | |||
| 176 | record_info.left -= actually_read; | ||
| 177 | if (record_info.left == 0) { | ||
| 178 | // @TODO Verify Poly1305. | ||
| 179 | reader.skipBytes(16, .{}) catch |err| switch (err) { | ||
| 180 | error.EndOfStream => return 0, | ||
| 181 | else => |e| return e, | ||
| 182 | }; | ||
| 183 | state.* = .none; | ||
| 184 | server_seq.* += 1; | ||
| 185 | } | ||
| 186 | return actually_read; | ||
| 187 | }, | ||
| 188 | } | ||
| 189 | } | ||
| 190 | }; | ||
| 191 | |||
| 192 | pub const ECDHE_RSA_AES128_GCM_SHA256 = struct { | ||
| 193 | pub const name = "ECDHE-RSA-AES128-GCM-SHA256"; | ||
| 194 | pub const tag = 0xC02F; | ||
| 195 | pub const key_exchange = .ecdhe; | ||
| 196 | pub const hash = .sha256; | ||
| 197 | |||
| 198 | pub const Keys = struct { | ||
| 199 | client_key: [16]u8, | ||
| 200 | server_key: [16]u8, | ||
| 201 | client_iv: [4]u8, | ||
| 202 | server_iv: [4]u8, | ||
| 203 | }; | ||
| 204 | |||
| 205 | const Aes = std.crypto.core.aes.Aes128; | ||
| 206 | pub const State = union(enum) { | ||
| 207 | none, | ||
| 208 | in_record: struct { | ||
| 209 | left: usize, | ||
| 210 | aes: @typeInfo(@TypeOf(Aes.initEnc)).Fn.return_type.?, | ||
| 211 | // ctr state | ||
| 212 | counterInt: u128, | ||
| 213 | idx: usize, | ||
| 214 | }, | ||
| 215 | }; | ||
| 216 | pub const default_state: State = .none; | ||
| 217 | |||
| 218 | pub fn check_verify_message( | ||
| 219 | key_data: anytype, | ||
| 220 | length: usize, | ||
| 221 | reader: anytype, | ||
| 222 | verify_message: [16]u8, | ||
| 223 | ) !bool { | ||
| 224 | if (length != 40) | ||
| 225 | return false; | ||
| 226 | |||
| 227 | var iv: [12]u8 = undefined; | ||
| 228 | iv[0..4].* = key_data.server_iv(@This()).*; | ||
| 229 | try reader.readNoEof(iv[4..12]); | ||
| 230 | |||
| 231 | var msg_in: [32]u8 = undefined; | ||
| 232 | try reader.readNoEof(&msg_in); | ||
| 233 | |||
| 234 | const additional_data: [13]u8 = ([1]u8{0} ** 8) ++ [5]u8{ 0x16, 0x03, 0x03, 0x00, 0x10 }; | ||
| 235 | var decrypted: [16]u8 = undefined; | ||
| 236 | Aes128Gcm.decrypt( | ||
| 237 | &decrypted, | ||
| 238 | msg_in[0..16], | ||
| 239 | msg_in[16..].*, | ||
| 240 | &additional_data, | ||
| 241 | iv, | ||
| 242 | key_data.server_key(@This()).*, | ||
| 243 | ) catch return false; | ||
| 244 | |||
| 245 | return mem.eql(u8, &decrypted, &verify_message); | ||
| 246 | } | ||
| 247 | |||
| 248 | pub fn raw_write( | ||
| 249 | comptime buffer_size: usize, | ||
| 250 | rand: *std.rand.Random, | ||
| 251 | key_data: anytype, | ||
| 252 | writer: anytype, | ||
| 253 | prefix: [3]u8, | ||
| 254 | seq: u64, | ||
| 255 | buffer: []const u8, | ||
| 256 | ) !void { | ||
| 257 | std.debug.assert(buffer.len <= buffer_size); | ||
| 258 | var iv: [12]u8 = undefined; | ||
| 259 | iv[0..4].* = key_data.client_iv(@This()).*; | ||
| 260 | rand.bytes(iv[4..12]); | ||
| 261 | |||
| 262 | var additional_data: [13]u8 = undefined; | ||
| 263 | mem.writeIntBig(u64, additional_data[0..8], seq); | ||
| 264 | additional_data[8..11].* = prefix; | ||
| 265 | mem.writeIntBig(u16, additional_data[11..13], @intCast(u16, buffer.len)); | ||
| 266 | |||
| 267 | try writer.writeAll(&prefix); | ||
| 268 | try writer.writeIntBig(u16, @intCast(u16, buffer.len + 24)); | ||
| 269 | try writer.writeAll(iv[4..12]); | ||
| 270 | |||
| 271 | var encrypted_data: [buffer_size]u8 = undefined; | ||
| 272 | var tag_data: [16]u8 = undefined; | ||
| 273 | |||
| 274 | Aes128Gcm.encrypt( | ||
| 275 | encrypted_data[0..buffer.len], | ||
| 276 | &tag_data, | ||
| 277 | buffer, | ||
| 278 | &additional_data, | ||
| 279 | iv, | ||
| 280 | key_data.client_key(@This()).*, | ||
| 281 | ); | ||
| 282 | try writer.writeAll(encrypted_data[0..buffer.len]); | ||
| 283 | try writer.writeAll(&tag_data); | ||
| 284 | } | ||
| 285 | |||
| 286 | pub fn read( | ||
| 287 | comptime buf_size: usize, | ||
| 288 | state: *State, | ||
| 289 | key_data: anytype, | ||
| 290 | reader: anytype, | ||
| 291 | server_seq: *u64, | ||
| 292 | buffer: []u8, | ||
| 293 | ) !usize { | ||
| 294 | switch (state.*) { | ||
| 295 | .none => { | ||
| 296 | const len = (record_length(0x17, reader) catch |err| switch (err) { | ||
| 297 | error.EndOfStream => return 0, | ||
| 298 | else => |e| return e, | ||
| 299 | }) - 24; | ||
| 300 | |||
| 301 | const curr_bytes = std.math.min(std.math.min(len, buf_size), buffer.len); | ||
| 302 | |||
| 303 | var iv: [12]u8 = undefined; | ||
| 304 | iv[0..4].* = key_data.server_iv(@This()).*; | ||
| 305 | reader.readNoEof(iv[4..12]) catch |err| switch (err) { | ||
| 306 | error.EndOfStream => return 0, | ||
| 307 | else => |e| return e, | ||
| 308 | }; | ||
| 309 | |||
| 310 | // Partially decrypt the data. | ||
| 311 | var encrypted: [buf_size]u8 = undefined; | ||
| 312 | const actually_read = try reader.read(encrypted[0..curr_bytes]); | ||
| 313 | |||
| 314 | const aes = Aes.initEnc(key_data.server_key(@This()).*); | ||
| 315 | |||
| 316 | var j: [16]u8 = undefined; | ||
| 317 | mem.copy(u8, j[0..12], iv[0..]); | ||
| 318 | mem.writeIntBig(u32, j[12..][0..4], 2); | ||
| 319 | |||
| 320 | var counterInt = mem.readInt(u128, &j, .Big); | ||
| 321 | var idx: usize = 0; | ||
| 322 | |||
| 323 | ctr( | ||
| 324 | @TypeOf(aes), | ||
| 325 | aes, | ||
| 326 | buffer[0..actually_read], | ||
| 327 | encrypted[0..actually_read], | ||
| 328 | &counterInt, | ||
| 329 | &idx, | ||
| 330 | .Big, | ||
| 331 | ); | ||
| 332 | |||
| 333 | if (actually_read < len) { | ||
| 334 | state.* = .{ | ||
| 335 | .in_record = .{ | ||
| 336 | .left = len - actually_read, | ||
| 337 | .aes = aes, | ||
| 338 | .counterInt = counterInt, | ||
| 339 | .idx = idx, | ||
| 340 | }, | ||
| 341 | }; | ||
| 342 | } else { | ||
| 343 | // @TODO Verify the message | ||
| 344 | reader.skipBytes(16, .{}) catch |err| switch (err) { | ||
| 345 | error.EndOfStream => return 0, | ||
| 346 | else => |e| return e, | ||
| 347 | }; | ||
| 348 | server_seq.* += 1; | ||
| 349 | } | ||
| 350 | return actually_read; | ||
| 351 | }, | ||
| 352 | .in_record => |*record_info| { | ||
| 353 | const curr_bytes = std.math.min(std.math.min(buf_size, buffer.len), record_info.left); | ||
| 354 | // Partially decrypt the data. | ||
| 355 | var encrypted: [buf_size]u8 = undefined; | ||
| 356 | const actually_read = try reader.read(encrypted[0..curr_bytes]); | ||
| 357 | |||
| 358 | ctr( | ||
| 359 | @TypeOf(record_info.aes), | ||
| 360 | record_info.aes, | ||
| 361 | buffer[0..actually_read], | ||
| 362 | encrypted[0..actually_read], | ||
| 363 | &record_info.counterInt, | ||
| 364 | &record_info.idx, | ||
| 365 | .Big, | ||
| 366 | ); | ||
| 367 | record_info.left -= actually_read; | ||
| 368 | if (record_info.left == 0) { | ||
| 369 | // @TODO Verify Poly1305. | ||
| 370 | reader.skipBytes(16, .{}) catch |err| switch (err) { | ||
| 371 | error.EndOfStream => return 0, | ||
| 372 | else => |e| return e, | ||
| 373 | }; | ||
| 374 | state.* = .none; | ||
| 375 | server_seq.* += 1; | ||
| 376 | } | ||
| 377 | return actually_read; | ||
| 378 | }, | ||
| 379 | } | ||
| 380 | } | ||
| 381 | }; | ||
| 382 | |||
| 383 | pub const all = &[_]type{ ECDHE_RSA_Chacha20_Poly1305, ECDHE_RSA_AES128_GCM_SHA256 }; | ||
| 384 | }; | ||
| 385 | |||
| 386 | fn key_field_width(comptime T: type, comptime field: anytype) ?usize { | ||
| 387 | if (!@hasField(T, @tagName(field))) | ||
| 388 | return null; | ||
| 389 | |||
| 390 | const field_info = std.meta.fieldInfo(T, field); | ||
| 391 | if (!comptime std.meta.trait.is(.Array)(field_info.field_type) or std.meta.Elem(field_info.field_type) != u8) | ||
| 392 | @compileError("Field '" ++ field ++ "' of type '" ++ @typeName(T) ++ "' should be an array of u8."); | ||
| 393 | |||
| 394 | return @typeInfo(field_info.field_type).Array.len; | ||
| 395 | } | ||
| 396 | |||
| 397 | pub fn key_data_size(comptime ciphersuites: []const type) usize { | ||
| 398 | var max: usize = 0; | ||
| 399 | for (ciphersuites) |cs| { | ||
| 400 | const curr = (key_field_width(cs.Keys, .client_mac) orelse 0) + | ||
| 401 | (key_field_width(cs.Keys, .server_mac) orelse 0) + | ||
| 402 | key_field_width(cs.Keys, .client_key).? + | ||
| 403 | key_field_width(cs.Keys, .server_key).? + | ||
| 404 | key_field_width(cs.Keys, .client_iv).? + | ||
| 405 | key_field_width(cs.Keys, .server_iv).?; | ||
| 406 | if (curr > max) | ||
| 407 | max = curr; | ||
| 408 | } | ||
| 409 | return max; | ||
| 410 | } | ||
| 411 | |||
| 412 | pub fn KeyData(comptime ciphersuites: []const type) type { | ||
| 413 | return struct { | ||
| 414 | data: [key_data_size(ciphersuites)]u8, | ||
| 415 | |||
| 416 | pub fn client_mac(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .client_mac) orelse 0]u8 { | ||
| 417 | return self.data[0..comptime (key_field_width(cs.Keys, .client_mac) orelse 0)]; | ||
| 418 | } | ||
| 419 | |||
| 420 | pub fn server_mac(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .server_mac) orelse 0]u8 { | ||
| 421 | const start = key_field_width(cs.Keys, .client_mac) orelse 0; | ||
| 422 | return self.data[start..][0..comptime (key_field_width(cs.Keys, .server_mac) orelse 0)]; | ||
| 423 | } | ||
| 424 | |||
| 425 | pub fn client_key(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .client_key).?]u8 { | ||
| 426 | const start = (key_field_width(cs.Keys, .client_mac) orelse 0) + | ||
| 427 | (key_field_width(cs.Keys, .server_mac) orelse 0); | ||
| 428 | return self.data[start..][0..comptime key_field_width(cs.Keys, .client_key).?]; | ||
| 429 | } | ||
| 430 | |||
| 431 | pub fn server_key(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .server_key).?]u8 { | ||
| 432 | const start = (key_field_width(cs.Keys, .client_mac) orelse 0) + | ||
| 433 | (key_field_width(cs.Keys, .server_mac) orelse 0) + | ||
| 434 | key_field_width(cs.Keys, .client_key).?; | ||
| 435 | return self.data[start..][0..comptime key_field_width(cs.Keys, .server_key).?]; | ||
| 436 | } | ||
| 437 | |||
| 438 | pub fn client_iv(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .client_iv).?]u8 { | ||
| 439 | const start = (key_field_width(cs.Keys, .client_mac) orelse 0) + | ||
| 440 | (key_field_width(cs.Keys, .server_mac) orelse 0) + | ||
| 441 | key_field_width(cs.Keys, .client_key).? + | ||
| 442 | key_field_width(cs.Keys, .server_key).?; | ||
| 443 | return self.data[start..][0..comptime key_field_width(cs.Keys, .client_iv).?]; | ||
| 444 | } | ||
| 445 | |||
| 446 | pub fn server_iv(self: *@This(), comptime cs: type) *[key_field_width(cs.Keys, .server_iv).?]u8 { | ||
| 447 | const start = (key_field_width(cs.Keys, .client_mac) orelse 0) + | ||
| 448 | (key_field_width(cs.Keys, .server_mac) orelse 0) + | ||
| 449 | key_field_width(cs.Keys, .client_key).? + | ||
| 450 | key_field_width(cs.Keys, .server_key).? + | ||
| 451 | key_field_width(cs.Keys, .client_iv).?; | ||
| 452 | return self.data[start..][0..comptime key_field_width(cs.Keys, .server_iv).?]; | ||
| 453 | } | ||
| 454 | }; | ||
| 455 | } | ||
| 456 | |||
| 457 | pub fn key_expansion( | ||
| 458 | comptime ciphersuites: []const type, | ||
| 459 | tag: u16, | ||
| 460 | context: anytype, | ||
| 461 | comptime next_32_bytes: anytype, | ||
| 462 | ) KeyData(ciphersuites) { | ||
| 463 | var res: KeyData(ciphersuites) = undefined; | ||
| 464 | inline for (ciphersuites) |cs| { | ||
| 465 | if (cs.tag == tag) { | ||
| 466 | var chunk: [32]u8 = undefined; | ||
| 467 | next_32_bytes(context, 0, &chunk); | ||
| 468 | comptime var chunk_idx = 1; | ||
| 469 | comptime var data_cursor = 0; | ||
| 470 | comptime var chunk_cursor = 0; | ||
| 471 | |||
| 472 | const fields = .{ | ||
| 473 | .client_mac, .server_mac, | ||
| 474 | .client_key, .server_key, | ||
| 475 | .client_iv, .server_iv, | ||
| 476 | }; | ||
| 477 | inline for (fields) |field| { | ||
| 478 | if (chunk_cursor == 32) { | ||
| 479 | next_32_bytes(context, chunk_idx, &chunk); | ||
| 480 | chunk_idx += 1; | ||
| 481 | chunk_cursor = 0; | ||
| 482 | } | ||
| 483 | |||
| 484 | const field_width = comptime (key_field_width(cs.Keys, field) orelse 0); | ||
| 485 | const first_read = comptime std.math.min(32 - chunk_cursor, field_width); | ||
| 486 | const second_read = field_width - first_read; | ||
| 487 | |||
| 488 | res.data[data_cursor..][0..first_read].* = chunk[chunk_cursor..][0..first_read].*; | ||
| 489 | data_cursor += first_read; | ||
| 490 | chunk_cursor += first_read; | ||
| 491 | |||
| 492 | if (second_read != 0) { | ||
| 493 | next_32_bytes(context, chunk_idx, &chunk); | ||
| 494 | chunk_idx += 1; | ||
| 495 | res.data[data_cursor..][0..second_read].* = chunk[chunk_cursor..][0..second_read].*; | ||
| 496 | data_cursor += second_read; | ||
| 497 | chunk_cursor = second_read; | ||
| 498 | comptime std.debug.assert(chunk_cursor != 32); | ||
| 499 | } | ||
| 500 | } | ||
| 501 | |||
| 502 | return res; | ||
| 503 | } | ||
| 504 | } | ||
| 505 | unreachable; | ||
| 506 | } | ||
| 507 | |||
| 508 | pub fn ClientState(comptime ciphersuites: []const type) type { | ||
| 509 | var fields: [ciphersuites.len]std.builtin.TypeInfo.UnionField = undefined; | ||
| 510 | for (ciphersuites) |cs, i| { | ||
| 511 | fields[i] = .{ | ||
| 512 | .name = cs.name, | ||
| 513 | .field_type = cs.State, | ||
| 514 | .alignment = if (@sizeOf(cs.State) > 0) @alignOf(cs.State) else 0, | ||
| 515 | }; | ||
| 516 | } | ||
| 517 | return @Type(.{ | ||
| 518 | .Union = .{ | ||
| 519 | .layout = .Extern, | ||
| 520 | .tag_type = null, | ||
| 521 | .fields = &fields, | ||
| 522 | .decls = &[0]std.builtin.TypeInfo.Declaration{}, | ||
| 523 | }, | ||
| 524 | }); | ||
| 525 | } | ||
| 526 | |||
| 527 | pub fn client_state_default(comptime ciphersuites: []const type, tag: u16) ClientState(ciphersuites) { | ||
| 528 | inline for (ciphersuites) |cs| { | ||
| 529 | if (cs.tag == tag) { | ||
| 530 | return @unionInit(ClientState(ciphersuites), cs.name, cs.default_state); | ||
| 531 | } | ||
| 532 | } | ||
| 533 | unreachable; | ||
| 534 | } | ||
libs/iguanatls/src/crypto.zig created+936| ... | @@ -0,0 +1,936 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const mem = std.mem; | ||
| 3 | |||
| 4 | // TODO See stdlib, this is a modified non vectorized implementation | ||
| 5 | pub const ChaCha20Stream = struct { | ||
| 6 | const math = std.math; | ||
| 7 | pub const BlockVec = [16]u32; | ||
| 8 | |||
| 9 | pub fn initContext(key: [8]u32, d: [4]u32) BlockVec { | ||
| 10 | const c = "expand 32-byte k"; | ||
| 11 | const constant_le = comptime [4]u32{ | ||
| 12 | mem.readIntLittle(u32, c[0..4]), | ||
| 13 | mem.readIntLittle(u32, c[4..8]), | ||
| 14 | mem.readIntLittle(u32, c[8..12]), | ||
| 15 | mem.readIntLittle(u32, c[12..16]), | ||
| 16 | }; | ||
| 17 | return BlockVec{ | ||
| 18 | constant_le[0], constant_le[1], constant_le[2], constant_le[3], | ||
| 19 | key[0], key[1], key[2], key[3], | ||
| 20 | key[4], key[5], key[6], key[7], | ||
| 21 | d[0], d[1], d[2], d[3], | ||
| 22 | }; | ||
| 23 | } | ||
| 24 | |||
| 25 | const QuarterRound = struct { | ||
| 26 | a: usize, | ||
| 27 | b: usize, | ||
| 28 | c: usize, | ||
| 29 | d: usize, | ||
| 30 | }; | ||
| 31 | |||
| 32 | fn Rp(a: usize, b: usize, c: usize, d: usize) QuarterRound { | ||
| 33 | return QuarterRound{ | ||
| 34 | .a = a, | ||
| 35 | .b = b, | ||
| 36 | .c = c, | ||
| 37 | .d = d, | ||
| 38 | }; | ||
| 39 | } | ||
| 40 | |||
| 41 | inline fn chacha20Core(x: *BlockVec, input: BlockVec) void { | ||
| 42 | x.* = input; | ||
| 43 | |||
| 44 | const rounds = comptime [_]QuarterRound{ | ||
| 45 | Rp(0, 4, 8, 12), | ||
| 46 | Rp(1, 5, 9, 13), | ||
| 47 | Rp(2, 6, 10, 14), | ||
| 48 | Rp(3, 7, 11, 15), | ||
| 49 | Rp(0, 5, 10, 15), | ||
| 50 | Rp(1, 6, 11, 12), | ||
| 51 | Rp(2, 7, 8, 13), | ||
| 52 | Rp(3, 4, 9, 14), | ||
| 53 | }; | ||
| 54 | |||
| 55 | comptime var j: usize = 0; | ||
| 56 | inline while (j < 20) : (j += 2) { | ||
| 57 | inline for (rounds) |r| { | ||
| 58 | x[r.a] +%= x[r.b]; | ||
| 59 | x[r.d] = math.rotl(u32, x[r.d] ^ x[r.a], @as(u32, 16)); | ||
| 60 | x[r.c] +%= x[r.d]; | ||
| 61 | x[r.b] = math.rotl(u32, x[r.b] ^ x[r.c], @as(u32, 12)); | ||
| 62 | x[r.a] +%= x[r.b]; | ||
| 63 | x[r.d] = math.rotl(u32, x[r.d] ^ x[r.a], @as(u32, 8)); | ||
| 64 | x[r.c] +%= x[r.d]; | ||
| 65 | x[r.b] = math.rotl(u32, x[r.b] ^ x[r.c], @as(u32, 7)); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | inline fn hashToBytes(out: *[64]u8, x: BlockVec) void { | ||
| 71 | var i: usize = 0; | ||
| 72 | while (i < 4) : (i += 1) { | ||
| 73 | mem.writeIntLittle(u32, out[16 * i + 0 ..][0..4], x[i * 4 + 0]); | ||
| 74 | mem.writeIntLittle(u32, out[16 * i + 4 ..][0..4], x[i * 4 + 1]); | ||
| 75 | mem.writeIntLittle(u32, out[16 * i + 8 ..][0..4], x[i * 4 + 2]); | ||
| 76 | mem.writeIntLittle(u32, out[16 * i + 12 ..][0..4], x[i * 4 + 3]); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | inline fn contextFeedback(x: *BlockVec, ctx: BlockVec) void { | ||
| 81 | var i: usize = 0; | ||
| 82 | while (i < 16) : (i += 1) { | ||
| 83 | x[i] +%= ctx[i]; | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | // TODO: Optimize this | ||
| 88 | pub fn chacha20Xor(out: []u8, in: []const u8, key: [8]u32, ctx: *BlockVec, idx: *usize, buf: *[64]u8) void { | ||
| 89 | var x: BlockVec = undefined; | ||
| 90 | |||
| 91 | const start_idx = idx.*; | ||
| 92 | var i: usize = 0; | ||
| 93 | while (i < in.len) { | ||
| 94 | if (idx.* % 64 == 0) { | ||
| 95 | if (idx.* != 0) { | ||
| 96 | ctx.*[12] += 1; | ||
| 97 | } | ||
| 98 | chacha20Core(x[0..], ctx.*); | ||
| 99 | contextFeedback(&x, ctx.*); | ||
| 100 | hashToBytes(buf, x); | ||
| 101 | } | ||
| 102 | |||
| 103 | out[i] = in[i] ^ buf[idx.* % 64]; | ||
| 104 | |||
| 105 | i += 1; | ||
| 106 | idx.* += 1; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | }; | ||
| 110 | |||
| 111 | pub fn keyToWords(key: [32]u8) [8]u32 { | ||
| 112 | var k: [8]u32 = undefined; | ||
| 113 | var i: usize = 0; | ||
| 114 | while (i < 8) : (i += 1) { | ||
| 115 | k[i] = mem.readIntLittle(u32, key[i * 4 ..][0..4]); | ||
| 116 | } | ||
| 117 | return k; | ||
| 118 | } | ||
| 119 | |||
| 120 | // See std.crypto.core.modes.ctr | ||
| 121 | /// This mode creates a key stream by encrypting an incrementing counter using a block cipher, and adding it to the source material. | ||
| 122 | pub fn ctr( | ||
| 123 | comptime BlockCipher: anytype, | ||
| 124 | block_cipher: BlockCipher, | ||
| 125 | dst: []u8, | ||
| 126 | src: []const u8, | ||
| 127 | counterInt: *u128, | ||
| 128 | idx: *usize, | ||
| 129 | endian: comptime std.builtin.Endian, | ||
| 130 | ) void { | ||
| 131 | std.debug.assert(dst.len >= src.len); | ||
| 132 | const block_length = BlockCipher.block_length; | ||
| 133 | var cur_idx: usize = 0; | ||
| 134 | |||
| 135 | const offset = idx.* % block_length; | ||
| 136 | if (offset != 0) { | ||
| 137 | const part_len = std.math.min(block_length - offset, src.len); | ||
| 138 | |||
| 139 | var counter: [BlockCipher.block_length]u8 = undefined; | ||
| 140 | mem.writeInt(u128, &counter, counterInt.*, endian); | ||
| 141 | var pad = [_]u8{0} ** block_length; | ||
| 142 | mem.copy(u8, pad[offset..], src[0..part_len]); | ||
| 143 | block_cipher.xor(&pad, &pad, counter); | ||
| 144 | mem.copy(u8, dst[0..part_len], pad[offset..][0..part_len]); | ||
| 145 | |||
| 146 | cur_idx += part_len; | ||
| 147 | idx.* += part_len; | ||
| 148 | if (idx.* % block_length == 0) | ||
| 149 | counterInt.* += 1; | ||
| 150 | } | ||
| 151 | |||
| 152 | const start_idx = cur_idx; | ||
| 153 | const remaining = src.len - cur_idx; | ||
| 154 | cur_idx = 0; | ||
| 155 | |||
| 156 | const parallel_count = BlockCipher.block.parallel.optimal_parallel_blocks; | ||
| 157 | const wide_block_length = parallel_count * 16; | ||
| 158 | if (remaining >= wide_block_length) { | ||
| 159 | var counters: [parallel_count * 16]u8 = undefined; | ||
| 160 | while (cur_idx + wide_block_length <= remaining) : (cur_idx += wide_block_length) { | ||
| 161 | comptime var j = 0; | ||
| 162 | inline while (j < parallel_count) : (j += 1) { | ||
| 163 | mem.writeInt(u128, counters[j * 16 .. j * 16 + 16], counterInt.*, endian); | ||
| 164 | counterInt.* +%= 1; | ||
| 165 | } | ||
| 166 | block_cipher.xorWide(parallel_count, dst[start_idx..][cur_idx .. cur_idx + wide_block_length][0..wide_block_length], src[start_idx..][cur_idx .. cur_idx + wide_block_length][0..wide_block_length], counters); | ||
| 167 | idx.* += wide_block_length; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | while (cur_idx + block_length <= remaining) : (cur_idx += block_length) { | ||
| 171 | var counter: [BlockCipher.block_length]u8 = undefined; | ||
| 172 | mem.writeInt(u128, &counter, counterInt.*, endian); | ||
| 173 | counterInt.* +%= 1; | ||
| 174 | block_cipher.xor(dst[start_idx..][cur_idx .. cur_idx + block_length][0..block_length], src[start_idx..][cur_idx .. cur_idx + block_length][0..block_length], counter); | ||
| 175 | idx.* += block_length; | ||
| 176 | } | ||
| 177 | if (cur_idx < remaining) { | ||
| 178 | std.debug.assert(idx.* % block_length == 0); | ||
| 179 | var counter: [BlockCipher.block_length]u8 = undefined; | ||
| 180 | mem.writeInt(u128, &counter, counterInt.*, endian); | ||
| 181 | |||
| 182 | var pad = [_]u8{0} ** block_length; | ||
| 183 | mem.copy(u8, &pad, src[start_idx..][cur_idx..]); | ||
| 184 | block_cipher.xor(&pad, &pad, counter); | ||
| 185 | mem.copy(u8, dst[cur_idx..], pad[0 .. remaining - cur_idx]); | ||
| 186 | |||
| 187 | idx.* += remaining - cur_idx; | ||
| 188 | if (idx.* % block_length == 0) | ||
| 189 | counterInt.* +%= 1; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | |||
| 193 | // Ported from BearSSL's ec_prime_i31 engine | ||
| 194 | pub const ecc = struct { | ||
| 195 | pub const SECP384R1 = struct { | ||
| 196 | pub const point_len = 96; | ||
| 197 | |||
| 198 | const order = [point_len / 2]u8{ | ||
| 199 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||
| 200 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||
| 201 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | ||
| 202 | 0xC7, 0x63, 0x4D, 0x81, 0xF4, 0x37, 0x2D, 0xDF, | ||
| 203 | 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, | ||
| 204 | 0xEC, 0xEC, 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, | ||
| 205 | }; | ||
| 206 | |||
| 207 | const P = [_]u32{ | ||
| 208 | 0x0000018C, 0x7FFFFFFF, 0x00000001, 0x00000000, | ||
| 209 | 0x7FFFFFF8, 0x7FFFFFEF, 0x7FFFFFFF, 0x7FFFFFFF, | ||
| 210 | 0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF, 0x7FFFFFFF, | ||
| 211 | 0x7FFFFFFF, 0x00000FFF, | ||
| 212 | }; | ||
| 213 | const R2 = [_]u32{ | ||
| 214 | 0x0000018C, 0x00000000, 0x00000080, 0x7FFFFE00, | ||
| 215 | 0x000001FF, 0x00000800, 0x00000000, 0x7FFFE000, | ||
| 216 | 0x00001FFF, 0x00008000, 0x00008000, 0x00000000, | ||
| 217 | 0x00000000, 0x00000000, | ||
| 218 | }; | ||
| 219 | const B = [_]u32{ | ||
| 220 | 0x0000018C, 0x6E666840, 0x070D0392, 0x5D810231, | ||
| 221 | 0x7651D50C, 0x17E218D6, 0x1B192002, 0x44EFE441, | ||
| 222 | 0x3A524E2B, 0x2719BA5F, 0x41F02209, 0x36C5643E, | ||
| 223 | 0x5813EFFE, 0x000008A5, | ||
| 224 | }; | ||
| 225 | |||
| 226 | const base_point = [point_len]u8{ | ||
| 227 | 0xAA, 0x87, 0xCA, 0x22, 0xBE, 0x8B, 0x05, 0x37, | ||
| 228 | 0x8E, 0xB1, 0xC7, 0x1E, 0xF3, 0x20, 0xAD, 0x74, | ||
| 229 | 0x6E, 0x1D, 0x3B, 0x62, 0x8B, 0xA7, 0x9B, 0x98, | ||
| 230 | 0x59, 0xF7, 0x41, 0xE0, 0x82, 0x54, 0x2A, 0x38, | ||
| 231 | 0x55, 0x02, 0xF2, 0x5D, 0xBF, 0x55, 0x29, 0x6C, | ||
| 232 | 0x3A, 0x54, 0x5E, 0x38, 0x72, 0x76, 0x0A, 0xB7, | ||
| 233 | 0x36, 0x17, 0xDE, 0x4A, 0x96, 0x26, 0x2C, 0x6F, | ||
| 234 | 0x5D, 0x9E, 0x98, 0xBF, 0x92, 0x92, 0xDC, 0x29, | ||
| 235 | 0xF8, 0xF4, 0x1D, 0xBD, 0x28, 0x9A, 0x14, 0x7C, | ||
| 236 | 0xE9, 0xDA, 0x31, 0x13, 0xB5, 0xF0, 0xB8, 0xC0, | ||
| 237 | 0x0A, 0x60, 0xB1, 0xCE, 0x1D, 0x7E, 0x81, 0x9D, | ||
| 238 | 0x7A, 0x43, 0x1D, 0x7C, 0x90, 0xEA, 0x0E, 0x5F, | ||
| 239 | }; | ||
| 240 | |||
| 241 | comptime { | ||
| 242 | std.debug.assert((P[0] - (P[0] >> 5) + 7) >> 2 == point_len + 1); | ||
| 243 | } | ||
| 244 | }; | ||
| 245 | |||
| 246 | fn jacobian_len(comptime Curve: type) usize { | ||
| 247 | return @divTrunc(Curve.order.len * 8 + 61, 31); | ||
| 248 | } | ||
| 249 | |||
| 250 | fn Jacobian(comptime Curve: type) type { | ||
| 251 | return [3][jacobian_len(Curve)]u32; | ||
| 252 | } | ||
| 253 | |||
| 254 | fn zero_jacobian(comptime Curve: type) Jacobian(Curve) { | ||
| 255 | var result = std.mem.zeroes(Jacobian(Curve)); | ||
| 256 | result[0][0] = Curve.P[0]; | ||
| 257 | result[1][0] = Curve.P[0]; | ||
| 258 | result[2][0] = Curve.P[0]; | ||
| 259 | return result; | ||
| 260 | } | ||
| 261 | |||
| 262 | pub fn scalarmult( | ||
| 263 | comptime Curve: type, | ||
| 264 | point: [Curve.point_len]u8, | ||
| 265 | k: []const u8, | ||
| 266 | ) ![Curve.point_len]u8 { | ||
| 267 | var P: Jacobian(Curve) = undefined; | ||
| 268 | var res: u32 = decode_to_jacobian(Curve, &P, point); | ||
| 269 | point_mul(Curve, &P, k); | ||
| 270 | var out: [Curve.point_len]u8 = undefined; | ||
| 271 | encode_from_jacobian(Curve, &out, P); | ||
| 272 | if (res == 0) | ||
| 273 | return error.MultiplicationFailed; | ||
| 274 | return out; | ||
| 275 | } | ||
| 276 | |||
| 277 | pub fn KeyPair(comptime Curve: type) type { | ||
| 278 | return struct { | ||
| 279 | public_key: [Curve.point_len]u8, | ||
| 280 | secret_key: [Curve.point_len / 2]u8, | ||
| 281 | }; | ||
| 282 | } | ||
| 283 | |||
| 284 | pub fn make_key_pair(comptime Curve: type, rand_bytes: [Curve.point_len / 2]u8) KeyPair(Curve) { | ||
| 285 | var key_bytes = rand_bytes; | ||
| 286 | comptime var mask: u8 = 0xFF; | ||
| 287 | comptime { | ||
| 288 | while (mask >= Curve.order[0]) { | ||
| 289 | mask >>= 1; | ||
| 290 | } | ||
| 291 | } | ||
| 292 | key_bytes[0] &= mask; | ||
| 293 | key_bytes[Curve.point_len / 2 - 1] |= 0x01; | ||
| 294 | |||
| 295 | return .{ | ||
| 296 | .secret_key = key_bytes, | ||
| 297 | .public_key = scalarmult(Curve, Curve.base_point, &key_bytes) catch unreachable, | ||
| 298 | }; | ||
| 299 | } | ||
| 300 | |||
| 301 | fn jacobian_with_one_set(comptime Curve: type, comptime fields: [2][jacobian_len(Curve)]u32) Jacobian(Curve) { | ||
| 302 | comptime const plen = (Curve.P[0] + 63) >> 5; | ||
| 303 | return fields ++ [1][jacobian_len(Curve)]u32{ | ||
| 304 | [2]u32{ Curve.P[0], 1 } ++ ([1]u32{0} ** (plen - 2)), | ||
| 305 | }; | ||
| 306 | } | ||
| 307 | |||
| 308 | fn encode_from_jacobian(comptime Curve: type, point: *[Curve.point_len]u8, P: Jacobian(Curve)) void { | ||
| 309 | var Q = P; | ||
| 310 | const T = comptime jacobian_with_one_set(Curve, [2][jacobian_len(Curve)]u32{ undefined, undefined }); | ||
| 311 | _ = run_code(Curve, &Q, T, &code.affine); | ||
| 312 | encode_jacobian_part(Curve, point[0 .. Curve.point_len / 2], Q[0]); | ||
| 313 | encode_jacobian_part(Curve, point[Curve.point_len / 2 ..], Q[1]); | ||
| 314 | } | ||
| 315 | |||
| 316 | fn point_mul(comptime Curve: type, P: *Jacobian(Curve), x: []const u8) void { | ||
| 317 | var P2 = P.*; | ||
| 318 | point_double(Curve, &P2); | ||
| 319 | var P3 = P.*; | ||
| 320 | point_add(Curve, &P3, P2); | ||
| 321 | var Q = zero_jacobian(Curve); | ||
| 322 | var qz: u32 = 1; | ||
| 323 | var xlen = x.len; | ||
| 324 | var xidx: usize = 0; | ||
| 325 | while (xlen > 0) : ({ | ||
| 326 | xlen -= 1; | ||
| 327 | xidx += 1; | ||
| 328 | }) { | ||
| 329 | var k: u3 = 6; | ||
| 330 | while (true) : (k -= 2) { | ||
| 331 | point_double(Curve, &Q); | ||
| 332 | point_double(Curve, &Q); | ||
| 333 | var T = P.*; | ||
| 334 | var U = Q; | ||
| 335 | const bits = @as(u32, x[xidx] >> k) & 3; | ||
| 336 | const bnz = NEQ(bits, 0); | ||
| 337 | CCOPY(EQ(bits, 2), mem.asBytes(&T), mem.asBytes(&P2)); | ||
| 338 | CCOPY(EQ(bits, 3), mem.asBytes(&T), mem.asBytes(&P3)); | ||
| 339 | point_add(Curve, &U, T); | ||
| 340 | CCOPY(bnz & qz, mem.asBytes(&Q), mem.asBytes(&T)); | ||
| 341 | CCOPY(bnz & ~qz, mem.asBytes(&Q), mem.asBytes(&U)); | ||
| 342 | qz &= ~bnz; | ||
| 343 | |||
| 344 | if (k == 0) | ||
| 345 | break; | ||
| 346 | } | ||
| 347 | } | ||
| 348 | P.* = Q; | ||
| 349 | } | ||
| 350 | |||
| 351 | inline fn point_double(comptime Curve: type, P: *Jacobian(Curve)) void { | ||
| 352 | _ = run_code(Curve, P, P.*, &code.double); | ||
| 353 | } | ||
| 354 | inline fn point_add(comptime Curve: type, P1: *Jacobian(Curve), P2: Jacobian(Curve)) void { | ||
| 355 | _ = run_code(Curve, P1, P2, &code._add); | ||
| 356 | } | ||
| 357 | |||
| 358 | fn decode_to_jacobian( | ||
| 359 | comptime Curve: type, | ||
| 360 | out: *Jacobian(Curve), | ||
| 361 | point: [Curve.point_len]u8, | ||
| 362 | ) u32 { | ||
| 363 | out.* = zero_jacobian(Curve); | ||
| 364 | var result = decode_mod(Curve, &out.*[0], point[0 .. Curve.point_len / 2].*); | ||
| 365 | result &= decode_mod(Curve, &out.*[1], point[Curve.point_len / 2 ..].*); | ||
| 366 | |||
| 367 | const zlen = comptime ((Curve.P[0] + 63) >> 5); | ||
| 368 | comptime std.debug.assert(zlen == @typeInfo(@TypeOf(Curve.R2)).Array.len); | ||
| 369 | comptime std.debug.assert(zlen == @typeInfo(@TypeOf(Curve.B)).Array.len); | ||
| 370 | |||
| 371 | const Q = comptime jacobian_with_one_set(Curve, [2][jacobian_len(Curve)]u32{ Curve.R2, Curve.B }); | ||
| 372 | result &= ~run_code(Curve, out, Q, &code.check); | ||
| 373 | return result; | ||
| 374 | } | ||
| 375 | |||
| 376 | const code = struct { | ||
| 377 | const P1x = 0; | ||
| 378 | const P1y = 1; | ||
| 379 | const P1z = 2; | ||
| 380 | const P2x = 3; | ||
| 381 | const P2y = 4; | ||
| 382 | const P2z = 5; | ||
| 383 | const Px = 0; | ||
| 384 | const Py = 1; | ||
| 385 | const Pz = 2; | ||
| 386 | const t1 = 6; | ||
| 387 | const t2 = 7; | ||
| 388 | const t3 = 8; | ||
| 389 | const t4 = 9; | ||
| 390 | const t5 = 10; | ||
| 391 | const t6 = 11; | ||
| 392 | const t7 = 12; | ||
| 393 | const t8 = 3; | ||
| 394 | const t9 = 4; | ||
| 395 | const t10 = 5; | ||
| 396 | fn MSET(comptime d: u16, comptime a: u16) u16 { | ||
| 397 | return 0x0000 + (d << 8) + (a << 4); | ||
| 398 | } | ||
| 399 | fn MADD(comptime d: u16, comptime a: u16) u16 { | ||
| 400 | return 0x1000 + (d << 8) + (a << 4); | ||
| 401 | } | ||
| 402 | fn MSUB(comptime d: u16, comptime a: u16) u16 { | ||
| 403 | return 0x2000 + (d << 8) + (a << 4); | ||
| 404 | } | ||
| 405 | fn MMUL(comptime d: u16, comptime a: u16, comptime b: u16) u16 { | ||
| 406 | return 0x3000 + (d << 8) + (a << 4) + b; | ||
| 407 | } | ||
| 408 | fn MINV(comptime d: u16, comptime a: u16, comptime b: u16) u16 { | ||
| 409 | return 0x4000 + (d << 8) + (a << 4) + b; | ||
| 410 | } | ||
| 411 | fn MTZ(comptime d: u16) u16 { | ||
| 412 | return 0x5000 + (d << 8); | ||
| 413 | } | ||
| 414 | const ENDCODE = 0; | ||
| 415 | |||
| 416 | const check = [_]u16{ | ||
| 417 | // Convert x and y to Montgomery representation. | ||
| 418 | MMUL(t1, P1x, P2x), | ||
| 419 | MMUL(t2, P1y, P2x), | ||
| 420 | MSET(P1x, t1), | ||
| 421 | MSET(P1y, t2), | ||
| 422 | // Compute x^3 in t1. | ||
| 423 | MMUL(t2, P1x, P1x), | ||
| 424 | MMUL(t1, P1x, t2), | ||
| 425 | // Subtract 3*x from t1. | ||
| 426 | MSUB(t1, P1x), | ||
| 427 | MSUB(t1, P1x), | ||
| 428 | MSUB(t1, P1x), | ||
| 429 | // Add b. | ||
| 430 | MADD(t1, P2y), | ||
| 431 | // Compute y^2 in t2. | ||
| 432 | MMUL(t2, P1y, P1y), | ||
| 433 | // Compare y^2 with x^3 - 3*x + b; they must match. | ||
| 434 | MSUB(t1, t2), | ||
| 435 | MTZ(t1), | ||
| 436 | // Set z to 1 (in Montgomery representation). | ||
| 437 | MMUL(P1z, P2x, P2z), | ||
| 438 | ENDCODE, | ||
| 439 | }; | ||
| 440 | const double = [_]u16{ | ||
| 441 | // Compute z^2 (in t1). | ||
| 442 | MMUL(t1, Pz, Pz), | ||
| 443 | // Compute x-z^2 (in t2) and then x+z^2 (in t1). | ||
| 444 | MSET(t2, Px), | ||
| 445 | MSUB(t2, t1), | ||
| 446 | MADD(t1, Px), | ||
| 447 | // Compute m = 3*(x+z^2)*(x-z^2) (in t1). | ||
| 448 | MMUL(t3, t1, t2), | ||
| 449 | MSET(t1, t3), | ||
| 450 | MADD(t1, t3), | ||
| 451 | MADD(t1, t3), | ||
| 452 | // Compute s = 4*x*y^2 (in t2) and 2*y^2 (in t3). | ||
| 453 | MMUL(t3, Py, Py), | ||
| 454 | MADD(t3, t3), | ||
| 455 | MMUL(t2, Px, t3), | ||
| 456 | MADD(t2, t2), | ||
| 457 | // Compute x' = m^2 - 2*s. | ||
| 458 | MMUL(Px, t1, t1), | ||
| 459 | MSUB(Px, t2), | ||
| 460 | MSUB(Px, t2), | ||
| 461 | // Compute z' = 2*y*z. | ||
| 462 | MMUL(t4, Py, Pz), | ||
| 463 | MSET(Pz, t4), | ||
| 464 | MADD(Pz, t4), | ||
| 465 | // Compute y' = m*(s - x') - 8*y^4. Note that we already have | ||
| 466 | // 2*y^2 in t3. | ||
| 467 | MSUB(t2, Px), | ||
| 468 | MMUL(Py, t1, t2), | ||
| 469 | MMUL(t4, t3, t3), | ||
| 470 | MSUB(Py, t4), | ||
| 471 | MSUB(Py, t4), | ||
| 472 | ENDCODE, | ||
| 473 | }; | ||
| 474 | const _add = [_]u16{ | ||
| 475 | // Compute u1 = x1*z2^2 (in t1) and s1 = y1*z2^3 (in t3). | ||
| 476 | MMUL(t3, P2z, P2z), | ||
| 477 | MMUL(t1, P1x, t3), | ||
| 478 | MMUL(t4, P2z, t3), | ||
| 479 | MMUL(t3, P1y, t4), | ||
| 480 | // Compute u2 = x2*z1^2 (in t2) and s2 = y2*z1^3 (in t4). | ||
| 481 | MMUL(t4, P1z, P1z), | ||
| 482 | MMUL(t2, P2x, t4), | ||
| 483 | MMUL(t5, P1z, t4), | ||
| 484 | MMUL(t4, P2y, t5), | ||
| 485 | //Compute h = u2 - u1 (in t2) and r = s2 - s1 (in t4). | ||
| 486 | MSUB(t2, t1), | ||
| 487 | MSUB(t4, t3), | ||
| 488 | // Report cases where r = 0 through the returned flag. | ||
| 489 | MTZ(t4), | ||
| 490 | // Compute u1*h^2 (in t6) and h^3 (in t5). | ||
| 491 | MMUL(t7, t2, t2), | ||
| 492 | MMUL(t6, t1, t7), | ||
| 493 | MMUL(t5, t7, t2), | ||
| 494 | // Compute x3 = r^2 - h^3 - 2*u1*h^2. | ||
| 495 | // t1 and t7 can be used as scratch registers. | ||
| 496 | MMUL(P1x, t4, t4), | ||
| 497 | MSUB(P1x, t5), | ||
| 498 | MSUB(P1x, t6), | ||
| 499 | MSUB(P1x, t6), | ||
| 500 | //Compute y3 = r*(u1*h^2 - x3) - s1*h^3. | ||
| 501 | MSUB(t6, P1x), | ||
| 502 | MMUL(P1y, t4, t6), | ||
| 503 | MMUL(t1, t5, t3), | ||
| 504 | MSUB(P1y, t1), | ||
| 505 | //Compute z3 = h*z1*z2. | ||
| 506 | MMUL(t1, P1z, P2z), | ||
| 507 | MMUL(P1z, t1, t2), | ||
| 508 | ENDCODE, | ||
| 509 | }; | ||
| 510 | const affine = [_]u16{ | ||
| 511 | // Save z*R in t1. | ||
| 512 | MSET(t1, P1z), | ||
| 513 | // Compute z^3 in t2. | ||
| 514 | MMUL(t2, P1z, P1z), | ||
| 515 | MMUL(t3, P1z, t2), | ||
| 516 | MMUL(t2, t3, P2z), | ||
| 517 | // Invert to (1/z^3) in t2. | ||
| 518 | MINV(t2, t3, t4), | ||
| 519 | // Compute y. | ||
| 520 | MSET(t3, P1y), | ||
| 521 | MMUL(P1y, t2, t3), | ||
| 522 | // Compute (1/z^2) in t3. | ||
| 523 | MMUL(t3, t2, t1), | ||
| 524 | // Compute x. | ||
| 525 | MSET(t2, P1x), | ||
| 526 | MMUL(P1x, t2, t3), | ||
| 527 | ENDCODE, | ||
| 528 | }; | ||
| 529 | }; | ||
| 530 | |||
| 531 | fn decode_mod( | ||
| 532 | comptime Curve: type, | ||
| 533 | x: *[jacobian_len(Curve)]u32, | ||
| 534 | src: [Curve.point_len / 2]u8, | ||
| 535 | ) u32 { | ||
| 536 | const mlen = comptime ((Curve.P[0] + 31) >> 5); | ||
| 537 | const tlen = comptime std.math.max(mlen << 2, Curve.point_len / 2) + 4; | ||
| 538 | |||
| 539 | var r: u32 = 0; | ||
| 540 | var pass: usize = 0; | ||
| 541 | while (pass < 2) : (pass += 1) { | ||
| 542 | var v: usize = 1; | ||
| 543 | var acc: u32 = 0; | ||
| 544 | var acc_len: u32 = 0; | ||
| 545 | |||
| 546 | var u: usize = 0; | ||
| 547 | while (u < tlen) : (u += 1) { | ||
| 548 | const b = if (u < Curve.point_len / 2) | ||
| 549 | @as(u32, src[Curve.point_len / 2 - 1 - u]) | ||
| 550 | else | ||
| 551 | 0; | ||
| 552 | acc |= b << @truncate(u5, acc_len); | ||
| 553 | acc_len += 8; | ||
| 554 | if (acc_len >= 31) { | ||
| 555 | const xw = acc & 0x7FFFFFFF; | ||
| 556 | acc_len -= 31; | ||
| 557 | acc = b >> @truncate(u5, 8 - acc_len); | ||
| 558 | if (v <= mlen) { | ||
| 559 | if (pass != 0) { | ||
| 560 | x[v] = r & xw; | ||
| 561 | } else { | ||
| 562 | const cc = @bitCast(u32, CMP(xw, Curve.P[v])); | ||
| 563 | r = MUX(EQ(cc, 0), r, cc); | ||
| 564 | } | ||
| 565 | } else if (pass == 0) { | ||
| 566 | r = MUX(EQ(xw, 0), r, 1); | ||
| 567 | } | ||
| 568 | v += 1; | ||
| 569 | } | ||
| 570 | } | ||
| 571 | r >>= 1; | ||
| 572 | r |= (r << 1); | ||
| 573 | } | ||
| 574 | x[0] = Curve.P[0]; | ||
| 575 | return r & 1; | ||
| 576 | } | ||
| 577 | |||
| 578 | fn run_code( | ||
| 579 | comptime Curve: type, | ||
| 580 | P1: *Jacobian(Curve), | ||
| 581 | P2: Jacobian(Curve), | ||
| 582 | comptime Code: []const u16, | ||
| 583 | ) u32 { | ||
| 584 | comptime const jaclen = jacobian_len(Curve); | ||
| 585 | |||
| 586 | var t: [13][jaclen]u32 = undefined; | ||
| 587 | var result: u32 = 1; | ||
| 588 | |||
| 589 | t[0..3].* = P1.*; | ||
| 590 | t[3..6].* = P2; | ||
| 591 | |||
| 592 | comptime var u: usize = 0; | ||
| 593 | inline while (true) : (u += 1) { | ||
| 594 | comptime var op = Code[u]; | ||
| 595 | if (op == 0) | ||
| 596 | break; | ||
| 597 | comptime const d = (op >> 8) & 0x0F; | ||
| 598 | comptime const a = (op >> 4) & 0x0F; | ||
| 599 | comptime const b = op & 0x0F; | ||
| 600 | op >>= 12; | ||
| 601 | |||
| 602 | switch (op) { | ||
| 603 | 0 => t[d] = t[a], | ||
| 604 | 1 => { | ||
| 605 | var ctl = add(jaclen, &t[d], t[a], 1); | ||
| 606 | ctl |= NOT(sub(jaclen, &t[d], Curve.P, 0)); | ||
| 607 | _ = sub(jaclen, &t[d], Curve.P, ctl); | ||
| 608 | }, | ||
| 609 | 2 => _ = add(jaclen, &t[d], Curve.P, sub(jaclen, &t[d], t[a], 1)), | ||
| 610 | 3 => montymul(Curve, &t[d], t[a], t[b], Curve.P, 1), | ||
| 611 | 4 => { | ||
| 612 | var tp: [Curve.point_len / 2]u8 = undefined; | ||
| 613 | encode_jacobian_part(Curve, &tp, Curve.P); | ||
| 614 | tp[Curve.point_len / 2 - 1] -= 2; | ||
| 615 | modpow(Curve, &t[d], tp, 1, &t[a], &t[b]); | ||
| 616 | }, | ||
| 617 | else => result &= ~iszero(jaclen, t[d]), | ||
| 618 | } | ||
| 619 | } | ||
| 620 | P1.* = t[0..3].*; | ||
| 621 | return result; | ||
| 622 | } | ||
| 623 | |||
| 624 | inline fn MUL31(x: u32, y: u32) u64 { | ||
| 625 | return @as(u64, x) * @as(u64, y); | ||
| 626 | } | ||
| 627 | |||
| 628 | inline fn MUL31_lo(x: u32, y: u32) u32 { | ||
| 629 | return (x *% y) & 0x7FFFFFFF; | ||
| 630 | } | ||
| 631 | |||
| 632 | inline fn MUX(ctl: u32, x: u32, y: u32) u32 { | ||
| 633 | return y ^ (@bitCast(u32, -@bitCast(i32, ctl)) & (x ^ y)); | ||
| 634 | } | ||
| 635 | inline fn NOT(ctl: u32) u32 { | ||
| 636 | return ctl ^ 1; | ||
| 637 | } | ||
| 638 | inline fn NEQ(x: u32, y: u32) u32 { | ||
| 639 | const q = x ^ y; | ||
| 640 | return (q | @bitCast(u32, -@bitCast(i32, q))) >> 31; | ||
| 641 | } | ||
| 642 | inline fn EQ(x: u32, y: u32) u32 { | ||
| 643 | const q = x ^ y; | ||
| 644 | return NOT((q | @bitCast(u32, -@bitCast(i32, q))) >> 31); | ||
| 645 | } | ||
| 646 | inline fn CMP(x: u32, y: u32) i32 { | ||
| 647 | return @bitCast(i32, GT(x, y)) | -@bitCast(i32, GT(y, x)); | ||
| 648 | } | ||
| 649 | inline fn GT(x: u32, y: u32) u32 { | ||
| 650 | const z = y -% x; | ||
| 651 | return (z ^ ((x ^ y) & (x ^ z))) >> 31; | ||
| 652 | } | ||
| 653 | inline fn LT(x: u32, y: u32) u32 { | ||
| 654 | return GT(y, x); | ||
| 655 | } | ||
| 656 | inline fn GE(x: u32, y: u32) u32 { | ||
| 657 | return NOT(GT(y, x)); | ||
| 658 | } | ||
| 659 | |||
| 660 | fn CCOPY(ctl: u32, dst: []u8, src: []const u8) void { | ||
| 661 | for (src) |s, i| { | ||
| 662 | dst[i] = @truncate(u8, MUX(ctl, s, dst[i])); | ||
| 663 | } | ||
| 664 | } | ||
| 665 | |||
| 666 | // @TODO Remove lots of len and Curve parameters, just use the first byte calcualtions | ||
| 667 | // This will make all these functions shared for and reduce code bloat | ||
| 668 | |||
| 669 | inline fn set_zero(comptime len: usize, out: *[len]u32, bit_len: u32) void { | ||
| 670 | out[0] = bit_len; | ||
| 671 | mem.set(u32, out[1..][0 .. (bit_len + 31) >> 5], 0); | ||
| 672 | } | ||
| 673 | |||
| 674 | fn divrem(_hi: u32, _lo: u32, d: u32, r: *u32) u32 { | ||
| 675 | var hi = _hi; | ||
| 676 | var lo = _lo; | ||
| 677 | var q: u32 = 0; | ||
| 678 | const ch = EQ(hi, d); | ||
| 679 | hi = MUX(ch, 0, hi); | ||
| 680 | |||
| 681 | var k: u5 = 31; | ||
| 682 | while (k > 0) : (k -= 1) { | ||
| 683 | const j = @truncate(u5, 32 - @as(u6, k)); | ||
| 684 | const w = (hi << j) | (lo >> k); | ||
| 685 | const ctl = GE(w, d) | (hi >> k); | ||
| 686 | const hi2 = (w -% d) >> j; | ||
| 687 | const lo2 = lo -% (d << k); | ||
| 688 | hi = MUX(ctl, hi2, hi); | ||
| 689 | lo = MUX(ctl, lo2, lo); | ||
| 690 | q |= ctl << k; | ||
| 691 | } | ||
| 692 | const cf = GE(lo, d) | hi; | ||
| 693 | q |= cf; | ||
| 694 | r.* = MUX(cf, lo -% d, lo); | ||
| 695 | return q; | ||
| 696 | } | ||
| 697 | |||
| 698 | inline fn div(hi: u32, lo: u32, d: u32) u32 { | ||
| 699 | var r: u32 = undefined; | ||
| 700 | return divrem(hi, lo, d, &r); | ||
| 701 | } | ||
| 702 | |||
| 703 | fn muladd_small(comptime len: usize, x: *[len]u32, z: u32, m: [len]u32) void { | ||
| 704 | var a0: u32 = undefined; | ||
| 705 | var a1: u32 = undefined; | ||
| 706 | var b0: u32 = undefined; | ||
| 707 | const mblr = @intCast(u5, m[0] & 31); | ||
| 708 | const mlen = (m[0] + 31) >> 5; | ||
| 709 | const hi = x[mlen]; | ||
| 710 | if (mblr == 0) { | ||
| 711 | a0 = x[mlen]; | ||
| 712 | mem.copyBackwards(u32, x[2..][0 .. mlen - 1], x[1..][0 .. mlen - 1]); | ||
| 713 | x[1] = z; | ||
| 714 | a1 = x[mlen]; | ||
| 715 | b0 = m[mlen]; | ||
| 716 | } else { | ||
| 717 | a0 = ((x[mlen] << (31 - mblr)) | (x[mlen - 1] >> mblr)) & 0x7FFFFFFF; | ||
| 718 | mem.copyBackwards(u32, x[2..][0 .. mlen - 1], x[1..][0 .. mlen - 1]); | ||
| 719 | x[1] = z; | ||
| 720 | a1 = ((x[mlen] << (31 - mblr)) | (x[mlen - 1] >> mblr)) & 0x7FFFFFFF; | ||
| 721 | b0 = ((m[mlen] << (31 - mblr)) | (m[mlen - 1] >> mblr)) & 0x7FFFFFFF; | ||
| 722 | } | ||
| 723 | |||
| 724 | const g = div(a0 >> 1, a1 | (a0 << 31), b0); | ||
| 725 | const q = MUX(EQ(a0, b0), 0x7FFFFFFF, MUX(EQ(g, 0), 0, g -% 1)); | ||
| 726 | |||
| 727 | var cc: u32 = 0; | ||
| 728 | var tb: u32 = 1; | ||
| 729 | var u: usize = 1; | ||
| 730 | while (u <= mlen) : (u += 1) { | ||
| 731 | const mw = m[u]; | ||
| 732 | const zl = MUL31(mw, q) + cc; | ||
| 733 | cc = @truncate(u32, zl >> 31); | ||
| 734 | const zw = @truncate(u32, zl) & 0x7FFFFFFF; | ||
| 735 | const xw = x[u]; | ||
| 736 | var nxw = xw -% zw; | ||
| 737 | cc += nxw >> 31; | ||
| 738 | nxw &= 0x7FFFFFFF; | ||
| 739 | x[u] = nxw; | ||
| 740 | tb = MUX(EQ(nxw, mw), tb, GT(nxw, mw)); | ||
| 741 | } | ||
| 742 | |||
| 743 | const over = GT(cc, hi); | ||
| 744 | const under = ~over & (tb | LT(cc, hi)); | ||
| 745 | _ = add(len, x, m, over); | ||
| 746 | _ = sub(len, x, m, under); | ||
| 747 | } | ||
| 748 | |||
| 749 | fn to_monty(comptime len: usize, x: *[len]u32, m: [len]u32) void { | ||
| 750 | const mlen = (m[0] + 31) >> 5; | ||
| 751 | var k = mlen; | ||
| 752 | while (k > 0) : (k -= 1) { | ||
| 753 | muladd_small(len, x, 0, m); | ||
| 754 | } | ||
| 755 | } | ||
| 756 | |||
| 757 | fn modpow( | ||
| 758 | comptime Curve: type, | ||
| 759 | x: *[jacobian_len(Curve)]u32, | ||
| 760 | e: [Curve.point_len / 2]u8, | ||
| 761 | m0i: u32, | ||
| 762 | t1: *[jacobian_len(Curve)]u32, | ||
| 763 | t2: *[jacobian_len(Curve)]u32, | ||
| 764 | ) void { | ||
| 765 | comptime const jaclen = jacobian_len(Curve); | ||
| 766 | t1.* = x.*; | ||
| 767 | to_monty(jaclen, t1, Curve.P); | ||
| 768 | set_zero(jaclen, x, Curve.P[0]); | ||
| 769 | x[1] = 1; | ||
| 770 | comptime const bitlen = (Curve.point_len / 2) << 3; | ||
| 771 | var k: usize = 0; | ||
| 772 | while (k < bitlen) : (k += 1) { | ||
| 773 | const ctl = (e[Curve.point_len / 2 - 1 - (k >> 3)] >> (@truncate(u3, k & 7))) & 1; | ||
| 774 | montymul(Curve, t2, x.*, t1.*, Curve.P, m0i); | ||
| 775 | CCOPY(ctl, mem.asBytes(x), mem.asBytes(t2)); | ||
| 776 | montymul(Curve, t2, t1.*, t1.*, Curve.P, m0i); | ||
| 777 | t1.* = t2.*; | ||
| 778 | } | ||
| 779 | } | ||
| 780 | |||
| 781 | fn encode_jacobian_part(comptime Curve: type, dst: *[Curve.point_len / 2]u8, x: [jacobian_len(Curve)]u32) void { | ||
| 782 | const xlen = (x[0] + 31) >> 5; | ||
| 783 | |||
| 784 | var buf = @ptrToInt(dst) + Curve.point_len / 2; | ||
| 785 | var len: usize = Curve.point_len / 2; | ||
| 786 | var k: usize = 1; | ||
| 787 | var acc: u32 = 0; | ||
| 788 | var acc_len: u5 = 0; | ||
| 789 | while (len != 0) { | ||
| 790 | const w = if (k <= xlen) x[k] else 0; | ||
| 791 | k += 1; | ||
| 792 | if (acc_len == 0) { | ||
| 793 | acc = w; | ||
| 794 | acc_len = 31; | ||
| 795 | } else { | ||
| 796 | const z = acc | (w << acc_len); | ||
| 797 | acc_len -= 1; | ||
| 798 | acc = w >> (31 - acc_len); | ||
| 799 | if (len >= 4) { | ||
| 800 | buf -= 4; | ||
| 801 | len -= 4; | ||
| 802 | mem.writeIntBig(u32, @intToPtr([*]u8, buf)[0..4], z); | ||
| 803 | } else { | ||
| 804 | switch (len) { | ||
| 805 | 3 => { | ||
| 806 | @intToPtr(*u8, buf - 3).* = @truncate(u8, z >> 16); | ||
| 807 | @intToPtr(*u8, buf - 2).* = @truncate(u8, z >> 8); | ||
| 808 | }, | ||
| 809 | 2 => @intToPtr(*u8, buf - 2).* = @truncate(u8, z >> 8), | ||
| 810 | 1 => {}, | ||
| 811 | else => unreachable, | ||
| 812 | } | ||
| 813 | @intToPtr(*u8, buf - 1).* = @truncate(u8, z); | ||
| 814 | return; | ||
| 815 | } | ||
| 816 | } | ||
| 817 | } | ||
| 818 | } | ||
| 819 | |||
| 820 | fn montymul( | ||
| 821 | comptime Curve: type, | ||
| 822 | out: *[jacobian_len(Curve)]u32, | ||
| 823 | x: [jacobian_len(Curve)]u32, | ||
| 824 | y: [jacobian_len(Curve)]u32, | ||
| 825 | m: [jacobian_len(Curve)]u32, | ||
| 826 | m0i: u32, | ||
| 827 | ) void { | ||
| 828 | comptime const jaclen = jacobian_len(Curve); | ||
| 829 | const len = (m[0] + 31) >> 5; | ||
| 830 | const len4 = len & ~@as(usize, 3); | ||
| 831 | set_zero(jaclen, out, m[0]); | ||
| 832 | var dh: u32 = 0; | ||
| 833 | var u: usize = 0; | ||
| 834 | while (u < len) : (u += 1) { | ||
| 835 | const xu = x[u + 1]; | ||
| 836 | const f = MUL31_lo(out[1] + MUL31_lo(x[u + 1], y[1]), m0i); | ||
| 837 | |||
| 838 | var r: u64 = 0; | ||
| 839 | var v: usize = 0; | ||
| 840 | while (v < len4) : (v += 4) { | ||
| 841 | comptime var j = 1; | ||
| 842 | inline while (j <= 4) : (j += 1) { | ||
| 843 | const z = out[v + j] +% MUL31(xu, y[v + j]) +% MUL31(f, m[v + j]) +% r; | ||
| 844 | r = z >> 31; | ||
| 845 | out[v + j - 1] = @truncate(u32, z) & 0x7FFFFFFF; | ||
| 846 | } | ||
| 847 | } | ||
| 848 | while (v < len) : (v += 1) { | ||
| 849 | const z = out[v + 1] +% MUL31(xu, y[v + 1]) +% MUL31(f, m[v + 1]) +% r; | ||
| 850 | r = z >> 31; | ||
| 851 | out[v] = @truncate(u32, z) & 0x7FFFFFFF; | ||
| 852 | } | ||
| 853 | dh += @truncate(u32, r); | ||
| 854 | out[len] = dh & 0x7FFFFFFF; | ||
| 855 | dh >>= 31; | ||
| 856 | } | ||
| 857 | out[0] = m[0]; | ||
| 858 | const ctl = NEQ(dh, 0) | NOT(sub(jaclen, out, m, 0)); | ||
| 859 | _ = sub(jaclen, out, m, ctl); | ||
| 860 | } | ||
| 861 | |||
| 862 | fn add(comptime len: usize, a: *[len]u32, b: [len]u32, ctl: u32) u32 { | ||
| 863 | var u: usize = 1; | ||
| 864 | var cc: u32 = 0; | ||
| 865 | while (u < len) : (u += 1) { | ||
| 866 | const aw = a[u]; | ||
| 867 | const bw = b[u]; | ||
| 868 | const naw = aw +% bw +% cc; | ||
| 869 | cc = naw >> 31; | ||
| 870 | a[u] = MUX(ctl, naw & 0x7FFFFFFF, aw); | ||
| 871 | } | ||
| 872 | return cc; | ||
| 873 | } | ||
| 874 | |||
| 875 | fn sub(comptime len: usize, a: *[len]u32, b: [len]u32, ctl: u32) u32 { | ||
| 876 | var cc: u32 = 0; | ||
| 877 | const m = (a[0] + 63) >> 5; | ||
| 878 | var u: usize = 1; | ||
| 879 | while (u < m) : (u += 1) { | ||
| 880 | const aw = a[u]; | ||
| 881 | const bw = b[u]; | ||
| 882 | const naw = aw -% bw -% cc; | ||
| 883 | cc = naw >> 31; | ||
| 884 | a[u] = MUX(ctl, naw & 0x7FFFFFFF, aw); | ||
| 885 | } | ||
| 886 | return cc; | ||
| 887 | } | ||
| 888 | |||
| 889 | fn iszero(comptime len: usize, arr: [len]u32) u32 { | ||
| 890 | var z: u32 = 0; | ||
| 891 | var u: usize = len - 1; | ||
| 892 | while (u > 0) : (u -= 1) { | ||
| 893 | z |= arr[u]; | ||
| 894 | } | ||
| 895 | return ~(z | @bitCast(u32, -@bitCast(i32, z))) >> 31; | ||
| 896 | } | ||
| 897 | }; | ||
| 898 | |||
| 899 | test "elliptic curve functions with secp384r1 curve" { | ||
| 900 | { | ||
| 901 | // Decode to Jacobian then encode again with no operations | ||
| 902 | var P: ecc.Jacobian(ecc.SECP384R1) = undefined; | ||
| 903 | var res: u32 = ecc.decode_to_jacobian(ecc.SECP384R1, &P, ecc.SECP384R1.base_point); | ||
| 904 | var out: [96]u8 = undefined; | ||
| 905 | ecc.encode_from_jacobian(ecc.SECP384R1, &out, P); | ||
| 906 | std.testing.expectEqual(ecc.SECP384R1.base_point, out); | ||
| 907 | |||
| 908 | // Multiply by one, check that the result is still the base point | ||
| 909 | mem.set(u8, &out, 0); | ||
| 910 | ecc.point_mul(ecc.SECP384R1, &P, &[1]u8{1}); | ||
| 911 | ecc.encode_from_jacobian(ecc.SECP384R1, &out, P); | ||
| 912 | std.testing.expectEqual(ecc.SECP384R1.base_point, out); | ||
| 913 | } | ||
| 914 | |||
| 915 | { | ||
| 916 | // @TODO Remove this once std.crypto.rand works in .evented mode | ||
| 917 | var rand = blk: { | ||
| 918 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; | ||
| 919 | try std.os.getrandom(&seed); | ||
| 920 | break :blk &std.rand.DefaultCsprng.init(seed).random; | ||
| 921 | }; | ||
| 922 | |||
| 923 | // Derive a shared secret from a Diffie-Hellman key exchange | ||
| 924 | var seed: [48]u8 = undefined; | ||
| 925 | rand.bytes(&seed); | ||
| 926 | const kp1 = ecc.make_key_pair(ecc.SECP384R1, seed); | ||
| 927 | rand.bytes(&seed); | ||
| 928 | const kp2 = ecc.make_key_pair(ecc.SECP384R1, seed); | ||
| 929 | |||
| 930 | const shared1 = try ecc.scalarmult(ecc.SECP384R1, kp1.public_key, &kp2.secret_key); | ||
| 931 | const shared2 = try ecc.scalarmult(ecc.SECP384R1, kp2.public_key, &kp1.secret_key); | ||
| 932 | std.testing.expectEqual(shared1, shared2); | ||
| 933 | } | ||
| 934 | |||
| 935 | // @TODO Add tests with known points. | ||
| 936 | } | ||
libs/iguanatls/src/main.zig created+1499| ... | @@ -0,0 +1,1499 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const mem = std.mem; | ||
| 3 | const Allocator = mem.Allocator; | ||
| 4 | const Sha384 = std.crypto.hash.sha2.Sha384; | ||
| 5 | const Sha512 = std.crypto.hash.sha2.Sha512; | ||
| 6 | const Sha256 = std.crypto.hash.sha2.Sha256; | ||
| 7 | const Hmac256 = std.crypto.auth.hmac.sha2.HmacSha256; | ||
| 8 | |||
| 9 | pub const asn1 = @import("asn1.zig"); | ||
| 10 | pub const x509 = @import("x509.zig"); | ||
| 11 | pub const crypto = @import("crypto.zig"); | ||
| 12 | |||
| 13 | const ciphers = @import("ciphersuites.zig"); | ||
| 14 | pub const ciphersuites = ciphers.suites; | ||
| 15 | |||
| 16 | comptime { | ||
| 17 | std.testing.refAllDecls(x509); | ||
| 18 | std.testing.refAllDecls(asn1); | ||
| 19 | std.testing.refAllDecls(crypto); | ||
| 20 | } | ||
| 21 | |||
| 22 | fn handshake_record_length(reader: anytype) !usize { | ||
| 23 | return try record_length(0x16, reader); | ||
| 24 | } | ||
| 25 | |||
| 26 | pub fn record_length(t: u8, reader: anytype) !usize { | ||
| 27 | try check_record_type(t, reader); | ||
| 28 | var record_header: [4]u8 = undefined; | ||
| 29 | try reader.readNoEof(&record_header); | ||
| 30 | if (!mem.eql(u8, record_header[0..2], "\x03\x03") and !mem.eql(u8, record_header[0..2], "\x03\x01")) | ||
| 31 | return error.ServerInvalidVersion; | ||
| 32 | return mem.readIntSliceBig(u16, record_header[2..4]); | ||
| 33 | } | ||
| 34 | |||
| 35 | pub const ServerAlert = error{ | ||
| 36 | AlertCloseNotify, | ||
| 37 | AlertUnexpectedMessage, | ||
| 38 | AlertBadRecordMAC, | ||
| 39 | AlertDecryptionFailed, | ||
| 40 | AlertRecordOverflow, | ||
| 41 | AlertDecompressionFailure, | ||
| 42 | AlertHandshakeFailure, | ||
| 43 | AlertNoCertificate, | ||
| 44 | AlertBadCertificate, | ||
| 45 | AlertUnsupportedCertificate, | ||
| 46 | AlertCertificateRevoked, | ||
| 47 | AlertCertificateExpired, | ||
| 48 | AlertCertificateUnknown, | ||
| 49 | AlertIllegalParameter, | ||
| 50 | AlertUnknownCA, | ||
| 51 | AlertAccessDenied, | ||
| 52 | AlertDecodeError, | ||
| 53 | AlertDecryptError, | ||
| 54 | AlertExportRestriction, | ||
| 55 | AlertProtocolVersion, | ||
| 56 | AlertInsufficientSecurity, | ||
| 57 | AlertInternalError, | ||
| 58 | AlertUserCanceled, | ||
| 59 | AlertNoRenegotiation, | ||
| 60 | AlertUnsupportedExtension, | ||
| 61 | }; | ||
| 62 | |||
| 63 | fn check_record_type( | ||
| 64 | expected: u8, | ||
| 65 | reader: anytype, | ||
| 66 | ) (@TypeOf(reader).Error || ServerAlert || error{ ServerMalformedResponse, EndOfStream })!void { | ||
| 67 | const record_type = try reader.readByte(); | ||
| 68 | // Alert | ||
| 69 | if (record_type == 0x15) { | ||
| 70 | // Skip SSL version, length of record | ||
| 71 | try reader.skipBytes(4, .{}); | ||
| 72 | |||
| 73 | const severity = try reader.readByte(); | ||
| 74 | const err_num = try reader.readByte(); | ||
| 75 | return switch (err_num) { | ||
| 76 | 0 => error.AlertCloseNotify, | ||
| 77 | 10 => error.AlertUnexpectedMessage, | ||
| 78 | 20 => error.AlertBadRecordMAC, | ||
| 79 | 21 => error.AlertDecryptionFailed, | ||
| 80 | 22 => error.AlertRecordOverflow, | ||
| 81 | 30 => error.AlertDecompressionFailure, | ||
| 82 | 40 => error.AlertHandshakeFailure, | ||
| 83 | 41 => error.AlertNoCertificate, | ||
| 84 | 42 => error.AlertBadCertificate, | ||
| 85 | 43 => error.AlertUnsupportedCertificate, | ||
| 86 | 44 => error.AlertCertificateRevoked, | ||
| 87 | 45 => error.AlertCertificateExpired, | ||
| 88 | 46 => error.AlertCertificateUnknown, | ||
| 89 | 47 => error.AlertIllegalParameter, | ||
| 90 | 48 => error.AlertUnknownCA, | ||
| 91 | 49 => error.AlertAccessDenied, | ||
| 92 | 50 => error.AlertDecodeError, | ||
| 93 | 51 => error.AlertDecryptError, | ||
| 94 | 60 => error.AlertExportRestriction, | ||
| 95 | 70 => error.AlertProtocolVersion, | ||
| 96 | 71 => error.AlertInsufficientSecurity, | ||
| 97 | 80 => error.AlertInternalError, | ||
| 98 | 90 => error.AlertUserCanceled, | ||
| 99 | 100 => error.AlertNoRenegotiation, | ||
| 100 | 110 => error.AlertUnsupportedExtension, | ||
| 101 | else => error.ServerMalformedResponse, | ||
| 102 | }; | ||
| 103 | } | ||
| 104 | if (record_type != expected) | ||
| 105 | return error.ServerMalformedResponse; | ||
| 106 | } | ||
| 107 | |||
| 108 | fn Sha256Reader(comptime Reader: anytype) type { | ||
| 109 | const State = struct { | ||
| 110 | sha256: *Sha256, | ||
| 111 | reader: Reader, | ||
| 112 | }; | ||
| 113 | const S = struct { | ||
| 114 | pub fn read(state: State, buffer: []u8) Reader.Error!usize { | ||
| 115 | const amt = try state.reader.read(buffer); | ||
| 116 | if (amt != 0) { | ||
| 117 | state.sha256.update(buffer[0..amt]); | ||
| 118 | } | ||
| 119 | return amt; | ||
| 120 | } | ||
| 121 | }; | ||
| 122 | return std.io.Reader(State, Reader.Error, S.read); | ||
| 123 | } | ||
| 124 | |||
| 125 | fn sha256_reader(sha256: *Sha256, reader: anytype) Sha256Reader(@TypeOf(reader)) { | ||
| 126 | return .{ .context = .{ .sha256 = sha256, .reader = reader } }; | ||
| 127 | } | ||
| 128 | |||
| 129 | fn Sha256Writer(comptime Writer: anytype) type { | ||
| 130 | const State = struct { | ||
| 131 | sha256: *Sha256, | ||
| 132 | writer: Writer, | ||
| 133 | }; | ||
| 134 | const S = struct { | ||
| 135 | pub fn write(state: State, buffer: []const u8) Writer.Error!usize { | ||
| 136 | const amt = try state.writer.write(buffer); | ||
| 137 | if (amt != 0) { | ||
| 138 | state.sha256.update(buffer[0..amt]); | ||
| 139 | } | ||
| 140 | return amt; | ||
| 141 | } | ||
| 142 | }; | ||
| 143 | return std.io.Writer(State, Writer.Error, S.write); | ||
| 144 | } | ||
| 145 | |||
| 146 | fn sha256_writer(sha256: *Sha256, writer: anytype) Sha256Writer(@TypeOf(writer)) { | ||
| 147 | return .{ .context = .{ .sha256 = sha256, .writer = writer } }; | ||
| 148 | } | ||
| 149 | |||
| 150 | fn CertificateReaderState(comptime Reader: type) type { | ||
| 151 | return struct { | ||
| 152 | reader: Reader, | ||
| 153 | length: usize, | ||
| 154 | idx: usize = 0, | ||
| 155 | }; | ||
| 156 | } | ||
| 157 | |||
| 158 | fn CertificateReader(comptime Reader: type) type { | ||
| 159 | const S = struct { | ||
| 160 | pub fn read(state: *CertificateReaderState(Reader), buffer: []u8) Reader.Error!usize { | ||
| 161 | const out_bytes = std.math.min(buffer.len, state.length - state.idx); | ||
| 162 | const res = try state.reader.readAll(buffer[0..out_bytes]); | ||
| 163 | state.idx += res; | ||
| 164 | return res; | ||
| 165 | } | ||
| 166 | }; | ||
| 167 | |||
| 168 | return std.io.Reader(*CertificateReaderState(Reader), Reader.Error, S.read); | ||
| 169 | } | ||
| 170 | |||
| 171 | pub const CertificateVerifier = union(enum) { | ||
| 172 | none, | ||
| 173 | function: anytype, | ||
| 174 | default, | ||
| 175 | }; | ||
| 176 | |||
| 177 | pub fn CertificateVerifierReader(comptime Reader: type) type { | ||
| 178 | return CertificateReader(Sha256Reader(Reader)); | ||
| 179 | } | ||
| 180 | |||
| 181 | pub fn ClientConnectError(comptime verifier: CertificateVerifier, comptime Reader: type, comptime Writer: type) type { | ||
| 182 | const Additional = error{ | ||
| 183 | ServerInvalidVersion, | ||
| 184 | ServerMalformedResponse, | ||
| 185 | EndOfStream, | ||
| 186 | ServerInvalidCipherSuite, | ||
| 187 | ServerInvalidCompressionMethod, | ||
| 188 | ServerInvalidRenegotiationData, | ||
| 189 | ServerInvalidECPointCompression, | ||
| 190 | ServerInvalidProtocol, | ||
| 191 | ServerInvalidExtension, | ||
| 192 | ServerInvalidCurve, | ||
| 193 | ServerInvalidSignature, | ||
| 194 | ServerInvalidSignatureAlgorithm, | ||
| 195 | ServerAuthenticationFailed, | ||
| 196 | ServerInvalidVerifyData, | ||
| 197 | PreMasterGenerationFailed, | ||
| 198 | OutOfMemory, | ||
| 199 | }; | ||
| 200 | const err_msg = "Certificate verifier function cannot be generic, use CertificateVerifierReader to get the reader argument type"; | ||
| 201 | return Reader.Error || Writer.Error || ServerAlert || Additional || switch (verifier) { | ||
| 202 | .none => error{}, | ||
| 203 | .function => |f| @typeInfo(@typeInfo(@TypeOf(f)).Fn.return_type orelse | ||
| 204 | @compileError(err_msg)).ErrorUnion.error_set || error{CertificateVerificationFailed}, | ||
| 205 | .default => error{CertificateVerificationFailed}, | ||
| 206 | }; | ||
| 207 | } | ||
| 208 | |||
| 209 | // See http://howardhinnant.github.io/date_algorithms.html | ||
| 210 | // Timestamp in seconds, only supports A.D. dates | ||
| 211 | fn unix_timestamp_from_civil_date(year: u16, month: u8, day: u8) i64 { | ||
| 212 | var y: i64 = year; | ||
| 213 | if (month <= 2) y -= 1; | ||
| 214 | const era = @divTrunc(y, 400); | ||
| 215 | const yoe = y - era * 400; // [0, 399] | ||
| 216 | const doy = @divTrunc((153 * (month + (if (month > 2) @as(i64, -3) else 9)) + 2), 5) + day - 1; // [0, 365] | ||
| 217 | const doe = yoe * 365 + @divTrunc(yoe, 4) - @divTrunc(yoe, 100) + doy; // [0, 146096] | ||
| 218 | return (era * 146097 + doe - 719468) * 86400; | ||
| 219 | } | ||
| 220 | |||
| 221 | fn read_der_utc_timestamp(reader: anytype) !i64 { | ||
| 222 | var buf: [17]u8 = undefined; | ||
| 223 | |||
| 224 | const tag = try reader.readByte(); | ||
| 225 | if (tag != 0x17) | ||
| 226 | return error.CertificateVerificationFailed; | ||
| 227 | const len = try asn1.der.parse_length(reader); | ||
| 228 | if (len > 17) | ||
| 229 | return error.CertificateVerificationFailed; | ||
| 230 | |||
| 231 | try reader.readNoEof(buf[0..len]); | ||
| 232 | const year = std.fmt.parseUnsigned(u16, buf[0..2], 10) catch | ||
| 233 | return error.CertificateVerificationFailed; | ||
| 234 | const month = std.fmt.parseUnsigned(u8, buf[2..4], 10) catch | ||
| 235 | return error.CertificateVerificationFailed; | ||
| 236 | const day = std.fmt.parseUnsigned(u8, buf[4..6], 10) catch | ||
| 237 | return error.CertificateVerificationFailed; | ||
| 238 | |||
| 239 | var time = unix_timestamp_from_civil_date(2000 + year, month, day); | ||
| 240 | time += (std.fmt.parseUnsigned(i64, buf[6..8], 10) catch | ||
| 241 | return error.CertificateVerificationFailed) * 3600; | ||
| 242 | time += (std.fmt.parseUnsigned(i64, buf[8..10], 10) catch | ||
| 243 | return error.CertificateVerificationFailed) * 60; | ||
| 244 | |||
| 245 | if (buf[len - 1] == 'Z') { | ||
| 246 | if (len == 13) { | ||
| 247 | time += std.fmt.parseUnsigned(u8, buf[10..12], 10) catch | ||
| 248 | return error.CertificateVerificationFailed; | ||
| 249 | } else if (len != 11) { | ||
| 250 | return error.CertificateVerificationFailed; | ||
| 251 | } | ||
| 252 | } else { | ||
| 253 | if (len == 15) { | ||
| 254 | if (buf[10] != '+' and buf[10] != '-') | ||
| 255 | return error.CertificateVerificationFailed; | ||
| 256 | |||
| 257 | var additional = (std.fmt.parseUnsigned(i64, buf[11..13], 10) catch | ||
| 258 | return error.CertificateVerificationFailed) * 3600; | ||
| 259 | additional += (std.fmt.parseUnsigned(i64, buf[13..15], 10) catch | ||
| 260 | return error.CertificateVerificationFailed) * 60; | ||
| 261 | |||
| 262 | time += if (buf[10] == '+') -additional else additional; | ||
| 263 | } else if (len == 17) { | ||
| 264 | if (buf[12] != '+' and buf[12] != '-') | ||
| 265 | return error.CertificateVerificationFailed; | ||
| 266 | time += std.fmt.parseUnsigned(u8, buf[10..12], 10) catch | ||
| 267 | return error.CertificateVerificationFailed; | ||
| 268 | |||
| 269 | var additional = (std.fmt.parseUnsigned(i64, buf[13..15], 10) catch | ||
| 270 | return error.CertificateVerificationFailed) * 3600; | ||
| 271 | additional += (std.fmt.parseUnsigned(i64, buf[15..17], 10) catch | ||
| 272 | return error.CertificateVerificationFailed) * 60; | ||
| 273 | |||
| 274 | time += if (buf[12] == '+') -additional else additional; | ||
| 275 | } else return error.CertificateVerificationFailed; | ||
| 276 | } | ||
| 277 | return time; | ||
| 278 | } | ||
| 279 | |||
| 280 | fn check_cert_timestamp(time: i64, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 281 | if (time < (try read_der_utc_timestamp(reader))) | ||
| 282 | return error.CertificateVerificationFailed; | ||
| 283 | if (time > (try read_der_utc_timestamp(reader))) | ||
| 284 | return error.CertificateVerificationFailed; | ||
| 285 | } | ||
| 286 | |||
| 287 | fn add_cert_subject_dn(state: *VerifierCaptureState, _: u8, length: usize, reader: anytype) !void { | ||
| 288 | state.list.items[state.list.items.len - 1].dn = state.fbs.buffer[state.fbs.pos .. state.fbs.pos + length]; | ||
| 289 | } | ||
| 290 | |||
| 291 | fn add_cert_public_key(state: *VerifierCaptureState, _: u8, length: usize, reader: anytype) !void { | ||
| 292 | state.list.items[state.list.items.len - 1].public_key = x509.parse_public_key( | ||
| 293 | state.allocator, | ||
| 294 | reader, | ||
| 295 | ) catch |err| switch (err) { | ||
| 296 | error.MalformedDER => return error.CertificateVerificationFailed, | ||
| 297 | else => |e| return e, | ||
| 298 | }; | ||
| 299 | } | ||
| 300 | |||
| 301 | fn add_server_cert(state: *VerifierCaptureState, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 302 | const is_ca = state.list.items.len != 0; | ||
| 303 | |||
| 304 | const encoded_length = asn1.der.encode_length(length).slice(); | ||
| 305 | const cert_bytes = try state.allocator.alloc(u8, length + 1 + encoded_length.len); | ||
| 306 | errdefer state.allocator.free(cert_bytes); | ||
| 307 | cert_bytes[0] = tag_byte; | ||
| 308 | mem.copy(u8, cert_bytes[1 .. 1 + encoded_length.len], encoded_length); | ||
| 309 | |||
| 310 | try reader.readNoEof(cert_bytes[1 + encoded_length.len ..]); | ||
| 311 | (try state.list.addOne(state.allocator)).* = .{ | ||
| 312 | .is_ca = is_ca, | ||
| 313 | .bytes = cert_bytes, | ||
| 314 | .dn = undefined, | ||
| 315 | .public_key = undefined, | ||
| 316 | .signature = asn1.BitString{ .data = &[0]u8{}, .bit_len = 0 }, | ||
| 317 | .signature_algorithm = undefined, | ||
| 318 | }; | ||
| 319 | errdefer state.allocator.free(state.list.items[state.list.items.len - 1].signature.data); | ||
| 320 | |||
| 321 | const schema = .{ | ||
| 322 | .sequence, | ||
| 323 | .{ | ||
| 324 | .{ .context_specific, 0 }, // version | ||
| 325 | .{.int}, // serialNumber | ||
| 326 | .{.sequence}, // signature | ||
| 327 | .{.sequence}, // issuer | ||
| 328 | .{ .capture, 0, .sequence }, // validity | ||
| 329 | .{ .capture, 1, .sequence }, // subject | ||
| 330 | .{ .capture, 2, .sequence }, // subjectPublicKeyInfo | ||
| 331 | .{ .optional, .context_specific, 1 }, // issuerUniqueID | ||
| 332 | .{ .optional, .context_specific, 2 }, // subjectUniqueID | ||
| 333 | .{ .optional, .context_specific, 3 }, // extensions | ||
| 334 | }, | ||
| 335 | }; | ||
| 336 | |||
| 337 | const captures = .{ | ||
| 338 | std.time.timestamp(), check_cert_timestamp, | ||
| 339 | state, add_cert_subject_dn, | ||
| 340 | state, add_cert_public_key, | ||
| 341 | }; | ||
| 342 | |||
| 343 | var fbs = std.io.fixedBufferStream(@as([]const u8, cert_bytes[1 + encoded_length.len ..])); | ||
| 344 | state.fbs = &fbs; | ||
| 345 | |||
| 346 | asn1.der.parse_schema_tag_len(tag_byte, length, schema, captures, fbs.reader()) catch |err| switch (err) { | ||
| 347 | error.InvalidLength, | ||
| 348 | error.InvalidTag, | ||
| 349 | error.InvalidContainerLength, | ||
| 350 | error.DoesNotMatchSchema, | ||
| 351 | => return error.CertificateVerificationFailed, | ||
| 352 | else => |e| return e, | ||
| 353 | }; | ||
| 354 | } | ||
| 355 | |||
| 356 | fn set_signature_algorithm(state: *VerifierCaptureState, _: u8, length: usize, reader: anytype) !void { | ||
| 357 | const oid_tag = try reader.readByte(); | ||
| 358 | if (oid_tag != 0x06) | ||
| 359 | return error.CertificateVerificationFailed; | ||
| 360 | |||
| 361 | const oid_length = try asn1.der.parse_length(reader); | ||
| 362 | if (oid_length == 9) { | ||
| 363 | var oid_bytes: [9]u8 = undefined; | ||
| 364 | try reader.readNoEof(&oid_bytes); | ||
| 365 | |||
| 366 | const cert = &state.list.items[state.list.items.len - 1]; | ||
| 367 | if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 })) { | ||
| 368 | cert.signature_algorithm = .rsa; | ||
| 369 | } else if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x04 })) { | ||
| 370 | cert.signature_algorithm = .rsa_md5; | ||
| 371 | } else if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05 })) { | ||
| 372 | cert.signature_algorithm = .rsa_sha1; | ||
| 373 | } else if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B })) { | ||
| 374 | cert.signature_algorithm = .rsa_sha256; | ||
| 375 | } else if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C })) { | ||
| 376 | cert.signature_algorithm = .rsa_sha384; | ||
| 377 | } else if (mem.eql(u8, &oid_bytes, &[_]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D })) { | ||
| 378 | cert.signature_algorithm = .rsa_sha512; | ||
| 379 | } else { | ||
| 380 | return error.CertificateVerificationFailed; | ||
| 381 | } | ||
| 382 | return; | ||
| 383 | } else if (oid_length == 10) { | ||
| 384 | // @TODO | ||
| 385 | // ECDSA + <Hash> algorithms | ||
| 386 | } | ||
| 387 | |||
| 388 | return error.CertificateVerificationFailed; | ||
| 389 | } | ||
| 390 | |||
| 391 | fn set_signature_value(state: *VerifierCaptureState, tag: u8, length: usize, reader: anytype) !void { | ||
| 392 | const unused_bits = try reader.readByte(); | ||
| 393 | const bit_count = (length - 1) * 8 - unused_bits; | ||
| 394 | const signature_bytes = try state.allocator.alloc(u8, length - 1); | ||
| 395 | errdefer state.allocator.free(signature_bytes); | ||
| 396 | try reader.readNoEof(signature_bytes); | ||
| 397 | state.list.items[state.list.items.len - 1].signature = .{ | ||
| 398 | .data = signature_bytes, | ||
| 399 | .bit_len = bit_count, | ||
| 400 | }; | ||
| 401 | } | ||
| 402 | |||
| 403 | fn verify_signature( | ||
| 404 | allocator: *Allocator, | ||
| 405 | signature_algorithm: SignatureAlgorithm, | ||
| 406 | signature: asn1.BitString, | ||
| 407 | hash: []const u8, | ||
| 408 | public_key: x509.PublicKey, | ||
| 409 | ) !bool { | ||
| 410 | // @TODO ECDSA algorithms | ||
| 411 | if (public_key != .rsa) return false; | ||
| 412 | const prefix: []const u8 = switch (signature_algorithm) { | ||
| 413 | // Deprecated hash algos | ||
| 414 | .rsa_md5, .rsa_sha1 => return false, | ||
| 415 | // @TODO How does this one work? | ||
| 416 | .rsa => return false, | ||
| 417 | .rsa_sha256 => &[_]u8{ | ||
| 418 | 0x30, 0x31, 0x30, 0x0d, 0x06, | ||
| 419 | 0x09, 0x60, 0x86, 0x48, 0x01, | ||
| 420 | 0x65, 0x03, 0x04, 0x02, 0x01, | ||
| 421 | 0x05, 0x00, 0x04, 0x20, | ||
| 422 | }, | ||
| 423 | .rsa_sha384 => &[_]u8{ | ||
| 424 | 0x30, 0x41, 0x30, 0x0d, 0x06, | ||
| 425 | 0x09, 0x60, 0x86, 0x48, 0x01, | ||
| 426 | 0x65, 0x03, 0x04, 0x02, 0x02, | ||
| 427 | 0x05, 0x00, 0x04, 0x30, | ||
| 428 | }, | ||
| 429 | .rsa_sha512 => &[_]u8{ | ||
| 430 | 0x30, 0x51, 0x30, 0x0d, 0x06, | ||
| 431 | 0x09, 0x60, 0x86, 0x48, 0x01, | ||
| 432 | 0x65, 0x03, 0x04, 0x02, 0x03, | ||
| 433 | 0x05, 0x00, 0x04, 0x40, | ||
| 434 | }, | ||
| 435 | }; | ||
| 436 | |||
| 437 | // RSA hash verification with PKCS 1 V1_5 padding | ||
| 438 | const modulus = std.math.big.int.Const{ .limbs = public_key.rsa.modulus, .positive = true }; | ||
| 439 | const exponent = std.math.big.int.Const{ .limbs = public_key.rsa.exponent, .positive = true }; | ||
| 440 | if (modulus.bitCountAbs() != signature.bit_len) | ||
| 441 | return false; | ||
| 442 | |||
| 443 | // encrypt the signature using the RSA key | ||
| 444 | // @TODO better algorithm, this is probably slow as hell | ||
| 445 | var encrypted_signature = try std.math.big.int.Managed.initSet(allocator, @as(usize, 1)); | ||
| 446 | defer encrypted_signature.deinit(); | ||
| 447 | |||
| 448 | { | ||
| 449 | var curr_exponent = try exponent.toManaged(allocator); | ||
| 450 | defer curr_exponent.deinit(); | ||
| 451 | |||
| 452 | const curr_base_limbs = try allocator.alloc( | ||
| 453 | usize, | ||
| 454 | std.math.divCeil(usize, signature.data.len, @sizeOf(usize)) catch unreachable, | ||
| 455 | ); | ||
| 456 | const curr_base_limb_bytes = @ptrCast([*]u8, curr_base_limbs)[0..signature.data.len]; | ||
| 457 | mem.copy(u8, curr_base_limb_bytes, signature.data); | ||
| 458 | mem.reverse(u8, curr_base_limb_bytes); | ||
| 459 | var curr_base = (std.math.big.int.Mutable{ | ||
| 460 | .limbs = curr_base_limbs, | ||
| 461 | .positive = true, | ||
| 462 | .len = curr_base_limbs.len, | ||
| 463 | }).toManaged(allocator); | ||
| 464 | defer curr_base.deinit(); | ||
| 465 | |||
| 466 | // encrypted = signature ^ key.exponent MOD key.modulus | ||
| 467 | while (curr_exponent.toConst().orderAgainstScalar(0) == .gt) { | ||
| 468 | if (curr_exponent.isOdd()) { | ||
| 469 | try encrypted_signature.ensureMulCapacity(encrypted_signature.toConst(), curr_base.toConst()); | ||
| 470 | try encrypted_signature.mul(encrypted_signature.toConst(), curr_base.toConst()); | ||
| 471 | try llmod(&encrypted_signature, modulus); | ||
| 472 | } | ||
| 473 | try curr_base.sqr(curr_base.toConst()); | ||
| 474 | try llmod(&curr_base, modulus); | ||
| 475 | try curr_exponent.shiftRight(curr_exponent, 1); | ||
| 476 | } | ||
| 477 | try llmod(&encrypted_signature, modulus); | ||
| 478 | } | ||
| 479 | // EMSA-PKCS1-V1_5-ENCODE | ||
| 480 | if (encrypted_signature.limbs.len * @sizeOf(usize) < signature.data.len) | ||
| 481 | return false; | ||
| 482 | |||
| 483 | const enc_buf = @ptrCast([*]u8, encrypted_signature.limbs.ptr)[0..signature.data.len]; | ||
| 484 | mem.reverse(u8, enc_buf); | ||
| 485 | |||
| 486 | if (enc_buf[0] != 0x00 or enc_buf[1] != 0x01) | ||
| 487 | return false; | ||
| 488 | if (!mem.endsWith(u8, enc_buf, hash)) | ||
| 489 | return false; | ||
| 490 | if (!mem.endsWith(u8, enc_buf[0 .. enc_buf.len - hash.len], prefix)) | ||
| 491 | return false; | ||
| 492 | if (enc_buf[enc_buf.len - hash.len - prefix.len - 1] != 0x00) | ||
| 493 | return false; | ||
| 494 | for (enc_buf[2 .. enc_buf.len - hash.len - prefix.len - 1]) |c| { | ||
| 495 | if (c != 0xff) return false; | ||
| 496 | } | ||
| 497 | |||
| 498 | return true; | ||
| 499 | } | ||
| 500 | |||
| 501 | fn certificate_verify_signature( | ||
| 502 | allocator: *Allocator, | ||
| 503 | signature_algorithm: SignatureAlgorithm, | ||
| 504 | signature: asn1.BitString, | ||
| 505 | bytes: []const u8, | ||
| 506 | public_key: x509.PublicKey, | ||
| 507 | ) !bool { | ||
| 508 | // @TODO ECDSA algorithms | ||
| 509 | if (public_key != .rsa) return false; | ||
| 510 | |||
| 511 | var hash_buf: [64]u8 = undefined; | ||
| 512 | var hash: []u8 = undefined; | ||
| 513 | |||
| 514 | switch (signature_algorithm) { | ||
| 515 | // Deprecated hash algos | ||
| 516 | .rsa_md5, .rsa_sha1 => return false, | ||
| 517 | // @TODO How does this one work? | ||
| 518 | .rsa => return false, | ||
| 519 | |||
| 520 | .rsa_sha256 => { | ||
| 521 | Sha256.hash(bytes, hash_buf[0..32], .{}); | ||
| 522 | hash = hash_buf[0..32]; | ||
| 523 | }, | ||
| 524 | .rsa_sha384 => { | ||
| 525 | Sha384.hash(bytes, hash_buf[0..48], .{}); | ||
| 526 | hash = hash_buf[0..48]; | ||
| 527 | }, | ||
| 528 | .rsa_sha512 => { | ||
| 529 | Sha512.hash(bytes, hash_buf[0..64], .{}); | ||
| 530 | hash = &hash_buf; | ||
| 531 | }, | ||
| 532 | } | ||
| 533 | return try verify_signature(allocator, signature_algorithm, signature, hash, public_key); | ||
| 534 | } | ||
| 535 | |||
| 536 | // res = res mod N | ||
| 537 | fn llmod(res: *std.math.big.int.Managed, n: std.math.big.int.Const) !void { | ||
| 538 | var temp = try std.math.big.int.Managed.init(res.allocator); | ||
| 539 | defer temp.deinit(); | ||
| 540 | try temp.divTrunc(res, res.toConst(), n); | ||
| 541 | } | ||
| 542 | |||
| 543 | const SignatureAlgorithm = enum { | ||
| 544 | rsa, | ||
| 545 | rsa_md5, | ||
| 546 | rsa_sha1, | ||
| 547 | rsa_sha256, | ||
| 548 | rsa_sha384, | ||
| 549 | rsa_sha512, | ||
| 550 | // @TODO ECDSA versions | ||
| 551 | }; | ||
| 552 | |||
| 553 | const ServerCertificate = struct { | ||
| 554 | bytes: []const u8, | ||
| 555 | dn: []const u8, | ||
| 556 | public_key: x509.PublicKey, | ||
| 557 | signature: asn1.BitString, | ||
| 558 | signature_algorithm: SignatureAlgorithm, | ||
| 559 | is_ca: bool, | ||
| 560 | }; | ||
| 561 | |||
| 562 | const VerifierCaptureState = struct { | ||
| 563 | list: std.ArrayListUnmanaged(ServerCertificate), | ||
| 564 | allocator: *Allocator, | ||
| 565 | // Used in `add_server_cert` to avoid an extra allocation | ||
| 566 | fbs: *std.io.FixedBufferStream([]const u8), | ||
| 567 | }; | ||
| 568 | |||
| 569 | pub fn default_cert_verifier( | ||
| 570 | allocator: *std.mem.Allocator, | ||
| 571 | reader: anytype, | ||
| 572 | certs_bytes: usize, | ||
| 573 | trusted_certificates: []const x509.TrustAnchor, | ||
| 574 | hostname: []const u8, | ||
| 575 | ) !x509.PublicKey { | ||
| 576 | var capture_state = VerifierCaptureState{ | ||
| 577 | .list = try std.ArrayListUnmanaged(ServerCertificate).initCapacity(allocator, 3), | ||
| 578 | .allocator = allocator, | ||
| 579 | .fbs = undefined, | ||
| 580 | }; | ||
| 581 | defer { | ||
| 582 | for (capture_state.list.items) |cert| { | ||
| 583 | cert.public_key.deinit(allocator); | ||
| 584 | allocator.free(cert.bytes); | ||
| 585 | allocator.free(cert.signature.data); | ||
| 586 | } | ||
| 587 | capture_state.list.deinit(allocator); | ||
| 588 | } | ||
| 589 | |||
| 590 | const schema = .{ | ||
| 591 | .sequence, .{ | ||
| 592 | // tbsCertificate | ||
| 593 | .{ .capture, 0, .sequence }, | ||
| 594 | // signatureAlgorithm | ||
| 595 | .{ .capture, 1, .sequence }, | ||
| 596 | // signatureValue | ||
| 597 | .{ .capture, 2, .bit_string }, | ||
| 598 | }, | ||
| 599 | }; | ||
| 600 | const captures = .{ | ||
| 601 | &capture_state, add_server_cert, | ||
| 602 | &capture_state, set_signature_algorithm, | ||
| 603 | &capture_state, set_signature_value, | ||
| 604 | }; | ||
| 605 | |||
| 606 | var bytes_read: u24 = 0; | ||
| 607 | while (bytes_read < certs_bytes) { | ||
| 608 | const cert_length = try reader.readIntBig(u24); | ||
| 609 | |||
| 610 | asn1.der.parse_schema(schema, captures, reader) catch |err| switch (err) { | ||
| 611 | error.InvalidLength, | ||
| 612 | error.InvalidTag, | ||
| 613 | error.InvalidContainerLength, | ||
| 614 | error.DoesNotMatchSchema, | ||
| 615 | => return error.CertificateVerificationFailed, | ||
| 616 | else => |e| return e, | ||
| 617 | }; | ||
| 618 | |||
| 619 | bytes_read += 3 + cert_length; | ||
| 620 | } | ||
| 621 | if (bytes_read != certs_bytes) | ||
| 622 | return error.CertificateVerificationFailed; | ||
| 623 | |||
| 624 | const chain = capture_state.list.items; | ||
| 625 | var i: usize = 0; | ||
| 626 | while (i < chain.len - 1) : (i += 1) { | ||
| 627 | if (!try certificate_verify_signature( | ||
| 628 | allocator, | ||
| 629 | chain[i].signature_algorithm, | ||
| 630 | chain[i].signature, | ||
| 631 | chain[i].bytes, | ||
| 632 | chain[i + 1].public_key, | ||
| 633 | )) { | ||
| 634 | return error.CertificateVerificationFailed; | ||
| 635 | } | ||
| 636 | } | ||
| 637 | |||
| 638 | for (chain) |cert| { | ||
| 639 | for (trusted_certificates) |trusted| { | ||
| 640 | // Try to find an exact match to a trusted certificate | ||
| 641 | if (cert.is_ca == trusted.is_ca and mem.eql(u8, cert.dn, trusted.dn) and | ||
| 642 | cert.public_key.eql(trusted.public_key)) | ||
| 643 | { | ||
| 644 | const key = chain[0].public_key; | ||
| 645 | chain[0].public_key = x509.PublicKey{ | ||
| 646 | .ec = .{ | ||
| 647 | .id = undefined, | ||
| 648 | .curve_point = &[0]u8{}, | ||
| 649 | }, | ||
| 650 | }; | ||
| 651 | return key; | ||
| 652 | } | ||
| 653 | |||
| 654 | if (!trusted.is_ca) | ||
| 655 | continue; | ||
| 656 | |||
| 657 | if (try certificate_verify_signature( | ||
| 658 | allocator, | ||
| 659 | cert.signature_algorithm, | ||
| 660 | cert.signature, | ||
| 661 | cert.bytes, | ||
| 662 | trusted.public_key, | ||
| 663 | )) { | ||
| 664 | const key = chain[0].public_key; | ||
| 665 | chain[0].public_key = x509.PublicKey{ | ||
| 666 | .ec = .{ | ||
| 667 | .id = undefined, | ||
| 668 | .curve_point = &[0]u8{}, | ||
| 669 | }, | ||
| 670 | }; | ||
| 671 | return key; | ||
| 672 | } | ||
| 673 | } | ||
| 674 | } | ||
| 675 | return error.CertificateVerificationFailed; | ||
| 676 | } | ||
| 677 | |||
| 678 | pub fn extract_cert_public_key(allocator: *Allocator, reader: anytype, length: usize) !x509.PublicKey { | ||
| 679 | const CaptureState = struct { | ||
| 680 | pub_key: x509.PublicKey, | ||
| 681 | allocator: *Allocator, | ||
| 682 | }; | ||
| 683 | var capture_state = CaptureState{ | ||
| 684 | .pub_key = undefined, | ||
| 685 | .allocator = allocator, | ||
| 686 | }; | ||
| 687 | |||
| 688 | var pub_key: x509.PublicKey = undefined; | ||
| 689 | const schema = .{ | ||
| 690 | .sequence, .{ | ||
| 691 | // tbsCertificate | ||
| 692 | .{ | ||
| 693 | .sequence, | ||
| 694 | .{ | ||
| 695 | .{ .context_specific, 0 }, // version | ||
| 696 | .{.int}, // serialNumber | ||
| 697 | .{.sequence}, // signature | ||
| 698 | .{.sequence}, // issuer | ||
| 699 | .{.sequence}, // validity | ||
| 700 | .{.sequence}, // subject | ||
| 701 | .{ .capture, 0, .sequence }, // subjectPublicKeyInfo | ||
| 702 | .{ .optional, .context_specific, 1 }, // issuerUniqueID | ||
| 703 | .{ .optional, .context_specific, 2 }, // subjectUniqueID | ||
| 704 | .{ .optional, .context_specific, 3 }, // extensions | ||
| 705 | }, | ||
| 706 | }, | ||
| 707 | // signatureAlgorithm | ||
| 708 | .{.sequence}, | ||
| 709 | // signatureValue | ||
| 710 | .{.bit_string}, | ||
| 711 | }, | ||
| 712 | }; | ||
| 713 | const captures = .{ | ||
| 714 | &capture_state, struct { | ||
| 715 | fn f(state: *CaptureState, tag: u8, _: usize, subreader: anytype) !void { | ||
| 716 | state.pub_key = x509.parse_public_key(state.allocator, subreader) catch |err| switch (err) { | ||
| 717 | error.MalformedDER => return error.ServerMalformedResponse, | ||
| 718 | else => |e| return e, | ||
| 719 | }; | ||
| 720 | } | ||
| 721 | }.f, | ||
| 722 | }; | ||
| 723 | |||
| 724 | const cert_length = try reader.readIntBig(u24); | ||
| 725 | asn1.der.parse_schema(schema, captures, reader) catch |err| switch (err) { | ||
| 726 | error.InvalidLength, | ||
| 727 | error.InvalidTag, | ||
| 728 | error.InvalidContainerLength, | ||
| 729 | error.DoesNotMatchSchema, | ||
| 730 | => return error.ServerMalformedResponse, | ||
| 731 | else => |e| return e, | ||
| 732 | }; | ||
| 733 | errdefer capture_state.pub_key.deinit(allocator); | ||
| 734 | |||
| 735 | try reader.skipBytes(length - cert_length - 3, .{}); | ||
| 736 | return capture_state.pub_key; | ||
| 737 | } | ||
| 738 | |||
| 739 | pub fn client_connect( | ||
| 740 | options: anytype, | ||
| 741 | hostname: []const u8, | ||
| 742 | ) ClientConnectError( | ||
| 743 | options.cert_verifier, | ||
| 744 | @TypeOf(options.reader), | ||
| 745 | @TypeOf(options.writer), | ||
| 746 | )!Client( | ||
| 747 | @TypeOf(options.reader), | ||
| 748 | @TypeOf(options.writer), | ||
| 749 | options.ciphersuites, | ||
| 750 | @hasField(@TypeOf(options), "protocols"), | ||
| 751 | ) { | ||
| 752 | const Options = @TypeOf(options); | ||
| 753 | if (@TypeOf(options.cert_verifier) != CertificateVerifier and | ||
| 754 | @TypeOf(options.cert_verifier) != @Type(.EnumLiteral)) | ||
| 755 | @compileError("cert_verifier should be of type CertificateVerifier"); | ||
| 756 | |||
| 757 | if (!@hasField(Options, "temp_allocator")) | ||
| 758 | @compileError("Option tuple is missing field 'temp_allocator'"); | ||
| 759 | if (options.cert_verifier == .default) { | ||
| 760 | if (!@hasField(Options, "trusted_certificates")) | ||
| 761 | @compileError("Option tuple is missing field 'trusted_certificates' for .default cert_verifier"); | ||
| 762 | } | ||
| 763 | |||
| 764 | const has_alpn = comptime @hasField(Options, "protocols"); | ||
| 765 | var handshake_record_hash = Sha256.init(.{}); | ||
| 766 | const reader = options.reader; | ||
| 767 | const writer = options.writer; | ||
| 768 | const hashing_reader = sha256_reader(&handshake_record_hash, reader); | ||
| 769 | const hashing_writer = sha256_writer(&handshake_record_hash, writer); | ||
| 770 | |||
| 771 | var client_random: [32]u8 = undefined; | ||
| 772 | const rand = if (!@hasField(Options, "rand")) | ||
| 773 | std.crypto.random | ||
| 774 | else | ||
| 775 | options.rand; | ||
| 776 | |||
| 777 | rand.bytes(&client_random); | ||
| 778 | |||
| 779 | var server_random: [32]u8 = undefined; | ||
| 780 | |||
| 781 | if (options.ciphersuites.len == 0) | ||
| 782 | @compileError("Must provide at least one ciphersuite."); | ||
| 783 | const ciphersuite_bytes = 2 * options.ciphersuites.len + 2; | ||
| 784 | // @TODO Make sure the individual lengths are u16s | ||
| 785 | const alpn_bytes = if (has_alpn) blk: { | ||
| 786 | var sum: usize = 0; | ||
| 787 | for (options.protocols) |proto| { | ||
| 788 | sum += proto.len; | ||
| 789 | } | ||
| 790 | break :blk 6 + options.protocols.len + sum; | ||
| 791 | } else 0; | ||
| 792 | var protocol: if (has_alpn) []const u8 else void = undefined; | ||
| 793 | { | ||
| 794 | const client_hello_start = comptime blk: { | ||
| 795 | // TODO: We assume the compiler is running in a little endian system | ||
| 796 | var starting_part: [46]u8 = [_]u8{ | ||
| 797 | // Record header: Handshake record type, protocol version, handshake size | ||
| 798 | 0x16, 0x03, 0x01, undefined, undefined, | ||
| 799 | // Handshake message type, bytes of client hello | ||
| 800 | 0x01, undefined, undefined, undefined, | ||
| 801 | // Client version (hardcoded to TLS 1.2 even for TLS 1.3) | ||
| 802 | 0x03, | ||
| 803 | 0x03, | ||
| 804 | } ++ ([1]u8{undefined} ** 32) ++ [_]u8{ | ||
| 805 | // Session ID | ||
| 806 | 0x00, | ||
| 807 | } ++ mem.toBytes(@byteSwap(u16, ciphersuite_bytes)); | ||
| 808 | // using .* = mem.asBytes(...).* or mem.writeIntBig didn't work... | ||
| 809 | |||
| 810 | // Same as above, couldnt achieve this with a single buffer. | ||
| 811 | // TLS_EMPTY_RENEGOTIATION_INFO_SCSV | ||
| 812 | var ciphersuite_buf: []const u8 = &[2]u8{ 0x00, 0x0f }; | ||
| 813 | for (options.ciphersuites) |cs, i| { | ||
| 814 | // Also check for properties of the ciphersuites here | ||
| 815 | if (cs.key_exchange != .ecdhe) | ||
| 816 | @compileError("Non ECDHE key exchange is not supported yet."); | ||
| 817 | if (cs.hash != .sha256) | ||
| 818 | @compileError("Non SHA256 hash algorithm is not supported yet."); | ||
| 819 | |||
| 820 | ciphersuite_buf = ciphersuite_buf ++ mem.toBytes(@byteSwap(u16, cs.tag)); | ||
| 821 | } | ||
| 822 | |||
| 823 | var ending_part: [13]u8 = [_]u8{ | ||
| 824 | // Compression methods (no compression) | ||
| 825 | 0x01, 0x00, | ||
| 826 | // Extensions length | ||
| 827 | undefined, undefined, | ||
| 828 | // Extension: server name | ||
| 829 | // id, length, length of entry | ||
| 830 | 0x00, 0x00, | ||
| 831 | undefined, undefined, | ||
| 832 | undefined, undefined, | ||
| 833 | // entry type, length of bytes | ||
| 834 | 0x00, undefined, | ||
| 835 | undefined, | ||
| 836 | }; | ||
| 837 | break :blk starting_part ++ ciphersuite_buf ++ ending_part; | ||
| 838 | }; | ||
| 839 | |||
| 840 | var msg_buf = client_hello_start.ptr[0..client_hello_start.len].*; | ||
| 841 | mem.writeIntBig(u16, msg_buf[3..5], @intCast(u16, alpn_bytes + hostname.len + 0x59 + ciphersuite_bytes)); | ||
| 842 | mem.writeIntBig(u24, msg_buf[6..9], @intCast(u24, alpn_bytes + hostname.len + 0x55 + ciphersuite_bytes)); | ||
| 843 | mem.copy(u8, msg_buf[11..43], &client_random); | ||
| 844 | mem.writeIntBig(u16, msg_buf[48 + ciphersuite_bytes ..][0..2], @intCast(u16, alpn_bytes + hostname.len + 0x2C)); | ||
| 845 | mem.writeIntBig(u16, msg_buf[52 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len + 5)); | ||
| 846 | mem.writeIntBig(u16, msg_buf[54 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len + 3)); | ||
| 847 | mem.writeIntBig(u16, msg_buf[57 + ciphersuite_bytes ..][0..2], @intCast(u16, hostname.len)); | ||
| 848 | try writer.writeAll(msg_buf[0..5]); | ||
| 849 | try hashing_writer.writeAll(msg_buf[5..]); | ||
| 850 | } | ||
| 851 | try hashing_writer.writeAll(hostname); | ||
| 852 | // @TODO Fix this with wikipedia test, add secp384r1 support (then app options.curves but default to all when not there (also do this for ciphersuites)) | ||
| 853 | if (has_alpn) { | ||
| 854 | var msg_buf = [6]u8{ 0x00, 0x10, undefined, undefined, undefined, undefined }; | ||
| 855 | mem.writeIntBig(u16, msg_buf[2..4], @intCast(u16, alpn_bytes - 4)); | ||
| 856 | mem.writeIntBig(u16, msg_buf[4..6], @intCast(u16, alpn_bytes - 6)); | ||
| 857 | try hashing_writer.writeAll(&msg_buf); | ||
| 858 | for (options.protocols) |proto| { | ||
| 859 | try hashing_writer.writeByte(@intCast(u8, proto.len)); | ||
| 860 | try hashing_writer.writeAll(proto); | ||
| 861 | } | ||
| 862 | } | ||
| 863 | try hashing_writer.writeAll(&[35]u8{ | ||
| 864 | // Extension: supported groups, for now just x25519 (00 1D) and secp384r1 (00 0x18) | ||
| 865 | 0x00, 0x0A, 0x00, 0x06, 0x00, 0x04, 0x00, 0x1D, 0x00, 0x18, | ||
| 866 | // Extension: EC point formats => uncompressed point format | ||
| 867 | 0x00, 0x0B, 0x00, 0x02, 0x01, 0x00, | ||
| 868 | // Extension: Signature algorithms | ||
| 869 | // RSA/PKCS1/SHA256, RSA/PKCS1/SHA512 | ||
| 870 | 0x00, 0x0D, 0x00, 0x06, | ||
| 871 | 0x00, 0x04, 0x04, 0x01, 0x06, 0x01, | ||
| 872 | // Extension: Renegotiation Info => new connection | ||
| 873 | 0xFF, 0x01, 0x00, 0x01, | ||
| 874 | 0x00, | ||
| 875 | // Extension: SCT (signed certificate timestamp) | ||
| 876 | 0x00, 0x12, 0x00, 0x00, | ||
| 877 | }); | ||
| 878 | |||
| 879 | // Read server hello | ||
| 880 | var ciphersuite: u16 = undefined; | ||
| 881 | { | ||
| 882 | const length = try handshake_record_length(reader); | ||
| 883 | if (length < 44) | ||
| 884 | return error.ServerMalformedResponse; | ||
| 885 | { | ||
| 886 | var hs_hdr_and_server_ver: [6]u8 = undefined; | ||
| 887 | try hashing_reader.readNoEof(&hs_hdr_and_server_ver); | ||
| 888 | if (hs_hdr_and_server_ver[0] != 0x02) | ||
| 889 | return error.ServerMalformedResponse; | ||
| 890 | if (!mem.eql(u8, hs_hdr_and_server_ver[4..6], "\x03\x03")) | ||
| 891 | return error.ServerInvalidVersion; | ||
| 892 | } | ||
| 893 | try hashing_reader.readNoEof(&server_random); | ||
| 894 | |||
| 895 | // Just skip the session id for now | ||
| 896 | const sess_id_len = try hashing_reader.readByte(); | ||
| 897 | if (sess_id_len != 0) | ||
| 898 | try hashing_reader.skipBytes(sess_id_len, .{}); | ||
| 899 | |||
| 900 | { | ||
| 901 | ciphersuite = try hashing_reader.readIntBig(u16); | ||
| 902 | var found = false; | ||
| 903 | inline for (options.ciphersuites) |cs| { | ||
| 904 | if (ciphersuite == cs.tag) { | ||
| 905 | found = true; | ||
| 906 | // TODO This segfaults stage1 | ||
| 907 | // break; | ||
| 908 | } | ||
| 909 | } | ||
| 910 | if (!found) | ||
| 911 | return error.ServerInvalidCipherSuite; | ||
| 912 | } | ||
| 913 | |||
| 914 | // Compression method | ||
| 915 | if ((try hashing_reader.readByte()) != 0x00) | ||
| 916 | return error.ServerInvalidCompressionMethod; | ||
| 917 | |||
| 918 | const exts_length = try hashing_reader.readIntBig(u16); | ||
| 919 | var ext_byte_idx: usize = 0; | ||
| 920 | while (ext_byte_idx < exts_length) { | ||
| 921 | var ext_tag: [2]u8 = undefined; | ||
| 922 | try hashing_reader.readNoEof(&ext_tag); | ||
| 923 | |||
| 924 | const ext_len = try hashing_reader.readIntBig(u16); | ||
| 925 | ext_byte_idx += 4 + ext_len; | ||
| 926 | if (ext_tag[0] == 0xFF and ext_tag[1] == 0x01) { | ||
| 927 | // Renegotiation info | ||
| 928 | const renegotiation_info = try hashing_reader.readByte(); | ||
| 929 | if (ext_len != 0x01 or renegotiation_info != 0x00) | ||
| 930 | return error.ServerInvalidRenegotiationData; | ||
| 931 | } else if (ext_tag[0] == 0x00 and ext_tag[1] == 0x00) { | ||
| 932 | // Server name | ||
| 933 | if (ext_len != 0) | ||
| 934 | try hashing_reader.skipBytes(ext_len, .{}); | ||
| 935 | } else if (ext_tag[0] == 0x00 and ext_tag[1] == 0x0B) { | ||
| 936 | const format_count = try hashing_reader.readByte(); | ||
| 937 | var found_uncompressed = false; | ||
| 938 | var i: usize = 0; | ||
| 939 | while (i < format_count) : (i += 1) { | ||
| 940 | const byte = try hashing_reader.readByte(); | ||
| 941 | if (byte == 0x0) | ||
| 942 | found_uncompressed = true; | ||
| 943 | } | ||
| 944 | if (!found_uncompressed) | ||
| 945 | return error.ServerInvalidECPointCompression; | ||
| 946 | } else if (has_alpn and ext_tag[0] == 0x00 and ext_tag[1] == 0x10) { | ||
| 947 | const alpn_ext_len = try hashing_reader.readIntBig(u16); | ||
| 948 | if (alpn_ext_len != ext_len - 2) | ||
| 949 | return error.ServerMalformedResponse; | ||
| 950 | const str_len = try hashing_reader.readByte(); | ||
| 951 | var buf: [256]u8 = undefined; | ||
| 952 | try hashing_reader.readNoEof(buf[0..str_len]); | ||
| 953 | const found = for (options.protocols) |proto| { | ||
| 954 | if (mem.eql(u8, proto, buf[0..str_len])) { | ||
| 955 | protocol = proto; | ||
| 956 | break true; | ||
| 957 | } | ||
| 958 | } else false; | ||
| 959 | if (!found) | ||
| 960 | return error.ServerInvalidProtocol; | ||
| 961 | try hashing_reader.skipBytes(alpn_ext_len - str_len - 1, .{}); | ||
| 962 | } else return error.ServerInvalidExtension; | ||
| 963 | } | ||
| 964 | if (ext_byte_idx != exts_length) | ||
| 965 | return error.ServerMalformedResponse; | ||
| 966 | } | ||
| 967 | // Read server certificates | ||
| 968 | var certificate_public_key: x509.PublicKey = undefined; | ||
| 969 | { | ||
| 970 | const length = try handshake_record_length(reader); | ||
| 971 | { | ||
| 972 | var handshake_header: [4]u8 = undefined; | ||
| 973 | try hashing_reader.readNoEof(&handshake_header); | ||
| 974 | if (handshake_header[0] != 0x0b) | ||
| 975 | return error.ServerMalformedResponse; | ||
| 976 | } | ||
| 977 | const certs_length = try hashing_reader.readIntBig(u24); | ||
| 978 | const cert_verifier: CertificateVerifier = options.cert_verifier; | ||
| 979 | switch (cert_verifier) { | ||
| 980 | .none => certificate_public_key = try extract_cert_public_key( | ||
| 981 | options.temp_allocator, | ||
| 982 | hashing_reader, | ||
| 983 | certs_length, | ||
| 984 | ), | ||
| 985 | .function => |f| { | ||
| 986 | var reader_state = CertificateReaderState(@TypeOf(hashing_reader)){ | ||
| 987 | .reader = hashing_reader, | ||
| 988 | .length = certs_length, | ||
| 989 | }; | ||
| 990 | var cert_reader = CertificateReader(@TypeOf(hashing_reader)){ .context = &reader_state }; | ||
| 991 | certificate_public_key = try f(cert_reader); | ||
| 992 | try hashing_reader.skipBytes(reader_state.length - reader_state.idx, .{}); | ||
| 993 | }, | ||
| 994 | .default => certificate_public_key = try default_cert_verifier( | ||
| 995 | options.temp_allocator, | ||
| 996 | hashing_reader, | ||
| 997 | certs_length, | ||
| 998 | options.trusted_certificates, | ||
| 999 | hostname, | ||
| 1000 | ), | ||
| 1001 | } | ||
| 1002 | } | ||
| 1003 | errdefer certificate_public_key.deinit(options.temp_allocator); | ||
| 1004 | // Read server ephemeral public key | ||
| 1005 | var server_public_key_buf: [97]u8 = undefined; | ||
| 1006 | var curve_id: enum { x25519, secp384r1 } = undefined; | ||
| 1007 | { | ||
| 1008 | const length = try handshake_record_length(reader); | ||
| 1009 | { | ||
| 1010 | var handshake_header: [4]u8 = undefined; | ||
| 1011 | try hashing_reader.readNoEof(&handshake_header); | ||
| 1012 | if (handshake_header[0] != 0x0c) | ||
| 1013 | return error.ServerMalformedResponse; | ||
| 1014 | |||
| 1015 | // Only x25519 and secp384r1 supported for now. | ||
| 1016 | var curve_bytes: [3]u8 = undefined; | ||
| 1017 | try hashing_reader.readNoEof(&curve_bytes); | ||
| 1018 | curve_id = if (mem.eql(u8, &curve_bytes, "\x03\x00\x1D")) | ||
| 1019 | .x25519 | ||
| 1020 | else if (mem.eql(u8, &curve_bytes, "\x03\x00\x18")) | ||
| 1021 | .secp384r1 | ||
| 1022 | else | ||
| 1023 | return error.ServerInvalidCurve; | ||
| 1024 | } | ||
| 1025 | |||
| 1026 | const pub_key_len = try hashing_reader.readByte(); | ||
| 1027 | if ((curve_id == .x25519 and pub_key_len != 32) or | ||
| 1028 | (curve_id == .secp384r1 and pub_key_len != 97)) | ||
| 1029 | return error.ServerMalformedResponse; | ||
| 1030 | try hashing_reader.readNoEof(server_public_key_buf[0..pub_key_len]); | ||
| 1031 | if (curve_id == .secp384r1 and server_public_key_buf[0] != 0x04) | ||
| 1032 | return error.ServerMalformedResponse; | ||
| 1033 | |||
| 1034 | // Signed public key | ||
| 1035 | const signature_id = try hashing_reader.readIntBig(u16); | ||
| 1036 | const signature_len = try hashing_reader.readIntBig(u16); | ||
| 1037 | |||
| 1038 | var hash_buf: [64]u8 = undefined; | ||
| 1039 | var hash: []const u8 = undefined; | ||
| 1040 | const signature_algoritm: SignatureAlgorithm = switch (signature_id) { | ||
| 1041 | // RSA/PKCS1/SHA256 | ||
| 1042 | 0x0401 => block: { | ||
| 1043 | var sha256 = Sha256.init(.{}); | ||
| 1044 | sha256.update(&client_random); | ||
| 1045 | sha256.update(&server_random); | ||
| 1046 | switch (curve_id) { | ||
| 1047 | .x25519 => sha256.update("\x03\x00\x1D\x20"), | ||
| 1048 | .secp384r1 => sha256.update("\x03\x00\x18\x20"), | ||
| 1049 | } | ||
| 1050 | sha256.update(server_public_key_buf[0..pub_key_len]); | ||
| 1051 | sha256.final(hash_buf[0..32]); | ||
| 1052 | hash = hash_buf[0..32]; | ||
| 1053 | break :block .rsa_sha256; | ||
| 1054 | }, | ||
| 1055 | // RSA/PKCS1/SHA512 | ||
| 1056 | 0x0601 => block: { | ||
| 1057 | var sha512 = Sha512.init(.{}); | ||
| 1058 | sha512.update(&client_random); | ||
| 1059 | sha512.update(&server_random); | ||
| 1060 | switch (curve_id) { | ||
| 1061 | .x25519 => sha512.update("\x03\x00\x1D"), | ||
| 1062 | .secp384r1 => sha512.update("\x03\x00\x18"), | ||
| 1063 | } | ||
| 1064 | sha512.update(&[1]u8{pub_key_len}); | ||
| 1065 | sha512.update(server_public_key_buf[0..pub_key_len]); | ||
| 1066 | sha512.final(hash_buf[0..64]); | ||
| 1067 | hash = hash_buf[0..64]; | ||
| 1068 | break :block .rsa_sha512; | ||
| 1069 | }, | ||
| 1070 | else => return error.ServerInvalidSignatureAlgorithm, | ||
| 1071 | }; | ||
| 1072 | const signature_bytes = try options.temp_allocator.alloc(u8, signature_len); | ||
| 1073 | defer options.temp_allocator.free(signature_bytes); | ||
| 1074 | try hashing_reader.readNoEof(signature_bytes); | ||
| 1075 | |||
| 1076 | if (!try verify_signature( | ||
| 1077 | options.temp_allocator, | ||
| 1078 | signature_algoritm, | ||
| 1079 | .{ .data = signature_bytes, .bit_len = signature_len * 8 }, | ||
| 1080 | hash, | ||
| 1081 | certificate_public_key, | ||
| 1082 | )) | ||
| 1083 | return error.ServerInvalidSignature; | ||
| 1084 | |||
| 1085 | certificate_public_key.deinit(options.temp_allocator); | ||
| 1086 | certificate_public_key = x509.PublicKey{ .ec = .{ .id = undefined, .curve_point = &[0]u8{} } }; | ||
| 1087 | } | ||
| 1088 | // Read server hello done | ||
| 1089 | { | ||
| 1090 | const length = try handshake_record_length(reader); | ||
| 1091 | const is_bytes = try hashing_reader.isBytes("\x0e\x00\x00\x00"); | ||
| 1092 | if (length != 4 or !is_bytes) | ||
| 1093 | return error.ServerMalformedResponse; | ||
| 1094 | } | ||
| 1095 | |||
| 1096 | // Generate keys for the session | ||
| 1097 | const client_key_pair: extern union { | ||
| 1098 | x25519: std.crypto.dh.X25519.KeyPair, | ||
| 1099 | secp384r1: crypto.ecc.KeyPair(crypto.ecc.SECP384R1), | ||
| 1100 | } = switch (curve_id) { | ||
| 1101 | .x25519 => while (true) { | ||
| 1102 | var seed: [32]u8 = undefined; | ||
| 1103 | rand.bytes(&seed); | ||
| 1104 | const tmp = std.crypto.dh.X25519.KeyPair.create(seed) catch continue; | ||
| 1105 | break .{ .x25519 = tmp }; | ||
| 1106 | } else unreachable, | ||
| 1107 | .secp384r1 => blk: { | ||
| 1108 | var seed: [48]u8 = undefined; | ||
| 1109 | rand.bytes(&seed); | ||
| 1110 | break :blk .{ .secp384r1 = crypto.ecc.make_key_pair(crypto.ecc.SECP384R1, seed) }; | ||
| 1111 | }, | ||
| 1112 | }; | ||
| 1113 | |||
| 1114 | // Client key exchange | ||
| 1115 | switch (curve_id) { | ||
| 1116 | .x25519 => { | ||
| 1117 | try writer.writeAll(&[5]u8{ 0x16, 0x03, 0x03, 0x00, 0x25 }); | ||
| 1118 | try hashing_writer.writeAll(&[5]u8{ 0x10, 0x00, 0x00, 0x21, 0x20 }); | ||
| 1119 | try hashing_writer.writeAll(&client_key_pair.x25519.public_key); | ||
| 1120 | }, | ||
| 1121 | .secp384r1 => { | ||
| 1122 | try writer.writeAll(&[5]u8{ 0x16, 0x03, 0x03, 0x00, 0x66 }); | ||
| 1123 | try hashing_writer.writeAll(&[6]u8{ 0x10, 0x00, 0x00, 0x62, 0x61, 0x04 }); | ||
| 1124 | try hashing_writer.writeAll(&client_key_pair.secp384r1.public_key); | ||
| 1125 | }, | ||
| 1126 | } | ||
| 1127 | // Client encryption keys calculation for ECDHE_RSA cipher suites with SHA256 hash | ||
| 1128 | var master_secret: [48]u8 = undefined; | ||
| 1129 | var key_data: ciphers.KeyData(options.ciphersuites) = undefined; | ||
| 1130 | { | ||
| 1131 | var pre_master_secret_buf: [96]u8 = undefined; | ||
| 1132 | const pre_master_secret = switch (curve_id) { | ||
| 1133 | .x25519 => blk: { | ||
| 1134 | pre_master_secret_buf[0..32].* = std.crypto.dh.X25519.scalarmult( | ||
| 1135 | client_key_pair.x25519.secret_key, | ||
| 1136 | server_public_key_buf[0..32].*, | ||
| 1137 | ) catch | ||
| 1138 | return error.PreMasterGenerationFailed; | ||
| 1139 | break :blk pre_master_secret_buf[0..32]; | ||
| 1140 | }, | ||
| 1141 | .secp384r1 => blk: { | ||
| 1142 | pre_master_secret_buf = crypto.ecc.scalarmult( | ||
| 1143 | crypto.ecc.SECP384R1, | ||
| 1144 | server_public_key_buf[1..].*, | ||
| 1145 | &client_key_pair.secp384r1.secret_key, | ||
| 1146 | ) catch | ||
| 1147 | return error.PreMasterGenerationFailed; | ||
| 1148 | break :blk pre_master_secret_buf[0..48]; | ||
| 1149 | }, | ||
| 1150 | }; | ||
| 1151 | |||
| 1152 | var seed: [77]u8 = undefined; | ||
| 1153 | seed[0..13].* = "master secret".*; | ||
| 1154 | seed[13..45].* = client_random; | ||
| 1155 | seed[45..77].* = server_random; | ||
| 1156 | |||
| 1157 | var a1: [32 + seed.len]u8 = undefined; | ||
| 1158 | Hmac256.create(a1[0..32], &seed, pre_master_secret); | ||
| 1159 | var a2: [32 + seed.len]u8 = undefined; | ||
| 1160 | Hmac256.create(a2[0..32], a1[0..32], pre_master_secret); | ||
| 1161 | |||
| 1162 | a1[32..].* = seed; | ||
| 1163 | a2[32..].* = seed; | ||
| 1164 | |||
| 1165 | var p1: [32]u8 = undefined; | ||
| 1166 | Hmac256.create(&p1, &a1, pre_master_secret); | ||
| 1167 | var p2: [32]u8 = undefined; | ||
| 1168 | Hmac256.create(&p2, &a2, pre_master_secret); | ||
| 1169 | |||
| 1170 | master_secret[0..32].* = p1; | ||
| 1171 | master_secret[32..48].* = p2[0..16].*; | ||
| 1172 | |||
| 1173 | // Key expansion | ||
| 1174 | seed[0..13].* = "key expansion".*; | ||
| 1175 | seed[13..45].* = server_random; | ||
| 1176 | seed[45..77].* = client_random; | ||
| 1177 | a1[32..].* = seed; | ||
| 1178 | a2[32..].* = seed; | ||
| 1179 | |||
| 1180 | const KeyExpansionState = struct { | ||
| 1181 | seed: *const [77]u8, | ||
| 1182 | a1: *[32 + seed.len]u8, | ||
| 1183 | a2: *[32 + seed.len]u8, | ||
| 1184 | master_secret: *const [48]u8, | ||
| 1185 | }; | ||
| 1186 | |||
| 1187 | const next_32_bytes = struct { | ||
| 1188 | inline fn f( | ||
| 1189 | state: *KeyExpansionState, | ||
| 1190 | comptime chunk_idx: comptime_int, | ||
| 1191 | chunk: *[32]u8, | ||
| 1192 | ) void { | ||
| 1193 | if (chunk_idx == 0) { | ||
| 1194 | Hmac256.create(state.a1[0..32], state.seed, state.master_secret); | ||
| 1195 | Hmac256.create(chunk, state.a1, state.master_secret); | ||
| 1196 | } else if (chunk_idx % 2 == 1) { | ||
| 1197 | Hmac256.create(state.a2[0..32], state.a1[0..32], state.master_secret); | ||
| 1198 | Hmac256.create(chunk, state.a2, state.master_secret); | ||
| 1199 | } else { | ||
| 1200 | Hmac256.create(state.a1[0..32], state.a2[0..32], state.master_secret); | ||
| 1201 | Hmac256.create(chunk, state.a1, state.master_secret); | ||
| 1202 | } | ||
| 1203 | } | ||
| 1204 | }.f; | ||
| 1205 | var state = KeyExpansionState{ | ||
| 1206 | .seed = &seed, | ||
| 1207 | .a1 = &a1, | ||
| 1208 | .a2 = &a2, | ||
| 1209 | .master_secret = &master_secret, | ||
| 1210 | }; | ||
| 1211 | |||
| 1212 | key_data = ciphers.key_expansion(options.ciphersuites, ciphersuite, &state, next_32_bytes); | ||
| 1213 | } | ||
| 1214 | |||
| 1215 | // Client change cipher spec and client handshake finished | ||
| 1216 | { | ||
| 1217 | try writer.writeAll(&[6]u8{ | ||
| 1218 | // Client change cipher spec | ||
| 1219 | 0x14, 0x03, 0x03, | ||
| 1220 | 0x00, 0x01, 0x01, | ||
| 1221 | }); | ||
| 1222 | // The message we need to encrypt is the following: | ||
| 1223 | // 0x14 0x00 0x00 0x0c | ||
| 1224 | // <12 bytes of verify_data> | ||
| 1225 | // seed = "client finished" + SHA256(all handshake messages) | ||
| 1226 | // a1 = HMAC-SHA256(key=MasterSecret, data=seed) | ||
| 1227 | // p1 = HMAC-SHA256(key=MasterSecret, data=a1 + seed) | ||
| 1228 | // verify_data = p1[0..12] | ||
| 1229 | var verify_message: [16]u8 = undefined; | ||
| 1230 | verify_message[0..4].* = "\x14\x00\x00\x0C".*; | ||
| 1231 | { | ||
| 1232 | var seed: [47]u8 = undefined; | ||
| 1233 | seed[0..15].* = "client finished".*; | ||
| 1234 | // We still need to update the hash one time, so we copy | ||
| 1235 | // to get the current digest here. | ||
| 1236 | var hash_copy = handshake_record_hash; | ||
| 1237 | hash_copy.final(seed[15..47]); | ||
| 1238 | |||
| 1239 | var a1: [32 + seed.len]u8 = undefined; | ||
| 1240 | Hmac256.create(a1[0..32], &seed, &master_secret); | ||
| 1241 | a1[32..].* = seed; | ||
| 1242 | var p1: [32]u8 = undefined; | ||
| 1243 | Hmac256.create(&p1, &a1, &master_secret); | ||
| 1244 | verify_message[4..16].* = p1[0..12].*; | ||
| 1245 | } | ||
| 1246 | handshake_record_hash.update(&verify_message); | ||
| 1247 | |||
| 1248 | inline for (options.ciphersuites) |cs| { | ||
| 1249 | if (cs.tag == ciphersuite) { | ||
| 1250 | try cs.raw_write( | ||
| 1251 | 256, | ||
| 1252 | rand, | ||
| 1253 | &key_data, | ||
| 1254 | writer, | ||
| 1255 | [3]u8{ 0x16, 0x03, 0x03 }, | ||
| 1256 | 0, | ||
| 1257 | &verify_message, | ||
| 1258 | ); | ||
| 1259 | } | ||
| 1260 | } | ||
| 1261 | } | ||
| 1262 | |||
| 1263 | // Server change cipher spec | ||
| 1264 | { | ||
| 1265 | const length = try record_length(0x14, reader); | ||
| 1266 | const next_byte = try reader.readByte(); | ||
| 1267 | if (length != 1 or next_byte != 0x01) | ||
| 1268 | return error.ServerMalformedResponse; | ||
| 1269 | } | ||
| 1270 | // Server handshake finished | ||
| 1271 | { | ||
| 1272 | const length = try handshake_record_length(reader); | ||
| 1273 | |||
| 1274 | var verify_message: [16]u8 = undefined; | ||
| 1275 | verify_message[0..4].* = "\x14\x00\x00\x0C".*; | ||
| 1276 | { | ||
| 1277 | var seed: [47]u8 = undefined; | ||
| 1278 | seed[0..15].* = "server finished".*; | ||
| 1279 | handshake_record_hash.final(seed[15..47]); | ||
| 1280 | var a1: [32 + seed.len]u8 = undefined; | ||
| 1281 | Hmac256.create(a1[0..32], &seed, &master_secret); | ||
| 1282 | a1[32..].* = seed; | ||
| 1283 | var p1: [32]u8 = undefined; | ||
| 1284 | Hmac256.create(&p1, &a1, &master_secret); | ||
| 1285 | verify_message[4..16].* = p1[0..12].*; | ||
| 1286 | } | ||
| 1287 | |||
| 1288 | inline for (options.ciphersuites) |cs| { | ||
| 1289 | if (cs.tag == ciphersuite) { | ||
| 1290 | if (!try cs.check_verify_message(&key_data, length, reader, verify_message)) | ||
| 1291 | return error.ServerInvalidVerifyData; | ||
| 1292 | } | ||
| 1293 | } | ||
| 1294 | } | ||
| 1295 | |||
| 1296 | return Client(@TypeOf(reader), @TypeOf(writer), options.ciphersuites, has_alpn){ | ||
| 1297 | .ciphersuite = ciphersuite, | ||
| 1298 | .key_data = key_data, | ||
| 1299 | .state = ciphers.client_state_default(options.ciphersuites, ciphersuite), | ||
| 1300 | .rand = rand, | ||
| 1301 | .parent_reader = reader, | ||
| 1302 | .parent_writer = writer, | ||
| 1303 | .protocol = protocol, | ||
| 1304 | }; | ||
| 1305 | } | ||
| 1306 | |||
| 1307 | pub fn Client( | ||
| 1308 | comptime _Reader: type, | ||
| 1309 | comptime _Writer: type, | ||
| 1310 | comptime _ciphersuites: anytype, | ||
| 1311 | comptime has_protocol: bool, | ||
| 1312 | ) type { | ||
| 1313 | return struct { | ||
| 1314 | const ReaderError = _Reader.Error || ServerAlert || error{ ServerMalformedResponse, ServerInvalidVersion }; | ||
| 1315 | pub const Reader = std.io.Reader(*@This(), ReaderError, read); | ||
| 1316 | pub const Writer = std.io.Writer(*@This(), _Writer.Error, write); | ||
| 1317 | |||
| 1318 | ciphersuite: u16, | ||
| 1319 | client_seq: u64 = 1, | ||
| 1320 | server_seq: u64 = 1, | ||
| 1321 | key_data: ciphers.KeyData(_ciphersuites), | ||
| 1322 | state: ciphers.ClientState(_ciphersuites), | ||
| 1323 | rand: *std.rand.Random, | ||
| 1324 | |||
| 1325 | parent_reader: _Reader, | ||
| 1326 | parent_writer: _Writer, | ||
| 1327 | |||
| 1328 | protocol: if (has_protocol) []const u8 else void, | ||
| 1329 | |||
| 1330 | pub fn reader(self: *@This()) Reader { | ||
| 1331 | return .{ .context = self }; | ||
| 1332 | } | ||
| 1333 | |||
| 1334 | pub fn writer(self: *@This()) Writer { | ||
| 1335 | return .{ .context = self }; | ||
| 1336 | } | ||
| 1337 | |||
| 1338 | pub fn read(self: *@This(), buffer: []u8) ReaderError!usize { | ||
| 1339 | inline for (_ciphersuites) |cs| { | ||
| 1340 | if (self.ciphersuite == cs.tag) { | ||
| 1341 | // @TODO Make this buffer size configurable | ||
| 1342 | return try cs.read( | ||
| 1343 | 1024, | ||
| 1344 | &@field(self.state, cs.name), | ||
| 1345 | &self.key_data, | ||
| 1346 | self.parent_reader, | ||
| 1347 | &self.server_seq, | ||
| 1348 | buffer, | ||
| 1349 | ); | ||
| 1350 | } | ||
| 1351 | } | ||
| 1352 | unreachable; | ||
| 1353 | } | ||
| 1354 | |||
| 1355 | pub fn write(self: *@This(), buffer: []const u8) _Writer.Error!usize { | ||
| 1356 | if (buffer.len == 0) return 0; | ||
| 1357 | |||
| 1358 | inline for (_ciphersuites) |cs| { | ||
| 1359 | if (self.ciphersuite == cs.tag) { | ||
| 1360 | // @TODO Make this buffer size configurable | ||
| 1361 | const curr_bytes = @truncate(u16, std.math.min(buffer.len, 1024)); | ||
| 1362 | try cs.raw_write( | ||
| 1363 | 1024, | ||
| 1364 | self.rand, | ||
| 1365 | &self.key_data, | ||
| 1366 | self.parent_writer, | ||
| 1367 | [3]u8{ 0x17, 0x03, 0x03 }, | ||
| 1368 | self.client_seq, | ||
| 1369 | buffer[0..curr_bytes], | ||
| 1370 | ); | ||
| 1371 | self.client_seq += 1; | ||
| 1372 | return curr_bytes; | ||
| 1373 | } | ||
| 1374 | } | ||
| 1375 | unreachable; | ||
| 1376 | } | ||
| 1377 | |||
| 1378 | pub fn close_notify(self: *@This()) !void { | ||
| 1379 | inline for (_ciphersuites) |cs| { | ||
| 1380 | if (self.ciphersuite == cs.tag) { | ||
| 1381 | try cs.raw_write( | ||
| 1382 | 1024, | ||
| 1383 | self.rand, | ||
| 1384 | &self.key_data, | ||
| 1385 | self.parent_writer, | ||
| 1386 | [3]u8{ 0x15, 0x03, 0x03 }, | ||
| 1387 | self.client_seq, | ||
| 1388 | "\x01\x00", | ||
| 1389 | ); | ||
| 1390 | self.client_seq += 1; | ||
| 1391 | return; | ||
| 1392 | } | ||
| 1393 | } | ||
| 1394 | unreachable; | ||
| 1395 | } | ||
| 1396 | }; | ||
| 1397 | } | ||
| 1398 | |||
| 1399 | test "HTTPS request on wikipedia main page" { | ||
| 1400 | const sock = try std.net.tcpConnectToHost(std.testing.allocator, "en.wikipedia.org", 443); | ||
| 1401 | defer sock.close(); | ||
| 1402 | |||
| 1403 | var fbs = std.io.fixedBufferStream(@embedFile("../test/DigiCertHighAssuranceEVRootCA.crt.pem")); | ||
| 1404 | var trusted_chain = try x509.TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); | ||
| 1405 | defer trusted_chain.deinit(); | ||
| 1406 | |||
| 1407 | // @TODO Remove this once std.crypto.rand works in .evented mode | ||
| 1408 | var rand = blk: { | ||
| 1409 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; | ||
| 1410 | try std.os.getrandom(&seed); | ||
| 1411 | break :blk &std.rand.DefaultCsprng.init(seed).random; | ||
| 1412 | }; | ||
| 1413 | |||
| 1414 | var client = try client_connect(.{ | ||
| 1415 | .rand = rand, | ||
| 1416 | .reader = sock.reader(), | ||
| 1417 | .writer = sock.writer(), | ||
| 1418 | .cert_verifier = .default, | ||
| 1419 | .temp_allocator = std.testing.allocator, | ||
| 1420 | .trusted_certificates = trusted_chain.data.items, | ||
| 1421 | .ciphersuites = ciphersuites.all, | ||
| 1422 | .protocols = &[_][]const u8{"http/1.1"}, | ||
| 1423 | }, "en.wikipedia.org"); | ||
| 1424 | defer client.close_notify() catch {}; | ||
| 1425 | |||
| 1426 | std.testing.expectEqualStrings("http/1.1", client.protocol); | ||
| 1427 | try client.writer().writeAll("GET /wiki/Main_Page HTTP/1.1\r\nHost: en.wikipedia.org\r\nAccept: */*\r\n\r\n"); | ||
| 1428 | |||
| 1429 | { | ||
| 1430 | const header = try client.reader().readUntilDelimiterAlloc(std.testing.allocator, '\n', std.math.maxInt(usize)); | ||
| 1431 | std.testing.expectEqualStrings("HTTP/1.1 200 OK", mem.trim(u8, header, &std.ascii.spaces)); | ||
| 1432 | std.testing.allocator.free(header); | ||
| 1433 | } | ||
| 1434 | |||
| 1435 | // Skip the rest of the headers expect for Content-Length | ||
| 1436 | var content_length: ?usize = null; | ||
| 1437 | hdr_loop: while (true) { | ||
| 1438 | const header = try client.reader().readUntilDelimiterAlloc(std.testing.allocator, '\n', std.math.maxInt(usize)); | ||
| 1439 | defer std.testing.allocator.free(header); | ||
| 1440 | |||
| 1441 | const hdr_contents = mem.trim(u8, header, &std.ascii.spaces); | ||
| 1442 | if (hdr_contents.len == 0) { | ||
| 1443 | break :hdr_loop; | ||
| 1444 | } | ||
| 1445 | |||
| 1446 | if (mem.startsWith(u8, hdr_contents, "Content-Length: ")) { | ||
| 1447 | content_length = try std.fmt.parseUnsigned(usize, hdr_contents[16..], 10); | ||
| 1448 | } | ||
| 1449 | } | ||
| 1450 | std.testing.expect(content_length != null); | ||
| 1451 | const html_contents = try std.testing.allocator.alloc(u8, content_length.?); | ||
| 1452 | defer std.testing.allocator.free(html_contents); | ||
| 1453 | |||
| 1454 | try client.reader().readNoEof(html_contents); | ||
| 1455 | } | ||
| 1456 | |||
| 1457 | test "HTTPS request on twitch oath2 endpoint" { | ||
| 1458 | const sock = try std.net.tcpConnectToHost(std.testing.allocator, "id.twitch.tv", 443); | ||
| 1459 | defer sock.close(); | ||
| 1460 | |||
| 1461 | // @TODO Remove this once std.crypto.rand works in .evented mode | ||
| 1462 | var rand = blk: { | ||
| 1463 | var seed: [std.rand.DefaultCsprng.secret_seed_length]u8 = undefined; | ||
| 1464 | try std.os.getrandom(&seed); | ||
| 1465 | break :blk &std.rand.DefaultCsprng.init(seed).random; | ||
| 1466 | }; | ||
| 1467 | |||
| 1468 | var client = try client_connect(.{ | ||
| 1469 | .rand = rand, | ||
| 1470 | .temp_allocator = std.testing.allocator, | ||
| 1471 | .reader = sock.reader(), | ||
| 1472 | .writer = sock.writer(), | ||
| 1473 | .cert_verifier = .none, | ||
| 1474 | .ciphersuites = ciphersuites.all, | ||
| 1475 | .protocols = &[_][]const u8{"http/1.1"}, | ||
| 1476 | }, "id.twitch.tv"); | ||
| 1477 | defer client.close_notify() catch {}; | ||
| 1478 | |||
| 1479 | try client.writer().writeAll("GET /oauth2/validate HTTP/1.1\r\nHost: id.twitch.tv\r\nAccept: */*\r\n\r\n"); | ||
| 1480 | var content_length: ?usize = null; | ||
| 1481 | hdr_loop: while (true) { | ||
| 1482 | const header = try client.reader().readUntilDelimiterAlloc(std.testing.allocator, '\n', std.math.maxInt(usize)); | ||
| 1483 | defer std.testing.allocator.free(header); | ||
| 1484 | |||
| 1485 | const hdr_contents = mem.trim(u8, header, &std.ascii.spaces); | ||
| 1486 | if (hdr_contents.len == 0) { | ||
| 1487 | break :hdr_loop; | ||
| 1488 | } | ||
| 1489 | |||
| 1490 | if (mem.startsWith(u8, hdr_contents, "Content-Length: ")) { | ||
| 1491 | content_length = try std.fmt.parseUnsigned(usize, hdr_contents[16..], 10); | ||
| 1492 | } | ||
| 1493 | } | ||
| 1494 | std.testing.expect(content_length != null); | ||
| 1495 | const html_contents = try std.testing.allocator.alloc(u8, content_length.?); | ||
| 1496 | defer std.testing.allocator.free(html_contents); | ||
| 1497 | |||
| 1498 | try client.reader().readNoEof(html_contents); | ||
| 1499 | } | ||
libs/iguanatls/src/x509.zig created+731| ... | @@ -0,0 +1,731 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const Allocator = std.mem.Allocator; | ||
| 3 | const mem = std.mem; | ||
| 4 | const trait = std.meta.trait; | ||
| 5 | |||
| 6 | const asn1 = @import("asn1.zig"); | ||
| 7 | |||
| 8 | // zig fmt: off | ||
| 9 | // http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 | ||
| 10 | // @TODO add backing integer, values | ||
| 11 | pub const CurveId = enum { | ||
| 12 | sect163k1, sect163r1, sect163r2, sect193r1, | ||
| 13 | sect193r2, sect233k1, sect233r1, sect239k1, | ||
| 14 | sect283k1, sect283r1, sect409k1, sect409r1, | ||
| 15 | sect571k1, sect571r1, secp160k1, secp160r1, | ||
| 16 | secp160r2, secp192k1, secp192r1, secp224k1, | ||
| 17 | secp224r1, secp256k1, secp256r1, secp384r1, | ||
| 18 | secp521r1,brainpoolP256r1, brainpoolP384r1, | ||
| 19 | brainpoolP512r1, curve25519, curve448, | ||
| 20 | }; | ||
| 21 | // zig fmt: on | ||
| 22 | |||
| 23 | pub const PublicKey = union(enum) { | ||
| 24 | /// RSA public key | ||
| 25 | rsa: struct { | ||
| 26 | //Positive std.math.big.int.Const numbers. | ||
| 27 | modulus: []const usize, | ||
| 28 | exponent: []const usize, | ||
| 29 | }, | ||
| 30 | /// Elliptic curve public key | ||
| 31 | ec: struct { | ||
| 32 | id: CurveId, | ||
| 33 | /// Public curve point (uncompressed format) | ||
| 34 | curve_point: []const u8, | ||
| 35 | }, | ||
| 36 | |||
| 37 | pub fn deinit(self: @This(), alloc: *Allocator) void { | ||
| 38 | switch (self) { | ||
| 39 | .rsa => |rsa| { | ||
| 40 | alloc.free(rsa.modulus); | ||
| 41 | alloc.free(rsa.exponent); | ||
| 42 | }, | ||
| 43 | .ec => |ec| alloc.free(ec.curve_point), | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | pub fn eql(self: @This(), other: @This()) bool { | ||
| 48 | if (@as(@TagType(@This()), self) != @as(@TagType(@This()), other)) | ||
| 49 | return false; | ||
| 50 | switch (self) { | ||
| 51 | .rsa => |mod_exp| return mem.eql(usize, mod_exp.exponent, other.rsa.exponent) and | ||
| 52 | mem.eql(usize, mod_exp.modulus, other.rsa.modulus), | ||
| 53 | .ec => |ec| return ec.id == other.ec.id and mem.eql(u8, ec.curve_point, other.ec.curve_point), | ||
| 54 | } | ||
| 55 | } | ||
| 56 | }; | ||
| 57 | |||
| 58 | pub fn parse_public_key(allocator: *Allocator, reader: anytype) !PublicKey { | ||
| 59 | if ((try reader.readByte()) != 0x30) | ||
| 60 | return error.MalformedDER; | ||
| 61 | const seq_len = try asn1.der.parse_length(reader); | ||
| 62 | |||
| 63 | if ((try reader.readByte()) != 0x06) | ||
| 64 | return error.MalformedDER; | ||
| 65 | const oid_bytes = try asn1.der.parse_length(reader); | ||
| 66 | if (oid_bytes == 9 ) { | ||
| 67 | // @TODO This fails in async if merged with the if | ||
| 68 | if (!try reader.isBytes(&[9]u8{ 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1 })) | ||
| 69 | return error.MalformedDER; | ||
| 70 | // OID is 1.2.840.113549.1.1.1 | ||
| 71 | // RSA key | ||
| 72 | // Skip past the NULL | ||
| 73 | const null_byte = try reader.readByte(); | ||
| 74 | if (null_byte != 0x05) | ||
| 75 | return error.MalformedDER; | ||
| 76 | const null_len = try asn1.der.parse_length(reader); | ||
| 77 | if (null_len != 0x00) | ||
| 78 | return error.MalformedDER; | ||
| 79 | { | ||
| 80 | // BitString next! | ||
| 81 | if ((try reader.readByte()) != 0x03) | ||
| 82 | return error.MalformedDER; | ||
| 83 | _ = try asn1.der.parse_length(reader); | ||
| 84 | const bit_string_unused_bits = try reader.readByte(); | ||
| 85 | if (bit_string_unused_bits != 0) | ||
| 86 | return error.MalformedDER; | ||
| 87 | |||
| 88 | if ((try reader.readByte()) != 0x30) | ||
| 89 | return error.MalformedDER; | ||
| 90 | _ = try asn1.der.parse_length(reader); | ||
| 91 | |||
| 92 | // Modulus | ||
| 93 | if ((try reader.readByte()) != 0x02) | ||
| 94 | return error.MalformedDER; | ||
| 95 | const modulus = try asn1.der.parse_int(allocator, reader); | ||
| 96 | errdefer allocator.free(modulus.limbs); | ||
| 97 | if (!modulus.positive) return error.MalformedDER; | ||
| 98 | // Exponent | ||
| 99 | if ((try reader.readByte()) != 0x02) | ||
| 100 | return error.MalformedDER; | ||
| 101 | const exponent = try asn1.der.parse_int(allocator, reader); | ||
| 102 | errdefer allocator.free(exponent.limbs); | ||
| 103 | if (!exponent.positive) return error.MalformedDER; | ||
| 104 | return PublicKey{ | ||
| 105 | .rsa = .{ | ||
| 106 | .modulus = modulus.limbs, | ||
| 107 | .exponent = exponent.limbs, | ||
| 108 | }, | ||
| 109 | }; | ||
| 110 | } | ||
| 111 | } else if (oid_bytes == 7) { | ||
| 112 | // @TODO This fails in async if merged with the if | ||
| 113 | if (!try reader.isBytes(&[7]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01 })) | ||
| 114 | return error.MalformedDER; | ||
| 115 | // OID is 1.2.840.10045.2.1 | ||
| 116 | // Elliptical curve | ||
| 117 | // We only support named curves, for which the parameter field is an OID. | ||
| 118 | const oid_tag = try reader.readByte(); | ||
| 119 | if (oid_tag != 0x06) | ||
| 120 | return error.MalformedDER; | ||
| 121 | const curve_oid_bytes = try asn1.der.parse_length(reader); | ||
| 122 | |||
| 123 | var key: PublicKey = undefined; | ||
| 124 | if (curve_oid_bytes == 5) { | ||
| 125 | if (!try reader.isBytes(&[4]u8{ 0x2B, 0x81, 0x04, 0x00 })) | ||
| 126 | return error.MalformedDER; | ||
| 127 | // 1.3.132.0.{34, 35} | ||
| 128 | const last_byte = try reader.readByte(); | ||
| 129 | if (last_byte == 0x22) | ||
| 130 | key.ec = .{ .id = .secp384r1, .curve_point = undefined } | ||
| 131 | else if (last_byte == 0x23) | ||
| 132 | key.ec = .{ .id = .secp521r1, .curve_point = undefined } | ||
| 133 | else | ||
| 134 | return error.MalformedDER; | ||
| 135 | } else if (curve_oid_bytes == 8) | ||
| 136 | { | ||
| 137 | if (!try reader.isBytes(&[8]u8{ 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x3, 0x1, 0x7 })) | ||
| 138 | return error.MalformedDER; | ||
| 139 | key.ec = .{ .id = .secp256r1, .curve_point = undefined }; | ||
| 140 | } else { | ||
| 141 | return error.MalformedDER; | ||
| 142 | } | ||
| 143 | |||
| 144 | if ((try reader.readByte()) != 0x03) | ||
| 145 | return error.MalformedDER; | ||
| 146 | const byte_len = try asn1.der.parse_length(reader); | ||
| 147 | const unused_bits = try reader.readByte(); | ||
| 148 | const bit_count = (byte_len - 1) * 8 - unused_bits; | ||
| 149 | if (bit_count % 8 != 0) | ||
| 150 | return error.MalformedDER; | ||
| 151 | const bit_memory = try allocator.alloc(u8, std.math.divCeil(usize, bit_count, 8) catch unreachable); | ||
| 152 | errdefer allocator.free(bit_memory); | ||
| 153 | try reader.readNoEof(bit_memory[0.. byte_len - 1]); | ||
| 154 | |||
| 155 | key.ec.curve_point = bit_memory; | ||
| 156 | return key; | ||
| 157 | } | ||
| 158 | return error.MalformedDER; | ||
| 159 | } | ||
| 160 | |||
| 161 | pub fn DecodeDERError(comptime Reader: type) type { | ||
| 162 | return Reader.Error || error{ | ||
| 163 | MalformedPEM, | ||
| 164 | MalformedDER, | ||
| 165 | EndOfStream, | ||
| 166 | OutOfMemory, | ||
| 167 | }; | ||
| 168 | } | ||
| 169 | |||
| 170 | pub const TrustAnchor = struct { | ||
| 171 | /// Subject distinguished name | ||
| 172 | dn: []const u8, | ||
| 173 | /// A "CA" anchor is deemed fit to verify signatures on certificates. | ||
| 174 | /// A "non-CA" anchor is accepted only for direct trust (server's certificate | ||
| 175 | /// name and key match the anchor). | ||
| 176 | is_ca: bool = false, | ||
| 177 | public_key: PublicKey, | ||
| 178 | |||
| 179 | const CaptureState = struct { | ||
| 180 | self: *TrustAnchor, | ||
| 181 | allocator: *Allocator, | ||
| 182 | dn_allocated: bool = false, | ||
| 183 | pk_allocated: bool = false, | ||
| 184 | }; | ||
| 185 | fn initSubjectDn(state: *CaptureState, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 186 | const dn_mem = try state.allocator.alloc(u8, length); | ||
| 187 | errdefer state.allocator.free(dn_mem); | ||
| 188 | try reader.readNoEof(dn_mem); | ||
| 189 | state.self.dn = dn_mem; | ||
| 190 | state.dn_allocated = true; | ||
| 191 | } | ||
| 192 | |||
| 193 | fn processExtension(state: *CaptureState, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 194 | const object_id = try asn1.der.parse_value(state.allocator, reader); | ||
| 195 | defer object_id.deinit(state.allocator); | ||
| 196 | if (object_id != .object_identifier) return error.DoesNotMatchSchema; | ||
| 197 | if (object_id.object_identifier.len != 4) | ||
| 198 | return; | ||
| 199 | |||
| 200 | const data = object_id.object_identifier.data; | ||
| 201 | // Basic constraints extension | ||
| 202 | if (data[0] != 2 or data[1] != 5 or data[2] != 29 or data[3] != 15) | ||
| 203 | return; | ||
| 204 | |||
| 205 | const basic_constraints = try asn1.der.parse_value(state.allocator, reader); | ||
| 206 | defer basic_constraints.deinit(state.allocator); | ||
| 207 | if (basic_constraints != .bool) | ||
| 208 | return error.DoesNotMatchSchema; | ||
| 209 | state.self.is_ca = basic_constraints.bool; | ||
| 210 | } | ||
| 211 | |||
| 212 | fn initExtensions(state: *CaptureState, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 213 | const schema = .{ | ||
| 214 | .sequence_of, | ||
| 215 | .{ .capture, 0, .sequence }, | ||
| 216 | }; | ||
| 217 | const captures = .{ | ||
| 218 | state, processExtension, | ||
| 219 | }; | ||
| 220 | try asn1.der.parse_schema(schema, captures, reader); | ||
| 221 | } | ||
| 222 | |||
| 223 | fn initPublicKeyInfo(state: *CaptureState, tag_byte: u8, length: usize, reader: anytype) !void { | ||
| 224 | state.self.public_key = try parse_public_key(state.allocator, reader); | ||
| 225 | state.pk_allocated = true; | ||
| 226 | } | ||
| 227 | |||
| 228 | /// Initialize a trusted anchor from distinguished encoding rules (DER) encoded data | ||
| 229 | pub fn create(allocator: *Allocator, der_reader: anytype) DecodeDERError(@TypeOf(der_reader))!@This() { | ||
| 230 | var self: @This() = undefined; | ||
| 231 | self.is_ca = false; | ||
| 232 | // https://tools.ietf.org/html/rfc5280#page-117 | ||
| 233 | const schema = .{ | ||
| 234 | .sequence, .{ | ||
| 235 | // tbsCertificate | ||
| 236 | .{ | ||
| 237 | .sequence, | ||
| 238 | .{ | ||
| 239 | .{ .context_specific, 0 }, // version | ||
| 240 | .{.int}, // serialNumber | ||
| 241 | .{.sequence}, // signature | ||
| 242 | .{.sequence}, // issuer | ||
| 243 | .{.sequence}, // validity, | ||
| 244 | .{ .capture, 0, .sequence }, // subject | ||
| 245 | .{ .capture, 1, .sequence }, // subjectPublicKeyInfo | ||
| 246 | .{ .optional, .context_specific, 1 }, // issuerUniqueID | ||
| 247 | .{ .optional, .context_specific, 2 }, // subjectUniqueID | ||
| 248 | .{ .capture, 2, .optional, .context_specific, 3 }, // extensions | ||
| 249 | }, | ||
| 250 | }, | ||
| 251 | // signatureAlgorithm | ||
| 252 | .{.sequence}, | ||
| 253 | // signatureValue | ||
| 254 | .{.bit_string}, | ||
| 255 | }, | ||
| 256 | }; | ||
| 257 | |||
| 258 | var capture_state = CaptureState{ | ||
| 259 | .self = &self, | ||
| 260 | .allocator = allocator, | ||
| 261 | }; | ||
| 262 | const captures = .{ | ||
| 263 | &capture_state, initSubjectDn, | ||
| 264 | &capture_state, initPublicKeyInfo, | ||
| 265 | &capture_state, initExtensions, | ||
| 266 | }; | ||
| 267 | |||
| 268 | errdefer { | ||
| 269 | if (capture_state.dn_allocated) | ||
| 270 | allocator.free(self.dn); | ||
| 271 | if (capture_state.pk_allocated) | ||
| 272 | self.public_key.deinit(allocator); | ||
| 273 | } | ||
| 274 | |||
| 275 | asn1.der.parse_schema(schema, captures, der_reader) catch |err| switch (err) { | ||
| 276 | error.InvalidLength, | ||
| 277 | error.InvalidTag, | ||
| 278 | error.InvalidContainerLength, | ||
| 279 | error.DoesNotMatchSchema, | ||
| 280 | => return error.MalformedDER, | ||
| 281 | else => |e| return e, | ||
| 282 | }; | ||
| 283 | return self; | ||
| 284 | } | ||
| 285 | |||
| 286 | pub fn deinit(self: @This(), alloc: *Allocator) void { | ||
| 287 | alloc.free(self.dn); | ||
| 288 | self.public_key.deinit(alloc); | ||
| 289 | } | ||
| 290 | |||
| 291 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { | ||
| 292 | try writer.print( | ||
| 293 | \\CERTIFICATE | ||
| 294 | \\----------- | ||
| 295 | \\IS CA: {} | ||
| 296 | \\Subject distinguished name (encoded): | ||
| 297 | \\{X} | ||
| 298 | \\Public key: | ||
| 299 | \\ | ||
| 300 | , .{ self.is_ca, self.dn }); | ||
| 301 | |||
| 302 | switch (self.public_key) { | ||
| 303 | .rsa => |mod_exp| { | ||
| 304 | const modulus = std.math.big.int.Const{ .positive = true, .limbs = mod_exp.modulus }; | ||
| 305 | const exponent = std.math.big.int.Const{ .positive = true, .limbs = mod_exp.exponent }; | ||
| 306 | try writer.print( | ||
| 307 | \\RSA | ||
| 308 | \\modulus: {} | ||
| 309 | \\exponent: {} | ||
| 310 | \\ | ||
| 311 | , .{ | ||
| 312 | modulus, | ||
| 313 | exponent, | ||
| 314 | }); | ||
| 315 | }, | ||
| 316 | .ec => |ec| { | ||
| 317 | try writer.print( | ||
| 318 | \\EC (Curve: {}) | ||
| 319 | \\point: {} | ||
| 320 | \\ | ||
| 321 | , .{ | ||
| 322 | ec.id, | ||
| 323 | ec.curve_point, | ||
| 324 | }); | ||
| 325 | }, | ||
| 326 | } | ||
| 327 | |||
| 328 | try writer.writeAll( | ||
| 329 | \\----------- | ||
| 330 | \\ | ||
| 331 | ); | ||
| 332 | } | ||
| 333 | }; | ||
| 334 | |||
| 335 | pub const TrustAnchorChain = struct { | ||
| 336 | data: std.ArrayList(TrustAnchor), | ||
| 337 | |||
| 338 | pub fn from_pem(allocator: *Allocator, pem_reader: anytype) DecodeDERError(@TypeOf(pem_reader))!@This() { | ||
| 339 | var self = @This(){ .data = std.ArrayList(TrustAnchor).init(allocator) }; | ||
| 340 | errdefer self.deinit(); | ||
| 341 | |||
| 342 | var it = pemCertificateIterator(pem_reader); | ||
| 343 | while (try it.next()) |cert_reader| { | ||
| 344 | var buffered = std.io.bufferedReader(cert_reader); | ||
| 345 | const anchor = try TrustAnchor.create(allocator, buffered.reader()); | ||
| 346 | errdefer anchor.deinit(allocator); | ||
| 347 | |||
| 348 | // This read forces the cert reader to find the `-----END` | ||
| 349 | // @TODO Should work without this read, investigate | ||
| 350 | _ = cert_reader.readByte() catch |err| switch (err) { | ||
| 351 | error.EndOfStream => { | ||
| 352 | try self.data.append(anchor); | ||
| 353 | break; | ||
| 354 | }, | ||
| 355 | else => |e| return e, | ||
| 356 | }; | ||
| 357 | return error.MalformedDER; | ||
| 358 | } | ||
| 359 | return self; | ||
| 360 | } | ||
| 361 | |||
| 362 | pub fn deinit(self: @This()) void { | ||
| 363 | const alloc = self.data.allocator; | ||
| 364 | for (self.data.items) |ta| ta.deinit(alloc); | ||
| 365 | self.data.deinit(); | ||
| 366 | } | ||
| 367 | }; | ||
| 368 | |||
| 369 | fn PEMSectionReader(comptime Reader: type) type { | ||
| 370 | const ReadError = Reader.Error || error{MalformedPEM}; | ||
| 371 | const S = struct { | ||
| 372 | pub fn read(self: *PEMCertificateIterator(Reader), buffer: []u8) ReadError!usize { | ||
| 373 | const end = "-----END "; | ||
| 374 | var end_letters_matched: ?usize = null; | ||
| 375 | |||
| 376 | var out_idx: usize = 0; | ||
| 377 | if (self.waiting_chars_len > 0) { | ||
| 378 | const rest_written = std.math.min(self.waiting_chars_len, buffer.len); | ||
| 379 | while (out_idx < rest_written) : (out_idx += 1) { | ||
| 380 | buffer[out_idx] = self.waiting_chars[out_idx]; | ||
| 381 | } | ||
| 382 | |||
| 383 | self.waiting_chars_len -= rest_written; | ||
| 384 | if (self.waiting_chars_len != 0) { | ||
| 385 | std.mem.copy(u8, self.waiting_chars[0..], self.waiting_chars[rest_written..]); | ||
| 386 | } | ||
| 387 | |||
| 388 | if (out_idx == buffer.len) { | ||
| 389 | return out_idx; | ||
| 390 | } | ||
| 391 | } | ||
| 392 | |||
| 393 | var base64_buf: [4]u8 = undefined; | ||
| 394 | var base64_idx: usize = 0; | ||
| 395 | |||
| 396 | while (true) { | ||
| 397 | var byte = self.reader.readByte() catch |err| switch (err) { | ||
| 398 | error.EndOfStream => { | ||
| 399 | if (self.skip_to_newline_exit) { | ||
| 400 | self.state = .none; | ||
| 401 | return 0; | ||
| 402 | } | ||
| 403 | return error.MalformedPEM; | ||
| 404 | }, | ||
| 405 | else => |e| return e, | ||
| 406 | }; | ||
| 407 | |||
| 408 | if (self.skip_to_newline_exit) { | ||
| 409 | if (byte == '\n') { | ||
| 410 | self.skip_to_newline_exit = false; | ||
| 411 | self.state = .none; | ||
| 412 | return 0; | ||
| 413 | } | ||
| 414 | continue; | ||
| 415 | } | ||
| 416 | |||
| 417 | if (byte == '\n' or byte == '\r') { | ||
| 418 | self.empty_line = true; | ||
| 419 | continue; | ||
| 420 | } | ||
| 421 | defer self.empty_line = false; | ||
| 422 | |||
| 423 | if (end_letters_matched) |*matched| { | ||
| 424 | if (end[matched.*] == byte) { | ||
| 425 | matched.* += 1; | ||
| 426 | if (matched.* == end.len) { | ||
| 427 | self.skip_to_newline_exit = true; | ||
| 428 | if (out_idx > 0) | ||
| 429 | return out_idx | ||
| 430 | else | ||
| 431 | continue; | ||
| 432 | } | ||
| 433 | continue; | ||
| 434 | } else return error.MalformedPEM; | ||
| 435 | } else if (self.empty_line and end[0] == byte) { | ||
| 436 | end_letters_matched = 1; | ||
| 437 | continue; | ||
| 438 | } | ||
| 439 | |||
| 440 | base64_buf[base64_idx] = byte; | ||
| 441 | base64_idx += 1; | ||
| 442 | if (base64_idx == base64_buf.len) { | ||
| 443 | base64_idx = 0; | ||
| 444 | |||
| 445 | const out_len = std.base64.standard_decoder.calcSize(&base64_buf) catch | ||
| 446 | return error.MalformedPEM; | ||
| 447 | const rest_chars = if (out_len > buffer.len - out_idx) | ||
| 448 | out_len - (buffer.len - out_idx) | ||
| 449 | else | ||
| 450 | 0; | ||
| 451 | const buf_chars = out_len - rest_chars; | ||
| 452 | |||
| 453 | var res_buffer: [3]u8 = undefined; | ||
| 454 | std.base64.standard_decoder_unsafe.decode(res_buffer[0..out_len], &base64_buf); | ||
| 455 | |||
| 456 | var i: u3 = 0; | ||
| 457 | while (i < buf_chars) : (i += 1) { | ||
| 458 | buffer[out_idx] = res_buffer[i]; | ||
| 459 | out_idx += 1; | ||
| 460 | } | ||
| 461 | |||
| 462 | if (rest_chars > 0) { | ||
| 463 | mem.copy(u8, &self.waiting_chars, res_buffer[i..]); | ||
| 464 | self.waiting_chars_len = @intCast(u2, rest_chars); | ||
| 465 | } | ||
| 466 | if (out_idx == buffer.len) | ||
| 467 | return out_idx; | ||
| 468 | } | ||
| 469 | } | ||
| 470 | } | ||
| 471 | }; | ||
| 472 | return std.io.Reader(*PEMCertificateIterator(Reader), ReadError, S.read); | ||
| 473 | } | ||
| 474 | |||
| 475 | fn PEMCertificateIterator(comptime Reader: type) type { | ||
| 476 | return struct { | ||
| 477 | pub const SectionReader = PEMSectionReader(Reader); | ||
| 478 | pub const NextError = SectionReader.Error || error{EndOfStream}; | ||
| 479 | |||
| 480 | reader: Reader, | ||
| 481 | // Internal state for the iterator and the current reader. | ||
| 482 | skip_to_newline_exit: bool = false, | ||
| 483 | empty_line: bool = false, | ||
| 484 | waiting_chars: [4]u8 = undefined, | ||
| 485 | waiting_chars_len: u2 = 0, | ||
| 486 | state: enum { | ||
| 487 | none, | ||
| 488 | in_section_name, | ||
| 489 | in_cert, | ||
| 490 | in_other, | ||
| 491 | } = .none, | ||
| 492 | |||
| 493 | pub fn next(self: *@This()) NextError!?SectionReader { | ||
| 494 | const end = "-----END "; | ||
| 495 | const begin = "-----BEGIN "; | ||
| 496 | const certificate = "CERTIFICATE"; | ||
| 497 | const x509_certificate = "X.509 CERTIFICATE"; | ||
| 498 | |||
| 499 | var line_empty = true; | ||
| 500 | var end_letters_matched: ?usize = null; | ||
| 501 | var begin_letters_matched: ?usize = null; | ||
| 502 | var certificate_letters_matched: ?usize = null; | ||
| 503 | var x509_certificate_letters_matched: ?usize = null; | ||
| 504 | var skip_to_newline = false; | ||
| 505 | var return_after_skip = false; | ||
| 506 | |||
| 507 | var base64_buf: [4]u8 = undefined; | ||
| 508 | var base64_buf_idx: usize = 0; | ||
| 509 | |||
| 510 | // Called next before reading all of the previous cert. | ||
| 511 | if (self.state == .in_cert) { | ||
| 512 | self.waiting_chars_len = 0; | ||
| 513 | } | ||
| 514 | |||
| 515 | while (true) { | ||
| 516 | var last_byte = false; | ||
| 517 | const byte = self.reader.readByte() catch |err| switch (err) { | ||
| 518 | error.EndOfStream => blk: { | ||
| 519 | if (line_empty and self.state == .none) { | ||
| 520 | return null; | ||
| 521 | } else { | ||
| 522 | last_byte = true; | ||
| 523 | break :blk '\n'; | ||
| 524 | } | ||
| 525 | }, | ||
| 526 | else => |e| return e, | ||
| 527 | }; | ||
| 528 | |||
| 529 | if (skip_to_newline) { | ||
| 530 | if (last_byte) | ||
| 531 | return null; | ||
| 532 | if (byte == '\n') { | ||
| 533 | if (return_after_skip) { | ||
| 534 | return SectionReader{ .context = self }; | ||
| 535 | } | ||
| 536 | skip_to_newline = false; | ||
| 537 | line_empty = true; | ||
| 538 | } | ||
| 539 | continue; | ||
| 540 | } else if (byte == '\r' or byte == '\n') { | ||
| 541 | line_empty = true; | ||
| 542 | continue; | ||
| 543 | } | ||
| 544 | |||
| 545 | defer line_empty = byte == '\n' or (line_empty and byte == ' '); | ||
| 546 | |||
| 547 | switch (self.state) { | ||
| 548 | .none => { | ||
| 549 | if (begin_letters_matched) |*matched| { | ||
| 550 | if (begin[matched.*] != byte) | ||
| 551 | return error.MalformedPEM; | ||
| 552 | |||
| 553 | matched.* += 1; | ||
| 554 | if (matched.* == begin.len) { | ||
| 555 | self.state = .in_section_name; | ||
| 556 | line_empty = true; | ||
| 557 | begin_letters_matched = null; | ||
| 558 | } | ||
| 559 | } else if (begin[0] == byte) { | ||
| 560 | begin_letters_matched = 1; | ||
| 561 | } else if (mem.indexOfScalar(u8, &std.ascii.spaces, byte) != null) { | ||
| 562 | if (last_byte) return null; | ||
| 563 | } else return error.MalformedPEM; | ||
| 564 | }, | ||
| 565 | .in_section_name => { | ||
| 566 | if (certificate_letters_matched) |*matched| { | ||
| 567 | if (certificate[matched.*] != byte) { | ||
| 568 | self.state = .in_other; | ||
| 569 | skip_to_newline = true; | ||
| 570 | continue; | ||
| 571 | } | ||
| 572 | matched.* += 1; | ||
| 573 | if (matched.* == certificate.len) { | ||
| 574 | self.state = .in_cert; | ||
| 575 | certificate_letters_matched = null; | ||
| 576 | skip_to_newline = true; | ||
| 577 | return_after_skip = true; | ||
| 578 | } | ||
| 579 | } else if (x509_certificate_letters_matched) |*matched| { | ||
| 580 | if (x509_certificate[matched.*] != byte) { | ||
| 581 | self.state = .in_other; | ||
| 582 | skip_to_newline = true; | ||
| 583 | continue; | ||
| 584 | } | ||
| 585 | matched.* += 1; | ||
| 586 | if (matched.* == x509_certificate.len) { | ||
| 587 | self.state = .in_cert; | ||
| 588 | x509_certificate_letters_matched = null; | ||
| 589 | skip_to_newline = true; | ||
| 590 | return_after_skip = true; | ||
| 591 | } | ||
| 592 | } else if (line_empty and certificate[0] == byte) { | ||
| 593 | certificate_letters_matched = 1; | ||
| 594 | } else if (line_empty and x509_certificate[0] == byte) { | ||
| 595 | x509_certificate_letters_matched = 1; | ||
| 596 | } else if (line_empty) { | ||
| 597 | self.state = .in_other; | ||
| 598 | skip_to_newline = true; | ||
| 599 | } else unreachable; | ||
| 600 | }, | ||
| 601 | .in_other, .in_cert => { | ||
| 602 | if (end_letters_matched) |*matched| { | ||
| 603 | if (end[matched.*] != byte) { | ||
| 604 | end_letters_matched = null; | ||
| 605 | skip_to_newline = true; | ||
| 606 | continue; | ||
| 607 | } | ||
| 608 | matched.* += 1; | ||
| 609 | if (matched.* == end.len) { | ||
| 610 | self.state = .none; | ||
| 611 | end_letters_matched = null; | ||
| 612 | skip_to_newline = true; | ||
| 613 | } | ||
| 614 | } else if (line_empty and end[0] == byte) { | ||
| 615 | end_letters_matched = 1; | ||
| 616 | } | ||
| 617 | }, | ||
| 618 | } | ||
| 619 | } | ||
| 620 | } | ||
| 621 | }; | ||
| 622 | } | ||
| 623 | |||
| 624 | /// Iterator of io.Reader that each decode one certificate from the PEM reader. | ||
| 625 | /// Readers do not have to be fully consumed until end of stream, but they must be | ||
| 626 | /// read from in order. | ||
| 627 | /// Iterator.SectionReader is the type of the io.Reader, Iterator.NextError is the error | ||
| 628 | /// set of the next() function. | ||
| 629 | pub fn pemCertificateIterator(reader: anytype) PEMCertificateIterator(@TypeOf(reader)) { | ||
| 630 | return .{ .reader = reader }; | ||
| 631 | } | ||
| 632 | |||
| 633 | pub const NameElement = struct { | ||
| 634 | // Encoded OID without tag | ||
| 635 | oid: asn1.ObjectIdentifier, | ||
| 636 | // Destination buffer | ||
| 637 | buf: []u8, | ||
| 638 | status: enum { | ||
| 639 | not_found, | ||
| 640 | found, | ||
| 641 | errored, | ||
| 642 | }, | ||
| 643 | }; | ||
| 644 | |||
| 645 | const github_pem = @embedFile("../test/github.pem"); | ||
| 646 | const github_der = @embedFile("../test/github.der"); | ||
| 647 | |||
| 648 | fn expected_pem_certificate_chain(bytes: []const u8, certs: []const []const u8) !void { | ||
| 649 | var fbs = std.io.fixedBufferStream(bytes); | ||
| 650 | |||
| 651 | var it = pemCertificateIterator(fbs.reader()); | ||
| 652 | var idx: usize = 0; | ||
| 653 | while (try it.next()) |cert_reader| : (idx += 1) { | ||
| 654 | const result_bytes = try cert_reader.readAllAlloc(std.testing.allocator, std.math.maxInt(usize)); | ||
| 655 | defer std.testing.allocator.free(result_bytes); | ||
| 656 | std.testing.expectEqualSlices(u8, certs[idx], result_bytes); | ||
| 657 | } | ||
| 658 | if (idx != certs.len) { | ||
| 659 | std.debug.panic("Read {} certificates, wanted {}", .{ idx, certs.len }); | ||
| 660 | } | ||
| 661 | std.testing.expect((try it.next()) == null); | ||
| 662 | } | ||
| 663 | |||
| 664 | fn expected_pem_certificate(bytes: []const u8, cert_bytes: []const u8) !void { | ||
| 665 | try expected_pem_certificate_chain(bytes, &[1][]const u8{cert_bytes}); | ||
| 666 | } | ||
| 667 | |||
| 668 | test "pemCertificateIterator" { | ||
| 669 | try expected_pem_certificate(github_pem, github_der); | ||
| 670 | try expected_pem_certificate( | ||
| 671 | \\-----BEGIN BOGUS----- | ||
| 672 | \\-----END BOGUS----- | ||
| 673 | \\ | ||
| 674 | ++ | ||
| 675 | github_pem, | ||
| 676 | github_der, | ||
| 677 | ); | ||
| 678 | |||
| 679 | try expected_pem_certificate_chain( | ||
| 680 | github_pem ++ | ||
| 681 | \\ | ||
| 682 | \\-----BEGIN BOGUS----- | ||
| 683 | \\-----END BOGUS----- | ||
| 684 | \\ | ||
| 685 | ++ github_pem, | ||
| 686 | &[2][]const u8{ github_der, github_der }, | ||
| 687 | ); | ||
| 688 | |||
| 689 | try expected_pem_certificate_chain( | ||
| 690 | \\-----BEGIN BOGUS----- | ||
| 691 | \\-----END BOGUS----- | ||
| 692 | \\ | ||
| 693 | , | ||
| 694 | &[0][]const u8{}, | ||
| 695 | ); | ||
| 696 | |||
| 697 | // Try reading byte by byte from a cert reader | ||
| 698 | { | ||
| 699 | var fbs = std.io.fixedBufferStream(github_pem ++ "\n" ++ github_pem); | ||
| 700 | var it = pemCertificateIterator(fbs.reader()); | ||
| 701 | |||
| 702 | // Read a couple of bytes from the first reader, then skip to the next | ||
| 703 | { | ||
| 704 | const first_reader = (try it.next()) orelse return error.NoCertificate; | ||
| 705 | var first_few: [8]u8 = undefined; | ||
| 706 | const bytes = try first_reader.readAll(&first_few); | ||
| 707 | std.testing.expectEqual(first_few.len, bytes); | ||
| 708 | std.testing.expectEqualSlices(u8, github_der[0..bytes], &first_few); | ||
| 709 | } | ||
| 710 | |||
| 711 | const next_reader = (try it.next()) orelse return error.NoCertificate; | ||
| 712 | var idx: usize = 0; | ||
| 713 | while (true) : (idx += 1) { | ||
| 714 | const byte = next_reader.readByte() catch |err| switch (err) { | ||
| 715 | error.EndOfStream => break, | ||
| 716 | else => |e| return e, | ||
| 717 | }; | ||
| 718 | if (github_der[idx] != byte) { | ||
| 719 | std.debug.panic("index {}: expected 0x{X}, found 0x{X}", .{ idx, github_der[idx], byte }); | ||
| 720 | } | ||
| 721 | } | ||
| 722 | std.testing.expectEqual(github_der.len, idx); | ||
| 723 | std.testing.expect((try it.next()) == null); | ||
| 724 | } | ||
| 725 | } | ||
| 726 | |||
| 727 | test "TrustAnchorChain" { | ||
| 728 | var fbs = std.io.fixedBufferStream(github_pem); | ||
| 729 | const chain = try TrustAnchorChain.from_pem(std.testing.allocator, fbs.reader()); | ||
| 730 | defer chain.deinit(); | ||
| 731 | } | ||
libs/iguanatls/test/DigiCertHighAssuranceEVRootCA.crt.pem created+23| ... | @@ -0,0 +1,23 @@ | ||
| 1 | -----BEGIN CERTIFICATE----- | ||
| 2 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs | ||
| 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 | ||
| 4 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j | ||
| 5 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL | ||
| 6 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 | ||
| 7 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug | ||
| 8 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm | ||
| 9 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW | ||
| 10 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM | ||
| 11 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB | ||
| 12 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 | ||
| 13 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg | ||
| 14 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF | ||
| 15 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA | ||
| 16 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec | ||
| 17 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z | ||
| 18 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF | ||
| 19 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 | ||
| 20 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe | ||
| 21 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep | ||
| 22 | +OkuE6N36B9K | ||
| 23 | -----END CERTIFICATE----- | ||
libs/iguanatls/test/github.der| Binary files /dev/null and b/libs/iguanatls/test/github.der differ | |||
libs/iguanatls/test/github.pem created+23| ... | @@ -0,0 +1,23 @@ | ||
| 1 | -----BEGIN CERTIFICATE----- | ||
| 2 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs | ||
| 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 | ||
| 4 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j | ||
| 5 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL | ||
| 6 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 | ||
| 7 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug | ||
| 8 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm | ||
| 9 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW | ||
| 10 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM | ||
| 11 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB | ||
| 12 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 | ||
| 13 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg | ||
| 14 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF | ||
| 15 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA | ||
| 16 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec | ||
| 17 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z | ||
| 18 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF | ||
| 19 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 | ||
| 20 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe | ||
| 21 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep | ||
| 22 | +OkuE6N36B9K | ||
| 23 | -----END CERTIFICATE----- | ||
libs/iguanatls/zig.mod created+4| ... | @@ -0,0 +1,4 @@ | ||
| 1 | id: csbnipaad8n77buaszsnjvlmn6j173fl7pkprsctelswjywe | ||
| 2 | name: iguanatls | ||
| 3 | main: src/main.zig | ||
| 4 | dependencies: | ||
libs/zuri/.github/workflows/ci.yml created+22| ... | @@ -0,0 +1,22 @@ | ||
| 1 | name: CI | ||
| 2 | on: [push] | ||
| 3 | jobs: | ||
| 4 | test: | ||
| 5 | strategy: | ||
| 6 | matrix: | ||
| 7 | os: [ubuntu-latest] | ||
| 8 | runs-on: ${{matrix.os}} | ||
| 9 | steps: | ||
| 10 | - uses: actions/checkout@v2 | ||
| 11 | - uses: goto-bus-stop/setup-zig@v1.2.1 | ||
| 12 | with: | ||
| 13 | version: master | ||
| 14 | - run: zig build test | ||
| 15 | fmt: | ||
| 16 | runs-on: ubuntu-latest | ||
| 17 | steps: | ||
| 18 | - uses: actions/checkout@v2 | ||
| 19 | - uses: goto-bus-stop/setup-zig@v1.2.1 | ||
| 20 | with: | ||
| 21 | version: master | ||
| 22 | - run: zig fmt build.zig src --check | ||
| \ No newline at end of file | |||
libs/zuri/.gitignore created+2| ... | @@ -0,0 +1,2 @@ | ||
| 1 | .vscode/ | ||
| 2 | zig-cache/ | ||
| \ No newline at end of file | |||
libs/zuri/LICENSE created+21| ... | @@ -0,0 +1,21 @@ | ||
| 1 | MIT License | ||
| 2 | |||
| 3 | Copyright (c) 2019 Vexu | ||
| 4 | |||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 6 | of this software and associated documentation files (the "Software"), to deal | ||
| 7 | in the Software without restriction, including without limitation the rights | ||
| 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 9 | copies of the Software, and to permit persons to whom the Software is | ||
| 10 | furnished to do so, subject to the following conditions: | ||
| 11 | |||
| 12 | The above copyright notice and this permission notice shall be included in all | ||
| 13 | copies or substantial portions of the Software. | ||
| 14 | |||
| 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 21 | SOFTWARE. | ||
| \ No newline at end of file | |||
libs/zuri/README.md created+16| ... | @@ -0,0 +1,16 @@ | ||
| 1 | # zuri | ||
| 2 | [URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) parser written in [Zig](https://ziglang.org/). | ||
| 3 | |||
| 4 | ## Example | ||
| 5 | ```Zig | ||
| 6 | const uri = try Uri.parse("https://ziglang.org/documentation/master/#toc-Introduction"); | ||
| 7 | assert(mem.eql(u8, uri.scheme, "https")); | ||
| 8 | assert(mem.eql(u8, uri.host, "ziglang.org")); | ||
| 9 | assert(mem.eql(u8, uri.path, "/documentation/master/")); | ||
| 10 | assert(mem.eql(u8, uri.fragment, "toc-Introduction")); | ||
| 11 | ``` | ||
| 12 | |||
| 13 | ## Zig version | ||
| 14 | By default the master branch version requires the latest Zig master version. | ||
| 15 | |||
| 16 | For Zig 0.6.0 use commit 89ff8258ccd09d0a4cfda649828cc1c45f2b1f8f. | ||
| \ No newline at end of file | |||
libs/zuri/build.zig created+10| ... | @@ -0,0 +1,10 @@ | ||
| 1 | const Builder = @import("std").build.Builder; | ||
| 2 | |||
| 3 | pub fn build(b: *Builder) void { | ||
| 4 | const tests = b.addTest("test.zig"); | ||
| 5 | tests.setBuildMode(b.standardReleaseOptions()); | ||
| 6 | |||
| 7 | const test_step = b.step("test", "Run library tests"); | ||
| 8 | test_step.dependOn(&tests.step); | ||
| 9 | b.default_step.dependOn(test_step); | ||
| 10 | } | ||
libs/zuri/src/zuri.zig created+592| ... | @@ -0,0 +1,592 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const mem = std.mem; | ||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const assert = std.debug.assert; | ||
| 5 | const parseUnsigned = std.fmt.parseUnsigned; | ||
| 6 | const net = std.net; | ||
| 7 | const expect = std.testing.expect; | ||
| 8 | |||
| 9 | const ValueMap = std.StringHashMap([]const u8); | ||
| 10 | |||
| 11 | pub const Uri = struct { | ||
| 12 | scheme: []const u8, | ||
| 13 | username: []const u8, | ||
| 14 | password: []const u8, | ||
| 15 | host: Host, | ||
| 16 | port: ?u16, | ||
| 17 | path: []const u8, | ||
| 18 | query: []const u8, | ||
| 19 | fragment: []const u8, | ||
| 20 | len: usize, | ||
| 21 | |||
| 22 | /// possible uri host values | ||
| 23 | pub const Host = union(enum) { | ||
| 24 | ip: net.Address, | ||
| 25 | name: []const u8, | ||
| 26 | }; | ||
| 27 | |||
| 28 | /// possible errors for mapQuery | ||
| 29 | pub const MapError = error{ | ||
| 30 | NoQuery, | ||
| 31 | OutOfMemory, | ||
| 32 | }; | ||
| 33 | |||
| 34 | /// map query string into a hashmap of key value pairs with no value being an empty string | ||
| 35 | pub fn mapQuery(allocator: *Allocator, query: []const u8) MapError!ValueMap { | ||
| 36 | if (query.len == 0) { | ||
| 37 | return error.NoQuery; | ||
| 38 | } | ||
| 39 | var map = ValueMap.init(allocator); | ||
| 40 | errdefer map.deinit(); | ||
| 41 | var start: u32 = 0; | ||
| 42 | var mid: u32 = 0; | ||
| 43 | for (query) |c, i| { | ||
| 44 | if (c == ';' or c == '&') { | ||
| 45 | if (mid != 0) { | ||
| 46 | _ = try map.put(query[start..mid], query[mid + 1 .. i]); | ||
| 47 | } else { | ||
| 48 | _ = try map.put(query[start..i], ""); | ||
| 49 | } | ||
| 50 | start = @truncate(u32, i + 1); | ||
| 51 | mid = 0; | ||
| 52 | } else if (c == '=') { | ||
| 53 | mid = @truncate(u32, i); | ||
| 54 | } | ||
| 55 | } | ||
| 56 | if (mid != 0) { | ||
| 57 | _ = try map.put(query[start..mid], query[mid + 1 ..]); | ||
| 58 | } else { | ||
| 59 | _ = try map.put(query[start..], ""); | ||
| 60 | } | ||
| 61 | |||
| 62 | return map; | ||
| 63 | } | ||
| 64 | |||
| 65 | /// possible errors for decode and encode | ||
| 66 | pub const EncodeError = error{ | ||
| 67 | InvalidCharacter, | ||
| 68 | OutOfMemory, | ||
| 69 | }; | ||
| 70 | |||
| 71 | /// decode path if it is percent encoded | ||
| 72 | pub fn decode(allocator: *Allocator, path: []const u8) EncodeError!?[]u8 { | ||
| 73 | var ret: ?[]u8 = null; | ||
| 74 | var ret_index: usize = 0; | ||
| 75 | var i: usize = 0; | ||
| 76 | |||
| 77 | while (i < path.len) : (i += 1) { | ||
| 78 | if (path[i] == '%') { | ||
| 79 | if (!isPchar(path[i..])) { | ||
| 80 | return error.InvalidCharacter; | ||
| 81 | } | ||
| 82 | if (ret == null) { | ||
| 83 | ret = try allocator.alloc(u8, path.len); | ||
| 84 | mem.copy(u8, ret.?, path[0..i]); | ||
| 85 | ret_index = i; | ||
| 86 | } | ||
| 87 | |||
| 88 | // charToDigit can't fail because the chars are validated earlier | ||
| 89 | var new = (std.fmt.charToDigit(path[i + 1], 16) catch unreachable) << 4; | ||
| 90 | new |= std.fmt.charToDigit(path[i + 2], 16) catch unreachable; | ||
| 91 | ret.?[ret_index] = new; | ||
| 92 | ret_index += 1; | ||
| 93 | i += 2; | ||
| 94 | } else if (path[i] != '/' and !isPchar(path[i..])) { | ||
| 95 | return error.InvalidCharacter; | ||
| 96 | } else if (ret != null) { | ||
| 97 | ret.?[ret_index] = path[i]; | ||
| 98 | ret_index += 1; | ||
| 99 | } | ||
| 100 | } | ||
| 101 | if (ret != null) { | ||
| 102 | return allocator.realloc(ret.?, ret_index) catch ret.?[0..ret_index]; | ||
| 103 | } | ||
| 104 | return ret; | ||
| 105 | } | ||
| 106 | |||
| 107 | /// percent encode if path contains characters not allowed in paths | ||
| 108 | pub fn encode(allocator: *Allocator, path: []const u8) EncodeError!?[]u8 { | ||
| 109 | var ret: ?[]u8 = null; | ||
| 110 | var ret_index: usize = 0; | ||
| 111 | for (path) |c, i| { | ||
| 112 | if (c != '/' and !isPchar(path[i..])) { | ||
| 113 | if (ret == null) { | ||
| 114 | ret = try allocator.alloc(u8, path.len * 3); | ||
| 115 | mem.copy(u8, ret.?, path[0..i]); | ||
| 116 | ret_index = i; | ||
| 117 | } | ||
| 118 | const hex_digits = "0123456789ABCDEF"; | ||
| 119 | ret.?[ret_index] = '%'; | ||
| 120 | ret.?[ret_index + 1] = hex_digits[(c & 0xF0) >> 4]; | ||
| 121 | ret.?[ret_index + 2] = hex_digits[c & 0x0F]; | ||
| 122 | ret_index += 3; | ||
| 123 | } else if (ret != null) { | ||
| 124 | ret.?[ret_index] = c; | ||
| 125 | ret_index += 1; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | if (ret != null) { | ||
| 129 | return allocator.realloc(ret.?, ret_index) catch ret.?[0..ret_index]; | ||
| 130 | } | ||
| 131 | return ret; | ||
| 132 | } | ||
| 133 | |||
| 134 | /// resolves `path`, leaves trailing '/' | ||
| 135 | /// assumes `path` to be valid | ||
| 136 | pub fn resolvePath(allocator: *Allocator, path: []const u8) error{OutOfMemory}![]u8 { | ||
| 137 | assert(path.len > 0); | ||
| 138 | var list = std.ArrayList([]const u8).init(allocator); | ||
| 139 | errdefer list.deinit(); | ||
| 140 | |||
| 141 | var it = mem.tokenize(path, "/"); | ||
| 142 | while (it.next()) |p| { | ||
| 143 | if (mem.eql(u8, p, ".")) { | ||
| 144 | continue; | ||
| 145 | } else if (mem.eql(u8, p, "..")) { | ||
| 146 | _ = list.popOrNull(); | ||
| 147 | } else { | ||
| 148 | try list.append(p); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | var buf = try allocator.alloc(u8, path.len); | ||
| 153 | errdefer allocator.free(buf); | ||
| 154 | var len: usize = 0; | ||
| 155 | var segments = list.toOwnedSlice(); | ||
| 156 | defer allocator.free(segments); | ||
| 157 | |||
| 158 | for (segments) |s| { | ||
| 159 | buf[len] = '/'; | ||
| 160 | len += 1; | ||
| 161 | mem.copy(u8, buf[len..], s); | ||
| 162 | len += s.len; | ||
| 163 | } | ||
| 164 | |||
| 165 | if (path[path.len - 1] == '/') { | ||
| 166 | buf[len] = '/'; | ||
| 167 | len += 1; | ||
| 168 | } | ||
| 169 | |||
| 170 | return allocator.realloc(buf, len) catch buf[0..len]; | ||
| 171 | } | ||
| 172 | |||
| 173 | /// possible errors for parse | ||
| 174 | pub const Error = error{ | ||
| 175 | /// input is not a valid uri due to a invalid character | ||
| 176 | /// mostly a result of invalid ipv6 | ||
| 177 | InvalidCharacter, | ||
| 178 | |||
| 179 | /// given input was empty | ||
| 180 | EmptyUri, | ||
| 181 | }; | ||
| 182 | |||
| 183 | /// parse URI from input | ||
| 184 | /// empty input is an error | ||
| 185 | /// if assume_auth is true then `example.com` will result in `example.com` being the host instead of path | ||
| 186 | pub fn parse(input: []const u8, assume_auth: bool) Error!Uri { | ||
| 187 | if (input.len == 0) { | ||
| 188 | return error.EmptyUri; | ||
| 189 | } | ||
| 190 | var uri = Uri{ | ||
| 191 | .scheme = "", | ||
| 192 | .username = "", | ||
| 193 | .password = "", | ||
| 194 | .host = .{ .name = "" }, | ||
| 195 | .port = null, | ||
| 196 | .path = "", | ||
| 197 | .query = "", | ||
| 198 | .fragment = "", | ||
| 199 | .len = 0, | ||
| 200 | }; | ||
| 201 | |||
| 202 | switch (input[0]) { | ||
| 203 | 'a'...'z', 'A'...'Z' => { | ||
| 204 | uri.parseMaybeScheme(input); | ||
| 205 | }, | ||
| 206 | else => {}, | ||
| 207 | } | ||
| 208 | |||
| 209 | if (input.len > uri.len + 2 and input[uri.len] == '/' and input[uri.len + 1] == '/') { | ||
| 210 | uri.len += 2; // for the '//' | ||
| 211 | try uri.parseAuth(input[uri.len..]); | ||
| 212 | } else if (assume_auth) { | ||
| 213 | try uri.parseAuth(input[uri.len..]); | ||
| 214 | } | ||
| 215 | |||
| 216 | // make host ip4 address if possible | ||
| 217 | if (uri.host == .name and uri.host.name.len > 0) blk: { | ||
| 218 | var a = net.Address.parseIp4(uri.host.name, 0) catch break :blk; | ||
| 219 | uri.host = .{ .ip = a }; // workaround for https://github.com/ziglang/zig/issues/3234 | ||
| 220 | } | ||
| 221 | |||
| 222 | if (uri.host == .ip and uri.port != null) { | ||
| 223 | uri.host.ip.setPort(uri.port.?); | ||
| 224 | } | ||
| 225 | |||
| 226 | uri.parsePath(input[uri.len..]); | ||
| 227 | |||
| 228 | if (input.len > uri.len + 1 and input[uri.len] == '?') { | ||
| 229 | uri.parseQuery(input[uri.len + 1 ..]); | ||
| 230 | } | ||
| 231 | |||
| 232 | if (input.len > uri.len + 1 and input[uri.len] == '#') { | ||
| 233 | uri.parseFragment(input[uri.len + 1 ..]); | ||
| 234 | } | ||
| 235 | return uri; | ||
| 236 | } | ||
| 237 | |||
| 238 | fn parseMaybeScheme(u: *Uri, input: []const u8) void { | ||
| 239 | for (input) |c, i| { | ||
| 240 | switch (c) { | ||
| 241 | 'a'...'z', 'A'...'Z', '0'...'9', '+', '-', '.' => { | ||
| 242 | // allowed characters | ||
| 243 | }, | ||
| 244 | ':' => { | ||
| 245 | u.scheme = input[0..i]; | ||
| 246 | u.len += u.scheme.len + 1; // +1 for the ':' | ||
| 247 | return; | ||
| 248 | }, | ||
| 249 | else => { | ||
| 250 | // not a valid scheme | ||
| 251 | return; | ||
| 252 | }, | ||
| 253 | } | ||
| 254 | } | ||
| 255 | return; | ||
| 256 | } | ||
| 257 | |||
| 258 | fn parseAuth(u: *Uri, input: []const u8) Error!void { | ||
| 259 | for (input) |c, i| { | ||
| 260 | switch (c) { | ||
| 261 | '@' => { | ||
| 262 | u.username = input[0..i]; | ||
| 263 | u.len += i + 1; // +1 for the '@' | ||
| 264 | return u.parseHost(input[i + 1 ..]); | ||
| 265 | }, | ||
| 266 | '[' => { | ||
| 267 | if (i != 0) | ||
| 268 | return error.InvalidCharacter; | ||
| 269 | return u.parseIP(input); | ||
| 270 | }, | ||
| 271 | ':' => { | ||
| 272 | u.host.name = input[0..i]; | ||
| 273 | u.len += i + 1; // +1 for the '@' | ||
| 274 | return u.parseAuthColon(input[i + 1 ..]); | ||
| 275 | }, | ||
| 276 | '/', '?', '#' => { | ||
| 277 | u.host.name = input[0..i]; | ||
| 278 | u.len += i; | ||
| 279 | return; | ||
| 280 | }, | ||
| 281 | else => if (!isPchar(input)) { | ||
| 282 | u.host.name = input[0..i]; | ||
| 283 | u.len += input.len; | ||
| 284 | return; | ||
| 285 | }, | ||
| 286 | } | ||
| 287 | } | ||
| 288 | u.host.name = input; | ||
| 289 | u.len += input.len; | ||
| 290 | } | ||
| 291 | |||
| 292 | fn parseAuthColon(u: *Uri, input: []const u8) Error!void { | ||
| 293 | for (input) |c, i| { | ||
| 294 | if (c == '@') { | ||
| 295 | u.username = u.host.name; | ||
| 296 | u.password = input[0..i]; | ||
| 297 | u.len += i + 1; //1 for the '@' | ||
| 298 | return u.parseHost(input[i + 1 ..]); | ||
| 299 | } else if (c == '/' or c == '?' or c == '#' or !isPchar(input)) { | ||
| 300 | u.port = parseUnsigned(u16, input[0..i], 10) catch return error.InvalidCharacter; | ||
| 301 | u.len += i; | ||
| 302 | return; | ||
| 303 | } | ||
| 304 | } | ||
| 305 | u.port = parseUnsigned(u16, input, 10) catch return error.InvalidCharacter; | ||
| 306 | u.len += input.len; | ||
| 307 | } | ||
| 308 | |||
| 309 | fn parseHost(u: *Uri, input: []const u8) Error!void { | ||
| 310 | for (input) |c, i| { | ||
| 311 | switch (c) { | ||
| 312 | ':' => { | ||
| 313 | u.host.name = input[0..i]; | ||
| 314 | u.len += i + 1; // +1 for the ':' | ||
| 315 | return u.parsePort(input[i..]); | ||
| 316 | }, | ||
| 317 | '[' => { | ||
| 318 | if (i != 0) | ||
| 319 | return error.InvalidCharacter; | ||
| 320 | return u.parseIP(input); | ||
| 321 | }, | ||
| 322 | else => if (c == '/' or c == '?' or c == '#' or !isPchar(input)) { | ||
| 323 | u.host.name = input[0..i]; | ||
| 324 | u.len += i; | ||
| 325 | return; | ||
| 326 | }, | ||
| 327 | } | ||
| 328 | } | ||
| 329 | u.host.name = input[0..]; | ||
| 330 | u.len += input.len; | ||
| 331 | } | ||
| 332 | |||
| 333 | fn parseIP(u: *Uri, input: []const u8) Error!void { | ||
| 334 | const end = mem.indexOfScalar(u8, input, ']') orelse return error.InvalidCharacter; | ||
| 335 | var addr = net.Address.parseIp6(input[1..end], 0) catch return error.InvalidCharacter; | ||
| 336 | u.host = .{ .ip = addr }; | ||
| 337 | u.len += end + 1; | ||
| 338 | |||
| 339 | if (input.len > end + 2 and input[end + 1] == ':') { | ||
| 340 | u.len += 1; | ||
| 341 | try u.parsePort(input[end + 2 ..]); | ||
| 342 | } | ||
| 343 | } | ||
| 344 | |||
| 345 | fn parsePort(u: *Uri, input: []const u8) Error!void { | ||
| 346 | for (input) |c, i| { | ||
| 347 | switch (c) { | ||
| 348 | '0'...'9' => { | ||
| 349 | // digits | ||
| 350 | }, | ||
| 351 | else => { | ||
| 352 | if (i == 0) return error.InvalidCharacter; | ||
| 353 | u.port = parseUnsigned(u16, input[0..i], 10) catch return error.InvalidCharacter; | ||
| 354 | u.len += i; | ||
| 355 | return; | ||
| 356 | }, | ||
| 357 | } | ||
| 358 | } | ||
| 359 | if (input.len == 0) return error.InvalidCharacter; | ||
| 360 | u.port = parseUnsigned(u16, input[0..], 10) catch return error.InvalidCharacter; | ||
| 361 | u.len += input.len; | ||
| 362 | } | ||
| 363 | |||
| 364 | fn parsePath(u: *Uri, input: []const u8) void { | ||
| 365 | for (input) |c, i| { | ||
| 366 | if (c != '/' and (c == '?' or c == '#' or !isPchar(input[i..]))) { | ||
| 367 | u.path = input[0..i]; | ||
| 368 | u.len += u.path.len; | ||
| 369 | return; | ||
| 370 | } | ||
| 371 | } | ||
| 372 | u.path = input[0..]; | ||
| 373 | u.len += u.path.len; | ||
| 374 | } | ||
| 375 | |||
| 376 | fn parseQuery(u: *Uri, input: []const u8) void { | ||
| 377 | u.len += 1; // +1 for the '?' | ||
| 378 | for (input) |c, i| { | ||
| 379 | if (c == '#' or (c != '/' and c != '?' and !isPchar(input[i..]))) { | ||
| 380 | u.query = input[0..i]; | ||
| 381 | u.len += u.query.len; | ||
| 382 | return; | ||
| 383 | } | ||
| 384 | } | ||
| 385 | u.query = input; | ||
| 386 | u.len += input.len; | ||
| 387 | } | ||
| 388 | |||
| 389 | fn parseFragment(u: *Uri, input: []const u8) void { | ||
| 390 | u.len += 1; // +1 for the '#' | ||
| 391 | for (input) |c, i| { | ||
| 392 | if (c != '/' and c != '?' and !isPchar(input[i..])) { | ||
| 393 | u.fragment = input[0..i]; | ||
| 394 | u.len += u.fragment.len; | ||
| 395 | return; | ||
| 396 | } | ||
| 397 | } | ||
| 398 | u.fragment = input; | ||
| 399 | u.len += u.fragment.len; | ||
| 400 | } | ||
| 401 | |||
| 402 | /// returns true if str starts with a valid path character or a percent encoded octet | ||
| 403 | pub fn isPchar(str: []const u8) bool { | ||
| 404 | assert(str.len > 0); | ||
| 405 | return switch (str[0]) { | ||
| 406 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@' => true, | ||
| 407 | '%' => str.len > 3 and isHex(str[1]) and isHex(str[2]), | ||
| 408 | else => false, | ||
| 409 | }; | ||
| 410 | } | ||
| 411 | |||
| 412 | /// returns true if c is a hexadecimal digit | ||
| 413 | pub fn isHex(c: u8) bool { | ||
| 414 | return switch (c) { | ||
| 415 | '0'...'9', 'a'...'f', 'A'...'F' => true, | ||
| 416 | else => false, | ||
| 417 | }; | ||
| 418 | } | ||
| 419 | }; | ||
| 420 | |||
| 421 | test "basic url" { | ||
| 422 | const uri = try Uri.parse("https://ziglang.org:80/documentation/master/?test#toc-Introduction", false); | ||
| 423 | expect(mem.eql(u8, uri.scheme, "https")); | ||
| 424 | expect(mem.eql(u8, uri.username, "")); | ||
| 425 | expect(mem.eql(u8, uri.password, "")); | ||
| 426 | expect(mem.eql(u8, uri.host.name, "ziglang.org")); | ||
| 427 | expect(uri.port.? == 80); | ||
| 428 | expect(mem.eql(u8, uri.path, "/documentation/master/")); | ||
| 429 | expect(mem.eql(u8, uri.query, "test")); | ||
| 430 | expect(mem.eql(u8, uri.fragment, "toc-Introduction")); | ||
| 431 | expect(uri.len == 66); | ||
| 432 | } | ||
| 433 | |||
| 434 | test "short" { | ||
| 435 | const uri = try Uri.parse("telnet://192.0.2.16:80/", false); | ||
| 436 | expect(mem.eql(u8, uri.scheme, "telnet")); | ||
| 437 | expect(mem.eql(u8, uri.username, "")); | ||
| 438 | expect(mem.eql(u8, uri.password, "")); | ||
| 439 | var buf = [_]u8{0} ** 100; | ||
| 440 | var ip = std.fmt.bufPrint(buf[0..], "{}", .{uri.host.ip}) catch unreachable; | ||
| 441 | expect(mem.eql(u8, ip, "192.0.2.16:80")); | ||
| 442 | expect(uri.port.? == 80); | ||
| 443 | expect(mem.eql(u8, uri.path, "/")); | ||
| 444 | expect(mem.eql(u8, uri.query, "")); | ||
| 445 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 446 | expect(uri.len == 23); | ||
| 447 | } | ||
| 448 | |||
| 449 | test "single char" { | ||
| 450 | const uri = try Uri.parse("a", false); | ||
| 451 | expect(mem.eql(u8, uri.scheme, "")); | ||
| 452 | expect(mem.eql(u8, uri.username, "")); | ||
| 453 | expect(mem.eql(u8, uri.password, "")); | ||
| 454 | expect(mem.eql(u8, uri.host.name, "")); | ||
| 455 | expect(uri.port == null); | ||
| 456 | expect(mem.eql(u8, uri.path, "a")); | ||
| 457 | expect(mem.eql(u8, uri.query, "")); | ||
| 458 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 459 | expect(uri.len == 1); | ||
| 460 | } | ||
| 461 | |||
| 462 | test "ipv6" { | ||
| 463 | const uri = try Uri.parse("ldap://[2001:db8::7]/c=GB?objectClass?one", false); | ||
| 464 | expect(mem.eql(u8, uri.scheme, "ldap")); | ||
| 465 | expect(mem.eql(u8, uri.username, "")); | ||
| 466 | expect(mem.eql(u8, uri.password, "")); | ||
| 467 | var buf = [_]u8{0} ** 100; | ||
| 468 | var ip = std.fmt.bufPrint(buf[0..], "{}", .{uri.host.ip}) catch unreachable; | ||
| 469 | expect(std.mem.eql(u8, ip, "[2001:db8::7]:0")); | ||
| 470 | expect(uri.port == null); | ||
| 471 | expect(mem.eql(u8, uri.path, "/c=GB")); | ||
| 472 | expect(mem.eql(u8, uri.query, "objectClass?one")); | ||
| 473 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 474 | expect(uri.len == 41); | ||
| 475 | } | ||
| 476 | |||
| 477 | test "mailto" { | ||
| 478 | const uri = try Uri.parse("mailto:John.Doe@example.com", false); | ||
| 479 | expect(mem.eql(u8, uri.scheme, "mailto")); | ||
| 480 | expect(mem.eql(u8, uri.username, "")); | ||
| 481 | expect(mem.eql(u8, uri.password, "")); | ||
| 482 | expect(mem.eql(u8, uri.host.name, "")); | ||
| 483 | expect(uri.port == null); | ||
| 484 | expect(mem.eql(u8, uri.path, "John.Doe@example.com")); | ||
| 485 | expect(mem.eql(u8, uri.query, "")); | ||
| 486 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 487 | expect(uri.len == 27); | ||
| 488 | } | ||
| 489 | |||
| 490 | test "tel" { | ||
| 491 | const uri = try Uri.parse("tel:+1-816-555-1212", false); | ||
| 492 | expect(mem.eql(u8, uri.scheme, "tel")); | ||
| 493 | expect(mem.eql(u8, uri.username, "")); | ||
| 494 | expect(mem.eql(u8, uri.password, "")); | ||
| 495 | expect(mem.eql(u8, uri.host.name, "")); | ||
| 496 | expect(uri.port == null); | ||
| 497 | expect(mem.eql(u8, uri.path, "+1-816-555-1212")); | ||
| 498 | expect(mem.eql(u8, uri.query, "")); | ||
| 499 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 500 | expect(uri.len == 19); | ||
| 501 | } | ||
| 502 | |||
| 503 | test "urn" { | ||
| 504 | const uri = try Uri.parse("urn:oasis:names:specification:docbook:dtd:xml:4.1.2", false); | ||
| 505 | expect(mem.eql(u8, uri.scheme, "urn")); | ||
| 506 | expect(mem.eql(u8, uri.username, "")); | ||
| 507 | expect(mem.eql(u8, uri.password, "")); | ||
| 508 | expect(mem.eql(u8, uri.host.name, "")); | ||
| 509 | expect(uri.port == null); | ||
| 510 | expect(mem.eql(u8, uri.path, "oasis:names:specification:docbook:dtd:xml:4.1.2")); | ||
| 511 | expect(mem.eql(u8, uri.query, "")); | ||
| 512 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 513 | expect(uri.len == 51); | ||
| 514 | } | ||
| 515 | |||
| 516 | test "userinfo" { | ||
| 517 | const uri = try Uri.parse("ftp://username:password@host.com/", false); | ||
| 518 | expect(mem.eql(u8, uri.scheme, "ftp")); | ||
| 519 | expect(mem.eql(u8, uri.username, "username")); | ||
| 520 | expect(mem.eql(u8, uri.password, "password")); | ||
| 521 | expect(mem.eql(u8, uri.host.name, "host.com")); | ||
| 522 | expect(uri.port == null); | ||
| 523 | expect(mem.eql(u8, uri.path, "/")); | ||
| 524 | expect(mem.eql(u8, uri.query, "")); | ||
| 525 | expect(mem.eql(u8, uri.fragment, "")); | ||
| 526 | expect(uri.len == 33); | ||
| 527 | } | ||
| 528 | |||
| 529 | test "map query" { | ||
| 530 | const uri = try Uri.parse("https://ziglang.org:80/documentation/master/?test;1=true&false#toc-Introduction", false); | ||
| 531 | expect(mem.eql(u8, uri.scheme, "https")); | ||
| 532 | expect(mem.eql(u8, uri.username, "")); | ||
| 533 | expect(mem.eql(u8, uri.password, "")); | ||
| 534 | expect(mem.eql(u8, uri.host.name, "ziglang.org")); | ||
| 535 | expect(uri.port.? == 80); | ||
| 536 | expect(mem.eql(u8, uri.path, "/documentation/master/")); | ||
| 537 | expect(mem.eql(u8, uri.query, "test;1=true&false")); | ||
| 538 | expect(mem.eql(u8, uri.fragment, "toc-Introduction")); | ||
| 539 | var map = try Uri.mapQuery(alloc, uri.query); | ||
| 540 | defer map.deinit(); | ||
| 541 | expect(mem.eql(u8, map.get("test").?, "")); | ||
| 542 | expect(mem.eql(u8, map.get("1").?, "true")); | ||
| 543 | expect(mem.eql(u8, map.get("false").?, "")); | ||
| 544 | } | ||
| 545 | |||
| 546 | test "ends in space" { | ||
| 547 | const uri = try Uri.parse("https://ziglang.org/documentation/master/ something else", false); | ||
| 548 | expect(mem.eql(u8, uri.scheme, "https")); | ||
| 549 | expect(mem.eql(u8, uri.username, "")); | ||
| 550 | expect(mem.eql(u8, uri.password, "")); | ||
| 551 | expect(mem.eql(u8, uri.host.name, "ziglang.org")); | ||
| 552 | expect(mem.eql(u8, uri.path, "/documentation/master/")); | ||
| 553 | expect(uri.len == 41); | ||
| 554 | } | ||
| 555 | |||
| 556 | test "assume auth" { | ||
| 557 | const uri = try Uri.parse("ziglang.org", true); | ||
| 558 | expect(mem.eql(u8, uri.host.name, "ziglang.org")); | ||
| 559 | expect(uri.len == 11); | ||
| 560 | } | ||
| 561 | |||
| 562 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); | ||
| 563 | const alloc = &arena.allocator; | ||
| 564 | |||
| 565 | test "encode" { | ||
| 566 | const path = (try Uri.encode(alloc, "/안녕하세요.html")).?; | ||
| 567 | expect(mem.eql(u8, path, "/%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94.html")); | ||
| 568 | } | ||
| 569 | |||
| 570 | test "decode" { | ||
| 571 | const path = (try Uri.decode(alloc, "/%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94.html")).?; | ||
| 572 | expect(mem.eql(u8, path, "/안녕하세요.html")); | ||
| 573 | } | ||
| 574 | |||
| 575 | test "resolvePath" { | ||
| 576 | var a = try Uri.resolvePath(alloc, "/a/b/.."); | ||
| 577 | expect(mem.eql(u8, a, "/a")); | ||
| 578 | a = try Uri.resolvePath(alloc, "/a/b/../"); | ||
| 579 | expect(mem.eql(u8, a, "/a/")); | ||
| 580 | a = try Uri.resolvePath(alloc, "/a/b/c/../d/../"); | ||
| 581 | expect(mem.eql(u8, a, "/a/b/")); | ||
| 582 | a = try Uri.resolvePath(alloc, "/a/b/c/../d/.."); | ||
| 583 | expect(mem.eql(u8, a, "/a/b")); | ||
| 584 | a = try Uri.resolvePath(alloc, "/a/b/c/../d/.././"); | ||
| 585 | expect(mem.eql(u8, a, "/a/b/")); | ||
| 586 | a = try Uri.resolvePath(alloc, "/a/b/c/../d/../."); | ||
| 587 | expect(mem.eql(u8, a, "/a/b")); | ||
| 588 | a = try Uri.resolvePath(alloc, "/a/../../"); | ||
| 589 | expect(mem.eql(u8, a, "/")); | ||
| 590 | |||
| 591 | arena.deinit(); | ||
| 592 | } | ||
libs/zuri/test.zig created+3| ... | @@ -0,0 +1,3 @@ | ||
| 1 | comptime { | ||
| 2 | _ = @import("src/zuri.zig"); | ||
| 3 | } | ||