mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-12 10:06:12 -04:00
Merge pull request #2296 from lightpanda-io/crypto_generateKey_errors
Improve correctness of generateKey error
This commit is contained in:
@@ -120,6 +120,135 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// helper
|
||||
window.expectGenKeyRejects = async function(algo, extractable, usages, expected) {
|
||||
try {
|
||||
await crypto.subtle.generateKey(algo, extractable, usages);
|
||||
testing.fail(`expected ${expected}, but generateKey resolved for ${JSON.stringify(algo)}`);
|
||||
} catch (err) {
|
||||
testing.expectEqual(expected, err.name);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script id=generateKey-unrecognized-algorithm>
|
||||
testing.async(async () => {
|
||||
// Unknown name (string and object form) -> NotSupportedError.
|
||||
await expectGenKeyRejects("AES", true, ["encrypt"], "NotSupportedError");
|
||||
await expectGenKeyRejects({ name: "AES" }, true, ["encrypt"], "NotSupportedError");
|
||||
await expectGenKeyRejects({ name: "AES-CMAC", length: 128 }, true, ["encrypt"], "NotSupportedError");
|
||||
await expectGenKeyRejects({ name: "EC", namedCurve: "P-256" }, true, ["sign"], "NotSupportedError");
|
||||
// Empty algorithm dictionary -> TypeError (missing required `name`).
|
||||
await expectGenKeyRejects({}, true, ["encrypt"], "TypeError");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=generateKey-aes-validation>
|
||||
testing.async(async () => {
|
||||
// Bad usages: SyntaxError.
|
||||
await expectGenKeyRejects({ name: "AES-CBC", length: 128 }, true, ["sign"], "SyntaxError");
|
||||
await expectGenKeyRejects({ name: "AES-GCM", length: 128 }, true, ["encrypt", "deriveBits"], "SyntaxError");
|
||||
// AES-KW only allows wrapKey / unwrapKey.
|
||||
await expectGenKeyRejects({ name: "AES-KW", length: 128 }, true, ["encrypt"], "SyntaxError");
|
||||
|
||||
// Bad length: OperationError.
|
||||
for (const length of [64, 127, 129, 255, 257, 512]) {
|
||||
await expectGenKeyRejects({ name: "AES-CBC", length }, true, ["encrypt"], "OperationError");
|
||||
}
|
||||
|
||||
// Empty usages on a secret key: SyntaxError.
|
||||
await expectGenKeyRejects({ name: "AES-CBC", length: 128 }, true, [], "SyntaxError");
|
||||
await expectGenKeyRejects({ name: "AES-KW", length: 256 }, true, [], "SyntaxError");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=generateKey-ec-validation>
|
||||
testing.async(async () => {
|
||||
// Bad usages: SyntaxError.
|
||||
await expectGenKeyRejects({ name: "ECDSA", namedCurve: "P-256" }, true, ["encrypt"], "SyntaxError");
|
||||
await expectGenKeyRejects({ name: "ECDH", namedCurve: "P-256" }, true, ["sign"], "SyntaxError");
|
||||
|
||||
// Unknown curve: NotSupportedError (not OperationError, per spec).
|
||||
await expectGenKeyRejects({ name: "ECDSA", namedCurve: "P-512" }, true, ["sign"], "NotSupportedError");
|
||||
await expectGenKeyRejects({ name: "ECDH", namedCurve: "Curve25519" }, true, ["deriveBits"], "NotSupportedError");
|
||||
|
||||
// Empty usages: SyntaxError (mandatoryUsages aside, the spec rejects empty for private keys).
|
||||
await expectGenKeyRejects({ name: "ECDSA", namedCurve: "P-256" }, true, [], "SyntaxError");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=generateKey-rsa-validation>
|
||||
testing.async(async () => {
|
||||
const goodExp = new Uint8Array([1, 0, 1]); // 65537
|
||||
|
||||
// Bad name: NotSupportedError.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA", hash: "SHA-256", modulusLength: 2048, publicExponent: goodExp },
|
||||
true, ["sign"], "NotSupportedError",
|
||||
);
|
||||
|
||||
// Bad hash: NotSupportedError.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA-PSS", hash: "SHA", modulusLength: 2048, publicExponent: goodExp },
|
||||
true, ["sign"], "NotSupportedError",
|
||||
);
|
||||
|
||||
// Bad usages: SyntaxError. RSASSA only allows sign/verify.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256", modulusLength: 2048, publicExponent: goodExp },
|
||||
true, ["encrypt"], "SyntaxError",
|
||||
);
|
||||
// RSA-OAEP only allows encrypt/decrypt/wrapKey/unwrapKey.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA-OAEP", hash: "SHA-256", modulusLength: 2048, publicExponent: goodExp },
|
||||
true, ["sign"], "SyntaxError",
|
||||
);
|
||||
|
||||
// Bad publicExponent: OperationError.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA-OAEP", hash: "SHA-256", modulusLength: 2048, publicExponent: new Uint8Array([1]) },
|
||||
true, ["encrypt"], "OperationError",
|
||||
);
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA-OAEP", hash: "SHA-256", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 0]) },
|
||||
true, ["encrypt"], "OperationError",
|
||||
);
|
||||
|
||||
// Empty usages: SyntaxError.
|
||||
await expectGenKeyRejects(
|
||||
{ name: "RSA-OAEP", hash: "SHA-256", modulusLength: 2048, publicExponent: goodExp },
|
||||
true, [], "SyntaxError",
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=generateKey-hmac-validation>
|
||||
testing.async(async () => {
|
||||
// Unknown hash: NotSupportedError.
|
||||
await expectGenKeyRejects({ name: "HMAC", hash: "MD5" }, true, ["sign"], "NotSupportedError");
|
||||
|
||||
// Bad usages: SyntaxError. HMAC only allows sign/verify.
|
||||
await expectGenKeyRejects({ name: "HMAC", hash: "SHA-256" }, true, ["encrypt"], "SyntaxError");
|
||||
await expectGenKeyRejects({ name: "HMAC", hash: "SHA-256" }, true, ["sign", "deriveBits"], "SyntaxError");
|
||||
|
||||
// Empty usages: SyntaxError.
|
||||
await expectGenKeyRejects({ name: "HMAC", hash: "SHA-256" }, true, [], "SyntaxError");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=generateKey-edxxx-validation>
|
||||
testing.async(async () => {
|
||||
// Ed25519 / Ed448 only allow sign/verify; X448 only allows deriveKey/deriveBits.
|
||||
await expectGenKeyRejects("Ed25519", true, ["encrypt"], "SyntaxError");
|
||||
await expectGenKeyRejects({ name: "Ed448" }, true, ["deriveBits"], "SyntaxError");
|
||||
await expectGenKeyRejects("X448", true, ["sign"], "SyntaxError");
|
||||
|
||||
// Empty usages: SyntaxError.
|
||||
await expectGenKeyRejects("Ed25519", true, [], "SyntaxError");
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id="digest">
|
||||
testing.async(async () => {
|
||||
async function hash(algo, data) {
|
||||
|
||||
@@ -59,12 +59,17 @@ pub fn fromError(err: anyerror) ?DOMException {
|
||||
error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
|
||||
error.DataClone => .{ ._code = .data_clone_error },
|
||||
error.InvalidAccessError => .{ ._code = .invalid_access_error },
|
||||
error.OperationError => .{ ._code = .operation_error },
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCode(self: *const DOMException) u8 {
|
||||
return @intFromEnum(self._code);
|
||||
return switch (self._code) {
|
||||
// WebCrypto-only error: no legacy numeric code.
|
||||
.operation_error => 0,
|
||||
else => @intFromEnum(self._code),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getName(self: *const DOMException) []const u8 {
|
||||
@@ -95,6 +100,7 @@ pub fn getName(self: *const DOMException) []const u8 {
|
||||
.timeout_error => "TimeoutError",
|
||||
.invalid_node_type_error => "InvalidNodeTypeError",
|
||||
.data_clone_error => "DataCloneError",
|
||||
.operation_error => "OperationError",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,6 +131,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
|
||||
.timeout_error => "The operation timed out",
|
||||
.invalid_node_type_error => "The supplied node is incorrect or has an incorrect ancestor for this operation",
|
||||
.data_clone_error => "The object can not be cloned",
|
||||
.operation_error => "The operation failed for an operation-specific reason",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,6 +171,8 @@ const Code = enum(u8) {
|
||||
timeout_error = 23,
|
||||
invalid_node_type_error = 24,
|
||||
data_clone_error = 25,
|
||||
/// Defined by WebCrypto; no legacy code, exposed via name only.
|
||||
operation_error = 0xFF,
|
||||
|
||||
/// Maps a standard error name to its legacy code
|
||||
/// Returns .none (code 0) for non-legacy error names
|
||||
@@ -190,6 +199,7 @@ const Code = enum(u8) {
|
||||
.{ "TimeoutError", .timeout_error },
|
||||
.{ "InvalidNodeTypeError", .invalid_node_type_error },
|
||||
.{ "DataCloneError", .data_clone_error },
|
||||
.{ "OperationError", .operation_error },
|
||||
});
|
||||
return lookup.get(name) orelse .none;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,14 @@ const js = @import("../js/js.zig");
|
||||
const CryptoKey = @import("CryptoKey.zig");
|
||||
|
||||
const algorithm = @import("crypto/algorithm.zig");
|
||||
const AES = @import("crypto/AES.zig");
|
||||
const EC = @import("crypto/EC.zig");
|
||||
const HMAC = @import("crypto/HMAC.zig");
|
||||
const RSA = @import("crypto/RSA.zig");
|
||||
const X25519 = @import("crypto/X25519.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const String = lp.String;
|
||||
|
||||
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
|
||||
/// cryptographic functions.
|
||||
@@ -47,28 +51,92 @@ pub fn generateKey(
|
||||
key_usages: []const []const u8,
|
||||
frame: *Frame,
|
||||
) !js.Promise {
|
||||
const local = frame.js.local.?;
|
||||
switch (algo) {
|
||||
.hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, frame),
|
||||
.name => |name| {
|
||||
if (std.mem.eql(u8, "X25519", name)) {
|
||||
return X25519.init(extractable, key_usages, frame);
|
||||
}
|
||||
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = name });
|
||||
.aes_key_gen => |params| {
|
||||
AES.validate(params, key_usages) catch |err| {
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
|
||||
};
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = params.name });
|
||||
},
|
||||
.object => |object| {
|
||||
// Ditto.
|
||||
const name = object.name;
|
||||
if (std.mem.eql(u8, "X25519", name)) {
|
||||
return X25519.init(extractable, key_usages, frame);
|
||||
}
|
||||
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = name });
|
||||
.ec_key_gen => |params| {
|
||||
EC.validate(params, key_usages) catch |err| {
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
|
||||
};
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = params.name });
|
||||
},
|
||||
else => log.warn(.not_implemented, "generateKey", .{}),
|
||||
.rsa_hashed_key_gen => |params| {
|
||||
RSA.validate(params, key_usages) catch |err| {
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
|
||||
};
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = params.name });
|
||||
},
|
||||
.name => |js_name| return generateKeyFromName(try js_name.toSSO(false), extractable, key_usages, frame),
|
||||
.object => |object| return generateKeyFromName(try object.name.toSSO(false), extractable, key_usages, frame),
|
||||
.invalid => return local.rejectPromise(.{ .type_error = "invalid algorithm" }),
|
||||
}
|
||||
|
||||
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||
}
|
||||
|
||||
fn generateKeyFromName(
|
||||
name: String,
|
||||
extractable: bool,
|
||||
key_usages: []const []const u8,
|
||||
frame: *Frame,
|
||||
) !js.Promise {
|
||||
return _generateKeyFromName(name, extractable, key_usages, frame) catch |err| {
|
||||
return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } });
|
||||
};
|
||||
}
|
||||
|
||||
fn _generateKeyFromName(
|
||||
name: String,
|
||||
extractable: bool,
|
||||
key_usages: []const []const u8,
|
||||
frame: *Frame,
|
||||
) !js.Promise {
|
||||
if (name.eql(comptime .wrap("X25519"))) {
|
||||
return X25519.init(extractable, key_usages, frame);
|
||||
}
|
||||
|
||||
{
|
||||
// Algorithms whose `generateKey` parameters are just `{name}` — Ed25519,
|
||||
// Ed448, X448. Validates usages so failure-path tests get the spec-mandated
|
||||
// error name; leaves real key generation to a future change.
|
||||
|
||||
const allowed: []const []const u8 = blk: {
|
||||
const str = name.str();
|
||||
if (std.ascii.eqlIgnoreCase(str, "Ed25519") or std.ascii.eqlIgnoreCase(str, "Ed448")) {
|
||||
break :blk &.{ "sign", "verify" };
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(str, "X448")) {
|
||||
break :blk &.{ "deriveKey", "deriveBits" };
|
||||
}
|
||||
return error.NotSupported;
|
||||
};
|
||||
|
||||
for (key_usages) |usage| {
|
||||
var ok = false;
|
||||
for (allowed) |a| {
|
||||
if (std.mem.eql(u8, a, usage)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
if (key_usages.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(.not_implemented, "generateKey", .{ .name = name });
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
|
||||
|
||||
70
src/browser/webapi/crypto/AES.zig
Normal file
70
src/browser/webapi/crypto/AES.zig
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! AES generateKey parameter validation.
|
||||
//!
|
||||
//! Key generation itself is not implemented; this module rejects malformed
|
||||
//! input with the spec-mandated error name so the failure-path WPT tests
|
||||
//! match. Successful inputs fall through to the caller's `not_implemented`
|
||||
//! warning + `NotSupportedError`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const algorithm = @import("algorithm.zig");
|
||||
|
||||
/// Per WebCrypto: "Generate Key" operation for AES-CBC/CTR/GCM/KW.
|
||||
/// Validation order matches the spec: usages → length → empty usages.
|
||||
pub fn validate(params: algorithm.Init.AesKeyGen, key_usages: []const []const u8) !void {
|
||||
const allowed: []const []const u8 = blk: {
|
||||
if (eql(params.name, "AES-CBC") or
|
||||
eql(params.name, "AES-CTR") or
|
||||
eql(params.name, "AES-GCM"))
|
||||
{
|
||||
break :blk &.{ "encrypt", "decrypt", "wrapKey", "unwrapKey" };
|
||||
}
|
||||
if (eql(params.name, "AES-KW")) {
|
||||
break :blk &.{ "wrapKey", "unwrapKey" };
|
||||
}
|
||||
return error.NotSupported;
|
||||
};
|
||||
|
||||
for (key_usages) |usage| {
|
||||
var ok = false;
|
||||
for (allowed) |a| {
|
||||
if (std.mem.eql(u8, a, usage)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.length != 128 and params.length != 192 and params.length != 256) {
|
||||
return error.OperationError;
|
||||
}
|
||||
|
||||
if (key_usages.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
fn eql(a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
66
src/browser/webapi/crypto/EC.zig
Normal file
66
src/browser/webapi/crypto/EC.zig
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! ECDSA / ECDH generateKey parameter validation. See AES.zig for
|
||||
//! the rationale on validate-without-generate.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const algorithm = @import("algorithm.zig");
|
||||
|
||||
pub fn validate(params: algorithm.Init.EcKeyGen, key_usages: []const []const u8) !void {
|
||||
const allowed: []const []const u8 = blk: {
|
||||
if (eql(params.name, "ECDSA")) {
|
||||
break :blk &.{ "sign", "verify" };
|
||||
}
|
||||
if (eql(params.name, "ECDH")) {
|
||||
break :blk &.{ "deriveKey", "deriveBits" };
|
||||
}
|
||||
return error.NotSupported;
|
||||
};
|
||||
|
||||
for (key_usages) |usage| {
|
||||
var ok = false;
|
||||
for (allowed) |a| {
|
||||
if (std.mem.eql(u8, a, usage)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
// Per spec, an unsupported `namedCurve` is NotSupportedError, not OperationError —
|
||||
// unlike AES length, where the algorithm registers the value as invalid.
|
||||
if (!eql(params.namedCurve, "P-256") and
|
||||
!eql(params.namedCurve, "P-384") and
|
||||
!eql(params.namedCurve, "P-521"))
|
||||
{
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
if (key_usages.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
fn eql(a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
@@ -35,30 +35,30 @@ pub fn init(
|
||||
frame: *Frame,
|
||||
) !js.Promise {
|
||||
const local = frame.js.local.?;
|
||||
// Find digest.
|
||||
// Per spec, an unrecognized hash is caught during algorithm normalization
|
||||
// and surfaces as NotSupportedError.
|
||||
const digest = crypto.findDigest(switch (params.hash) {
|
||||
.string => |str| str,
|
||||
.object => |obj| obj.name,
|
||||
}) catch return local.rejectPromise(.{
|
||||
.dom_exception = .{ .err = error.SyntaxError },
|
||||
.dom_exception = .{ .err = error.NotSupported },
|
||||
});
|
||||
|
||||
// Calculate usages mask.
|
||||
if (key_usages.len == 0) {
|
||||
return local.rejectPromise(.{
|
||||
.dom_exception = .{ .err = error.SyntaxError },
|
||||
});
|
||||
}
|
||||
const decls = @typeInfo(CryptoKey.Usages).@"struct".decls;
|
||||
// HMAC only accepts sign / verify; any other usage is a SyntaxError per
|
||||
// the spec, even when the entry exists elsewhere in CryptoKey.Usages.
|
||||
var mask: u8 = 0;
|
||||
iter_usages: for (key_usages) |usage| {
|
||||
inline for (decls) |decl| {
|
||||
if (std.mem.eql(u8, decl.name, usage)) {
|
||||
mask |= @field(CryptoKey.Usages, decl.name);
|
||||
continue :iter_usages;
|
||||
}
|
||||
for (key_usages) |usage| {
|
||||
if (std.mem.eql(u8, usage, "sign")) {
|
||||
mask |= CryptoKey.Usages.sign;
|
||||
} else if (std.mem.eql(u8, usage, "verify")) {
|
||||
mask |= CryptoKey.Usages.verify;
|
||||
} else {
|
||||
return local.rejectPromise(.{
|
||||
.dom_exception = .{ .err = error.SyntaxError },
|
||||
});
|
||||
}
|
||||
// Unknown usage if got here.
|
||||
}
|
||||
if (key_usages.len == 0) {
|
||||
return local.rejectPromise(.{
|
||||
.dom_exception = .{ .err = error.SyntaxError },
|
||||
});
|
||||
|
||||
85
src/browser/webapi/crypto/RSA.zig
Normal file
85
src/browser/webapi/crypto/RSA.zig
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RSA generateKey parameter validation. See AES.zig for the rationale.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const algorithm = @import("algorithm.zig");
|
||||
|
||||
pub fn validate(params: algorithm.Init.RsaHashedKeyGen, key_usages: []const []const u8) !void {
|
||||
const allowed: []const []const u8 = blk: {
|
||||
if (eql(params.name, "RSASSA-PKCS1-v1_5") or eql(params.name, "RSA-PSS")) {
|
||||
break :blk &.{ "sign", "verify" };
|
||||
}
|
||||
if (eql(params.name, "RSA-OAEP")) {
|
||||
break :blk &.{ "encrypt", "decrypt", "wrapKey", "unwrapKey" };
|
||||
}
|
||||
return error.NotSupported;
|
||||
};
|
||||
|
||||
const hash_name = switch (params.hash) {
|
||||
.string => |s| s,
|
||||
.object => |o| o.name,
|
||||
};
|
||||
if (!eql(hash_name, "SHA-1") and
|
||||
!eql(hash_name, "SHA-256") and
|
||||
!eql(hash_name, "SHA-384") and
|
||||
!eql(hash_name, "SHA-512"))
|
||||
{
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
for (key_usages) |usage| {
|
||||
var ok = false;
|
||||
for (allowed) |a| {
|
||||
if (std.mem.eql(u8, a, usage)) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidPublicExponent(params.publicExponent.values)) {
|
||||
return error.OperationError;
|
||||
}
|
||||
|
||||
if (key_usages.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
}
|
||||
|
||||
// WebCrypto only mandates rejection on key-generation failure, but in
|
||||
// practice browsers accept the standard exponents 3 and 65537 and reject
|
||||
// the rest. Match that.
|
||||
fn isValidPublicExponent(bytes: []const u8) bool {
|
||||
if (bytes.len == 0) return false;
|
||||
var i: usize = 0;
|
||||
while (i + 1 < bytes.len and bytes[i] == 0) : (i += 1) {}
|
||||
const trimmed = bytes[i..];
|
||||
if (trimmed.len == 1 and trimmed[0] == 3) return true;
|
||||
if (trimmed.len == 3 and trimmed[0] == 0x01 and trimmed[1] == 0x00 and trimmed[2] == 0x01) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn eql(a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
@@ -28,10 +28,19 @@ pub const Init = union(enum) {
|
||||
rsa_hashed_key_gen: RsaHashedKeyGen,
|
||||
/// For HMAC: pass an HmacKeyGenParams object.
|
||||
hmac_key_gen: HmacKeyGen,
|
||||
/// For AES variants: pass an AesKeyGenParams object.
|
||||
aes_key_gen: AesKeyGen,
|
||||
/// For ECDSA / ECDH: pass an EcKeyGenParams object.
|
||||
ec_key_gen: EcKeyGen,
|
||||
|
||||
/// don't use []const u8 here, we don't want non-strings coerced. Let those
|
||||
/// fall to the invalid case
|
||||
/// Can be Ed25519 or X25519.
|
||||
name: []const u8,
|
||||
object: struct { name: js.String },
|
||||
/// Can be Ed25519 or X25519.
|
||||
object: struct { name: []const u8 },
|
||||
name: js.String,
|
||||
|
||||
invalid: js.Value,
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
|
||||
pub const RsaHashedKeyGen = struct {
|
||||
@@ -46,6 +55,18 @@ pub const Init = union(enum) {
|
||||
},
|
||||
};
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/AesKeyGenParams
|
||||
pub const AesKeyGen = struct {
|
||||
name: []const u8,
|
||||
length: u32,
|
||||
};
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams
|
||||
pub const EcKeyGen = struct {
|
||||
name: []const u8,
|
||||
namedCurve: []const u8,
|
||||
};
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams
|
||||
pub const HmacKeyGen = struct {
|
||||
/// Always HMAC.
|
||||
|
||||
Reference in New Issue
Block a user