| author | |
| committer | |
| log | a7122b73231808a8a07b79c84e1eaac9cf4c28aa |
| tree | edafeb2bbf26110b704a23167f9cb08008460b4b |
| parent | 9352f379e8a08bcc5a3bfc851bfb6c6a662000af |
| signature |
std.crypto: add constant-time codecs
Add constant-time hex/base64 codecs designed to process cryptographic
secrets, adapted from libsodium's implementations.
Introduce a `crypto.codecs` namespace for crypto-related encoders and
decoders. Move ASN.1 codecs to this namespace.
This will also naturally accommodate the proposed PEM codecs.22 files changed, 1608 insertions(+), 1139 deletions(-)
.gitattributes+1| ... | ... | @@ -5,6 +5,7 @@ langref.html.in text eol=lf |
| 5 | 5 | lib/std/compress/testdata/** binary |
| 6 | 6 | lib/std/compress/deflate/testdata/** binary |
| 7 | 7 | lib/std/compress/flate/testdata/** binary |
| 8 | lib/std/crypto/codecs/asn1/der/testdata/** binary | |
| 8 | 9 | |
| 9 | 10 | lib/include/** linguist-vendored |
| 10 | 11 | lib/libc/** linguist-vendored |
lib/std/crypto.zig+4-2| ... | ... | @@ -214,13 +214,15 @@ pub const ff = @import("crypto/ff.zig"); |
| 214 | 214 | /// This is a thread-local, cryptographically secure pseudo random number generator. |
| 215 | 215 | pub const random = @import("crypto/tlcsprng.zig").interface; |
| 216 | 216 | |
| 217 | /// Encoding and decoding | |
| 218 | pub const codecs = @import("crypto/codecs.zig"); | |
| 219 | ||
| 217 | 220 | const std = @import("std.zig"); |
| 218 | 221 | |
| 219 | 222 | pub const errors = @import("crypto/errors.zig"); |
| 220 | 223 | |
| 221 | 224 | pub const tls = @import("crypto/tls.zig"); |
| 222 | 225 | pub const Certificate = @import("crypto/Certificate.zig"); |
| 223 | pub const asn1 = @import("crypto/asn1.zig"); | |
| 224 | 226 | |
| 225 | 227 | /// Side-channels mitigations. |
| 226 | 228 | pub const SideChannelsMitigations = enum { |
| ... | ... | @@ -335,7 +337,7 @@ test { |
| 335 | 337 | _ = errors; |
| 336 | 338 | _ = tls; |
| 337 | 339 | _ = Certificate; |
| 338 | _ = asn1; | |
| 340 | _ = codecs; | |
| 339 | 341 | } |
| 340 | 342 | |
| 341 | 343 | test "CSPRNG" { |
lib/std/crypto/asn1.zig deleted-359| ... | ... | @@ -1,359 +0,0 @@ |
| 1 | //! ASN.1 types for public consumption. | |
| 2 | const std = @import("std"); | |
| 3 | pub const der = @import("./asn1/der.zig"); | |
| 4 | pub const Oid = @import("./asn1/Oid.zig"); | |
| 5 | ||
| 6 | pub const Index = u32; | |
| 7 | ||
| 8 | pub const Tag = struct { | |
| 9 | number: Number, | |
| 10 | /// Whether this ASN.1 type contains other ASN.1 types. | |
| 11 | constructed: bool, | |
| 12 | class: Class, | |
| 13 | ||
| 14 | /// These values apply to class == .universal. | |
| 15 | pub const Number = enum(u16) { | |
| 16 | // 0 is reserved by spec | |
| 17 | boolean = 1, | |
| 18 | integer = 2, | |
| 19 | bitstring = 3, | |
| 20 | octetstring = 4, | |
| 21 | null = 5, | |
| 22 | oid = 6, | |
| 23 | object_descriptor = 7, | |
| 24 | real = 9, | |
| 25 | enumerated = 10, | |
| 26 | embedded = 11, | |
| 27 | string_utf8 = 12, | |
| 28 | oid_relative = 13, | |
| 29 | time = 14, | |
| 30 | // 15 is reserved to mean that the tag is >= 32 | |
| 31 | sequence = 16, | |
| 32 | /// Elements may appear in any order. | |
| 33 | sequence_of = 17, | |
| 34 | string_numeric = 18, | |
| 35 | string_printable = 19, | |
| 36 | string_teletex = 20, | |
| 37 | string_videotex = 21, | |
| 38 | string_ia5 = 22, | |
| 39 | utc_time = 23, | |
| 40 | generalized_time = 24, | |
| 41 | string_graphic = 25, | |
| 42 | string_visible = 26, | |
| 43 | string_general = 27, | |
| 44 | string_universal = 28, | |
| 45 | string_char = 29, | |
| 46 | string_bmp = 30, | |
| 47 | date = 31, | |
| 48 | time_of_day = 32, | |
| 49 | date_time = 33, | |
| 50 | duration = 34, | |
| 51 | /// IRI = Internationalized Resource Identifier | |
| 52 | oid_iri = 35, | |
| 53 | oid_iri_relative = 36, | |
| 54 | _, | |
| 55 | }; | |
| 56 | ||
| 57 | pub const Class = enum(u2) { | |
| 58 | universal, | |
| 59 | application, | |
| 60 | context_specific, | |
| 61 | private, | |
| 62 | }; | |
| 63 | ||
| 64 | pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag { | |
| 65 | return .{ .number = number, .constructed = constructed, .class = class }; | |
| 66 | } | |
| 67 | ||
| 68 | pub fn universal(number: Tag.Number, constructed: bool) Tag { | |
| 69 | return .{ .number = number, .constructed = constructed, .class = .universal }; | |
| 70 | } | |
| 71 | ||
| 72 | pub fn decode(reader: anytype) !Tag { | |
| 73 | const tag1: FirstTag = @bitCast(try reader.readByte()); | |
| 74 | var number: u14 = tag1.number; | |
| 75 | ||
| 76 | if (tag1.number == 15) { | |
| 77 | const tag2: NextTag = @bitCast(try reader.readByte()); | |
| 78 | number = tag2.number; | |
| 79 | if (tag2.continues) { | |
| 80 | const tag3: NextTag = @bitCast(try reader.readByte()); | |
| 81 | number = (number << 7) + tag3.number; | |
| 82 | if (tag3.continues) return error.InvalidLength; | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | return Tag{ | |
| 87 | .number = @enumFromInt(number), | |
| 88 | .constructed = tag1.constructed, | |
| 89 | .class = tag1.class, | |
| 90 | }; | |
| 91 | } | |
| 92 | ||
| 93 | pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void { | |
| 94 | var tag1 = FirstTag{ | |
| 95 | .number = undefined, | |
| 96 | .constructed = self.constructed, | |
| 97 | .class = self.class, | |
| 98 | }; | |
| 99 | ||
| 100 | var buffer: [3]u8 = undefined; | |
| 101 | var stream = std.io.fixedBufferStream(&buffer); | |
| 102 | var writer2 = stream.writer(); | |
| 103 | ||
| 104 | switch (@intFromEnum(self.number)) { | |
| 105 | 0...std.math.maxInt(u5) => |n| { | |
| 106 | tag1.number = @intCast(n); | |
| 107 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 108 | }, | |
| 109 | std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| { | |
| 110 | tag1.number = 15; | |
| 111 | const tag2 = NextTag{ .number = @intCast(n), .continues = false }; | |
| 112 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 113 | writer2.writeByte(@bitCast(tag2)) catch unreachable; | |
| 114 | }, | |
| 115 | else => |n| { | |
| 116 | tag1.number = 15; | |
| 117 | const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true }; | |
| 118 | const tag3 = NextTag{ .number = @truncate(n), .continues = false }; | |
| 119 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 120 | writer2.writeByte(@bitCast(tag2)) catch unreachable; | |
| 121 | writer2.writeByte(@bitCast(tag3)) catch unreachable; | |
| 122 | }, | |
| 123 | } | |
| 124 | ||
| 125 | _ = try writer.write(stream.getWritten()); | |
| 126 | } | |
| 127 | ||
| 128 | const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; | |
| 129 | const NextTag = packed struct(u8) { number: u7, continues: bool }; | |
| 130 | ||
| 131 | pub fn toExpected(self: Tag) ExpectedTag { | |
| 132 | return ExpectedTag{ | |
| 133 | .number = self.number, | |
| 134 | .constructed = self.constructed, | |
| 135 | .class = self.class, | |
| 136 | }; | |
| 137 | } | |
| 138 | ||
| 139 | pub fn fromZig(comptime T: type) Tag { | |
| 140 | switch (@typeInfo(T)) { | |
| 141 | .@"struct", .@"enum", .@"union" => { | |
| 142 | if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; | |
| 143 | }, | |
| 144 | else => {}, | |
| 145 | } | |
| 146 | ||
| 147 | switch (@typeInfo(T)) { | |
| 148 | .@"struct", .@"union" => return universal(.sequence, true), | |
| 149 | .bool => return universal(.boolean, false), | |
| 150 | .int => return universal(.integer, false), | |
| 151 | .@"enum" => |e| { | |
| 152 | if (@hasDecl(T, "oids")) return Oid.asn1_tag; | |
| 153 | return universal(if (e.is_exhaustive) .enumerated else .integer, false); | |
| 154 | }, | |
| 155 | .optional => |o| return fromZig(o.child), | |
| 156 | .null => return universal(.null, false), | |
| 157 | else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), | |
| 158 | } | |
| 159 | } | |
| 160 | }; | |
| 161 | ||
| 162 | test Tag { | |
| 163 | const buf = [_]u8{0xa3}; | |
| 164 | var stream = std.io.fixedBufferStream(&buf); | |
| 165 | const t = Tag.decode(stream.reader()); | |
| 166 | try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); | |
| 167 | } | |
| 168 | ||
| 169 | /// A decoded view. | |
| 170 | pub const Element = struct { | |
| 171 | tag: Tag, | |
| 172 | slice: Slice, | |
| 173 | ||
| 174 | pub const Slice = struct { | |
| 175 | start: Index, | |
| 176 | end: Index, | |
| 177 | ||
| 178 | pub fn len(self: Slice) Index { | |
| 179 | return self.end - self.start; | |
| 180 | } | |
| 181 | ||
| 182 | pub fn view(self: Slice, bytes: []const u8) []const u8 { | |
| 183 | return bytes[self.start..self.end]; | |
| 184 | } | |
| 185 | }; | |
| 186 | ||
| 187 | pub const DecodeError = error{ InvalidLength, EndOfStream }; | |
| 188 | ||
| 189 | /// Safely decode a DER/BER/CER element at `index`: | |
| 190 | /// - Ensures length uses shortest form | |
| 191 | /// - Ensures length is within `bytes` | |
| 192 | /// - Ensures length is less than `std.math.maxInt(Index)` | |
| 193 | pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { | |
| 194 | var stream = std.io.fixedBufferStream(bytes[index..]); | |
| 195 | var reader = stream.reader(); | |
| 196 | ||
| 197 | const tag = try Tag.decode(reader); | |
| 198 | const size_or_len_size = try reader.readByte(); | |
| 199 | ||
| 200 | var start = index + 2; | |
| 201 | var end = start + size_or_len_size; | |
| 202 | // short form between 0-127 | |
| 203 | if (size_or_len_size < 128) { | |
| 204 | if (end > bytes.len) return error.InvalidLength; | |
| 205 | } else { | |
| 206 | // long form between 0 and std.math.maxInt(u1024) | |
| 207 | const len_size: u7 = @truncate(size_or_len_size); | |
| 208 | start += len_size; | |
| 209 | if (len_size > @sizeOf(Index)) return error.InvalidLength; | |
| 210 | ||
| 211 | const len = try reader.readVarInt(Index, .big, len_size); | |
| 212 | if (len < 128) return error.InvalidLength; // should have used short form | |
| 213 | ||
| 214 | end = std.math.add(Index, start, len) catch return error.InvalidLength; | |
| 215 | if (end > bytes.len) return error.InvalidLength; | |
| 216 | } | |
| 217 | ||
| 218 | return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; | |
| 219 | } | |
| 220 | }; | |
| 221 | ||
| 222 | test Element { | |
| 223 | const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; | |
| 224 | try std.testing.expectEqual(Element{ | |
| 225 | .tag = Tag.universal(.sequence, true), | |
| 226 | .slice = Element.Slice{ .start = 2, .end = short_form.len }, | |
| 227 | }, Element.decode(&short_form, 0)); | |
| 228 | ||
| 229 | const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129; | |
| 230 | try std.testing.expectEqual(Element{ | |
| 231 | .tag = Tag.universal(.sequence, true), | |
| 232 | .slice = Element.Slice{ .start = 3, .end = long_form.len }, | |
| 233 | }, Element.decode(&long_form, 0)); | |
| 234 | } | |
| 235 | ||
| 236 | /// For decoding. | |
| 237 | pub const ExpectedTag = struct { | |
| 238 | number: ?Tag.Number = null, | |
| 239 | constructed: ?bool = null, | |
| 240 | class: ?Tag.Class = null, | |
| 241 | ||
| 242 | pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { | |
| 243 | return .{ .number = number, .constructed = constructed, .class = class }; | |
| 244 | } | |
| 245 | ||
| 246 | pub fn primitive(number: ?Tag.Number) ExpectedTag { | |
| 247 | return .{ .number = number, .constructed = false, .class = .universal }; | |
| 248 | } | |
| 249 | ||
| 250 | pub fn match(self: ExpectedTag, tag: Tag) bool { | |
| 251 | if (self.number) |e| { | |
| 252 | if (tag.number != e) return false; | |
| 253 | } | |
| 254 | if (self.constructed) |e| { | |
| 255 | if (tag.constructed != e) return false; | |
| 256 | } | |
| 257 | if (self.class) |e| { | |
| 258 | if (tag.class != e) return false; | |
| 259 | } | |
| 260 | return true; | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | pub const FieldTag = struct { | |
| 265 | number: std.meta.Tag(Tag.Number), | |
| 266 | class: Tag.Class, | |
| 267 | explicit: bool = true, | |
| 268 | ||
| 269 | pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { | |
| 270 | return .{ .number = number, .class = class, .explicit = true }; | |
| 271 | } | |
| 272 | ||
| 273 | pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { | |
| 274 | return .{ .number = number, .class = class, .explicit = false }; | |
| 275 | } | |
| 276 | ||
| 277 | pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { | |
| 278 | if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { | |
| 279 | return @field(Container.asn1_tags, field_name); | |
| 280 | } | |
| 281 | ||
| 282 | return null; | |
| 283 | } | |
| 284 | ||
| 285 | pub fn toTag(self: FieldTag) Tag { | |
| 286 | return Tag.init(@enumFromInt(self.number), self.explicit, self.class); | |
| 287 | } | |
| 288 | }; | |
| 289 | ||
| 290 | pub const BitString = struct { | |
| 291 | /// Number of bits in rightmost byte that are unused. | |
| 292 | right_padding: u3 = 0, | |
| 293 | bytes: []const u8, | |
| 294 | ||
| 295 | pub fn bitLen(self: BitString) usize { | |
| 296 | return self.bytes.len * 8 - self.right_padding; | |
| 297 | } | |
| 298 | ||
| 299 | const asn1_tag = Tag.universal(.bitstring, false); | |
| 300 | ||
| 301 | pub fn decodeDer(decoder: *der.Decoder) !BitString { | |
| 302 | const ele = try decoder.element(asn1_tag.toExpected()); | |
| 303 | const bytes = decoder.view(ele); | |
| 304 | ||
| 305 | if (bytes.len < 1) return error.InvalidBitString; | |
| 306 | const padding = bytes[0]; | |
| 307 | if (padding >= 8) return error.InvalidBitString; | |
| 308 | const right_padding: u3 = @intCast(padding); | |
| 309 | ||
| 310 | // DER requires that unused bits be zero. | |
| 311 | if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; | |
| 312 | ||
| 313 | return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; | |
| 314 | } | |
| 315 | ||
| 316 | pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { | |
| 317 | try encoder.writer().writeAll(self.bytes); | |
| 318 | try encoder.writer().writeByte(self.right_padding); | |
| 319 | try encoder.length(self.bytes.len + 1); | |
| 320 | try encoder.tag(asn1_tag); | |
| 321 | } | |
| 322 | }; | |
| 323 | ||
| 324 | pub fn Opaque(comptime tag: Tag) type { | |
| 325 | return struct { | |
| 326 | bytes: []const u8, | |
| 327 | ||
| 328 | pub fn decodeDer(decoder: *der.Decoder) !@This() { | |
| 329 | const ele = try decoder.element(tag.toExpected()); | |
| 330 | if (tag.constructed) decoder.index = ele.slice.end; | |
| 331 | return .{ .bytes = decoder.view(ele) }; | |
| 332 | } | |
| 333 | ||
| 334 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { | |
| 335 | try encoder.tagBytes(tag, self.bytes); | |
| 336 | } | |
| 337 | }; | |
| 338 | } | |
| 339 | ||
| 340 | /// Use sparingly. | |
| 341 | pub const Any = struct { | |
| 342 | tag: Tag, | |
| 343 | bytes: []const u8, | |
| 344 | ||
| 345 | pub fn decodeDer(decoder: *der.Decoder) !@This() { | |
| 346 | const ele = try decoder.element(ExpectedTag{}); | |
| 347 | return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; | |
| 348 | } | |
| 349 | ||
| 350 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { | |
| 351 | try encoder.tagBytes(self.tag, self.bytes); | |
| 352 | } | |
| 353 | }; | |
| 354 | ||
| 355 | test { | |
| 356 | _ = der; | |
| 357 | _ = Oid; | |
| 358 | _ = @import("asn1/test.zig"); | |
| 359 | } |
lib/std/crypto/asn1/Oid.zig deleted-210| ... | ... | @@ -1,210 +0,0 @@ |
| 1 | //! Globally unique hierarchical identifier made of a sequence of integers. | |
| 2 | //! | |
| 3 | //! Commonly used to identify standards, algorithms, certificate extensions, | |
| 4 | //! organizations, or policy documents. | |
| 5 | encoded: []const u8, | |
| 6 | ||
| 7 | pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError; | |
| 8 | ||
| 9 | pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid { | |
| 10 | var split = std.mem.splitScalar(u8, dot_notation, '.'); | |
| 11 | const first_str = split.next() orelse return error.MissingPrefix; | |
| 12 | const second_str = split.next() orelse return error.MissingPrefix; | |
| 13 | ||
| 14 | const first = try std.fmt.parseInt(u8, first_str, 10); | |
| 15 | const second = try std.fmt.parseInt(u8, second_str, 10); | |
| 16 | ||
| 17 | var stream = std.io.fixedBufferStream(out); | |
| 18 | var writer = stream.writer(); | |
| 19 | ||
| 20 | try writer.writeByte(first * 40 + second); | |
| 21 | ||
| 22 | var i: usize = 1; | |
| 23 | while (split.next()) |s| { | |
| 24 | var parsed = try std.fmt.parseUnsigned(Arc, s, 10); | |
| 25 | const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); | |
| 26 | ||
| 27 | for (0..n_bytes) |j| { | |
| 28 | const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); | |
| 29 | const digit: u8 = @intCast(@divFloor(parsed, place)); | |
| 30 | ||
| 31 | try writer.writeByte(digit | 0x80); | |
| 32 | parsed -= digit * place; | |
| 33 | ||
| 34 | i += 1; | |
| 35 | } | |
| 36 | try writer.writeByte(@intCast(parsed)); | |
| 37 | i += 1; | |
| 38 | } | |
| 39 | ||
| 40 | return .{ .encoded = stream.getWritten() }; | |
| 41 | } | |
| 42 | ||
| 43 | test fromDot { | |
| 44 | var buf: [256]u8 = undefined; | |
| 45 | for (test_cases) |t| { | |
| 46 | const actual = try fromDot(t.dot_notation, &buf); | |
| 47 | try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded); | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { | |
| 52 | const encoded = self.encoded; | |
| 53 | const first = @divTrunc(encoded[0], 40); | |
| 54 | const second = encoded[0] - first * 40; | |
| 55 | try writer.print("{d}.{d}", .{ first, second }); | |
| 56 | ||
| 57 | var i: usize = 1; | |
| 58 | while (i != encoded.len) { | |
| 59 | const n_bytes: usize = brk: { | |
| 60 | var res: usize = 1; | |
| 61 | var j: usize = i; | |
| 62 | while (encoded[j] & 0x80 != 0) { | |
| 63 | res += 1; | |
| 64 | j += 1; | |
| 65 | } | |
| 66 | break :brk res; | |
| 67 | }; | |
| 68 | ||
| 69 | var n: usize = 0; | |
| 70 | for (0..n_bytes) |j| { | |
| 71 | const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); | |
| 72 | n += place * (encoded[i] & 0b01111111); | |
| 73 | i += 1; | |
| 74 | } | |
| 75 | try writer.print(".{d}", .{n}); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | test toDot { | |
| 80 | var buf: [256]u8 = undefined; | |
| 81 | ||
| 82 | for (test_cases) |t| { | |
| 83 | var stream = std.io.fixedBufferStream(&buf); | |
| 84 | try toDot(Oid{ .encoded = t.encoded }, stream.writer()); | |
| 85 | try std.testing.expectEqualStrings(t.dot_notation, stream.getWritten()); | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | const TestCase = struct { | |
| 90 | encoded: []const u8, | |
| 91 | dot_notation: []const u8, | |
| 92 | ||
| 93 | pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase { | |
| 94 | return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation }; | |
| 95 | } | |
| 96 | }; | |
| 97 | ||
| 98 | const test_cases = [_]TestCase{ | |
| 99 | // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier | |
| 100 | TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"), | |
| 101 | // https://luca.ntop.org/Teaching/Appunti/asn1.html | |
| 102 | TestCase.init("2a864886f70d", "1.2.840.113549"), | |
| 103 | // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx | |
| 104 | TestCase.init("2a868d20", "1.2.100000"), | |
| 105 | TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"), | |
| 106 | TestCase.init("2b6570", "1.3.101.112"), | |
| 107 | }; | |
| 108 | ||
| 109 | pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); | |
| 110 | ||
| 111 | pub fn decodeDer(decoder: *der.Decoder) !Oid { | |
| 112 | const ele = try decoder.element(asn1_tag.toExpected()); | |
| 113 | return Oid{ .encoded = decoder.view(ele) }; | |
| 114 | } | |
| 115 | ||
| 116 | pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { | |
| 117 | try encoder.tagBytes(asn1_tag, self.encoded); | |
| 118 | } | |
| 119 | ||
| 120 | fn encodedLen(dot_notation: []const u8) usize { | |
| 121 | var buf: [256]u8 = undefined; | |
| 122 | const oid = fromDot(dot_notation, &buf) catch unreachable; | |
| 123 | return oid.encoded.len; | |
| 124 | } | |
| 125 | ||
| 126 | /// Returns encoded bytes of OID. | |
| 127 | fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 { | |
| 128 | @setEvalBranchQuota(4000); | |
| 129 | comptime var buf: [256]u8 = undefined; | |
| 130 | const oid = comptime fromDot(dot_notation, &buf) catch unreachable; | |
| 131 | return oid.encoded[0..oid.encoded.len].*; | |
| 132 | } | |
| 133 | ||
| 134 | test encodeComptime { | |
| 135 | try std.testing.expectEqual( | |
| 136 | hexToBytes("2b0601040182371514"), | |
| 137 | comptime encodeComptime("1.3.6.1.4.1.311.21.20"), | |
| 138 | ); | |
| 139 | } | |
| 140 | ||
| 141 | pub fn fromDotComptime(comptime dot_notation: []const u8) Oid { | |
| 142 | const tmp = comptime encodeComptime(dot_notation); | |
| 143 | return Oid{ .encoded = &tmp }; | |
| 144 | } | |
| 145 | ||
| 146 | /// Maps of: | |
| 147 | /// - Oid -> enum | |
| 148 | /// - Enum -> oid | |
| 149 | pub fn StaticMap(comptime Enum: type) type { | |
| 150 | const enum_info = @typeInfo(Enum).@"enum"; | |
| 151 | const EnumToOid = std.EnumArray(Enum, []const u8); | |
| 152 | const ReturnType = struct { | |
| 153 | oid_to_enum: std.StaticStringMap(Enum), | |
| 154 | enum_to_oid: EnumToOid, | |
| 155 | ||
| 156 | pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum { | |
| 157 | return self.oid_to_enum.get(encoded); | |
| 158 | } | |
| 159 | ||
| 160 | pub fn enumToOid(self: @This(), value: Enum) Oid { | |
| 161 | const bytes = self.enum_to_oid.get(value); | |
| 162 | return .{ .encoded = bytes }; | |
| 163 | } | |
| 164 | }; | |
| 165 | ||
| 166 | return struct { | |
| 167 | pub fn initComptime(comptime key_pairs: anytype) ReturnType { | |
| 168 | const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct"; | |
| 169 | const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID"; | |
| 170 | if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) { | |
| 171 | @compileError(error_msg); | |
| 172 | } | |
| 173 | ||
| 174 | comptime var enum_to_oid = EnumToOid.initUndefined(); | |
| 175 | ||
| 176 | const KeyPair = struct { []const u8, Enum }; | |
| 177 | comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined; | |
| 178 | ||
| 179 | comptime for (enum_info.fields, 0..) |f, i| { | |
| 180 | if (!@hasField(@TypeOf(key_pairs), f.name)) { | |
| 181 | @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry"); | |
| 182 | } | |
| 183 | const encoded = &encodeComptime(@field(key_pairs, f.name)); | |
| 184 | const tag: Enum = @enumFromInt(f.value); | |
| 185 | static_key_pairs[i] = .{ encoded, tag }; | |
| 186 | enum_to_oid.set(tag, encoded); | |
| 187 | }; | |
| 188 | ||
| 189 | const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs); | |
| 190 | if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg); | |
| 191 | ||
| 192 | return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid }; | |
| 193 | } | |
| 194 | }; | |
| 195 | } | |
| 196 | ||
| 197 | /// Strictly for testing. | |
| 198 | fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 { | |
| 199 | var res: [hex.len / 2]u8 = undefined; | |
| 200 | _ = std.fmt.hexToBytes(&res, hex) catch unreachable; | |
| 201 | return res; | |
| 202 | } | |
| 203 | ||
| 204 | const std = @import("std"); | |
| 205 | const Oid = @This(); | |
| 206 | const Arc = u32; | |
| 207 | const encoding_base = 128; | |
| 208 | const Allocator = std.mem.Allocator; | |
| 209 | const der = @import("der.zig"); | |
| 210 | const asn1 = @import("../asn1.zig"); |
lib/std/crypto/asn1/der.zig deleted-55| ... | ... | @@ -1,55 +0,0 @@ |
| 1 | //! Distinguised Encoding Rules as defined in X.690 and X.691. | |
| 2 | //! | |
| 3 | //! Subset of Basic Encoding Rules (BER) which eliminates flexibility in | |
| 4 | //! an effort to acheive normality. Used in PKI. | |
| 5 | const std = @import("std"); | |
| 6 | const asn1 = @import("../asn1.zig"); | |
| 7 | ||
| 8 | pub const Decoder = @import("der/Decoder.zig"); | |
| 9 | pub const Encoder = @import("der/Encoder.zig"); | |
| 10 | ||
| 11 | pub fn decode(comptime T: type, encoded: []const u8) !T { | |
| 12 | var decoder = Decoder{ .bytes = encoded }; | |
| 13 | const res = try decoder.any(T); | |
| 14 | std.debug.assert(decoder.index == encoded.len); | |
| 15 | return res; | |
| 16 | } | |
| 17 | ||
| 18 | /// Caller owns returned memory. | |
| 19 | pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 { | |
| 20 | var encoder = Encoder.init(allocator); | |
| 21 | defer encoder.deinit(); | |
| 22 | try encoder.any(value); | |
| 23 | return try encoder.buffer.toOwnedSlice(); | |
| 24 | } | |
| 25 | ||
| 26 | test encode { | |
| 27 | // https://lapo.it/asn1js/#MAgGAyoDBAIBBA | |
| 28 | const Value = struct { a: asn1.Oid, b: i32 }; | |
| 29 | const test_case = .{ | |
| 30 | .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, | |
| 31 | .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, | |
| 32 | }; | |
| 33 | const allocator = std.testing.allocator; | |
| 34 | const actual = try encode(allocator, test_case.value); | |
| 35 | defer allocator.free(actual); | |
| 36 | ||
| 37 | try std.testing.expectEqualSlices(u8, test_case.encoded, actual); | |
| 38 | } | |
| 39 | ||
| 40 | test decode { | |
| 41 | // https://lapo.it/asn1js/#MAgGAyoDBAIBBA | |
| 42 | const Value = struct { a: asn1.Oid, b: i32 }; | |
| 43 | const test_case = .{ | |
| 44 | .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, | |
| 45 | .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, | |
| 46 | }; | |
| 47 | const decoded = try decode(Value, test_case.encoded); | |
| 48 | ||
| 49 | try std.testing.expectEqualDeep(test_case.value, decoded); | |
| 50 | } | |
| 51 | ||
| 52 | test { | |
| 53 | _ = Decoder; | |
| 54 | _ = Encoder; | |
| 55 | } |
lib/std/crypto/asn1/der/ArrayListReverse.zig deleted-97| ... | ... | @@ -1,97 +0,0 @@ |
| 1 | //! An ArrayList that grows backwards. Counts nested prefix length fields | |
| 2 | //! in O(n) instead of O(n^depth) at the cost of extra buffering. | |
| 3 | //! | |
| 4 | //! Laid out in memory like: | |
| 5 | //! capacity |--------------------------| | |
| 6 | //! data |-------------| | |
| 7 | data: []u8, | |
| 8 | capacity: usize, | |
| 9 | allocator: Allocator, | |
| 10 | ||
| 11 | const ArrayListReverse = @This(); | |
| 12 | const Error = Allocator.Error; | |
| 13 | ||
| 14 | pub fn init(allocator: Allocator) ArrayListReverse { | |
| 15 | return .{ .data = &.{}, .capacity = 0, .allocator = allocator }; | |
| 16 | } | |
| 17 | ||
| 18 | pub fn deinit(self: *ArrayListReverse) void { | |
| 19 | self.allocator.free(self.allocatedSlice()); | |
| 20 | } | |
| 21 | ||
| 22 | pub fn ensureCapacity(self: *ArrayListReverse, new_capacity: usize) Error!void { | |
| 23 | if (self.capacity >= new_capacity) return; | |
| 24 | ||
| 25 | const old_memory = self.allocatedSlice(); | |
| 26 | // Just make a new allocation to not worry about aliasing. | |
| 27 | const new_memory = try self.allocator.alloc(u8, new_capacity); | |
| 28 | @memcpy(new_memory[new_capacity - self.data.len ..], self.data); | |
| 29 | self.allocator.free(old_memory); | |
| 30 | self.data.ptr = new_memory.ptr + new_capacity - self.data.len; | |
| 31 | self.capacity = new_memory.len; | |
| 32 | } | |
| 33 | ||
| 34 | pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void { | |
| 35 | try self.ensureCapacity(self.data.len + data.len); | |
| 36 | const old_len = self.data.len; | |
| 37 | const new_len = old_len + data.len; | |
| 38 | assert(new_len <= self.capacity); | |
| 39 | self.data.len = new_len; | |
| 40 | ||
| 41 | const end = self.data.ptr; | |
| 42 | const begin = end - data.len; | |
| 43 | const slice = begin[0..data.len]; | |
| 44 | @memcpy(slice, data); | |
| 45 | self.data.ptr = begin; | |
| 46 | } | |
| 47 | ||
| 48 | pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); | |
| 49 | /// Warning: This writer writes backwards. `fn print` will NOT work as expected. | |
| 50 | pub fn writer(self: *ArrayListReverse) Writer { | |
| 51 | return .{ .context = self }; | |
| 52 | } | |
| 53 | ||
| 54 | fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize { | |
| 55 | try self.prependSlice(data); | |
| 56 | return data.len; | |
| 57 | } | |
| 58 | ||
| 59 | fn allocatedSlice(self: *ArrayListReverse) []u8 { | |
| 60 | return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity]; | |
| 61 | } | |
| 62 | ||
| 63 | /// Invalidates all element pointers. | |
| 64 | pub fn clearAndFree(self: *ArrayListReverse) void { | |
| 65 | self.allocator.free(self.allocatedSlice()); | |
| 66 | self.data.len = 0; | |
| 67 | self.capacity = 0; | |
| 68 | } | |
| 69 | ||
| 70 | /// The caller owns the returned memory. | |
| 71 | /// Capacity is cleared, making deinit() safe but unnecessary to call. | |
| 72 | pub fn toOwnedSlice(self: *ArrayListReverse) Error![]u8 { | |
| 73 | const new_memory = try self.allocator.alloc(u8, self.data.len); | |
| 74 | @memcpy(new_memory, self.data); | |
| 75 | @memset(self.data, undefined); | |
| 76 | self.clearAndFree(); | |
| 77 | return new_memory; | |
| 78 | } | |
| 79 | ||
| 80 | const std = @import("std"); | |
| 81 | const Allocator = std.mem.Allocator; | |
| 82 | const assert = std.debug.assert; | |
| 83 | const testing = std.testing; | |
| 84 | ||
| 85 | test ArrayListReverse { | |
| 86 | var b = ArrayListReverse.init(testing.allocator); | |
| 87 | defer b.deinit(); | |
| 88 | const data: []const u8 = &.{ 4, 5, 6 }; | |
| 89 | try b.prependSlice(data); | |
| 90 | try testing.expectEqual(data.len, b.data.len); | |
| 91 | try testing.expectEqualSlices(u8, data, b.data); | |
| 92 | ||
| 93 | const data2: []const u8 = &.{ 1, 2, 3 }; | |
| 94 | try b.prependSlice(data2); | |
| 95 | try testing.expectEqual(data.len + data2.len, b.data.len); | |
| 96 | try testing.expectEqualSlices(u8, data2 ++ data, b.data); | |
| 97 | } |
lib/std/crypto/asn1/der/Decoder.zig deleted-170| ... | ... | @@ -1,170 +0,0 @@ |
| 1 | //! A secure DER parser that: | |
| 2 | //! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)` | |
| 3 | //! - Does NOT allocate. If you wish to parse lists you can do so lazily | |
| 4 | //! with an opaque type. | |
| 5 | //! - Does NOT read memory outside `bytes`. | |
| 6 | //! - Does NOT return elements with slices outside `bytes`. | |
| 7 | //! - Errors on values that do NOT follow DER rules: | |
| 8 | //! - Lengths that could be represented in a shorter form. | |
| 9 | //! - Booleans that are not 0xff or 0x00. | |
| 10 | bytes: []const u8, | |
| 11 | index: Index = 0, | |
| 12 | /// The field tag of the most recently visited field. | |
| 13 | /// This is needed because we might visit an implicitly tagged container with a `fn decodeDer`. | |
| 14 | field_tag: ?FieldTag = null, | |
| 15 | ||
| 16 | /// Expect a value. | |
| 17 | pub fn any(self: *Decoder, comptime T: type) !T { | |
| 18 | if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self); | |
| 19 | ||
| 20 | const tag = Tag.fromZig(T).toExpected(); | |
| 21 | switch (@typeInfo(T)) { | |
| 22 | .@"struct" => { | |
| 23 | const ele = try self.element(tag); | |
| 24 | defer self.index = ele.slice.end; // don't force parsing all fields | |
| 25 | ||
| 26 | var res: T = undefined; | |
| 27 | ||
| 28 | inline for (std.meta.fields(T)) |f| { | |
| 29 | self.field_tag = FieldTag.fromContainer(T, f.name); | |
| 30 | ||
| 31 | if (self.field_tag) |ft| { | |
| 32 | if (ft.explicit) { | |
| 33 | const seq = try self.element(ft.toTag().toExpected()); | |
| 34 | self.index = seq.slice.start; | |
| 35 | self.field_tag = null; | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | @field(res, f.name) = self.any(f.type) catch |err| brk: { | |
| 40 | if (f.defaultValue()) |d| { | |
| 41 | break :brk d; | |
| 42 | } | |
| 43 | return err; | |
| 44 | }; | |
| 45 | // DER encodes null values by skipping them. | |
| 46 | if (@typeInfo(f.type) == .optional and @field(res, f.name) == null) { | |
| 47 | if (f.defaultValue()) |d| @field(res, f.name) = d; | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | return res; | |
| 52 | }, | |
| 53 | .bool => { | |
| 54 | const ele = try self.element(tag); | |
| 55 | const bytes = self.view(ele); | |
| 56 | if (bytes.len != 1) return error.InvalidBool; | |
| 57 | ||
| 58 | return switch (bytes[0]) { | |
| 59 | 0x00 => false, | |
| 60 | 0xff => true, | |
| 61 | else => error.InvalidBool, | |
| 62 | }; | |
| 63 | }, | |
| 64 | .int => { | |
| 65 | const ele = try self.element(tag); | |
| 66 | const bytes = self.view(ele); | |
| 67 | return try int(T, bytes); | |
| 68 | }, | |
| 69 | .@"enum" => |e| { | |
| 70 | const ele = try self.element(tag); | |
| 71 | const bytes = self.view(ele); | |
| 72 | if (@hasDecl(T, "oids")) { | |
| 73 | return T.oids.oidToEnum(bytes) orelse return error.UnknownOid; | |
| 74 | } | |
| 75 | return @enumFromInt(try int(e.tag_type, bytes)); | |
| 76 | }, | |
| 77 | .optional => |o| return self.any(o.child) catch return null, | |
| 78 | else => @compileError("cannot decode type " ++ @typeName(T)), | |
| 79 | } | |
| 80 | } | |
| 81 | ||
| 82 | //// Expect a sequence. | |
| 83 | pub fn sequence(self: *Decoder) !Element { | |
| 84 | return try self.element(ExpectedTag.init(.sequence, true, .universal)); | |
| 85 | } | |
| 86 | ||
| 87 | //// Expect an element. | |
| 88 | pub fn element( | |
| 89 | self: *Decoder, | |
| 90 | expected: ExpectedTag, | |
| 91 | ) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element { | |
| 92 | if (self.index >= self.bytes.len) return error.EndOfStream; | |
| 93 | ||
| 94 | const res = try Element.decode(self.bytes, self.index); | |
| 95 | var e = expected; | |
| 96 | if (self.field_tag) |ft| { | |
| 97 | e.number = @enumFromInt(ft.number); | |
| 98 | e.class = ft.class; | |
| 99 | } | |
| 100 | if (!e.match(res.tag)) { | |
| 101 | return error.UnexpectedElement; | |
| 102 | } | |
| 103 | ||
| 104 | self.index = if (res.tag.constructed) res.slice.start else res.slice.end; | |
| 105 | return res; | |
| 106 | } | |
| 107 | ||
| 108 | /// View of element bytes. | |
| 109 | pub fn view(self: Decoder, elem: Element) []const u8 { | |
| 110 | return elem.slice.view(self.bytes); | |
| 111 | } | |
| 112 | ||
| 113 | fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T { | |
| 114 | if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned"); | |
| 115 | ||
| 116 | var bytes = value; | |
| 117 | if (bytes.len >= 2) { | |
| 118 | if (bytes[0] == 0) { | |
| 119 | if (@clz(bytes[1]) > 0) return error.NonCanonical; | |
| 120 | bytes.ptr += 1; | |
| 121 | } | |
| 122 | if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical; | |
| 123 | } | |
| 124 | ||
| 125 | if (bytes.len > @sizeOf(T)) return error.LargeValue; | |
| 126 | if (@sizeOf(T) == 1) return @bitCast(bytes[0]); | |
| 127 | ||
| 128 | return std.mem.readVarInt(T, bytes, .big); | |
| 129 | } | |
| 130 | ||
| 131 | test int { | |
| 132 | try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1})); | |
| 133 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 })); | |
| 134 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff })); | |
| 135 | ||
| 136 | const big = [_]u8{ 0xef, 0xff }; | |
| 137 | try expectError(error.LargeValue, int(u8, &big)); | |
| 138 | try expectEqual(0xefff, int(u16, &big)); | |
| 139 | } | |
| 140 | ||
| 141 | test Decoder { | |
| 142 | var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") }; | |
| 143 | const seq = try parser.sequence(); | |
| 144 | ||
| 145 | { | |
| 146 | const seq2 = try parser.sequence(); | |
| 147 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 148 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 149 | ||
| 150 | try std.testing.expectEqual(parser.index, seq2.slice.end); | |
| 151 | } | |
| 152 | _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal)); | |
| 153 | ||
| 154 | try std.testing.expectEqual(parser.index, seq.slice.end); | |
| 155 | try std.testing.expectEqual(parser.index, parser.bytes.len); | |
| 156 | } | |
| 157 | ||
| 158 | const std = @import("std"); | |
| 159 | const builtin = @import("builtin"); | |
| 160 | const asn1 = @import("../../asn1.zig"); | |
| 161 | const Oid = @import("../Oid.zig"); | |
| 162 | ||
| 163 | const expectEqual = std.testing.expectEqual; | |
| 164 | const expectError = std.testing.expectError; | |
| 165 | const Decoder = @This(); | |
| 166 | const Index = asn1.Index; | |
| 167 | const Tag = asn1.Tag; | |
| 168 | const FieldTag = asn1.FieldTag; | |
| 169 | const ExpectedTag = asn1.ExpectedTag; | |
| 170 | const Element = asn1.Element; |
lib/std/crypto/asn1/der/Encoder.zig deleted-166| ... | ... | @@ -1,166 +0,0 @@ |
| 1 | //! A buffered DER encoder. | |
| 2 | //! | |
| 3 | //! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. | |
| 4 | //! That function should encode values, lengths, then tags. | |
| 5 | buffer: ArrayListReverse, | |
| 6 | /// The field tag set by a parent container. | |
| 7 | /// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`. | |
| 8 | field_tag: ?FieldTag = null, | |
| 9 | ||
| 10 | pub fn init(allocator: std.mem.Allocator) Encoder { | |
| 11 | return Encoder{ .buffer = ArrayListReverse.init(allocator) }; | |
| 12 | } | |
| 13 | ||
| 14 | pub fn deinit(self: *Encoder) void { | |
| 15 | self.buffer.deinit(); | |
| 16 | } | |
| 17 | ||
| 18 | /// Encode any value. | |
| 19 | pub fn any(self: *Encoder, val: anytype) !void { | |
| 20 | const T = @TypeOf(val); | |
| 21 | try self.anyTag(Tag.fromZig(T), val); | |
| 22 | } | |
| 23 | ||
| 24 | fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { | |
| 25 | const T = @TypeOf(val); | |
| 26 | if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); | |
| 27 | const start = self.buffer.data.len; | |
| 28 | const merged_tag = self.mergedTag(tag_); | |
| 29 | ||
| 30 | switch (@typeInfo(T)) { | |
| 31 | .@"struct" => |info| { | |
| 32 | inline for (0..info.fields.len) |i| { | |
| 33 | const f = info.fields[info.fields.len - i - 1]; | |
| 34 | const field_val = @field(val, f.name); | |
| 35 | const field_tag = FieldTag.fromContainer(T, f.name); | |
| 36 | ||
| 37 | // > The encoding of a set value or sequence value shall not include an encoding for any | |
| 38 | // > component value which is equal to its default value. | |
| 39 | const is_default = if (f.is_comptime) false else if (f.default_value_ptr) |v| brk: { | |
| 40 | const default_val: *const f.type = @alignCast(@ptrCast(v)); | |
| 41 | break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val)); | |
| 42 | } else false; | |
| 43 | ||
| 44 | if (!is_default) { | |
| 45 | const start2 = self.buffer.data.len; | |
| 46 | self.field_tag = field_tag; | |
| 47 | // will merge with self.field_tag. | |
| 48 | // may mutate self.field_tag. | |
| 49 | try self.anyTag(Tag.fromZig(f.type), field_val); | |
| 50 | if (field_tag) |ft| { | |
| 51 | if (ft.explicit) { | |
| 52 | try self.length(self.buffer.data.len - start2); | |
| 53 | try self.tag(ft.toTag()); | |
| 54 | self.field_tag = null; | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | }, | |
| 60 | .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), | |
| 61 | .int => try self.int(T, val), | |
| 62 | .@"enum" => |e| { | |
| 63 | if (@hasDecl(T, "oids")) { | |
| 64 | return self.any(T.oids.enumToOid(val)); | |
| 65 | } else { | |
| 66 | try self.int(e.tag_type, @intFromEnum(val)); | |
| 67 | } | |
| 68 | }, | |
| 69 | .optional => if (val) |v| return try self.anyTag(tag_, v), | |
| 70 | .null => {}, | |
| 71 | else => @compileError("cannot encode type " ++ @typeName(T)), | |
| 72 | } | |
| 73 | ||
| 74 | try self.length(self.buffer.data.len - start); | |
| 75 | try self.tag(merged_tag); | |
| 76 | } | |
| 77 | ||
| 78 | /// Encode a tag. | |
| 79 | pub fn tag(self: *Encoder, tag_: Tag) !void { | |
| 80 | const t = self.mergedTag(tag_); | |
| 81 | try t.encode(self.writer()); | |
| 82 | } | |
| 83 | ||
| 84 | fn mergedTag(self: *Encoder, tag_: Tag) Tag { | |
| 85 | var res = tag_; | |
| 86 | if (self.field_tag) |ft| { | |
| 87 | if (!ft.explicit) { | |
| 88 | res.number = @enumFromInt(ft.number); | |
| 89 | res.class = ft.class; | |
| 90 | } | |
| 91 | } | |
| 92 | return res; | |
| 93 | } | |
| 94 | ||
| 95 | /// Encode a length. | |
| 96 | pub fn length(self: *Encoder, len: usize) !void { | |
| 97 | const writer_ = self.writer(); | |
| 98 | if (len < 128) { | |
| 99 | try writer_.writeInt(u8, @intCast(len), .big); | |
| 100 | return; | |
| 101 | } | |
| 102 | inline for ([_]type{ u8, u16, u32 }) |T| { | |
| 103 | if (len < std.math.maxInt(T)) { | |
| 104 | try writer_.writeInt(T, @intCast(len), .big); | |
| 105 | try writer_.writeInt(u8, @sizeOf(T) | 0x80, .big); | |
| 106 | return; | |
| 107 | } | |
| 108 | } | |
| 109 | return error.InvalidLength; | |
| 110 | } | |
| 111 | ||
| 112 | /// Encode a tag and length-prefixed bytes. | |
| 113 | pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { | |
| 114 | try self.buffer.prependSlice(bytes); | |
| 115 | try self.length(bytes.len); | |
| 116 | try self.tag(tag_); | |
| 117 | } | |
| 118 | ||
| 119 | /// Warning: This writer writes backwards. `fn print` will NOT work as expected. | |
| 120 | pub fn writer(self: *Encoder) ArrayListReverse.Writer { | |
| 121 | return self.buffer.writer(); | |
| 122 | } | |
| 123 | ||
| 124 | fn int(self: *Encoder, comptime T: type, value: T) !void { | |
| 125 | const big = std.mem.nativeTo(T, value, .big); | |
| 126 | const big_bytes = std.mem.asBytes(&big); | |
| 127 | ||
| 128 | const bits_needed = @bitSizeOf(T) - @clz(value); | |
| 129 | const needs_padding: u1 = if (value == 0) | |
| 130 | 1 | |
| 131 | else if (bits_needed > 8) brk: { | |
| 132 | const RightShift = std.meta.Int(.unsigned, @bitSizeOf(@TypeOf(bits_needed)) - 1); | |
| 133 | const right_shift: RightShift = @intCast(bits_needed - 9); | |
| 134 | break :brk if (value >> right_shift == 0x1ff) 1 else 0; | |
| 135 | } else 0; | |
| 136 | const bytes_needed = try std.math.divCeil(usize, bits_needed, 8) + needs_padding; | |
| 137 | ||
| 138 | const writer_ = self.writer(); | |
| 139 | for (0..bytes_needed - needs_padding) |i| try writer_.writeByte(big_bytes[big_bytes.len - i - 1]); | |
| 140 | if (needs_padding == 1) try writer_.writeByte(0); | |
| 141 | } | |
| 142 | ||
| 143 | test int { | |
| 144 | const allocator = std.testing.allocator; | |
| 145 | var encoder = Encoder.init(allocator); | |
| 146 | defer encoder.deinit(); | |
| 147 | ||
| 148 | try encoder.int(u8, 0); | |
| 149 | try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); | |
| 150 | ||
| 151 | encoder.buffer.clearAndFree(); | |
| 152 | try encoder.int(u16, 0x00ff); | |
| 153 | try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); | |
| 154 | ||
| 155 | encoder.buffer.clearAndFree(); | |
| 156 | try encoder.int(u32, 0xffff); | |
| 157 | try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); | |
| 158 | } | |
| 159 | ||
| 160 | const std = @import("std"); | |
| 161 | const Oid = @import("../Oid.zig"); | |
| 162 | const asn1 = @import("../../asn1.zig"); | |
| 163 | const ArrayListReverse = @import("./ArrayListReverse.zig"); | |
| 164 | const Tag = asn1.Tag; | |
| 165 | const FieldTag = asn1.FieldTag; | |
| 166 | const Encoder = @This(); |
lib/std/crypto/asn1/der/testdata/all_types.der deleted| Binary files a/lib/std/crypto/asn1/der/testdata/all_types.der and /dev/null differ |
lib/std/crypto/asn1/der/testdata/id_ecc.pub.der deleted| Binary files a/lib/std/crypto/asn1/der/testdata/id_ecc.pub.der and /dev/null differ |
lib/std/crypto/asn1/test.zig deleted-80| ... | ... | @@ -1,80 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const asn1 = @import("../asn1.zig"); | |
| 3 | ||
| 4 | const der = asn1.der; | |
| 5 | const Tag = asn1.Tag; | |
| 6 | const FieldTag = asn1.FieldTag; | |
| 7 | ||
| 8 | /// An example that uses all ASN1 types and available implementation features. | |
| 9 | const AllTypes = struct { | |
| 10 | a: u8 = 0, | |
| 11 | b: asn1.BitString, | |
| 12 | c: C, | |
| 13 | d: asn1.Opaque(Tag.universal(.string_utf8, false)), | |
| 14 | e: asn1.Opaque(Tag.universal(.octetstring, false)), | |
| 15 | f: ?u16, | |
| 16 | g: ?Nested, | |
| 17 | h: asn1.Any, | |
| 18 | ||
| 19 | pub const asn1_tags = .{ | |
| 20 | .a = FieldTag.initExplicit(0, .context_specific), | |
| 21 | .b = FieldTag.initExplicit(1, .context_specific), | |
| 22 | .c = FieldTag.initImplicit(2, .context_specific), | |
| 23 | .g = FieldTag.initImplicit(3, .context_specific), | |
| 24 | }; | |
| 25 | ||
| 26 | const C = enum { | |
| 27 | a, | |
| 28 | b, | |
| 29 | ||
| 30 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ | |
| 31 | .a = "1.2.3.4", | |
| 32 | .b = "1.2.3.5", | |
| 33 | }); | |
| 34 | }; | |
| 35 | ||
| 36 | const Nested = struct { | |
| 37 | inner: Asn1T, | |
| 38 | sum: i16, | |
| 39 | ||
| 40 | const Asn1T = struct { a: u8, b: i16 }; | |
| 41 | ||
| 42 | pub fn decodeDer(decoder: *der.Decoder) !Nested { | |
| 43 | const inner = try decoder.any(Asn1T); | |
| 44 | return Nested{ .inner = inner, .sum = inner.a + inner.b }; | |
| 45 | } | |
| 46 | ||
| 47 | pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void { | |
| 48 | try encoder.any(self.inner); | |
| 49 | } | |
| 50 | }; | |
| 51 | }; | |
| 52 | ||
| 53 | test AllTypes { | |
| 54 | const expected = AllTypes{ | |
| 55 | .a = 2, | |
| 56 | .b = asn1.BitString{ .bytes = &[_]u8{ 0x04, 0xa0 } }, | |
| 57 | .c = .a, | |
| 58 | .d = .{ .bytes = "asdf" }, | |
| 59 | .e = .{ .bytes = "fdsa" }, | |
| 60 | .f = (1 << 8) + 1, | |
| 61 | .g = .{ .inner = .{ .a = 4, .b = 5 }, .sum = 9 }, | |
| 62 | .h = .{ .tag = Tag.init(.string_ia5, false, .universal), .bytes = "asdf" }, | |
| 63 | }; | |
| 64 | // https://lapo.it/asn1js/#MC-gAwIBAqEFAwMABKCCAyoDBAwEYXNkZgQEZmRzYQICAQGjBgIBBAIBBRYEYXNkZg | |
| 65 | const path = "./der/testdata/all_types.der"; | |
| 66 | const encoded = @embedFile(path); | |
| 67 | const actual = try asn1.der.decode(AllTypes, encoded); | |
| 68 | try std.testing.expectEqualDeep(expected, actual); | |
| 69 | ||
| 70 | const allocator = std.testing.allocator; | |
| 71 | const buf = try asn1.der.encode(allocator, expected); | |
| 72 | defer allocator.free(buf); | |
| 73 | try std.testing.expectEqualSlices(u8, encoded, buf); | |
| 74 | ||
| 75 | // Use this to update test file. | |
| 76 | // const dir = try std.fs.cwd().openDir("lib/std/crypto/asn1", .{}); | |
| 77 | // var file = try dir.createFile(path, .{}); | |
| 78 | // defer file.close(); | |
| 79 | // try file.writeAll(buf); | |
| 80 | } |
lib/std/crypto/codecs.zig created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | pub const asn1 = @import("codecs/asn1.zig"); | |
| 2 | pub const Base64 = @import("codecs/base64_hex_ct.zig").Base64; | |
| 3 | pub const Hex = @import("codecs/base64_hex_ct.zig").Hex; |
lib/std/crypto/codecs/asn1.zig created+359| ... | ... | @@ -0,0 +1,359 @@ |
| 1 | //! ASN.1 types for public consumption. | |
| 2 | const std = @import("std"); | |
| 3 | pub const der = @import("./asn1/der.zig"); | |
| 4 | pub const Oid = @import("./asn1/Oid.zig"); | |
| 5 | ||
| 6 | pub const Index = u32; | |
| 7 | ||
| 8 | pub const Tag = struct { | |
| 9 | number: Number, | |
| 10 | /// Whether this ASN.1 type contains other ASN.1 types. | |
| 11 | constructed: bool, | |
| 12 | class: Class, | |
| 13 | ||
| 14 | /// These values apply to class == .universal. | |
| 15 | pub const Number = enum(u16) { | |
| 16 | // 0 is reserved by spec | |
| 17 | boolean = 1, | |
| 18 | integer = 2, | |
| 19 | bitstring = 3, | |
| 20 | octetstring = 4, | |
| 21 | null = 5, | |
| 22 | oid = 6, | |
| 23 | object_descriptor = 7, | |
| 24 | real = 9, | |
| 25 | enumerated = 10, | |
| 26 | embedded = 11, | |
| 27 | string_utf8 = 12, | |
| 28 | oid_relative = 13, | |
| 29 | time = 14, | |
| 30 | // 15 is reserved to mean that the tag is >= 32 | |
| 31 | sequence = 16, | |
| 32 | /// Elements may appear in any order. | |
| 33 | sequence_of = 17, | |
| 34 | string_numeric = 18, | |
| 35 | string_printable = 19, | |
| 36 | string_teletex = 20, | |
| 37 | string_videotex = 21, | |
| 38 | string_ia5 = 22, | |
| 39 | utc_time = 23, | |
| 40 | generalized_time = 24, | |
| 41 | string_graphic = 25, | |
| 42 | string_visible = 26, | |
| 43 | string_general = 27, | |
| 44 | string_universal = 28, | |
| 45 | string_char = 29, | |
| 46 | string_bmp = 30, | |
| 47 | date = 31, | |
| 48 | time_of_day = 32, | |
| 49 | date_time = 33, | |
| 50 | duration = 34, | |
| 51 | /// IRI = Internationalized Resource Identifier | |
| 52 | oid_iri = 35, | |
| 53 | oid_iri_relative = 36, | |
| 54 | _, | |
| 55 | }; | |
| 56 | ||
| 57 | pub const Class = enum(u2) { | |
| 58 | universal, | |
| 59 | application, | |
| 60 | context_specific, | |
| 61 | private, | |
| 62 | }; | |
| 63 | ||
| 64 | pub fn init(number: Tag.Number, constructed: bool, class: Tag.Class) Tag { | |
| 65 | return .{ .number = number, .constructed = constructed, .class = class }; | |
| 66 | } | |
| 67 | ||
| 68 | pub fn universal(number: Tag.Number, constructed: bool) Tag { | |
| 69 | return .{ .number = number, .constructed = constructed, .class = .universal }; | |
| 70 | } | |
| 71 | ||
| 72 | pub fn decode(reader: anytype) !Tag { | |
| 73 | const tag1: FirstTag = @bitCast(try reader.readByte()); | |
| 74 | var number: u14 = tag1.number; | |
| 75 | ||
| 76 | if (tag1.number == 15) { | |
| 77 | const tag2: NextTag = @bitCast(try reader.readByte()); | |
| 78 | number = tag2.number; | |
| 79 | if (tag2.continues) { | |
| 80 | const tag3: NextTag = @bitCast(try reader.readByte()); | |
| 81 | number = (number << 7) + tag3.number; | |
| 82 | if (tag3.continues) return error.InvalidLength; | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | return Tag{ | |
| 87 | .number = @enumFromInt(number), | |
| 88 | .constructed = tag1.constructed, | |
| 89 | .class = tag1.class, | |
| 90 | }; | |
| 91 | } | |
| 92 | ||
| 93 | pub fn encode(self: Tag, writer: anytype) @TypeOf(writer).Error!void { | |
| 94 | var tag1 = FirstTag{ | |
| 95 | .number = undefined, | |
| 96 | .constructed = self.constructed, | |
| 97 | .class = self.class, | |
| 98 | }; | |
| 99 | ||
| 100 | var buffer: [3]u8 = undefined; | |
| 101 | var stream = std.io.fixedBufferStream(&buffer); | |
| 102 | var writer2 = stream.writer(); | |
| 103 | ||
| 104 | switch (@intFromEnum(self.number)) { | |
| 105 | 0...std.math.maxInt(u5) => |n| { | |
| 106 | tag1.number = @intCast(n); | |
| 107 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 108 | }, | |
| 109 | std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| { | |
| 110 | tag1.number = 15; | |
| 111 | const tag2 = NextTag{ .number = @intCast(n), .continues = false }; | |
| 112 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 113 | writer2.writeByte(@bitCast(tag2)) catch unreachable; | |
| 114 | }, | |
| 115 | else => |n| { | |
| 116 | tag1.number = 15; | |
| 117 | const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true }; | |
| 118 | const tag3 = NextTag{ .number = @truncate(n), .continues = false }; | |
| 119 | writer2.writeByte(@bitCast(tag1)) catch unreachable; | |
| 120 | writer2.writeByte(@bitCast(tag2)) catch unreachable; | |
| 121 | writer2.writeByte(@bitCast(tag3)) catch unreachable; | |
| 122 | }, | |
| 123 | } | |
| 124 | ||
| 125 | _ = try writer.write(stream.getWritten()); | |
| 126 | } | |
| 127 | ||
| 128 | const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; | |
| 129 | const NextTag = packed struct(u8) { number: u7, continues: bool }; | |
| 130 | ||
| 131 | pub fn toExpected(self: Tag) ExpectedTag { | |
| 132 | return ExpectedTag{ | |
| 133 | .number = self.number, | |
| 134 | .constructed = self.constructed, | |
| 135 | .class = self.class, | |
| 136 | }; | |
| 137 | } | |
| 138 | ||
| 139 | pub fn fromZig(comptime T: type) Tag { | |
| 140 | switch (@typeInfo(T)) { | |
| 141 | .@"struct", .@"enum", .@"union" => { | |
| 142 | if (@hasDecl(T, "asn1_tag")) return T.asn1_tag; | |
| 143 | }, | |
| 144 | else => {}, | |
| 145 | } | |
| 146 | ||
| 147 | switch (@typeInfo(T)) { | |
| 148 | .@"struct", .@"union" => return universal(.sequence, true), | |
| 149 | .bool => return universal(.boolean, false), | |
| 150 | .int => return universal(.integer, false), | |
| 151 | .@"enum" => |e| { | |
| 152 | if (@hasDecl(T, "oids")) return Oid.asn1_tag; | |
| 153 | return universal(if (e.is_exhaustive) .enumerated else .integer, false); | |
| 154 | }, | |
| 155 | .optional => |o| return fromZig(o.child), | |
| 156 | .null => return universal(.null, false), | |
| 157 | else => @compileError("cannot map Zig type to asn1_tag " ++ @typeName(T)), | |
| 158 | } | |
| 159 | } | |
| 160 | }; | |
| 161 | ||
| 162 | test Tag { | |
| 163 | const buf = [_]u8{0xa3}; | |
| 164 | var stream = std.io.fixedBufferStream(&buf); | |
| 165 | const t = Tag.decode(stream.reader()); | |
| 166 | try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); | |
| 167 | } | |
| 168 | ||
| 169 | /// A decoded view. | |
| 170 | pub const Element = struct { | |
| 171 | tag: Tag, | |
| 172 | slice: Slice, | |
| 173 | ||
| 174 | pub const Slice = struct { | |
| 175 | start: Index, | |
| 176 | end: Index, | |
| 177 | ||
| 178 | pub fn len(self: Slice) Index { | |
| 179 | return self.end - self.start; | |
| 180 | } | |
| 181 | ||
| 182 | pub fn view(self: Slice, bytes: []const u8) []const u8 { | |
| 183 | return bytes[self.start..self.end]; | |
| 184 | } | |
| 185 | }; | |
| 186 | ||
| 187 | pub const DecodeError = error{ InvalidLength, EndOfStream }; | |
| 188 | ||
| 189 | /// Safely decode a DER/BER/CER element at `index`: | |
| 190 | /// - Ensures length uses shortest form | |
| 191 | /// - Ensures length is within `bytes` | |
| 192 | /// - Ensures length is less than `std.math.maxInt(Index)` | |
| 193 | pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { | |
| 194 | var stream = std.io.fixedBufferStream(bytes[index..]); | |
| 195 | var reader = stream.reader(); | |
| 196 | ||
| 197 | const tag = try Tag.decode(reader); | |
| 198 | const size_or_len_size = try reader.readByte(); | |
| 199 | ||
| 200 | var start = index + 2; | |
| 201 | var end = start + size_or_len_size; | |
| 202 | // short form between 0-127 | |
| 203 | if (size_or_len_size < 128) { | |
| 204 | if (end > bytes.len) return error.InvalidLength; | |
| 205 | } else { | |
| 206 | // long form between 0 and std.math.maxInt(u1024) | |
| 207 | const len_size: u7 = @truncate(size_or_len_size); | |
| 208 | start += len_size; | |
| 209 | if (len_size > @sizeOf(Index)) return error.InvalidLength; | |
| 210 | ||
| 211 | const len = try reader.readVarInt(Index, .big, len_size); | |
| 212 | if (len < 128) return error.InvalidLength; // should have used short form | |
| 213 | ||
| 214 | end = std.math.add(Index, start, len) catch return error.InvalidLength; | |
| 215 | if (end > bytes.len) return error.InvalidLength; | |
| 216 | } | |
| 217 | ||
| 218 | return Element{ .tag = tag, .slice = Slice{ .start = start, .end = end } }; | |
| 219 | } | |
| 220 | }; | |
| 221 | ||
| 222 | test Element { | |
| 223 | const short_form = [_]u8{ 0x30, 0x03, 0x02, 0x01, 0x09 }; | |
| 224 | try std.testing.expectEqual(Element{ | |
| 225 | .tag = Tag.universal(.sequence, true), | |
| 226 | .slice = Element.Slice{ .start = 2, .end = short_form.len }, | |
| 227 | }, Element.decode(&short_form, 0)); | |
| 228 | ||
| 229 | const long_form = [_]u8{ 0x30, 129, 129 } ++ [_]u8{0} ** 129; | |
| 230 | try std.testing.expectEqual(Element{ | |
| 231 | .tag = Tag.universal(.sequence, true), | |
| 232 | .slice = Element.Slice{ .start = 3, .end = long_form.len }, | |
| 233 | }, Element.decode(&long_form, 0)); | |
| 234 | } | |
| 235 | ||
| 236 | /// For decoding. | |
| 237 | pub const ExpectedTag = struct { | |
| 238 | number: ?Tag.Number = null, | |
| 239 | constructed: ?bool = null, | |
| 240 | class: ?Tag.Class = null, | |
| 241 | ||
| 242 | pub fn init(number: ?Tag.Number, constructed: ?bool, class: ?Tag.Class) ExpectedTag { | |
| 243 | return .{ .number = number, .constructed = constructed, .class = class }; | |
| 244 | } | |
| 245 | ||
| 246 | pub fn primitive(number: ?Tag.Number) ExpectedTag { | |
| 247 | return .{ .number = number, .constructed = false, .class = .universal }; | |
| 248 | } | |
| 249 | ||
| 250 | pub fn match(self: ExpectedTag, tag: Tag) bool { | |
| 251 | if (self.number) |e| { | |
| 252 | if (tag.number != e) return false; | |
| 253 | } | |
| 254 | if (self.constructed) |e| { | |
| 255 | if (tag.constructed != e) return false; | |
| 256 | } | |
| 257 | if (self.class) |e| { | |
| 258 | if (tag.class != e) return false; | |
| 259 | } | |
| 260 | return true; | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | pub const FieldTag = struct { | |
| 265 | number: std.meta.Tag(Tag.Number), | |
| 266 | class: Tag.Class, | |
| 267 | explicit: bool = true, | |
| 268 | ||
| 269 | pub fn initExplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { | |
| 270 | return .{ .number = number, .class = class, .explicit = true }; | |
| 271 | } | |
| 272 | ||
| 273 | pub fn initImplicit(number: std.meta.Tag(Tag.Number), class: Tag.Class) FieldTag { | |
| 274 | return .{ .number = number, .class = class, .explicit = false }; | |
| 275 | } | |
| 276 | ||
| 277 | pub fn fromContainer(comptime Container: type, comptime field_name: []const u8) ?FieldTag { | |
| 278 | if (@hasDecl(Container, "asn1_tags") and @hasField(@TypeOf(Container.asn1_tags), field_name)) { | |
| 279 | return @field(Container.asn1_tags, field_name); | |
| 280 | } | |
| 281 | ||
| 282 | return null; | |
| 283 | } | |
| 284 | ||
| 285 | pub fn toTag(self: FieldTag) Tag { | |
| 286 | return Tag.init(@enumFromInt(self.number), self.explicit, self.class); | |
| 287 | } | |
| 288 | }; | |
| 289 | ||
| 290 | pub const BitString = struct { | |
| 291 | /// Number of bits in rightmost byte that are unused. | |
| 292 | right_padding: u3 = 0, | |
| 293 | bytes: []const u8, | |
| 294 | ||
| 295 | pub fn bitLen(self: BitString) usize { | |
| 296 | return self.bytes.len * 8 - self.right_padding; | |
| 297 | } | |
| 298 | ||
| 299 | const asn1_tag = Tag.universal(.bitstring, false); | |
| 300 | ||
| 301 | pub fn decodeDer(decoder: *der.Decoder) !BitString { | |
| 302 | const ele = try decoder.element(asn1_tag.toExpected()); | |
| 303 | const bytes = decoder.view(ele); | |
| 304 | ||
| 305 | if (bytes.len < 1) return error.InvalidBitString; | |
| 306 | const padding = bytes[0]; | |
| 307 | if (padding >= 8) return error.InvalidBitString; | |
| 308 | const right_padding: u3 = @intCast(padding); | |
| 309 | ||
| 310 | // DER requires that unused bits be zero. | |
| 311 | if (@ctz(bytes[bytes.len - 1]) < right_padding) return error.InvalidBitString; | |
| 312 | ||
| 313 | return BitString{ .bytes = bytes[1..], .right_padding = right_padding }; | |
| 314 | } | |
| 315 | ||
| 316 | pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { | |
| 317 | try encoder.writer().writeAll(self.bytes); | |
| 318 | try encoder.writer().writeByte(self.right_padding); | |
| 319 | try encoder.length(self.bytes.len + 1); | |
| 320 | try encoder.tag(asn1_tag); | |
| 321 | } | |
| 322 | }; | |
| 323 | ||
| 324 | pub fn Opaque(comptime tag: Tag) type { | |
| 325 | return struct { | |
| 326 | bytes: []const u8, | |
| 327 | ||
| 328 | pub fn decodeDer(decoder: *der.Decoder) !@This() { | |
| 329 | const ele = try decoder.element(tag.toExpected()); | |
| 330 | if (tag.constructed) decoder.index = ele.slice.end; | |
| 331 | return .{ .bytes = decoder.view(ele) }; | |
| 332 | } | |
| 333 | ||
| 334 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { | |
| 335 | try encoder.tagBytes(tag, self.bytes); | |
| 336 | } | |
| 337 | }; | |
| 338 | } | |
| 339 | ||
| 340 | /// Use sparingly. | |
| 341 | pub const Any = struct { | |
| 342 | tag: Tag, | |
| 343 | bytes: []const u8, | |
| 344 | ||
| 345 | pub fn decodeDer(decoder: *der.Decoder) !@This() { | |
| 346 | const ele = try decoder.element(ExpectedTag{}); | |
| 347 | return .{ .tag = ele.tag, .bytes = decoder.view(ele) }; | |
| 348 | } | |
| 349 | ||
| 350 | pub fn encodeDer(self: @This(), encoder: *der.Encoder) !void { | |
| 351 | try encoder.tagBytes(self.tag, self.bytes); | |
| 352 | } | |
| 353 | }; | |
| 354 | ||
| 355 | test { | |
| 356 | _ = der; | |
| 357 | _ = Oid; | |
| 358 | _ = @import("asn1/test.zig"); | |
| 359 | } |
lib/std/crypto/codecs/asn1/Oid.zig created+210| ... | ... | @@ -0,0 +1,210 @@ |
| 1 | //! Globally unique hierarchical identifier made of a sequence of integers. | |
| 2 | //! | |
| 3 | //! Commonly used to identify standards, algorithms, certificate extensions, | |
| 4 | //! organizations, or policy documents. | |
| 5 | encoded: []const u8, | |
| 6 | ||
| 7 | pub const InitError = std.fmt.ParseIntError || error{MissingPrefix} || std.io.FixedBufferStream(u8).WriteError; | |
| 8 | ||
| 9 | pub fn fromDot(dot_notation: []const u8, out: []u8) InitError!Oid { | |
| 10 | var split = std.mem.splitScalar(u8, dot_notation, '.'); | |
| 11 | const first_str = split.next() orelse return error.MissingPrefix; | |
| 12 | const second_str = split.next() orelse return error.MissingPrefix; | |
| 13 | ||
| 14 | const first = try std.fmt.parseInt(u8, first_str, 10); | |
| 15 | const second = try std.fmt.parseInt(u8, second_str, 10); | |
| 16 | ||
| 17 | var stream = std.io.fixedBufferStream(out); | |
| 18 | var writer = stream.writer(); | |
| 19 | ||
| 20 | try writer.writeByte(first * 40 + second); | |
| 21 | ||
| 22 | var i: usize = 1; | |
| 23 | while (split.next()) |s| { | |
| 24 | var parsed = try std.fmt.parseUnsigned(Arc, s, 10); | |
| 25 | const n_bytes = if (parsed == 0) 0 else std.math.log(Arc, encoding_base, parsed); | |
| 26 | ||
| 27 | for (0..n_bytes) |j| { | |
| 28 | const place = std.math.pow(Arc, encoding_base, n_bytes - @as(Arc, @intCast(j))); | |
| 29 | const digit: u8 = @intCast(@divFloor(parsed, place)); | |
| 30 | ||
| 31 | try writer.writeByte(digit | 0x80); | |
| 32 | parsed -= digit * place; | |
| 33 | ||
| 34 | i += 1; | |
| 35 | } | |
| 36 | try writer.writeByte(@intCast(parsed)); | |
| 37 | i += 1; | |
| 38 | } | |
| 39 | ||
| 40 | return .{ .encoded = stream.getWritten() }; | |
| 41 | } | |
| 42 | ||
| 43 | test fromDot { | |
| 44 | var buf: [256]u8 = undefined; | |
| 45 | for (test_cases) |t| { | |
| 46 | const actual = try fromDot(t.dot_notation, &buf); | |
| 47 | try std.testing.expectEqualSlices(u8, t.encoded, actual.encoded); | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { | |
| 52 | const encoded = self.encoded; | |
| 53 | const first = @divTrunc(encoded[0], 40); | |
| 54 | const second = encoded[0] - first * 40; | |
| 55 | try writer.print("{d}.{d}", .{ first, second }); | |
| 56 | ||
| 57 | var i: usize = 1; | |
| 58 | while (i != encoded.len) { | |
| 59 | const n_bytes: usize = brk: { | |
| 60 | var res: usize = 1; | |
| 61 | var j: usize = i; | |
| 62 | while (encoded[j] & 0x80 != 0) { | |
| 63 | res += 1; | |
| 64 | j += 1; | |
| 65 | } | |
| 66 | break :brk res; | |
| 67 | }; | |
| 68 | ||
| 69 | var n: usize = 0; | |
| 70 | for (0..n_bytes) |j| { | |
| 71 | const place = std.math.pow(usize, encoding_base, n_bytes - j - 1); | |
| 72 | n += place * (encoded[i] & 0b01111111); | |
| 73 | i += 1; | |
| 74 | } | |
| 75 | try writer.print(".{d}", .{n}); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | test toDot { | |
| 80 | var buf: [256]u8 = undefined; | |
| 81 | ||
| 82 | for (test_cases) |t| { | |
| 83 | var stream = std.io.fixedBufferStream(&buf); | |
| 84 | try toDot(Oid{ .encoded = t.encoded }, stream.writer()); | |
| 85 | try std.testing.expectEqualStrings(t.dot_notation, stream.getWritten()); | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | const TestCase = struct { | |
| 90 | encoded: []const u8, | |
| 91 | dot_notation: []const u8, | |
| 92 | ||
| 93 | pub fn init(comptime hex: []const u8, dot_notation: []const u8) TestCase { | |
| 94 | return .{ .encoded = &hexToBytes(hex), .dot_notation = dot_notation }; | |
| 95 | } | |
| 96 | }; | |
| 97 | ||
| 98 | const test_cases = [_]TestCase{ | |
| 99 | // https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier | |
| 100 | TestCase.init("2b0601040182371514", "1.3.6.1.4.1.311.21.20"), | |
| 101 | // https://luca.ntop.org/Teaching/Appunti/asn1.html | |
| 102 | TestCase.init("2a864886f70d", "1.2.840.113549"), | |
| 103 | // https://www.sysadmins.lv/blog-en/how-to-encode-object-identifier-to-an-asn1-der-encoded-string.aspx | |
| 104 | TestCase.init("2a868d20", "1.2.100000"), | |
| 105 | TestCase.init("2a864886f70d01010b", "1.2.840.113549.1.1.11"), | |
| 106 | TestCase.init("2b6570", "1.3.101.112"), | |
| 107 | }; | |
| 108 | ||
| 109 | pub const asn1_tag = asn1.Tag.init(.oid, false, .universal); | |
| 110 | ||
| 111 | pub fn decodeDer(decoder: *der.Decoder) !Oid { | |
| 112 | const ele = try decoder.element(asn1_tag.toExpected()); | |
| 113 | return Oid{ .encoded = decoder.view(ele) }; | |
| 114 | } | |
| 115 | ||
| 116 | pub fn encodeDer(self: Oid, encoder: *der.Encoder) !void { | |
| 117 | try encoder.tagBytes(asn1_tag, self.encoded); | |
| 118 | } | |
| 119 | ||
| 120 | fn encodedLen(dot_notation: []const u8) usize { | |
| 121 | var buf: [256]u8 = undefined; | |
| 122 | const oid = fromDot(dot_notation, &buf) catch unreachable; | |
| 123 | return oid.encoded.len; | |
| 124 | } | |
| 125 | ||
| 126 | /// Returns encoded bytes of OID. | |
| 127 | fn encodeComptime(comptime dot_notation: []const u8) [encodedLen(dot_notation)]u8 { | |
| 128 | @setEvalBranchQuota(4000); | |
| 129 | comptime var buf: [256]u8 = undefined; | |
| 130 | const oid = comptime fromDot(dot_notation, &buf) catch unreachable; | |
| 131 | return oid.encoded[0..oid.encoded.len].*; | |
| 132 | } | |
| 133 | ||
| 134 | test encodeComptime { | |
| 135 | try std.testing.expectEqual( | |
| 136 | hexToBytes("2b0601040182371514"), | |
| 137 | comptime encodeComptime("1.3.6.1.4.1.311.21.20"), | |
| 138 | ); | |
| 139 | } | |
| 140 | ||
| 141 | pub fn fromDotComptime(comptime dot_notation: []const u8) Oid { | |
| 142 | const tmp = comptime encodeComptime(dot_notation); | |
| 143 | return Oid{ .encoded = &tmp }; | |
| 144 | } | |
| 145 | ||
| 146 | /// Maps of: | |
| 147 | /// - Oid -> enum | |
| 148 | /// - Enum -> oid | |
| 149 | pub fn StaticMap(comptime Enum: type) type { | |
| 150 | const enum_info = @typeInfo(Enum).@"enum"; | |
| 151 | const EnumToOid = std.EnumArray(Enum, []const u8); | |
| 152 | const ReturnType = struct { | |
| 153 | oid_to_enum: std.StaticStringMap(Enum), | |
| 154 | enum_to_oid: EnumToOid, | |
| 155 | ||
| 156 | pub fn oidToEnum(self: @This(), encoded: []const u8) ?Enum { | |
| 157 | return self.oid_to_enum.get(encoded); | |
| 158 | } | |
| 159 | ||
| 160 | pub fn enumToOid(self: @This(), value: Enum) Oid { | |
| 161 | const bytes = self.enum_to_oid.get(value); | |
| 162 | return .{ .encoded = bytes }; | |
| 163 | } | |
| 164 | }; | |
| 165 | ||
| 166 | return struct { | |
| 167 | pub fn initComptime(comptime key_pairs: anytype) ReturnType { | |
| 168 | const struct_info = @typeInfo(@TypeOf(key_pairs)).@"struct"; | |
| 169 | const error_msg = "Each field of '" ++ @typeName(Enum) ++ "' must map to exactly one OID"; | |
| 170 | if (!enum_info.is_exhaustive or enum_info.fields.len != struct_info.fields.len) { | |
| 171 | @compileError(error_msg); | |
| 172 | } | |
| 173 | ||
| 174 | comptime var enum_to_oid = EnumToOid.initUndefined(); | |
| 175 | ||
| 176 | const KeyPair = struct { []const u8, Enum }; | |
| 177 | comptime var static_key_pairs: [enum_info.fields.len]KeyPair = undefined; | |
| 178 | ||
| 179 | comptime for (enum_info.fields, 0..) |f, i| { | |
| 180 | if (!@hasField(@TypeOf(key_pairs), f.name)) { | |
| 181 | @compileError("Field '" ++ f.name ++ "' missing Oid.StaticMap entry"); | |
| 182 | } | |
| 183 | const encoded = &encodeComptime(@field(key_pairs, f.name)); | |
| 184 | const tag: Enum = @enumFromInt(f.value); | |
| 185 | static_key_pairs[i] = .{ encoded, tag }; | |
| 186 | enum_to_oid.set(tag, encoded); | |
| 187 | }; | |
| 188 | ||
| 189 | const oid_to_enum = std.StaticStringMap(Enum).initComptime(static_key_pairs); | |
| 190 | if (oid_to_enum.values().len != enum_info.fields.len) @compileError(error_msg); | |
| 191 | ||
| 192 | return ReturnType{ .oid_to_enum = oid_to_enum, .enum_to_oid = enum_to_oid }; | |
| 193 | } | |
| 194 | }; | |
| 195 | } | |
| 196 | ||
| 197 | /// Strictly for testing. | |
| 198 | fn hexToBytes(comptime hex: []const u8) [hex.len / 2]u8 { | |
| 199 | var res: [hex.len / 2]u8 = undefined; | |
| 200 | _ = std.fmt.hexToBytes(&res, hex) catch unreachable; | |
| 201 | return res; | |
| 202 | } | |
| 203 | ||
| 204 | const std = @import("std"); | |
| 205 | const Oid = @This(); | |
| 206 | const Arc = u32; | |
| 207 | const encoding_base = 128; | |
| 208 | const Allocator = std.mem.Allocator; | |
| 209 | const der = @import("der.zig"); | |
| 210 | const asn1 = @import("../asn1.zig"); |
lib/std/crypto/codecs/asn1/der.zig created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | //! Distinguised Encoding Rules as defined in X.690 and X.691. | |
| 2 | //! | |
| 3 | //! Subset of Basic Encoding Rules (BER) which eliminates flexibility in | |
| 4 | //! an effort to acheive normality. Used in PKI. | |
| 5 | const std = @import("std"); | |
| 6 | const asn1 = @import("../asn1.zig"); | |
| 7 | ||
| 8 | pub const Decoder = @import("der/Decoder.zig"); | |
| 9 | pub const Encoder = @import("der/Encoder.zig"); | |
| 10 | ||
| 11 | pub fn decode(comptime T: type, encoded: []const u8) !T { | |
| 12 | var decoder = Decoder{ .bytes = encoded }; | |
| 13 | const res = try decoder.any(T); | |
| 14 | std.debug.assert(decoder.index == encoded.len); | |
| 15 | return res; | |
| 16 | } | |
| 17 | ||
| 18 | /// Caller owns returned memory. | |
| 19 | pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 { | |
| 20 | var encoder = Encoder.init(allocator); | |
| 21 | defer encoder.deinit(); | |
| 22 | try encoder.any(value); | |
| 23 | return try encoder.buffer.toOwnedSlice(); | |
| 24 | } | |
| 25 | ||
| 26 | test encode { | |
| 27 | // https://lapo.it/asn1js/#MAgGAyoDBAIBBA | |
| 28 | const Value = struct { a: asn1.Oid, b: i32 }; | |
| 29 | const test_case = .{ | |
| 30 | .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, | |
| 31 | .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, | |
| 32 | }; | |
| 33 | const allocator = std.testing.allocator; | |
| 34 | const actual = try encode(allocator, test_case.value); | |
| 35 | defer allocator.free(actual); | |
| 36 | ||
| 37 | try std.testing.expectEqualSlices(u8, test_case.encoded, actual); | |
| 38 | } | |
| 39 | ||
| 40 | test decode { | |
| 41 | // https://lapo.it/asn1js/#MAgGAyoDBAIBBA | |
| 42 | const Value = struct { a: asn1.Oid, b: i32 }; | |
| 43 | const test_case = .{ | |
| 44 | .value = Value{ .a = asn1.Oid.fromDotComptime("1.2.3.4"), .b = 4 }, | |
| 45 | .encoded = &[_]u8{ 0x30, 0x08, 0x06, 0x03, 0x2A, 0x03, 0x04, 0x02, 0x01, 0x04 }, | |
| 46 | }; | |
| 47 | const decoded = try decode(Value, test_case.encoded); | |
| 48 | ||
| 49 | try std.testing.expectEqualDeep(test_case.value, decoded); | |
| 50 | } | |
| 51 | ||
| 52 | test { | |
| 53 | _ = Decoder; | |
| 54 | _ = Encoder; | |
| 55 | } |
lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig created+97| ... | ... | @@ -0,0 +1,97 @@ |
| 1 | //! An ArrayList that grows backwards. Counts nested prefix length fields | |
| 2 | //! in O(n) instead of O(n^depth) at the cost of extra buffering. | |
| 3 | //! | |
| 4 | //! Laid out in memory like: | |
| 5 | //! capacity |--------------------------| | |
| 6 | //! data |-------------| | |
| 7 | data: []u8, | |
| 8 | capacity: usize, | |
| 9 | allocator: Allocator, | |
| 10 | ||
| 11 | const ArrayListReverse = @This(); | |
| 12 | const Error = Allocator.Error; | |
| 13 | ||
| 14 | pub fn init(allocator: Allocator) ArrayListReverse { | |
| 15 | return .{ .data = &.{}, .capacity = 0, .allocator = allocator }; | |
| 16 | } | |
| 17 | ||
| 18 | pub fn deinit(self: *ArrayListReverse) void { | |
| 19 | self.allocator.free(self.allocatedSlice()); | |
| 20 | } | |
| 21 | ||
| 22 | pub fn ensureCapacity(self: *ArrayListReverse, new_capacity: usize) Error!void { | |
| 23 | if (self.capacity >= new_capacity) return; | |
| 24 | ||
| 25 | const old_memory = self.allocatedSlice(); | |
| 26 | // Just make a new allocation to not worry about aliasing. | |
| 27 | const new_memory = try self.allocator.alloc(u8, new_capacity); | |
| 28 | @memcpy(new_memory[new_capacity - self.data.len ..], self.data); | |
| 29 | self.allocator.free(old_memory); | |
| 30 | self.data.ptr = new_memory.ptr + new_capacity - self.data.len; | |
| 31 | self.capacity = new_memory.len; | |
| 32 | } | |
| 33 | ||
| 34 | pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void { | |
| 35 | try self.ensureCapacity(self.data.len + data.len); | |
| 36 | const old_len = self.data.len; | |
| 37 | const new_len = old_len + data.len; | |
| 38 | assert(new_len <= self.capacity); | |
| 39 | self.data.len = new_len; | |
| 40 | ||
| 41 | const end = self.data.ptr; | |
| 42 | const begin = end - data.len; | |
| 43 | const slice = begin[0..data.len]; | |
| 44 | @memcpy(slice, data); | |
| 45 | self.data.ptr = begin; | |
| 46 | } | |
| 47 | ||
| 48 | pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); | |
| 49 | /// Warning: This writer writes backwards. `fn print` will NOT work as expected. | |
| 50 | pub fn writer(self: *ArrayListReverse) Writer { | |
| 51 | return .{ .context = self }; | |
| 52 | } | |
| 53 | ||
| 54 | fn prependSliceSize(self: *ArrayListReverse, data: []const u8) Error!usize { | |
| 55 | try self.prependSlice(data); | |
| 56 | return data.len; | |
| 57 | } | |
| 58 | ||
| 59 | fn allocatedSlice(self: *ArrayListReverse) []u8 { | |
| 60 | return (self.data.ptr + self.data.len - self.capacity)[0..self.capacity]; | |
| 61 | } | |
| 62 | ||
| 63 | /// Invalidates all element pointers. | |
| 64 | pub fn clearAndFree(self: *ArrayListReverse) void { | |
| 65 | self.allocator.free(self.allocatedSlice()); | |
| 66 | self.data.len = 0; | |
| 67 | self.capacity = 0; | |
| 68 | } | |
| 69 | ||
| 70 | /// The caller owns the returned memory. | |
| 71 | /// Capacity is cleared, making deinit() safe but unnecessary to call. | |
| 72 | pub fn toOwnedSlice(self: *ArrayListReverse) Error![]u8 { | |
| 73 | const new_memory = try self.allocator.alloc(u8, self.data.len); | |
| 74 | @memcpy(new_memory, self.data); | |
| 75 | @memset(self.data, undefined); | |
| 76 | self.clearAndFree(); | |
| 77 | return new_memory; | |
| 78 | } | |
| 79 | ||
| 80 | const std = @import("std"); | |
| 81 | const Allocator = std.mem.Allocator; | |
| 82 | const assert = std.debug.assert; | |
| 83 | const testing = std.testing; | |
| 84 | ||
| 85 | test ArrayListReverse { | |
| 86 | var b = ArrayListReverse.init(testing.allocator); | |
| 87 | defer b.deinit(); | |
| 88 | const data: []const u8 = &.{ 4, 5, 6 }; | |
| 89 | try b.prependSlice(data); | |
| 90 | try testing.expectEqual(data.len, b.data.len); | |
| 91 | try testing.expectEqualSlices(u8, data, b.data); | |
| 92 | ||
| 93 | const data2: []const u8 = &.{ 1, 2, 3 }; | |
| 94 | try b.prependSlice(data2); | |
| 95 | try testing.expectEqual(data.len + data2.len, b.data.len); | |
| 96 | try testing.expectEqualSlices(u8, data2 ++ data, b.data); | |
| 97 | } |
lib/std/crypto/codecs/asn1/der/Decoder.zig created+170| ... | ... | @@ -0,0 +1,170 @@ |
| 1 | //! A secure DER parser that: | |
| 2 | //! - Prefers calling `fn decodeDer(self: @This(), decoder: *der.Decoder)` | |
| 3 | //! - Does NOT allocate. If you wish to parse lists you can do so lazily | |
| 4 | //! with an opaque type. | |
| 5 | //! - Does NOT read memory outside `bytes`. | |
| 6 | //! - Does NOT return elements with slices outside `bytes`. | |
| 7 | //! - Errors on values that do NOT follow DER rules: | |
| 8 | //! - Lengths that could be represented in a shorter form. | |
| 9 | //! - Booleans that are not 0xff or 0x00. | |
| 10 | bytes: []const u8, | |
| 11 | index: Index = 0, | |
| 12 | /// The field tag of the most recently visited field. | |
| 13 | /// This is needed because we might visit an implicitly tagged container with a `fn decodeDer`. | |
| 14 | field_tag: ?FieldTag = null, | |
| 15 | ||
| 16 | /// Expect a value. | |
| 17 | pub fn any(self: *Decoder, comptime T: type) !T { | |
| 18 | if (std.meta.hasFn(T, "decodeDer")) return try T.decodeDer(self); | |
| 19 | ||
| 20 | const tag = Tag.fromZig(T).toExpected(); | |
| 21 | switch (@typeInfo(T)) { | |
| 22 | .@"struct" => { | |
| 23 | const ele = try self.element(tag); | |
| 24 | defer self.index = ele.slice.end; // don't force parsing all fields | |
| 25 | ||
| 26 | var res: T = undefined; | |
| 27 | ||
| 28 | inline for (std.meta.fields(T)) |f| { | |
| 29 | self.field_tag = FieldTag.fromContainer(T, f.name); | |
| 30 | ||
| 31 | if (self.field_tag) |ft| { | |
| 32 | if (ft.explicit) { | |
| 33 | const seq = try self.element(ft.toTag().toExpected()); | |
| 34 | self.index = seq.slice.start; | |
| 35 | self.field_tag = null; | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | @field(res, f.name) = self.any(f.type) catch |err| brk: { | |
| 40 | if (f.defaultValue()) |d| { | |
| 41 | break :brk d; | |
| 42 | } | |
| 43 | return err; | |
| 44 | }; | |
| 45 | // DER encodes null values by skipping them. | |
| 46 | if (@typeInfo(f.type) == .optional and @field(res, f.name) == null) { | |
| 47 | if (f.defaultValue()) |d| @field(res, f.name) = d; | |
| 48 | } | |
| 49 | } | |
| 50 | ||
| 51 | return res; | |
| 52 | }, | |
| 53 | .bool => { | |
| 54 | const ele = try self.element(tag); | |
| 55 | const bytes = self.view(ele); | |
| 56 | if (bytes.len != 1) return error.InvalidBool; | |
| 57 | ||
| 58 | return switch (bytes[0]) { | |
| 59 | 0x00 => false, | |
| 60 | 0xff => true, | |
| 61 | else => error.InvalidBool, | |
| 62 | }; | |
| 63 | }, | |
| 64 | .int => { | |
| 65 | const ele = try self.element(tag); | |
| 66 | const bytes = self.view(ele); | |
| 67 | return try int(T, bytes); | |
| 68 | }, | |
| 69 | .@"enum" => |e| { | |
| 70 | const ele = try self.element(tag); | |
| 71 | const bytes = self.view(ele); | |
| 72 | if (@hasDecl(T, "oids")) { | |
| 73 | return T.oids.oidToEnum(bytes) orelse return error.UnknownOid; | |
| 74 | } | |
| 75 | return @enumFromInt(try int(e.tag_type, bytes)); | |
| 76 | }, | |
| 77 | .optional => |o| return self.any(o.child) catch return null, | |
| 78 | else => @compileError("cannot decode type " ++ @typeName(T)), | |
| 79 | } | |
| 80 | } | |
| 81 | ||
| 82 | //// Expect a sequence. | |
| 83 | pub fn sequence(self: *Decoder) !Element { | |
| 84 | return try self.element(ExpectedTag.init(.sequence, true, .universal)); | |
| 85 | } | |
| 86 | ||
| 87 | //// Expect an element. | |
| 88 | pub fn element( | |
| 89 | self: *Decoder, | |
| 90 | expected: ExpectedTag, | |
| 91 | ) (error{ EndOfStream, UnexpectedElement } || Element.DecodeError)!Element { | |
| 92 | if (self.index >= self.bytes.len) return error.EndOfStream; | |
| 93 | ||
| 94 | const res = try Element.decode(self.bytes, self.index); | |
| 95 | var e = expected; | |
| 96 | if (self.field_tag) |ft| { | |
| 97 | e.number = @enumFromInt(ft.number); | |
| 98 | e.class = ft.class; | |
| 99 | } | |
| 100 | if (!e.match(res.tag)) { | |
| 101 | return error.UnexpectedElement; | |
| 102 | } | |
| 103 | ||
| 104 | self.index = if (res.tag.constructed) res.slice.start else res.slice.end; | |
| 105 | return res; | |
| 106 | } | |
| 107 | ||
| 108 | /// View of element bytes. | |
| 109 | pub fn view(self: Decoder, elem: Element) []const u8 { | |
| 110 | return elem.slice.view(self.bytes); | |
| 111 | } | |
| 112 | ||
| 113 | fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T { | |
| 114 | if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned"); | |
| 115 | ||
| 116 | var bytes = value; | |
| 117 | if (bytes.len >= 2) { | |
| 118 | if (bytes[0] == 0) { | |
| 119 | if (@clz(bytes[1]) > 0) return error.NonCanonical; | |
| 120 | bytes.ptr += 1; | |
| 121 | } | |
| 122 | if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical; | |
| 123 | } | |
| 124 | ||
| 125 | if (bytes.len > @sizeOf(T)) return error.LargeValue; | |
| 126 | if (@sizeOf(T) == 1) return @bitCast(bytes[0]); | |
| 127 | ||
| 128 | return std.mem.readVarInt(T, bytes, .big); | |
| 129 | } | |
| 130 | ||
| 131 | test int { | |
| 132 | try expectEqual(@as(u8, 1), try int(u8, &[_]u8{1})); | |
| 133 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0, 1 })); | |
| 134 | try expectError(error.NonCanonical, int(u8, &[_]u8{ 0xff, 0xff })); | |
| 135 | ||
| 136 | const big = [_]u8{ 0xef, 0xff }; | |
| 137 | try expectError(error.LargeValue, int(u8, &big)); | |
| 138 | try expectEqual(0xefff, int(u16, &big)); | |
| 139 | } | |
| 140 | ||
| 141 | test Decoder { | |
| 142 | var parser = Decoder{ .bytes = @embedFile("./testdata/id_ecc.pub.der") }; | |
| 143 | const seq = try parser.sequence(); | |
| 144 | ||
| 145 | { | |
| 146 | const seq2 = try parser.sequence(); | |
| 147 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 148 | _ = try parser.element(ExpectedTag.init(.oid, false, .universal)); | |
| 149 | ||
| 150 | try std.testing.expectEqual(parser.index, seq2.slice.end); | |
| 151 | } | |
| 152 | _ = try parser.element(ExpectedTag.init(.bitstring, false, .universal)); | |
| 153 | ||
| 154 | try std.testing.expectEqual(parser.index, seq.slice.end); | |
| 155 | try std.testing.expectEqual(parser.index, parser.bytes.len); | |
| 156 | } | |
| 157 | ||
| 158 | const std = @import("std"); | |
| 159 | const builtin = @import("builtin"); | |
| 160 | const asn1 = @import("../../asn1.zig"); | |
| 161 | const Oid = @import("../Oid.zig"); | |
| 162 | ||
| 163 | const expectEqual = std.testing.expectEqual; | |
| 164 | const expectError = std.testing.expectError; | |
| 165 | const Decoder = @This(); | |
| 166 | const Index = asn1.Index; | |
| 167 | const Tag = asn1.Tag; | |
| 168 | const FieldTag = asn1.FieldTag; | |
| 169 | const ExpectedTag = asn1.ExpectedTag; | |
| 170 | const Element = asn1.Element; |
lib/std/crypto/codecs/asn1/der/Encoder.zig created+166| ... | ... | @@ -0,0 +1,166 @@ |
| 1 | //! A buffered DER encoder. | |
| 2 | //! | |
| 3 | //! Prefers calling container's `fn encodeDer(self: @This(), encoder: *der.Encoder)`. | |
| 4 | //! That function should encode values, lengths, then tags. | |
| 5 | buffer: ArrayListReverse, | |
| 6 | /// The field tag set by a parent container. | |
| 7 | /// This is needed because we might visit an implicitly tagged container with a `fn encodeDer`. | |
| 8 | field_tag: ?FieldTag = null, | |
| 9 | ||
| 10 | pub fn init(allocator: std.mem.Allocator) Encoder { | |
| 11 | return Encoder{ .buffer = ArrayListReverse.init(allocator) }; | |
| 12 | } | |
| 13 | ||
| 14 | pub fn deinit(self: *Encoder) void { | |
| 15 | self.buffer.deinit(); | |
| 16 | } | |
| 17 | ||
| 18 | /// Encode any value. | |
| 19 | pub fn any(self: *Encoder, val: anytype) !void { | |
| 20 | const T = @TypeOf(val); | |
| 21 | try self.anyTag(Tag.fromZig(T), val); | |
| 22 | } | |
| 23 | ||
| 24 | fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { | |
| 25 | const T = @TypeOf(val); | |
| 26 | if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); | |
| 27 | const start = self.buffer.data.len; | |
| 28 | const merged_tag = self.mergedTag(tag_); | |
| 29 | ||
| 30 | switch (@typeInfo(T)) { | |
| 31 | .@"struct" => |info| { | |
| 32 | inline for (0..info.fields.len) |i| { | |
| 33 | const f = info.fields[info.fields.len - i - 1]; | |
| 34 | const field_val = @field(val, f.name); | |
| 35 | const field_tag = FieldTag.fromContainer(T, f.name); | |
| 36 | ||
| 37 | // > The encoding of a set value or sequence value shall not include an encoding for any | |
| 38 | // > component value which is equal to its default value. | |
| 39 | const is_default = if (f.is_comptime) false else if (f.default_value_ptr) |v| brk: { | |
| 40 | const default_val: *const f.type = @alignCast(@ptrCast(v)); | |
| 41 | break :brk std.mem.eql(u8, std.mem.asBytes(default_val), std.mem.asBytes(&field_val)); | |
| 42 | } else false; | |
| 43 | ||
| 44 | if (!is_default) { | |
| 45 | const start2 = self.buffer.data.len; | |
| 46 | self.field_tag = field_tag; | |
| 47 | // will merge with self.field_tag. | |
| 48 | // may mutate self.field_tag. | |
| 49 | try self.anyTag(Tag.fromZig(f.type), field_val); | |
| 50 | if (field_tag) |ft| { | |
| 51 | if (ft.explicit) { | |
| 52 | try self.length(self.buffer.data.len - start2); | |
| 53 | try self.tag(ft.toTag()); | |
| 54 | self.field_tag = null; | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | }, | |
| 60 | .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), | |
| 61 | .int => try self.int(T, val), | |
| 62 | .@"enum" => |e| { | |
| 63 | if (@hasDecl(T, "oids")) { | |
| 64 | return self.any(T.oids.enumToOid(val)); | |
| 65 | } else { | |
| 66 | try self.int(e.tag_type, @intFromEnum(val)); | |
| 67 | } | |
| 68 | }, | |
| 69 | .optional => if (val) |v| return try self.anyTag(tag_, v), | |
| 70 | .null => {}, | |
| 71 | else => @compileError("cannot encode type " ++ @typeName(T)), | |
| 72 | } | |
| 73 | ||
| 74 | try self.length(self.buffer.data.len - start); | |
| 75 | try self.tag(merged_tag); | |
| 76 | } | |
| 77 | ||
| 78 | /// Encode a tag. | |
| 79 | pub fn tag(self: *Encoder, tag_: Tag) !void { | |
| 80 | const t = self.mergedTag(tag_); | |
| 81 | try t.encode(self.writer()); | |
| 82 | } | |
| 83 | ||
| 84 | fn mergedTag(self: *Encoder, tag_: Tag) Tag { | |
| 85 | var res = tag_; | |
| 86 | if (self.field_tag) |ft| { | |
| 87 | if (!ft.explicit) { | |
| 88 | res.number = @enumFromInt(ft.number); | |
| 89 | res.class = ft.class; | |
| 90 | } | |
| 91 | } | |
| 92 | return res; | |
| 93 | } | |
| 94 | ||
| 95 | /// Encode a length. | |
| 96 | pub fn length(self: *Encoder, len: usize) !void { | |
| 97 | const writer_ = self.writer(); | |
| 98 | if (len < 128) { | |
| 99 | try writer_.writeInt(u8, @intCast(len), .big); | |
| 100 | return; | |
| 101 | } | |
| 102 | inline for ([_]type{ u8, u16, u32 }) |T| { | |
| 103 | if (len < std.math.maxInt(T)) { | |
| 104 | try writer_.writeInt(T, @intCast(len), .big); | |
| 105 | try writer_.writeInt(u8, @sizeOf(T) | 0x80, .big); | |
| 106 | return; | |
| 107 | } | |
| 108 | } | |
| 109 | return error.InvalidLength; | |
| 110 | } | |
| 111 | ||
| 112 | /// Encode a tag and length-prefixed bytes. | |
| 113 | pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { | |
| 114 | try self.buffer.prependSlice(bytes); | |
| 115 | try self.length(bytes.len); | |
| 116 | try self.tag(tag_); | |
| 117 | } | |
| 118 | ||
| 119 | /// Warning: This writer writes backwards. `fn print` will NOT work as expected. | |
| 120 | pub fn writer(self: *Encoder) ArrayListReverse.Writer { | |
| 121 | return self.buffer.writer(); | |
| 122 | } | |
| 123 | ||
| 124 | fn int(self: *Encoder, comptime T: type, value: T) !void { | |
| 125 | const big = std.mem.nativeTo(T, value, .big); | |
| 126 | const big_bytes = std.mem.asBytes(&big); | |
| 127 | ||
| 128 | const bits_needed = @bitSizeOf(T) - @clz(value); | |
| 129 | const needs_padding: u1 = if (value == 0) | |
| 130 | 1 | |
| 131 | else if (bits_needed > 8) brk: { | |
| 132 | const RightShift = std.meta.Int(.unsigned, @bitSizeOf(@TypeOf(bits_needed)) - 1); | |
| 133 | const right_shift: RightShift = @intCast(bits_needed - 9); | |
| 134 | break :brk if (value >> right_shift == 0x1ff) 1 else 0; | |
| 135 | } else 0; | |
| 136 | const bytes_needed = try std.math.divCeil(usize, bits_needed, 8) + needs_padding; | |
| 137 | ||
| 138 | const writer_ = self.writer(); | |
| 139 | for (0..bytes_needed - needs_padding) |i| try writer_.writeByte(big_bytes[big_bytes.len - i - 1]); | |
| 140 | if (needs_padding == 1) try writer_.writeByte(0); | |
| 141 | } | |
| 142 | ||
| 143 | test int { | |
| 144 | const allocator = std.testing.allocator; | |
| 145 | var encoder = Encoder.init(allocator); | |
| 146 | defer encoder.deinit(); | |
| 147 | ||
| 148 | try encoder.int(u8, 0); | |
| 149 | try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); | |
| 150 | ||
| 151 | encoder.buffer.clearAndFree(); | |
| 152 | try encoder.int(u16, 0x00ff); | |
| 153 | try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); | |
| 154 | ||
| 155 | encoder.buffer.clearAndFree(); | |
| 156 | try encoder.int(u32, 0xffff); | |
| 157 | try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); | |
| 158 | } | |
| 159 | ||
| 160 | const std = @import("std"); | |
| 161 | const Oid = @import("../Oid.zig"); | |
| 162 | const asn1 = @import("../../asn1.zig"); | |
| 163 | const ArrayListReverse = @import("./ArrayListReverse.zig"); | |
| 164 | const Tag = asn1.Tag; | |
| 165 | const FieldTag = asn1.FieldTag; | |
| 166 | const Encoder = @This(); |
lib/std/crypto/codecs/asn1/der/testdata/all_types.der created| Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/all_types.der differ |
lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der created| Binary files /dev/null and b/lib/std/crypto/codecs/asn1/der/testdata/id_ecc.pub.der differ |
lib/std/crypto/codecs/asn1/test.zig created+80| ... | ... | @@ -0,0 +1,80 @@ |
| 1 | const std = @import("std"); | |
| 2 | const asn1 = @import("../asn1.zig"); | |
| 3 | ||
| 4 | const der = asn1.der; | |
| 5 | const Tag = asn1.Tag; | |
| 6 | const FieldTag = asn1.FieldTag; | |
| 7 | ||
| 8 | /// An example that uses all ASN1 types and available implementation features. | |
| 9 | const AllTypes = struct { | |
| 10 | a: u8 = 0, | |
| 11 | b: asn1.BitString, | |
| 12 | c: C, | |
| 13 | d: asn1.Opaque(Tag.universal(.string_utf8, false)), | |
| 14 | e: asn1.Opaque(Tag.universal(.octetstring, false)), | |
| 15 | f: ?u16, | |
| 16 | g: ?Nested, | |
| 17 | h: asn1.Any, | |
| 18 | ||
| 19 | pub const asn1_tags = .{ | |
| 20 | .a = FieldTag.initExplicit(0, .context_specific), | |
| 21 | .b = FieldTag.initExplicit(1, .context_specific), | |
| 22 | .c = FieldTag.initImplicit(2, .context_specific), | |
| 23 | .g = FieldTag.initImplicit(3, .context_specific), | |
| 24 | }; | |
| 25 | ||
| 26 | const C = enum { | |
| 27 | a, | |
| 28 | b, | |
| 29 | ||
| 30 | pub const oids = asn1.Oid.StaticMap(@This()).initComptime(.{ | |
| 31 | .a = "1.2.3.4", | |
| 32 | .b = "1.2.3.5", | |
| 33 | }); | |
| 34 | }; | |
| 35 | ||
| 36 | const Nested = struct { | |
| 37 | inner: Asn1T, | |
| 38 | sum: i16, | |
| 39 | ||
| 40 | const Asn1T = struct { a: u8, b: i16 }; | |
| 41 | ||
| 42 | pub fn decodeDer(decoder: *der.Decoder) !Nested { | |
| 43 | const inner = try decoder.any(Asn1T); | |
| 44 | return Nested{ .inner = inner, .sum = inner.a + inner.b }; | |
| 45 | } | |
| 46 | ||
| 47 | pub fn encodeDer(self: Nested, encoder: *der.Encoder) !void { | |
| 48 | try encoder.any(self.inner); | |
| 49 | } | |
| 50 | }; | |
| 51 | }; | |
| 52 | ||
| 53 | test AllTypes { | |
| 54 | const expected = AllTypes{ | |
| 55 | .a = 2, | |
| 56 | .b = asn1.BitString{ .bytes = &[_]u8{ 0x04, 0xa0 } }, | |
| 57 | .c = .a, | |
| 58 | .d = .{ .bytes = "asdf" }, | |
| 59 | .e = .{ .bytes = "fdsa" }, | |
| 60 | .f = (1 << 8) + 1, | |
| 61 | .g = .{ .inner = .{ .a = 4, .b = 5 }, .sum = 9 }, | |
| 62 | .h = .{ .tag = Tag.init(.string_ia5, false, .universal), .bytes = "asdf" }, | |
| 63 | }; | |
| 64 | // https://lapo.it/asn1js/#MC-gAwIBAqEFAwMABKCCAyoDBAwEYXNkZgQEZmRzYQICAQGjBgIBBAIBBRYEYXNkZg | |
| 65 | const path = "./der/testdata/all_types.der"; | |
| 66 | const encoded = @embedFile(path); | |
| 67 | const actual = try asn1.der.decode(AllTypes, encoded); | |
| 68 | try std.testing.expectEqualDeep(expected, actual); | |
| 69 | ||
| 70 | const allocator = std.testing.allocator; | |
| 71 | const buf = try asn1.der.encode(allocator, expected); | |
| 72 | defer allocator.free(buf); | |
| 73 | try std.testing.expectEqualSlices(u8, encoded, buf); | |
| 74 | ||
| 75 | // Use this to update test file. | |
| 76 | // const dir = try std.fs.cwd().openDir("lib/std/crypto/asn1", .{}); | |
| 77 | // var file = try dir.createFile(path, .{}); | |
| 78 | // defer file.close(); | |
| 79 | // try file.writeAll(buf); | |
| 80 | } |
lib/std/crypto/codecs/base64_hex_ct.zig created+463| ... | ... | @@ -0,0 +1,463 @@ |
| 1 | //! Hexadecimal and Base64 codecs designed for cryptographic use. | |
| 2 | //! This file provides (best-effort) constant-time encoding and decoding functions for hexadecimal and Base64 formats. | |
| 3 | //! This is designed to be used in cryptographic applications where timing attacks are a concern. | |
| 4 | const std = @import("std"); | |
| 5 | const testing = std.testing; | |
| 6 | const StaticBitSet = std.StaticBitSet; | |
| 7 | ||
| 8 | pub const Error = error{ | |
| 9 | /// An invalid character was found in the input. | |
| 10 | InvalidCharacter, | |
| 11 | /// The input is not properly padded. | |
| 12 | InvalidPadding, | |
| 13 | /// The input buffer is too small to hold the output. | |
| 14 | NoSpaceLeft, | |
| 15 | /// The input and output buffers are not the same size. | |
| 16 | SizeMismatch, | |
| 17 | }; | |
| 18 | ||
| 19 | /// (best-effort) constant time hexadecimal encoding and decoding. | |
| 20 | pub const hex = struct { | |
| 21 | /// Encodes a binary buffer into a hexadecimal string. | |
| 22 | /// The output buffer must be twice the size of the input buffer. | |
| 23 | pub fn encode(encoded: []u8, bin: []const u8, comptime case: std.fmt.Case) error{SizeMismatch}!void { | |
| 24 | if (encoded.len / 2 != bin.len) { | |
| 25 | return error.SizeMismatch; | |
| 26 | } | |
| 27 | for (bin, 0..) |v, i| { | |
| 28 | const b: u16 = v >> 4; | |
| 29 | const c: u16 = v & 0xf; | |
| 30 | const off = if (case == .upper) 32 else 0; | |
| 31 | const x = | |
| 32 | ((87 - off + c + (((c -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff) << 8 | | |
| 33 | ((87 - off + b + (((b -% 10) >> 8) & ~@as(u16, 38 - off))) & 0xff); | |
| 34 | encoded[i * 2] = @truncate(x); | |
| 35 | encoded[i * 2 + 1] = @truncate(x >> 8); | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | /// Decodes a hexadecimal string into a binary buffer. | |
| 40 | /// The output buffer must be half the size of the input buffer. | |
| 41 | pub fn decode(bin: []u8, encoded: []const u8) error{ SizeMismatch, InvalidCharacter, InvalidPadding }!void { | |
| 42 | if (encoded.len % 2 != 0) { | |
| 43 | return error.InvalidPadding; | |
| 44 | } | |
| 45 | if (bin.len < encoded.len / 2) { | |
| 46 | return error.SizeMismatch; | |
| 47 | } | |
| 48 | _ = decodeAny(bin, encoded, null) catch |err| { | |
| 49 | switch (err) { | |
| 50 | error.InvalidCharacter => return error.InvalidCharacter, | |
| 51 | error.InvalidPadding => return error.InvalidPadding, | |
| 52 | else => unreachable, | |
| 53 | } | |
| 54 | }; | |
| 55 | } | |
| 56 | ||
| 57 | /// A decoder that ignores certain characters. | |
| 58 | /// The decoder will skip any characters that are in the ignore list. | |
| 59 | pub const DecoderWithIgnore = struct { | |
| 60 | /// The characters to ignore. | |
| 61 | ignored_chars: StaticBitSet(256) = undefined, | |
| 62 | ||
| 63 | /// Decodes a hexadecimal string into a binary buffer. | |
| 64 | /// The output buffer must be half the size of the input buffer. | |
| 65 | pub fn decode( | |
| 66 | self: DecoderWithIgnore, | |
| 67 | bin: []u8, | |
| 68 | encoded: []const u8, | |
| 69 | ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { | |
| 70 | return decodeAny(bin, encoded, self.ignored_chars); | |
| 71 | } | |
| 72 | ||
| 73 | /// Returns the decoded length of a hexadecimal string, ignoring any characters in the ignore list. | |
| 74 | /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying hexadecimal string. | |
| 75 | pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8) !usize { | |
| 76 | var hex_len = encoded.len; | |
| 77 | for (encoded) |c| { | |
| 78 | if (decoder.ignored_chars.isSet(c)) hex_len -= 1; | |
| 79 | } | |
| 80 | if (hex_len % 2 != 0) { | |
| 81 | return error.InvalidPadding; | |
| 82 | } | |
| 83 | return hex_len / 2; | |
| 84 | } | |
| 85 | ||
| 86 | /// Returns the maximum possible decoded size for a given input length after skipping ignored characters. | |
| 87 | pub fn decodedLenUpperBound(hex_len: usize) usize { | |
| 88 | return hex_len / 2; | |
| 89 | } | |
| 90 | }; | |
| 91 | ||
| 92 | /// Creates a new decoder that ignores certain characters. | |
| 93 | /// The decoder will skip any characters that are in the ignore list. | |
| 94 | /// The ignore list must not contain any valid hexadecimal characters. | |
| 95 | pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore { | |
| 96 | var ignored_chars = StaticBitSet(256).initEmpty(); | |
| 97 | for (ignore_chars) |c| { | |
| 98 | switch (c) { | |
| 99 | '0'...'9', 'a'...'f', 'A'...'F' => return error.InvalidCharacter, | |
| 100 | else => if (ignored_chars.isSet(c)) return error.InvalidCharacter, | |
| 101 | } | |
| 102 | ignored_chars.set(c); | |
| 103 | } | |
| 104 | return DecoderWithIgnore{ .ignored_chars = ignored_chars }; | |
| 105 | } | |
| 106 | ||
| 107 | fn decodeAny( | |
| 108 | bin: []u8, | |
| 109 | encoded: []const u8, | |
| 110 | ignored_chars: ?StaticBitSet(256), | |
| 111 | ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { | |
| 112 | var bin_pos: usize = 0; | |
| 113 | var state: bool = false; | |
| 114 | var c_acc: u8 = 0; | |
| 115 | for (encoded) |c| { | |
| 116 | const c_num = c ^ 48; | |
| 117 | const c_num0: u8 = @truncate((@as(u16, c_num) -% 10) >> 8); | |
| 118 | const c_alpha: u8 = (c & ~@as(u8, 32)) -% 55; | |
| 119 | const c_alpha0: u8 = @truncate(((@as(u16, c_alpha) -% 10) ^ (@as(u16, c_alpha) -% 16)) >> 8); | |
| 120 | if ((c_num0 | c_alpha0) == 0) { | |
| 121 | if (ignored_chars) |set| { | |
| 122 | if (set.isSet(c)) { | |
| 123 | continue; | |
| 124 | } | |
| 125 | } | |
| 126 | return error.InvalidCharacter; | |
| 127 | } | |
| 128 | const c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha); | |
| 129 | if (bin_pos >= bin.len) { | |
| 130 | return error.NoSpaceLeft; | |
| 131 | } | |
| 132 | if (!state) { | |
| 133 | c_acc = c_val << 4; | |
| 134 | } else { | |
| 135 | bin[bin_pos] = c_acc | c_val; | |
| 136 | bin_pos += 1; | |
| 137 | } | |
| 138 | state = !state; | |
| 139 | } | |
| 140 | if (state) { | |
| 141 | return error.InvalidPadding; | |
| 142 | } | |
| 143 | return bin[0..bin_pos]; | |
| 144 | } | |
| 145 | }; | |
| 146 | ||
| 147 | /// (best-effort) constant time base64 encoding and decoding. | |
| 148 | pub const base64 = struct { | |
| 149 | /// The base64 variant to use. | |
| 150 | pub const Variant = packed struct { | |
| 151 | /// Use the URL-safe alphabet instead of the standard alphabet. | |
| 152 | urlsafe_alphabet: bool = false, | |
| 153 | /// Enable padding with '=' characters. | |
| 154 | padding: bool = true, | |
| 155 | ||
| 156 | /// The standard base64 variant. | |
| 157 | pub const standard: Variant = .{ .urlsafe_alphabet = false, .padding = true }; | |
| 158 | /// The URL-safe base64 variant. | |
| 159 | pub const urlsafe: Variant = .{ .urlsafe_alphabet = true, .padding = true }; | |
| 160 | /// The standard base64 variant without padding. | |
| 161 | pub const standard_nopad: Variant = .{ .urlsafe_alphabet = false, .padding = false }; | |
| 162 | /// The URL-safe base64 variant without padding. | |
| 163 | pub const urlsafe_nopad: Variant = .{ .urlsafe_alphabet = true, .padding = false }; | |
| 164 | }; | |
| 165 | ||
| 166 | /// Returns the length of the encoded base64 string for a given length. | |
| 167 | pub fn encodedLen(bin_len: usize, variant: Variant) usize { | |
| 168 | if (variant.padding) { | |
| 169 | return (bin_len + 2) / 3 * 4; | |
| 170 | } else { | |
| 171 | const leftover = bin_len % 3; | |
| 172 | return bin_len / 3 * 4 + (leftover * 4 + 2) / 3; | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | /// Returns the maximum possible decoded size for a given input length - The actual length may be less if the input includes padding. | |
| 177 | /// `InvalidPadding` is returned if the input length is not valid. | |
| 178 | pub fn decodedLen(b64_len: usize, variant: Variant) !usize { | |
| 179 | var result = b64_len / 4 * 3; | |
| 180 | const leftover = b64_len % 4; | |
| 181 | if (variant.padding) { | |
| 182 | if (leftover % 4 != 0) return error.InvalidPadding; | |
| 183 | } else { | |
| 184 | if (leftover % 4 == 1) return error.InvalidPadding; | |
| 185 | result += leftover * 3 / 4; | |
| 186 | } | |
| 187 | return result; | |
| 188 | } | |
| 189 | ||
| 190 | /// Encodes a binary buffer into a base64 string. | |
| 191 | /// The output buffer must be at least `encodedLen(bin.len)` bytes long. | |
| 192 | pub fn encode(encoded: []u8, bin: []const u8, comptime variant: Variant) error{NoSpaceLeft}![]const u8 { | |
| 193 | var acc_len: u4 = 0; | |
| 194 | var b64_pos: usize = 0; | |
| 195 | var acc: u16 = 0; | |
| 196 | const nibbles = bin.len / 3; | |
| 197 | const remainder = bin.len - 3 * nibbles; | |
| 198 | var b64_len = nibbles * 4; | |
| 199 | if (remainder != 0) { | |
| 200 | b64_len += if (variant.padding) 4 else 2 + (remainder >> 1); | |
| 201 | } | |
| 202 | if (encoded.len < b64_len) { | |
| 203 | return error.NoSpaceLeft; | |
| 204 | } | |
| 205 | const urlsafe = variant.urlsafe_alphabet; | |
| 206 | for (bin) |v| { | |
| 207 | acc = (acc << 8) + v; | |
| 208 | acc_len += 8; | |
| 209 | while (acc_len >= 6) { | |
| 210 | acc_len -= 6; | |
| 211 | encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc >> acc_len)), urlsafe); | |
| 212 | b64_pos += 1; | |
| 213 | } | |
| 214 | } | |
| 215 | if (acc_len > 0) { | |
| 216 | encoded[b64_pos] = charFromByte(@as(u6, @truncate(acc << (6 - acc_len))), urlsafe); | |
| 217 | b64_pos += 1; | |
| 218 | } | |
| 219 | while (b64_pos < b64_len) { | |
| 220 | encoded[b64_pos] = '='; | |
| 221 | b64_pos += 1; | |
| 222 | } | |
| 223 | return encoded[0..b64_pos]; | |
| 224 | } | |
| 225 | ||
| 226 | /// Decodes a base64 string into a binary buffer. | |
| 227 | /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long. | |
| 228 | pub fn decode(bin: []u8, encoded: []const u8, comptime variant: Variant) error{ InvalidCharacter, InvalidPadding }![]const u8 { | |
| 229 | return decodeAny(bin, encoded, variant, null) catch |err| { | |
| 230 | switch (err) { | |
| 231 | error.InvalidCharacter => return error.InvalidCharacter, | |
| 232 | error.InvalidPadding => return error.InvalidPadding, | |
| 233 | else => unreachable, | |
| 234 | } | |
| 235 | }; | |
| 236 | } | |
| 237 | ||
| 238 | //// A decoder that ignores certain characters. | |
| 239 | pub const DecoderWithIgnore = struct { | |
| 240 | /// The characters to ignore. | |
| 241 | ignored_chars: StaticBitSet(256) = undefined, | |
| 242 | ||
| 243 | /// Decodes a base64 string into a binary buffer. | |
| 244 | /// The output buffer must be at least `decodedLenUpperBound(encoded.len)` bytes long. | |
| 245 | pub fn decode( | |
| 246 | self: DecoderWithIgnore, | |
| 247 | bin: []u8, | |
| 248 | encoded: []const u8, | |
| 249 | comptime variant: Variant, | |
| 250 | ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { | |
| 251 | return decodeAny(bin, encoded, variant, self.ignored_chars); | |
| 252 | } | |
| 253 | ||
| 254 | /// Returns the decoded length of a base64 string, ignoring any characters in the ignore list. | |
| 255 | /// This operation does not run in constant time, but it aims to avoid leaking information about the underlying base64 string. | |
| 256 | pub fn decodedLenForSlice(decoder: DecoderWithIgnore, encoded: []const u8, variant: Variant) !usize { | |
| 257 | var b64_len = encoded.len; | |
| 258 | for (encoded) |c| { | |
| 259 | if (decoder.ignored_chars.isSet(c)) b64_len -= 1; | |
| 260 | } | |
| 261 | return base64.decodedLen(b64_len, variant); | |
| 262 | } | |
| 263 | ||
| 264 | /// Returns the maximum possible decoded size for a given input length after skipping ignored characters. | |
| 265 | pub fn decodedLenUpperBound(b64_len: usize) usize { | |
| 266 | return b64_len / 3 * 4; | |
| 267 | } | |
| 268 | }; | |
| 269 | ||
| 270 | /// Creates a new decoder that ignores certain characters. | |
| 271 | pub fn decoderWithIgnore(ignore_chars: []const u8) error{InvalidCharacter}!DecoderWithIgnore { | |
| 272 | var ignored_chars = StaticBitSet(256).initEmpty(); | |
| 273 | for (ignore_chars) |c| { | |
| 274 | switch (c) { | |
| 275 | 'A'...'Z', 'a'...'z', '0'...'9' => return error.InvalidCharacter, | |
| 276 | else => if (ignored_chars.isSet(c)) return error.InvalidCharacter, | |
| 277 | } | |
| 278 | ignored_chars.set(c); | |
| 279 | } | |
| 280 | return DecoderWithIgnore{ .ignored_chars = ignored_chars }; | |
| 281 | } | |
| 282 | ||
| 283 | inline fn eq(x: u8, y: u8) u8 { | |
| 284 | return ~@as(u8, @truncate((0 -% (@as(u16, x) ^ @as(u16, y))) >> 8)); | |
| 285 | } | |
| 286 | ||
| 287 | inline fn gt(x: u8, y: u8) u8 { | |
| 288 | return @truncate((@as(u16, y) -% @as(u16, x)) >> 8); | |
| 289 | } | |
| 290 | ||
| 291 | inline fn ge(x: u8, y: u8) u8 { | |
| 292 | return ~gt(y, x); | |
| 293 | } | |
| 294 | ||
| 295 | inline fn lt(x: u8, y: u8) u8 { | |
| 296 | return gt(y, x); | |
| 297 | } | |
| 298 | ||
| 299 | inline fn le(x: u8, y: u8) u8 { | |
| 300 | return ge(y, x); | |
| 301 | } | |
| 302 | ||
| 303 | inline fn charFromByte(x: u8, comptime urlsafe: bool) u8 { | |
| 304 | return (lt(x, 26) & (x +% 'A')) | | |
| 305 | (ge(x, 26) & lt(x, 52) & (x +% 'a' -% 26)) | | |
| 306 | (ge(x, 52) & lt(x, 62) & (x +% '0' -% 52)) | | |
| 307 | (eq(x, 62) & '+') | (eq(x, 63) & if (urlsafe) '_' else '/'); | |
| 308 | } | |
| 309 | ||
| 310 | inline fn byteFromChar(c: u8, comptime urlsafe: bool) u8 { | |
| 311 | const x = | |
| 312 | (ge(c, 'A') & le(c, 'Z') & (c -% 'A')) | | |
| 313 | (ge(c, 'a') & le(c, 'z') & (c -% 'a' +% 26)) | | |
| 314 | (ge(c, '0') & le(c, '9') & (c -% '0' +% 52)) | | |
| 315 | (eq(c, '+') & 62) | (eq(c, if (urlsafe) '_' else '/') & 63); | |
| 316 | return x | (eq(x, 0) & ~eq(c, 'A')); | |
| 317 | } | |
| 318 | ||
| 319 | fn skipPadding( | |
| 320 | encoded: []const u8, | |
| 321 | padding_len: usize, | |
| 322 | ignored_chars: ?StaticBitSet(256), | |
| 323 | ) error{InvalidPadding}![]const u8 { | |
| 324 | var b64_pos: usize = 0; | |
| 325 | var i = padding_len; | |
| 326 | while (i > 0) { | |
| 327 | if (b64_pos >= encoded.len) { | |
| 328 | return error.InvalidPadding; | |
| 329 | } | |
| 330 | const c = encoded[b64_pos]; | |
| 331 | if (c == '=') { | |
| 332 | i -= 1; | |
| 333 | } else if (ignored_chars) |set| { | |
| 334 | if (!set.isSet(c)) { | |
| 335 | return error.InvalidPadding; | |
| 336 | } | |
| 337 | } | |
| 338 | b64_pos += 1; | |
| 339 | } | |
| 340 | return encoded[b64_pos..]; | |
| 341 | } | |
| 342 | ||
| 343 | fn decodeAny( | |
| 344 | bin: []u8, | |
| 345 | encoded: []const u8, | |
| 346 | comptime variant: Variant, | |
| 347 | ignored_chars: ?StaticBitSet(256), | |
| 348 | ) error{ NoSpaceLeft, InvalidCharacter, InvalidPadding }![]const u8 { | |
| 349 | var acc: u16 = 0; | |
| 350 | var acc_len: u4 = 0; | |
| 351 | var bin_pos: usize = 0; | |
| 352 | var premature_end: ?usize = null; | |
| 353 | const urlsafe = variant.urlsafe_alphabet; | |
| 354 | for (encoded, 0..) |c, b64_pos| { | |
| 355 | const d = byteFromChar(c, urlsafe); | |
| 356 | if (d == 0xff) { | |
| 357 | if (ignored_chars) |set| { | |
| 358 | if (set.isSet(c)) continue; | |
| 359 | } | |
| 360 | premature_end = b64_pos; | |
| 361 | break; | |
| 362 | } | |
| 363 | acc = (acc << 6) + d; | |
| 364 | acc_len += 6; | |
| 365 | if (acc_len >= 8) { | |
| 366 | acc_len -= 8; | |
| 367 | if (bin_pos >= bin.len) { | |
| 368 | return error.NoSpaceLeft; | |
| 369 | } | |
| 370 | bin[bin_pos] = @truncate(acc >> acc_len); | |
| 371 | bin_pos += 1; | |
| 372 | } | |
| 373 | } | |
| 374 | if (acc_len > 4 or (acc & ((@as(u16, 1) << acc_len) -% 1)) != 0) { | |
| 375 | return error.InvalidCharacter; | |
| 376 | } | |
| 377 | const padding_len = acc_len / 2; | |
| 378 | if (premature_end) |pos| { | |
| 379 | const remaining = | |
| 380 | if (variant.padding) | |
| 381 | try skipPadding(encoded[pos..], padding_len, ignored_chars) | |
| 382 | else | |
| 383 | encoded[pos..]; | |
| 384 | if (ignored_chars) |set| { | |
| 385 | for (remaining) |c| { | |
| 386 | if (!set.isSet(c)) { | |
| 387 | return error.InvalidCharacter; | |
| 388 | } | |
| 389 | } | |
| 390 | } else if (remaining.len != 0) { | |
| 391 | return error.InvalidCharacter; | |
| 392 | } | |
| 393 | } else if (variant.padding and padding_len != 0) { | |
| 394 | return error.InvalidPadding; | |
| 395 | } | |
| 396 | return bin[0..bin_pos]; | |
| 397 | } | |
| 398 | }; | |
| 399 | ||
| 400 | test "hex" { | |
| 401 | var default_rng = std.Random.DefaultPrng.init(testing.random_seed); | |
| 402 | var rng = default_rng.random(); | |
| 403 | var bin_buf: [1000]u8 = undefined; | |
| 404 | rng.bytes(&bin_buf); | |
| 405 | var bin2_buf: [bin_buf.len]u8 = undefined; | |
| 406 | var hex_buf: [bin_buf.len * 2]u8 = undefined; | |
| 407 | for (0..1000) |_| { | |
| 408 | const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len); | |
| 409 | const bin = bin_buf[0..bin_len]; | |
| 410 | const bin2 = bin2_buf[0..bin_len]; | |
| 411 | inline for (.{ .lower, .upper }) |case| { | |
| 412 | const hex_len = bin_len * 2; | |
| 413 | const encoded = hex_buf[0..hex_len]; | |
| 414 | try hex.encode(encoded, bin, case); | |
| 415 | try hex.decode(bin2, encoded); | |
| 416 | try testing.expectEqualSlices(u8, bin, bin2); | |
| 417 | } | |
| 418 | } | |
| 419 | } | |
| 420 | ||
| 421 | test "base64" { | |
| 422 | var default_rng = std.Random.DefaultPrng.init(testing.random_seed); | |
| 423 | var rng = default_rng.random(); | |
| 424 | var bin_buf: [1000]u8 = undefined; | |
| 425 | rng.bytes(&bin_buf); | |
| 426 | var bin2_buf: [bin_buf.len]u8 = undefined; | |
| 427 | var b64_buf: [(bin_buf.len + 3) / 3 * 4]u8 = undefined; | |
| 428 | for (0..1000) |_| { | |
| 429 | const bin_len = rng.intRangeAtMost(usize, 0, bin_buf.len); | |
| 430 | const bin = bin_buf[0..bin_len]; | |
| 431 | const bin2 = bin2_buf[0..bin_len]; | |
| 432 | inline for ([_]base64.Variant{ | |
| 433 | .standard, | |
| 434 | .standard_nopad, | |
| 435 | .urlsafe, | |
| 436 | .urlsafe_nopad, | |
| 437 | }) |variant| { | |
| 438 | const b64_len = base64.encodedLen(bin_len, variant); | |
| 439 | const encoded_buf = b64_buf[0..b64_len]; | |
| 440 | const encoded = try base64.encode(encoded_buf, bin, variant); | |
| 441 | const decoded = try base64.decode(bin2, encoded, variant); | |
| 442 | try testing.expectEqualSlices(u8, bin, decoded); | |
| 443 | } | |
| 444 | } | |
| 445 | } | |
| 446 | ||
| 447 | test "hex with ignored chars" { | |
| 448 | const encoded = "01020304050607\n08090A0B0C0D0E0F\n"; | |
| 449 | const expected = [_]u8{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; | |
| 450 | var bin_buf: [encoded.len / 2]u8 = undefined; | |
| 451 | try testing.expectError(error.InvalidCharacter, hex.decode(&bin_buf, encoded)); | |
| 452 | const bin = try (try hex.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded); | |
| 453 | try testing.expectEqualSlices(u8, &expected, bin); | |
| 454 | } | |
| 455 | ||
| 456 | test "base64 with ignored chars" { | |
| 457 | const encoded = "dGVzdCBi\r\nYXNlNjQ=\n"; | |
| 458 | const expected = "test base64"; | |
| 459 | var bin_buf: [base64.DecoderWithIgnore.decodedLenUpperBound(encoded.len)]u8 = undefined; | |
| 460 | try testing.expectError(error.InvalidCharacter, base64.decode(&bin_buf, encoded, .standard)); | |
| 461 | const bin = try (try base64.decoderWithIgnore("\r\n")).decode(&bin_buf, encoded, .standard); | |
| 462 | try testing.expectEqualSlices(u8, expected, bin); | |
| 463 | } |