@pulse-compute/crypto 1.0.0-beta.5 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/dist/contracts.d.ts +34 -12
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +9 -3
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/rsa.d.ts +58 -0
- package/dist/internal/rsa.d.ts.map +1 -0
- package/dist/internal/rsa.js +185 -0
- package/dist/internal/rsa.js.map +1 -0
- package/dist/internal/verification.d.ts +3 -3
- package/dist/internal/verification.d.ts.map +1 -1
- package/dist/internal/verification.js +2 -2
- package/dist/internal/verification.js.map +1 -1
- package/guests/es256-rustcrypto/README.md +43 -0
- package/guests/es256-rustcrypto/build.cjs +297 -0
- package/guests/es256-rustcrypto/prebuilt/es256-verifier.unoptimized.wasm +0 -0
- package/guests/es256-rustcrypto/prebuilt/es256-verifier.wasm +0 -0
- package/guests/es256-rustcrypto/probe/.cargo/config.toml +18 -0
- package/guests/es256-rustcrypto/probe/Cargo.lock +238 -0
- package/guests/es256-rustcrypto/probe/Cargo.toml +26 -0
- package/guests/es256-rustcrypto/probe/README.md +35 -0
- package/guests/es256-rustcrypto/probe/rust-toolchain.toml +5 -0
- package/guests/es256-rustcrypto/probe/src/lib.rs +108 -0
- package/guests/es256-rustcrypto/pulse.guest-unit.json +119 -0
- package/guests/es256-rustcrypto/source/.cargo/config.toml +18 -0
- package/guests/es256-rustcrypto/source/Cargo.lock +265 -0
- package/guests/es256-rustcrypto/source/Cargo.toml +23 -0
- package/guests/es256-rustcrypto/source/README.md +26 -0
- package/guests/es256-rustcrypto/source/bearssl/LICENSE.txt +21 -0
- package/guests/es256-rustcrypto/source/bearssl/UPSTREAM.json +49 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl.h +170 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_aead.h +1059 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_block.h +2618 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_ec.h +883 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_hash.h +1346 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_hmac.h +241 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_kdf.h +185 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_pem.h +294 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_prf.h +150 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_rand.h +397 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_rsa.h +1366 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_ssl.h +4296 -0
- package/guests/es256-rustcrypto/source/bearssl/inc/bearssl_x509.h +1397 -0
- package/guests/es256-rustcrypto/source/bearssl/src/codec/ccopy.c +44 -0
- package/guests/es256-rustcrypto/source/bearssl/src/codec/dec32be.c +38 -0
- package/guests/es256-rustcrypto/source/bearssl/src/codec/enc32be.c +38 -0
- package/guests/es256-rustcrypto/source/bearssl/src/config.h +229 -0
- package/guests/es256-rustcrypto/source/bearssl/src/inner.h +2532 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_add.c +46 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_bitlen.c +44 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_decmod.c +124 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_decode.c +57 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_decred.c +103 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_encode.c +79 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_fmont.c +60 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_iszero.c +39 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_moddiv.c +488 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_modpow.c +65 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_modpow2.c +160 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_montmul.c +93 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_mulacc.c +61 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_muladd.c +157 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_ninv31.c +39 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_reduce.c +66 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_rshift.c +47 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_sub.c +46 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i31_tmont.c +36 -0
- package/guests/es256-rustcrypto/source/bearssl/src/int/i32_div32.c +56 -0
- package/guests/es256-rustcrypto/source/bearssl/src/rsa/rsa_i31_priv.c +203 -0
- package/guests/es256-rustcrypto/source/bearssl/src/rsa/rsa_i31_pub.c +106 -0
- package/guests/es256-rustcrypto/source/bearssl/src/rsa/rsa_pkcs1_sig_pad.c +100 -0
- package/guests/es256-rustcrypto/source/build.rs +38 -0
- package/guests/es256-rustcrypto/source/freestanding/string.h +8 -0
- package/guests/es256-rustcrypto/source/rs256.c +103 -0
- package/guests/es256-rustcrypto/source/rust-toolchain.toml +5 -0
- package/guests/es256-rustcrypto/source/src/lib.rs +308 -0
- package/package.json +6 -4
- package/pulsewasm.native.cjs +66 -0
- package/src/provider.cjs +71 -1
- package/src/provider.d.cts +6 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#![no_std]
|
|
2
|
+
|
|
3
|
+
use core::{panic::PanicInfo, slice};
|
|
4
|
+
use ecdsa::{
|
|
5
|
+
hazmat::{bits2field, verify_prehashed},
|
|
6
|
+
Signature,
|
|
7
|
+
};
|
|
8
|
+
use p256::{
|
|
9
|
+
elliptic_curve::sec1::FromEncodedPoint,
|
|
10
|
+
AffinePoint,
|
|
11
|
+
EncodedPoint,
|
|
12
|
+
NistP256,
|
|
13
|
+
ProjectivePoint,
|
|
14
|
+
};
|
|
15
|
+
use sha2::{Digest, Sha256};
|
|
16
|
+
|
|
17
|
+
type P256Signature = Signature<NistP256>;
|
|
18
|
+
|
|
19
|
+
const MEMORY_BYTES: u32 = 32 * 65_536;
|
|
20
|
+
const INVOCATION_REGION_START: u32 = 8 * 65_536;
|
|
21
|
+
const FRAME_MAGIC: u32 = 0x3253_4550;
|
|
22
|
+
const FRAME_VERSION: u32 = 2;
|
|
23
|
+
const FRAME_HEADER_BYTES: u32 = 64;
|
|
24
|
+
const FRAME_CAPACITY_BYTES: u32 = 16_640;
|
|
25
|
+
const FRAME_ALIGNMENT_BYTES: u32 = 16;
|
|
26
|
+
const FRAME_ALGORITHM_ES256: u32 = 1;
|
|
27
|
+
const P256_PUBLIC_KEY_BYTES: u32 = 64;
|
|
28
|
+
const ES256_SIGNATURE_BYTES: u32 = 64;
|
|
29
|
+
const ES256_SIGNING_INPUT_BYTES_MAXIMUM: u32 = 16_340;
|
|
30
|
+
|
|
31
|
+
const STATUS_VALID: i32 = 1;
|
|
32
|
+
const STATUS_INVALID_AUTHENTICATOR: i32 = 0;
|
|
33
|
+
const STATUS_INVALID_KEY: i32 = -1;
|
|
34
|
+
const STATUS_INVALID_INPUT: i32 = -2;
|
|
35
|
+
|
|
36
|
+
const HEADER_MAGIC: usize = 0;
|
|
37
|
+
const HEADER_VERSION: usize = 4;
|
|
38
|
+
const HEADER_LENGTH: usize = 8;
|
|
39
|
+
const HEADER_TOTAL_LENGTH: usize = 12;
|
|
40
|
+
const HEADER_ALGORITHM: usize = 16;
|
|
41
|
+
const HEADER_FLAGS: usize = 20;
|
|
42
|
+
const HEADER_SIGNING_INPUT_OFFSET: usize = 24;
|
|
43
|
+
const HEADER_SIGNING_INPUT_LENGTH: usize = 28;
|
|
44
|
+
const HEADER_PUBLIC_KEY_OFFSET: usize = 32;
|
|
45
|
+
const HEADER_PUBLIC_KEY_LENGTH: usize = 36;
|
|
46
|
+
const HEADER_SIGNATURE_OFFSET: usize = 40;
|
|
47
|
+
const HEADER_SIGNATURE_LENGTH: usize = 44;
|
|
48
|
+
const HEADER_RESERVED_0: usize = 48;
|
|
49
|
+
const HEADER_RESERVED_1: usize = 52;
|
|
50
|
+
const HEADER_RESERVED_2: usize = 56;
|
|
51
|
+
const HEADER_RESERVED_3: usize = 60;
|
|
52
|
+
|
|
53
|
+
#[derive(Clone, Copy)]
|
|
54
|
+
struct Span {
|
|
55
|
+
start: u32,
|
|
56
|
+
end: u32,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl Span {
|
|
60
|
+
fn checked(start: u32, length: u32, total_length: u32) -> Option<Self> {
|
|
61
|
+
if start < FRAME_HEADER_BYTES
|
|
62
|
+
|| start % FRAME_ALIGNMENT_BYTES != 0
|
|
63
|
+
{
|
|
64
|
+
return None;
|
|
65
|
+
}
|
|
66
|
+
let end = start.checked_add(length)?;
|
|
67
|
+
if end > total_length {
|
|
68
|
+
return None;
|
|
69
|
+
}
|
|
70
|
+
Some(Self { start, end })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn overlaps(self, other: Self) -> bool {
|
|
74
|
+
self.start < other.end && other.start < self.end
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[panic_handler]
|
|
79
|
+
fn panic(_info: &PanicInfo<'_>) -> ! {
|
|
80
|
+
loop {
|
|
81
|
+
core::hint::spin_loop();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
|
86
|
+
u32::from_le_bytes([
|
|
87
|
+
bytes[offset],
|
|
88
|
+
bytes[offset + 1],
|
|
89
|
+
bytes[offset + 2],
|
|
90
|
+
bytes[offset + 3],
|
|
91
|
+
])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn outer_frame_is_valid(frame_pointer: u32, frame_capacity: u32) -> bool {
|
|
95
|
+
if frame_capacity != FRAME_CAPACITY_BYTES
|
|
96
|
+
|| frame_pointer < INVOCATION_REGION_START
|
|
97
|
+
|| frame_pointer % FRAME_ALIGNMENT_BYTES != 0
|
|
98
|
+
{
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
match frame_pointer.checked_add(frame_capacity) {
|
|
102
|
+
Some(end) => end <= MEMORY_BYTES,
|
|
103
|
+
None => false,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn header_is_valid(frame: &[u8], key_bytes: u32) -> Option<(Span, Span, Span)> {
|
|
108
|
+
signature_header(frame, key_bytes, ES256_SIGNATURE_BYTES, FRAME_ALGORITHM_ES256, ES256_SIGNING_INPUT_BYTES_MAXIMUM)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn signature_header(frame: &[u8], key_bytes: u32, signature_bytes: u32, algorithm: u32, input_max: u32) -> Option<(Span, Span, Span)> {
|
|
112
|
+
let total_length = read_u32_le(frame, HEADER_TOTAL_LENGTH);
|
|
113
|
+
if read_u32_le(frame, HEADER_MAGIC) != FRAME_MAGIC
|
|
114
|
+
|| read_u32_le(frame, HEADER_VERSION) != FRAME_VERSION
|
|
115
|
+
|| read_u32_le(frame, HEADER_LENGTH) != FRAME_HEADER_BYTES
|
|
116
|
+
|| total_length < 192
|
|
117
|
+
|| total_length > FRAME_CAPACITY_BYTES
|
|
118
|
+
|| total_length % FRAME_ALIGNMENT_BYTES != 0
|
|
119
|
+
|| read_u32_le(frame, HEADER_ALGORITHM) != algorithm
|
|
120
|
+
|| read_u32_le(frame, HEADER_FLAGS) != 0
|
|
121
|
+
|| read_u32_le(frame, HEADER_RESERVED_0) != 0
|
|
122
|
+
|| read_u32_le(frame, HEADER_RESERVED_1) != 0
|
|
123
|
+
|| read_u32_le(frame, HEADER_RESERVED_2) != 0
|
|
124
|
+
|| read_u32_le(frame, HEADER_RESERVED_3) != 0
|
|
125
|
+
{
|
|
126
|
+
return None;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let signing_input_length =
|
|
130
|
+
read_u32_le(frame, HEADER_SIGNING_INPUT_LENGTH);
|
|
131
|
+
let public_key_length = read_u32_le(frame, HEADER_PUBLIC_KEY_LENGTH);
|
|
132
|
+
let signature_length = read_u32_le(frame, HEADER_SIGNATURE_LENGTH);
|
|
133
|
+
if signing_input_length > input_max
|
|
134
|
+
|| public_key_length != key_bytes
|
|
135
|
+
|| signature_length != signature_bytes
|
|
136
|
+
{
|
|
137
|
+
return None;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let signing_input = Span::checked(
|
|
141
|
+
read_u32_le(frame, HEADER_SIGNING_INPUT_OFFSET),
|
|
142
|
+
signing_input_length,
|
|
143
|
+
total_length,
|
|
144
|
+
)?;
|
|
145
|
+
let public_key = Span::checked(
|
|
146
|
+
read_u32_le(frame, HEADER_PUBLIC_KEY_OFFSET),
|
|
147
|
+
public_key_length,
|
|
148
|
+
total_length,
|
|
149
|
+
)?;
|
|
150
|
+
let signature = Span::checked(
|
|
151
|
+
read_u32_le(frame, HEADER_SIGNATURE_OFFSET),
|
|
152
|
+
signature_length,
|
|
153
|
+
total_length,
|
|
154
|
+
)?;
|
|
155
|
+
|
|
156
|
+
if signing_input.start == public_key.start
|
|
157
|
+
|| signing_input.start == signature.start
|
|
158
|
+
|| public_key.start == signature.start
|
|
159
|
+
|| signing_input.overlaps(public_key)
|
|
160
|
+
|| signing_input.overlaps(signature)
|
|
161
|
+
|| public_key.overlaps(signature)
|
|
162
|
+
{
|
|
163
|
+
return None;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Some((signing_input, public_key, signature))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn verify_es256(
|
|
170
|
+
signing_input: &[u8],
|
|
171
|
+
public_key_bytes: &[u8],
|
|
172
|
+
signature_bytes: &[u8],
|
|
173
|
+
) -> i32 {
|
|
174
|
+
let mut sec1_key = [0_u8; P256_PUBLIC_KEY_BYTES as usize + 1];
|
|
175
|
+
sec1_key[0] = 0x04;
|
|
176
|
+
sec1_key[1..].copy_from_slice(public_key_bytes);
|
|
177
|
+
|
|
178
|
+
let encoded_point = match EncodedPoint::from_bytes(sec1_key) {
|
|
179
|
+
Ok(value) => value,
|
|
180
|
+
Err(_) => return STATUS_INVALID_KEY,
|
|
181
|
+
};
|
|
182
|
+
sec1_key.fill(0);
|
|
183
|
+
|
|
184
|
+
let affine_point = match Option::<AffinePoint>::from(
|
|
185
|
+
AffinePoint::from_encoded_point(&encoded_point),
|
|
186
|
+
) {
|
|
187
|
+
Some(value) => value,
|
|
188
|
+
None => return STATUS_INVALID_KEY,
|
|
189
|
+
};
|
|
190
|
+
let signature = match P256Signature::from_slice(signature_bytes) {
|
|
191
|
+
Ok(value) => value,
|
|
192
|
+
Err(_) => return STATUS_INVALID_INPUT,
|
|
193
|
+
};
|
|
194
|
+
let digest = Sha256::digest(signing_input);
|
|
195
|
+
let digest_field = match bits2field::<NistP256>(&digest) {
|
|
196
|
+
Ok(value) => value,
|
|
197
|
+
Err(_) => return STATUS_INVALID_INPUT,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
match verify_prehashed::<NistP256>(
|
|
201
|
+
&ProjectivePoint::from(affine_point),
|
|
202
|
+
&digest_field,
|
|
203
|
+
&signature,
|
|
204
|
+
) {
|
|
205
|
+
Ok(()) => STATUS_VALID,
|
|
206
|
+
Err(_) => STATUS_INVALID_AUTHENTICATOR,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Verifies one ES256 request encoded in the frozen private invocation frame.
|
|
211
|
+
///
|
|
212
|
+
/// The caller owns and clears the fixed-capacity frame after capturing the
|
|
213
|
+
/// scalar result. This guest does not retain frame pointers or mutate input.
|
|
214
|
+
///
|
|
215
|
+
/// # Safety
|
|
216
|
+
///
|
|
217
|
+
/// The host must instantiate this module with the declared fixed, unshared
|
|
218
|
+
/// 32-page `env.memory`. This function validates the outer range before its
|
|
219
|
+
/// first dereference and validates every described field before field access.
|
|
220
|
+
#[no_mangle]
|
|
221
|
+
pub unsafe extern "C" fn pulse_crypto_es256_verify(
|
|
222
|
+
frame_pointer: u32,
|
|
223
|
+
frame_capacity: u32,
|
|
224
|
+
) -> i32 {
|
|
225
|
+
if !outer_frame_is_valid(frame_pointer, frame_capacity) {
|
|
226
|
+
return STATUS_INVALID_INPUT;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let frame = slice::from_raw_parts(
|
|
230
|
+
frame_pointer as *const u8,
|
|
231
|
+
frame_capacity as usize,
|
|
232
|
+
);
|
|
233
|
+
let (signing_input, public_key, signature) =
|
|
234
|
+
match header_is_valid(frame, P256_PUBLIC_KEY_BYTES) {
|
|
235
|
+
Some(value) => value,
|
|
236
|
+
None => return STATUS_INVALID_INPUT,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
verify_es256(
|
|
240
|
+
&frame[signing_input.start as usize..signing_input.end as usize],
|
|
241
|
+
&frame[public_key.start as usize..public_key.end as usize],
|
|
242
|
+
&frame[signature.start as usize..signature.end as usize],
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/// Sign one ES256 request using x || y || d private-key bytes. The frame
|
|
248
|
+
/// follows the verification envelope with a 96-byte key and writable output.
|
|
249
|
+
/// Input spans must not overlap. Callers clear the frame and borrowed Rust
|
|
250
|
+
/// stack after capturing the signature; this function retains no pointers.
|
|
251
|
+
#[no_mangle]
|
|
252
|
+
pub unsafe extern "C" fn pulse_crypto_es256_sign(frame_pointer: u32, frame_capacity: u32) -> i32 {
|
|
253
|
+
use p256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey};
|
|
254
|
+
if !outer_frame_is_valid(frame_pointer, frame_capacity) { return STATUS_INVALID_INPUT; }
|
|
255
|
+
let frame = slice::from_raw_parts_mut(frame_pointer as *mut u8, frame_capacity as usize);
|
|
256
|
+
let (input, key, output) = match header_is_valid(frame, 96) {
|
|
257
|
+
Some(value) => value, None => return STATUS_INVALID_INPUT,
|
|
258
|
+
};
|
|
259
|
+
// Clear output before validating the key so failed calls cannot expose a
|
|
260
|
+
// prior signature. Header validation already proved disjoint spans.
|
|
261
|
+
frame[output.start as usize..output.end as usize].fill(0);
|
|
262
|
+
let key_start = key.start as usize;
|
|
263
|
+
let signer = match SigningKey::from_slice(&frame[key_start + 64..key.end as usize]) {
|
|
264
|
+
Ok(value) => value, Err(_) => return STATUS_INVALID_KEY,
|
|
265
|
+
};
|
|
266
|
+
let public = signer.verifying_key().to_encoded_point(false);
|
|
267
|
+
if public.as_bytes()[1..] != frame[key_start..key_start + 64] { return STATUS_INVALID_KEY; }
|
|
268
|
+
let digest = Sha256::digest(&frame[input.start as usize..input.end as usize]);
|
|
269
|
+
// RustCrypto's deterministic RFC6979 signing requires no host randomness.
|
|
270
|
+
let signature: P256Signature = match signer.sign_prehash(&digest) {
|
|
271
|
+
Ok(value) => value, Err(_) => return STATUS_INVALID_KEY,
|
|
272
|
+
};
|
|
273
|
+
frame[output.start as usize..output.end as usize].copy_from_slice(&signature.to_bytes());
|
|
274
|
+
STATUS_VALID
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
extern "C" {
|
|
279
|
+
fn pulse_rs256(key: *const u8, key_len: usize, hash: *const u8,
|
|
280
|
+
signature: *mut u8, modulus_len: usize, sign: i32) -> i32;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
unsafe fn rsa_frame(pointer: u32, capacity: u32, sign: bool) -> i32 {
|
|
284
|
+
if !outer_frame_is_valid(pointer, capacity) { return STATUS_INVALID_INPUT; }
|
|
285
|
+
let frame = slice::from_raw_parts_mut(pointer as *mut u8, capacity as usize);
|
|
286
|
+
let k = read_u32_le(frame, HEADER_SIGNATURE_LENGTH);
|
|
287
|
+
if !matches!(k, 256 | 384 | 512) { return STATUS_INVALID_INPUT; }
|
|
288
|
+
let key_len = if sign { 8 + k * 9 / 2 } else { 8 + k };
|
|
289
|
+
let (input, key, output) = match signature_header(frame, key_len, k, 2, 12_288) {
|
|
290
|
+
Some(value) => value, None => return STATUS_INVALID_INPUT,
|
|
291
|
+
};
|
|
292
|
+
if sign { frame[output.start as usize..output.end as usize].fill(0); }
|
|
293
|
+
let hash = Sha256::digest(&frame[input.start as usize..input.end as usize]);
|
|
294
|
+
pulse_rs256(frame.as_ptr().add(key.start as usize), key_len as usize,
|
|
295
|
+
hash.as_ptr(), frame.as_mut_ptr().add(output.start as usize), k as usize, sign as i32)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// RS256 uses the same checked v2 envelope, algorithm code 2, variable-width
|
|
299
|
+
/// key/signature spans, and a 12288-byte data bound. Caller clears frame/stack.
|
|
300
|
+
#[no_mangle]
|
|
301
|
+
pub unsafe extern "C" fn pulse_crypto_rs256_sign(pointer: u32, capacity: u32) -> i32 {
|
|
302
|
+
rsa_frame(pointer, capacity, true)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[no_mangle]
|
|
306
|
+
pub unsafe extern "C" fn pulse_crypto_rs256_verify(pointer: u32, capacity: u32) -> i32 {
|
|
307
|
+
rsa_frame(pointer, capacity, false)
|
|
308
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/crypto",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.14.0 || ^24.0.0"
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"as",
|
|
24
|
+
"guests",
|
|
24
25
|
"dist",
|
|
25
26
|
"pulsewasm.native.cjs",
|
|
26
27
|
"README.md",
|
|
27
28
|
"src/provider.cjs",
|
|
29
|
+
"src/provider.d.cts",
|
|
28
30
|
"pulse.package.json",
|
|
29
31
|
"pulsewasm.manifest.cjs",
|
|
30
32
|
"pulsewasm.compiler.cjs",
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
"bugs": {
|
|
44
46
|
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
45
47
|
},
|
|
46
|
-
"homepage": "https://pulsecompute.io/v1.0.0-beta.
|
|
48
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.6/packages/crypto/",
|
|
47
49
|
"main": "./dist/index.js",
|
|
48
50
|
"types": "./dist/index.d.ts",
|
|
49
51
|
"pulse": {
|
|
@@ -53,8 +55,8 @@
|
|
|
53
55
|
"manifest": "./pulsewasm.manifest.cjs"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
|
-
"@pulse-compute/runtime": "1.0.0-beta.
|
|
57
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
58
|
+
"@pulse-compute/runtime": "1.0.0-beta.6",
|
|
59
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.6"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
60
62
|
"build": "tsc -b",
|
package/pulsewasm.native.cjs
CHANGED
|
@@ -124,7 +124,73 @@ function pulseEs256GuestUnit() {
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
|
|
128
|
+
function bindNativeEs256Signer(moduleExports) {
|
|
129
|
+
const memory = moduleExports && moduleExports.memory;
|
|
130
|
+
const sign = moduleExports && moduleExports.pulse_crypto_es256_sign;
|
|
131
|
+
if (!(memory instanceof WebAssembly.Memory) || memory.buffer.byteLength !== 2097152 || typeof sign !== 'function') {
|
|
132
|
+
throw new TypeError('Native ES256 signing exports are unavailable.');
|
|
133
|
+
}
|
|
134
|
+
return (key, data) => {
|
|
135
|
+
if (!(key instanceof Uint8Array) || key.length !== 96 || !(data instanceof Uint8Array) || data.length > 16340) {
|
|
136
|
+
throw new TypeError('Invalid ES256 signing byte request.');
|
|
137
|
+
}
|
|
138
|
+
const pointer = 524288, capacity = 16640;
|
|
139
|
+
const frame = new Uint8Array(memory.buffer, pointer, capacity);
|
|
140
|
+
frame.fill(0);
|
|
141
|
+
try {
|
|
142
|
+
const header = new DataView(memory.buffer, pointer, 64);
|
|
143
|
+
for (const [offset, value] of [[0, 0x32534550], [4, 2], [8, 64], [12, (224 + data.length + 15) & ~15],
|
|
144
|
+
[16, 1], [24, 224], [28, data.length], [32, 64], [36, 96], [40, 160], [44, 64]]) header.setUint32(offset, value, true);
|
|
145
|
+
frame.set(key, 64); frame.set(data, 224);
|
|
146
|
+
const status = sign(pointer, capacity);
|
|
147
|
+
if (status === -1) throw Object.assign(new TypeError('Invalid P-256 signing key.'), { code: 'PULSE_CRYPTO_KEY_INVALID' });
|
|
148
|
+
if (status !== 1) throw new TypeError('Native ES256 signing failed.');
|
|
149
|
+
return frame.slice(160, 224);
|
|
150
|
+
} finally {
|
|
151
|
+
frame.fill(0);
|
|
152
|
+
// The synchronous guest has returned: clear its borrowed stack as well.
|
|
153
|
+
new Uint8Array(memory.buffer, 0, 65536).fill(0);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function bindNativeRs256(moduleExports) {
|
|
159
|
+
const memory = moduleExports && moduleExports.memory;
|
|
160
|
+
if (!(memory instanceof WebAssembly.Memory) || memory.buffer.byteLength !== 2097152
|
|
161
|
+
|| typeof moduleExports.pulse_crypto_rs256_sign !== 'function' || typeof moduleExports.pulse_crypto_rs256_verify !== 'function') {
|
|
162
|
+
throw new TypeError('Native RS256 exports are unavailable.');
|
|
163
|
+
}
|
|
164
|
+
const invalidKey = () => Object.assign(new TypeError('Invalid RSA signing key.'), { code: 'PULSE_CRYPTO_KEY_INVALID' });
|
|
165
|
+
function run(key, data, signature) {
|
|
166
|
+
const signing = signature === undefined;
|
|
167
|
+
if (!(key instanceof Uint8Array) || key.length < 8) throw invalidKey();
|
|
168
|
+
const k = new DataView(key.buffer, key.byteOffset, 4).getUint32(0, true);
|
|
169
|
+
if (![256, 384, 512].includes(k) || key.length !== (signing ? 8 + k * 9 / 2 : 8 + k)) throw invalidKey();
|
|
170
|
+
if (!(data instanceof Uint8Array) || data.length > 12288
|
|
171
|
+
|| !signing && (!(signature instanceof Uint8Array) || signature.length !== k)) throw new TypeError('Invalid RS256 byte request.');
|
|
172
|
+
const pointer = 524288, capacity = 16640, output = (64 + key.length + 15) & ~15, input = output + k;
|
|
173
|
+
const frame = new Uint8Array(memory.buffer, pointer, capacity);
|
|
174
|
+
frame.fill(0);
|
|
175
|
+
try {
|
|
176
|
+
const header = new DataView(memory.buffer, pointer, 64);
|
|
177
|
+
for (const [offset, value] of [[0, 0x32534550], [4, 2], [8, 64], [12, (input + data.length + 15) & ~15],
|
|
178
|
+
[16, 2], [24, input], [28, data.length], [32, 64], [36, key.length], [40, output], [44, k]]) header.setUint32(offset, value, true);
|
|
179
|
+
frame.set(key, 64); frame.set(data, input);
|
|
180
|
+
if (!signing) frame.set(signature, output);
|
|
181
|
+
const status = moduleExports[signing ? 'pulse_crypto_rs256_sign' : 'pulse_crypto_rs256_verify'](pointer, capacity);
|
|
182
|
+
if (!signing) return Object.freeze({ status: ({ 1: 'valid', 0: 'invalid-authenticator', '-1': 'invalid-key', '-2': 'invalid-input' })[status] || 'realization-failure' });
|
|
183
|
+
if (status === -1) throw invalidKey();
|
|
184
|
+
if (status !== 1) throw new TypeError('Native RS256 signing failed.');
|
|
185
|
+
return frame.slice(output, output + k);
|
|
186
|
+
} finally { frame.fill(0); new Uint8Array(memory.buffer, 0, 65536).fill(0); }
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({ sign: (key, data) => run(key, data), verify: (key, data, signature) => run(key, data, signature) });
|
|
189
|
+
}
|
|
190
|
+
|
|
127
191
|
module.exports = Object.freeze({
|
|
192
|
+
bindNativeEs256Signer,
|
|
193
|
+
bindNativeRs256,
|
|
128
194
|
CRYPTO_GUEST_SOURCE_CONTRACT_VERSION,
|
|
129
195
|
GUEST_UNIT_CONTRIBUTION_VERSION,
|
|
130
196
|
pulseHmacAssemblyScriptSource,
|
package/src/provider.cjs
CHANGED
|
@@ -108,5 +108,75 @@ function createJavascriptTextDigest(options = {}) {
|
|
|
108
108
|
cryptoVerifier: selected, cryptoRealization: selected?.realization });
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
|
|
112
|
+
function bindJavascriptEs256Signer(subtle = globalThis.crypto?.subtle) {
|
|
113
|
+
if (!subtle || typeof subtle.importKey !== 'function' || typeof subtle.sign !== 'function'
|
|
114
|
+
|| typeof subtle.verify !== 'function') throw new TypeError('Selected ES256 signing realization is unavailable.');
|
|
115
|
+
const encode = bytes => {
|
|
116
|
+
let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
117
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
118
|
+
};
|
|
119
|
+
const invalidKey = () => Object.assign(new TypeError('Invalid P-256 signing key.'), { code: 'PULSE_CRYPTO_KEY_INVALID' });
|
|
120
|
+
return Object.freeze({ bytes: Object.freeze({ async es256Sign(key, data) {
|
|
121
|
+
if (!(key instanceof Uint8Array) || key.length !== 96) throw invalidKey();
|
|
122
|
+
if (!(data instanceof Uint8Array) || data.length > 16340) throw new TypeError('ES256 input exceeds its byte contract.');
|
|
123
|
+
const jwk = { kty: 'EC', crv: 'P-256', x: encode(key.subarray(0, 32)), y: encode(key.subarray(32, 64)), d: encode(key.subarray(64)) };
|
|
124
|
+
let signingKey, verifyingKey;
|
|
125
|
+
try {
|
|
126
|
+
signingKey = await subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
|
|
127
|
+
verifyingKey = await subtle.importKey('jwk', { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y },
|
|
128
|
+
{ name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
|
|
129
|
+
} catch { throw invalidKey(); }
|
|
130
|
+
const signature = new Uint8Array(await subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, signingKey, data));
|
|
131
|
+
try {
|
|
132
|
+
if (signature.length !== 64 || !await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, verifyingKey, signature, data)) {
|
|
133
|
+
throw invalidKey();
|
|
134
|
+
}
|
|
135
|
+
return signature.slice();
|
|
136
|
+
} finally { signature.fill(0); }
|
|
137
|
+
} }) });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Metadata parsing remains JWT-owned. This adapter normalizes only RSA integers.
|
|
141
|
+
async function rsaPrivateKeyBytes(components) {
|
|
142
|
+
return (await import('../dist/internal/rsa.js')).rsaKeyBytes(components, true);
|
|
143
|
+
}
|
|
144
|
+
function bindJavascriptRs256Signer(subtle = globalThis.crypto?.subtle) {
|
|
145
|
+
if (!subtle || typeof subtle.importKey !== 'function' || typeof subtle.sign !== 'function'
|
|
146
|
+
|| typeof subtle.verify !== 'function') throw new TypeError('Selected RS256 signing realization is unavailable.');
|
|
147
|
+
const invalidKey = () => Object.assign(new TypeError('Invalid RSA signing key.'), { code: 'PULSE_CRYPTO_KEY_INVALID' });
|
|
148
|
+
return Object.freeze({ bytes: Object.freeze({ async rs256Sign(key, data) {
|
|
149
|
+
const { rsaWireJwk, RSA_DATA_BYTES_MAXIMUM } = await import('../dist/internal/rsa.js');
|
|
150
|
+
if (!(data instanceof Uint8Array) || data.length > RSA_DATA_BYTES_MAXIMUM) throw new TypeError('RS256 input exceeds its byte contract.');
|
|
151
|
+
let jwk, signingKey, verifyingKey;
|
|
152
|
+
const algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
|
|
153
|
+
try {
|
|
154
|
+
jwk = rsaWireJwk(key, true);
|
|
155
|
+
// Web Crypto implementations need not check every redundant CRT field.
|
|
156
|
+
// Check consistency explicitly at import. JavaScript BigInt has no
|
|
157
|
+
// constant-time guarantee; private exponentiation remains Web Crypto.
|
|
158
|
+
const integer = text => {
|
|
159
|
+
const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));
|
|
160
|
+
let value = 0n; for (const byte of binary) value = value * 256n + BigInt(byte.charCodeAt(0));
|
|
161
|
+
return value;
|
|
162
|
+
};
|
|
163
|
+
const { n, e, d, p, q, dp, dq, qi } = Object.fromEntries(['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'].map(name => [name, integer(jwk[name])]));
|
|
164
|
+
const k = key.length > 8 ? new DataView(key.buffer, key.byteOffset, 4).getUint32(0, true) : 0;
|
|
165
|
+
const halfMinimum = 1n << BigInt(k * 4 - 1);
|
|
166
|
+
if (p < halfMinimum || q < halfMinimum || !(p & q & 1n) || p === q || p*q !== n
|
|
167
|
+
|| d <= 0n || d >= n || d % (p-1n) !== dp || d % (q-1n) !== dq
|
|
168
|
+
|| e*dp % (p-1n) !== 1n || e*dq % (q-1n) !== 1n || qi <= 0n || qi >= p || q*qi % p !== 1n) throw invalidKey();
|
|
169
|
+
signingKey = await subtle.importKey('jwk', jwk, algorithm, false, ['sign']);
|
|
170
|
+
verifyingKey = await subtle.importKey('jwk', { kty: 'RSA', n: jwk.n, e: jwk.e }, algorithm, false, ['verify']);
|
|
171
|
+
} catch { throw invalidKey(); }
|
|
172
|
+
const signature = new Uint8Array(await subtle.sign(algorithm, signingKey, data));
|
|
173
|
+
try {
|
|
174
|
+
if (signature.length !== new DataView(key.buffer, key.byteOffset, 4).getUint32(0, true)
|
|
175
|
+
|| !await subtle.verify(algorithm, verifyingKey, signature, data)) throw invalidKey();
|
|
176
|
+
return signature.slice();
|
|
177
|
+
} finally { signature.fill(0); }
|
|
178
|
+
} }) });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = Object.freeze({ rsaPrivateKeyBytes, bindJavascriptRs256Signer, bindJavascriptEs256Signer, IMPLEMENTATIONS, bindJavascriptDigestMac, DIGEST_TEXT_MAX_BYTES,
|
|
112
182
|
textDigestByteLength, normalizeTextDigestResult, executeTextDigest, createJavascriptTextDigest });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TextDigestResult } from './digest.js';
|
|
2
|
+
export declare function normalizeTextDigestResult(value: unknown): TextDigestResult;
|
|
3
|
+
declare const provider: { rsaPrivateKeyBytes: typeof rsaPrivateKeyBytes; normalizeTextDigestResult: typeof normalizeTextDigestResult };
|
|
4
|
+
export default provider;
|
|
5
|
+
|
|
6
|
+
export declare function rsaPrivateKeyBytes(components: Readonly<Record<string, unknown>>): Promise<Uint8Array>;
|