diff --git a/build.zig.zon b/build.zig.zon index 5665b22..7894e99 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,5 +1,6 @@ .{ - .name = "jwt", + .name = .jwt, + .fingerprint = 0x8d17cdf07e2c9836, .version = "0.1.0", .minimum_zig_version = "0.13.0", .dependencies = .{ diff --git a/src/decode.zig b/src/decode.zig index aca51a7..198ad7f 100644 --- a/src/decode.zig +++ b/src/decode.zig @@ -65,6 +65,37 @@ pub fn decode( return error.MalformedJWT; } +pub fn decodeNoVerify( + allocator: std.mem.Allocator, + comptime ClaimSet: type, + str: []const u8, +) !JWT(ClaimSet) { + var arena = try allocator.create(std.heap.ArenaAllocator); + arena.* = std.heap.ArenaAllocator.init(allocator); + errdefer { + arena.deinit(); + allocator.destroy(arena); + } + if (std.mem.count(u8, str, ".") == 2) { + const sigSplit = std.mem.lastIndexOfScalar(u8, str, '.').?; + const messageEnc = str[0..sigSplit]; + + const header = try decodePart(arena.allocator(), Header, messageEnc[0..std.mem.indexOfScalar(u8, messageEnc, '.').?]); + const claims = try decodePart( + allocator, + ClaimSet, + messageEnc[std.mem.indexOfScalar(u8, messageEnc, '.').? + 1 ..], + ); + + return .{ + .arena = arena, + .header = header, + .claims = claims, + }; + } + return error.MalformedJWT; +} + pub fn verify( allocator: std.mem.Allocator, algo: Algorithm, @@ -86,6 +117,9 @@ pub fn verify( .secret => |v| v, else => return error.InvalidDecodingKey, }); + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); if (!std.crypto.utils.timingSafeEql([dest.len]u8, src, dest)) { return error.InvalidSignature; @@ -98,6 +132,9 @@ pub fn verify( .secret => |v| v, else => return error.InvalidDecodingKey, }); + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); if (!std.crypto.utils.timingSafeEql([dest.len]u8, src, dest)) { return error.InvalidSignature; @@ -110,6 +147,9 @@ pub fn verify( .secret => |v| v, else => return error.InvalidDecodingKey, }); + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); if (!std.crypto.utils.timingSafeEql([dest.len]u8, src, dest)) { return error.InvalidSignature; @@ -117,6 +157,9 @@ pub fn verify( }, .ES256 => { var src: [std.crypto.sign.ecdsa.EcdsaP256Sha256.Signature.encoded_length]u8 = undefined; + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); std.crypto.sign.ecdsa.EcdsaP256Sha256.Signature.fromBytes(src).verify(msg, switch (key) { .es256 => |v| v, @@ -127,6 +170,9 @@ pub fn verify( }, .ES384 => { var src: [std.crypto.sign.ecdsa.EcdsaP384Sha384.Signature.encoded_length]u8 = undefined; + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); std.crypto.sign.ecdsa.EcdsaP384Sha384.Signature.fromBytes(src).verify(msg, switch (key) { .es384 => |v| v, @@ -147,6 +193,9 @@ pub fn verify( // }, .EdDSA => { var src: [std.crypto.sign.Ed25519.Signature.encoded_length]u8 = undefined; + if (src.len != sig.len) { + return error.InvalidSignature; + } @memcpy(&src, sig); std.crypto.sign.Ed25519.Signature.fromBytes(src).verify(msg, switch (key) { .edsa => |v| v, diff --git a/src/encode.zig b/src/encode.zig index cb542a3..f03c53b 100644 --- a/src/encode.zig +++ b/src/encode.zig @@ -101,7 +101,7 @@ pub fn encode( key: EncodingKey, ) ![]const u8 { comptime { - if (@typeInfo(@TypeOf(claims)) != .Struct) { + if (@typeInfo(@TypeOf(claims)) != .@"struct") { @compileError("expected claims to be a struct but was a " ++ @typeName(@TypeOf(claims))); } } diff --git a/src/root.zig b/src/root.zig index 541feca..3a53fcc 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub const decode = @import("decode.zig").decode; +pub const decodeNoVerify = @import("decode.zig").decodeNoVerify; pub const DecodingKey = @import("decode.zig").DecodingKey; pub const Validation = @import("validation.zig").Validation; pub const encode = @import("encode.zig").encode; @@ -82,133 +83,369 @@ pub fn JWT(comptime ClaimSet: type) type { }; } -test "ES256.roundrip" { - const allocator = std.testing.allocator; - const validation: Validation = .{ - .now = struct { - fn func() u64 { - return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds - } - }.func, - }; +// ES256 tests - // predicable key generation - var seed: [32]u8 = undefined; - _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); - const pair = try std.crypto.sign.ecdsa.EcdsaP256Sha256.KeyPair.create(seed); +const TestES256Data = struct { + allocator: std.mem.Allocator, + token: []const u8, + validation: Validation, + pair: std.crypto.sign.ecdsa.EcdsaP256Sha256.KeyPair, - const token = try encode( - allocator, - .{ .alg = .ES256 }, - .{ .sub = "test", .exp = validation.now() + 60 }, - .{ .es256 = pair.secret_key }, - ); - defer allocator.free(token); - try std.testing.expectEqualStrings("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.0ZiqWyJd3TKN2yB01Xhg91p8qmW-L0XrZunsHwkr2L3D79T45g8Imrqk5V5AhfLbBjqd2NPuZHcChpsSxiGtNw", token); + fn init(allocator: std.mem.Allocator) !TestES256Data { + const validation: Validation = .{ + .now = struct { + fn func() u64 { + return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds + } + }.func, + }; + + // predicable key generation + var seed: [32]u8 = undefined; + _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); + const pair = try std.crypto.sign.ecdsa.EcdsaP256Sha256.KeyPair.generateDeterministic(seed); + + const token = try encode( + allocator, + .{ .alg = .ES256 }, + .{ .sub = "test", .exp = validation.now() + 60 }, + .{ .es256 = pair.secret_key }, + ); + return .{ .token = token, .validation = validation, .pair = pair, .allocator = allocator }; + } + + fn deinit(self: TestES256Data) void { + self.allocator.free(self.token); + } +}; + +test "ES256.roundtrip" { + const allocator = std.testing.allocator; + const data = try TestES256Data.init(allocator); + defer data.deinit(); + + try std.testing.expectEqualStrings("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.0ZiqWyJd3TKN2yB01Xhg91p8qmW-L0XrZunsHwkr2L3D79T45g8Imrqk5V5AhfLbBjqd2NPuZHcChpsSxiGtNw", data.token); var jwt = try decode( allocator, struct { sub: []const u8 }, - token, - .{ .es256 = pair.public_key }, - validation, + data.token, + .{ .es256 = data.pair.public_key }, + data.validation, ); defer jwt.deinit(); try std.testing.expectEqualStrings("test", jwt.claims.sub); } -test "ES384.roundrip" { +test "ES256.roundtrip.invalidsignature" { const allocator = std.testing.allocator; - const validation: Validation = .{ - .now = struct { - fn func() u64 { - return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds - } - }.func, + const data = try TestES256Data.init(allocator); + defer data.deinit(); + + const invalid_token = try std.fmt.allocPrint(allocator, "{s}ab", .{data.token}); + defer allocator.free(invalid_token); + + var jwt = decode( + allocator, + struct { sub: []const u8 }, + invalid_token, + .{ .es256 = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.InvalidSignature); }; + defer jwt.deinit(); - // predicable key generation - var seed: [48]u8 = undefined; - _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); - const pair = try std.crypto.sign.ecdsa.EcdsaP384Sha384.KeyPair.create(seed); + return error.TestUnexpectedResult; +} - const token = try encode( +test "ES256.malformedjwt" { + const allocator = std.testing.allocator; + const data = try TestES256Data.init(allocator); + defer data.deinit(); + + var jwt = decode( allocator, - .{ .alg = .ES384 }, - .{ .sub = "test", .exp = validation.now() + 60 }, - .{ .es384 = pair.secret_key }, - ); - defer allocator.free(token); - //try std.testing.expectEqualStrings("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.0ZiqWyJd3TKN2yB01Xhg91p8qmW-L0XrZunsHwkr2L3D79T45g8Imrqk5V5AhfLbBjqd2NPuZHcChpsSxiGtNw", token); + struct { sub: []const u8 }, + "a", + .{ .es256 = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.MalformedJWT); + }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + +// ES384 tests + +const TestES384Data = struct { + allocator: std.mem.Allocator, + token: []const u8, + validation: Validation, + pair: std.crypto.sign.ecdsa.EcdsaP384Sha384.KeyPair, + + fn init(allocator: std.mem.Allocator) !TestES384Data { + const validation: Validation = .{ + .now = struct { + fn func() u64 { + return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds + } + }.func, + }; + + // predicable key generation + var seed: [48]u8 = undefined; + _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); + const pair = try std.crypto.sign.ecdsa.EcdsaP384Sha384.KeyPair.generateDeterministic(seed); + + const token = try encode( + allocator, + .{ .alg = .ES384 }, + .{ .sub = "test", .exp = validation.now() + 60 }, + .{ .es384 = pair.secret_key }, + ); + return .{ .allocator = allocator, .token = token, .pair = pair, .validation = validation }; + } + + fn deinit(self: TestES384Data) void { + self.allocator.free(self.token); + } +}; + +test "ES384.roundtrip" { + const allocator = std.testing.allocator; + const data = try TestES384Data.init(allocator); + defer data.deinit(); + try std.testing.expectEqualStrings("eyJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.2ZdUkqVjxip97kY-oWdMUXa6ryahNwHUSqWv2iO3k6dtcIHdD3tD3cO2uh8UGFncJBc4n6lCT-F9Q357pazj1uG0Obvr7whnSt_Suc9mP-MwPuCXyQrnb-QGw1lHBKlj", data.token); var jwt = try decode( allocator, struct { sub: []const u8 }, - token, - .{ .es384 = pair.public_key }, - validation, + data.token, + .{ .es384 = data.pair.public_key }, + data.validation, ); defer jwt.deinit(); try std.testing.expectEqualStrings("test", jwt.claims.sub); } -test "EdDSA.roundtrip" { +test "ES384.roundtrip.invalidsignature" { const allocator = std.testing.allocator; - const validation: Validation = .{ - .now = struct { - fn func() u64 { - return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds - } - }.func, + const data = try TestES384Data.init(allocator); + defer data.deinit(); + + const invalid_token = try std.fmt.allocPrint(allocator, "{s}abc", .{data.token}); + defer allocator.free(invalid_token); + + var jwt = decode( + allocator, + struct { sub: []const u8 }, + invalid_token, + .{ .es384 = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.InvalidSignature); }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} - // predicable key generation - var seed: [32]u8 = undefined; - _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); - const pair = try std.crypto.sign.Ed25519.KeyPair.create(seed); +test "ES384.malformedjwt" { + const allocator = std.testing.allocator; + const data = try TestES384Data.init(allocator); + defer data.deinit(); - const token = try encode( + var jwt = decode( allocator, - .{ .alg = .EdDSA }, - .{ .sub = "test", .exp = validation.now() + 60 }, - .{ .edsa = pair.secret_key }, - ); - defer allocator.free(token); - try std.testing.expectEqualStrings("eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.qV1oOiw9DmKfaxVv3_W6zn878ke6D-G70bzAMTtNB4-3dCk5reLaqrXEMluP-0vjgfdQaJc-J0XANMP2CVymDQ", token); + struct { sub: []const u8 }, + "a", + .{ .es384 = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.MalformedJWT); + }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + +// EdDSA tests + +const TestEdDSAData = struct { + token: []const u8, + pair: std.crypto.sign.Ed25519.KeyPair, + validation: Validation, + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) !TestEdDSAData { + const validation: Validation = .{ + .now = struct { + fn func() u64 { + return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds + } + }.func, + }; + + // predicable key generation + var seed: [32]u8 = undefined; + _ = try std.fmt.hexToBytes(seed[0..], "8052030376d47112be7f73ed7a019293dd12ad910b654455798b4667d73de166"); + const pair = try std.crypto.sign.Ed25519.KeyPair.generateDeterministic(seed); + + const token = try encode( + allocator, + .{ .alg = .EdDSA }, + .{ .sub = "test", .exp = validation.now() + 60 }, + .{ .edsa = pair.secret_key }, + ); + return .{ .token = token, .pair = pair, .validation = validation, .allocator = allocator }; + } + + fn deinit(self: TestEdDSAData) void { + self.allocator.free(self.token); + } +}; + +test "EdDSA.roundtrip" { + const allocator = std.testing.allocator; + const data = try TestEdDSAData.init(allocator); + defer data.deinit(); + + try std.testing.expectEqualStrings("eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzIyNDQxMzM0fQ.qV1oOiw9DmKfaxVv3_W6zn878ke6D-G70bzAMTtNB4-3dCk5reLaqrXEMluP-0vjgfdQaJc-J0XANMP2CVymDQ", data.token); var jwt = try decode( allocator, struct { sub: []const u8 }, - token, - .{ .edsa = pair.public_key }, - validation, + data.token, + .{ .edsa = data.pair.public_key }, + data.validation, ); defer jwt.deinit(); try std.testing.expectEqualStrings("test", jwt.claims.sub); } -test "HS256.roundtrip" { +test "EdDSA.roundtrip.invalidsignature" { + const allocator = std.testing.allocator; + const data = try TestEdDSAData.init(allocator); + defer data.deinit(); + + const invalid_token = try std.fmt.allocPrint(allocator, "{s}ab", .{data.token}); + defer allocator.free(invalid_token); + + var jwt = decode( + allocator, + struct { sub: []const u8 }, + invalid_token, + .{ .edsa = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.InvalidSignature); + }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + +test "EdDSA.malformedjwt" { const allocator = std.testing.allocator; - const validation: Validation = .{ - .now = struct { - fn func() u64 { - return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds - } - }.func, + const data = try TestEdDSAData.init(allocator); + defer data.deinit(); + + var jwt = decode( + allocator, + struct { sub: []const u8 }, + "a", + .{ .edsa = data.pair.public_key }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.MalformedJWT); }; - const token = try encode(allocator, .{ .alg = .HS256 }, .{ .sub = "test", .exp = validation.now() + 60 }, .{ .secret = "secret" }); - defer allocator.free(token); + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + +// HS256 tests + +const TestHS256Data = struct { + allocator: std.mem.Allocator, + token: []const u8, + validation: Validation, + + fn init(allocator: std.mem.Allocator) !TestHS256Data { + const validation: Validation = .{ + .now = struct { + fn func() u64 { + return 1722441274; // Wednesday, July 31, 2024 3:54:34 PM - in seconds + } + }.func, + }; + const token = try encode(allocator, .{ .alg = .HS256 }, .{ .sub = "test", .exp = validation.now() + 60 }, .{ .secret = "secret" }); + return .{ .token = token, .validation = validation, .allocator = allocator }; + } + fn deinit(self: TestHS256Data) void { + self.allocator.free(self.token); + } +}; + +test "HS256.roundtrip" { + const allocator = std.testing.allocator; + const data = try TestHS256Data.init(allocator); + defer data.deinit(); + var jwt = try decode( std.testing.allocator, struct { sub: []const u8 }, - token, + data.token, .{ .secret = "secret" }, - validation, + data.validation, ); defer jwt.deinit(); try std.testing.expectEqualStrings("test", jwt.claims.sub); } +test "HS256.roundtrip.invalidsignature" { + const allocator = std.testing.allocator; + const data = try TestHS256Data.init(allocator); + defer data.deinit(); + + const invalid_token = try std.fmt.allocPrint(allocator, "{s}a", .{data.token}); + defer allocator.free(invalid_token); + var jwt = decode( + std.testing.allocator, + struct { sub: []const u8 }, + invalid_token, + .{ .secret = "secret" }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.InvalidSignature); + }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + +test "HS256.malformedjwt" { + const allocator = std.testing.allocator; + const data = try TestHS256Data.init(allocator); + defer data.deinit(); + + var jwt = decode( + std.testing.allocator, + struct { sub: []const u8 }, + "a", + .{ .secret = "secret" }, + data.validation, + ) catch |err| { + return try std.testing.expect(err == error.MalformedJWT); + }; + defer jwt.deinit(); + + return error.TestUnexpectedResult; +} + test { std.testing.refAllDecls(@This()); }