Skip to content

Commit e574a15

Browse files
onspeedhpclaude
andcommitted
feat(auth): port Secp256r1 auth to clientDataJSON-embedding format
Byte-identical with lazorkit-protocol/program/src/auth/secp256r1/. Replaces the older typeAndFlags format (which reconstructed clientDataJSON server-side from a single byte at auth_payload[13]) with the format that embeds the full raw clientDataJSON in the auth payload. Required for slot-share strategy: a wallet created on either binary (commercial or foundation) must remain verifiable after binary swap. Both binaries now share the same auth verification logic + on-chain authority account layout. Verification: - 58/65 vitest E2E tests pass against live validator (up from 12/65 before port). The 7 remaining failures are in 08-deferred.test.ts and reflect a test-side bug (missing expiryBuf in signedPayload), addressed in P5.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5d808b3 commit e574a15

3 files changed

Lines changed: 924 additions & 169 deletions

File tree

program/src/auth/secp256r1/introspection.rs

Lines changed: 247 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,18 @@ impl Secp256r1SignatureOffsets {
6161
}
6262

6363
/// Verify the secp256r1 instruction data contains the expected signature and
64-
/// public key. This also validates that the secp256r1 precompile offsets point
65-
/// to the expected locations, ensuring proper data alignment.
64+
/// public key. Also validates that the secp256r1 precompile offsets point to
65+
/// the expected locations, ensuring proper data alignment.
66+
///
67+
/// The expected precompile message is passed as TWO slices — the
68+
/// authenticator_data and the clientDataJSON hash — which are concatenated
69+
/// by the on-chain secp256r1 precompile as its signed message. Accepting
70+
/// two slices here lets the caller skip a Vec allocation for the concat.
6671
pub fn verify_secp256r1_instruction_data(
6772
instruction_data: &[u8],
6873
expected_pubkey: &[u8; 33],
69-
expected_message: &[u8],
74+
auth_data: &[u8],
75+
client_data_hash: &[u8; 32],
7076
) -> Result<(), ProgramError> {
7177
// Minimum check: must have at least the header and offsets
7278
if instruction_data.len() < DATA_START {
@@ -111,25 +117,257 @@ pub fn verify_secp256r1_instruction_data(
111117
if offsets.message_data_offset as usize != MESSAGE_DATA_OFFSET {
112118
return Err(AuthError::InvalidInstruction.into());
113119
}
114-
if offsets.message_data_size as usize != expected_message.len() {
120+
let expected_msg_len = auth_data.len() + client_data_hash.len();
121+
if offsets.message_data_size as usize != expected_msg_len {
115122
return Err(AuthError::InvalidInstruction.into());
116123
}
117124

118125
// Dynamic length check: instruction must contain the full message
119-
if instruction_data.len() < MESSAGE_DATA_OFFSET + expected_message.len() {
126+
if instruction_data.len() < MESSAGE_DATA_OFFSET + expected_msg_len {
120127
return Err(AuthError::InvalidInstruction.into());
121128
}
122129

123130
let pubkey_data = &instruction_data
124131
[PUBKEY_DATA_OFFSET..PUBKEY_DATA_OFFSET + COMPRESSED_PUBKEY_SERIALIZED_SIZE];
125-
let message_data =
126-
&instruction_data[MESSAGE_DATA_OFFSET..MESSAGE_DATA_OFFSET + expected_message.len()];
127-
128132
if pubkey_data != expected_pubkey {
129133
return Err(AuthError::InvalidPubkey.into());
130134
}
131-
if message_data != expected_message {
135+
136+
// Compare the precompile's message area against the two caller-supplied
137+
// slices piecewise — no concat, no allocation.
138+
let msg_auth = &instruction_data[MESSAGE_DATA_OFFSET..MESSAGE_DATA_OFFSET + auth_data.len()];
139+
if msg_auth != auth_data {
140+
return Err(AuthError::InvalidMessageHash.into());
141+
}
142+
let hash_start = MESSAGE_DATA_OFFSET + auth_data.len();
143+
let msg_hash = &instruction_data[hash_start..hash_start + client_data_hash.len()];
144+
if msg_hash != client_data_hash {
132145
return Err(AuthError::InvalidMessageHash.into());
133146
}
134147
Ok(())
135148
}
149+
150+
#[cfg(test)]
151+
mod tests {
152+
use super::*;
153+
154+
/// Helper: build valid secp256r1 precompile instruction data with the standard layout.
155+
fn build_precompile_ix_data(
156+
pubkey: &[u8; 33],
157+
signature: &[u8; 64],
158+
message: &[u8],
159+
) -> Vec<u8> {
160+
let total_len = DATA_START + 64 + 33 + 1 + message.len();
161+
let mut data = vec![0u8; total_len];
162+
163+
// Header
164+
data[0] = 1; // num_signatures
165+
data[1] = 0; // padding
166+
167+
// Offsets (little-endian)
168+
data[2..4].copy_from_slice(&(SIGNATURE_DATA_OFFSET as u16).to_le_bytes());
169+
data[4..6].copy_from_slice(&0xFFFFu16.to_le_bytes()); // sig ix index
170+
data[6..8].copy_from_slice(&(PUBKEY_DATA_OFFSET as u16).to_le_bytes());
171+
data[8..10].copy_from_slice(&0xFFFFu16.to_le_bytes()); // pubkey ix index
172+
data[10..12].copy_from_slice(&(MESSAGE_DATA_OFFSET as u16).to_le_bytes());
173+
data[12..14].copy_from_slice(&(message.len() as u16).to_le_bytes()); // msg size
174+
data[14..16].copy_from_slice(&0xFFFFu16.to_le_bytes()); // msg ix index
175+
176+
// Data
177+
data[SIGNATURE_DATA_OFFSET..SIGNATURE_DATA_OFFSET + 64].copy_from_slice(signature);
178+
data[PUBKEY_DATA_OFFSET..PUBKEY_DATA_OFFSET + 33].copy_from_slice(pubkey);
179+
// Byte at offset 113 is alignment padding (zero)
180+
data[MESSAGE_DATA_OFFSET..MESSAGE_DATA_OFFSET + message.len()]
181+
.copy_from_slice(message);
182+
183+
data
184+
}
185+
186+
#[test]
187+
fn test_verify_valid_instruction_data() {
188+
let pubkey = [0x02; 33];
189+
let signature = [0xAB; 64];
190+
let message = [0x11; 32];
191+
192+
let ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
193+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_ok());
194+
}
195+
196+
#[test]
197+
fn test_verify_variable_length_message() {
198+
// Mode 1 messages are authenticatorData(37+) + clientDataJsonHash(32) = 69+ bytes.
199+
// We split into the two halves exactly like the caller does post-refactor.
200+
let pubkey = [0x03; 33];
201+
let signature = [0xCD; 64];
202+
let message = [0x22; 69];
203+
204+
let ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
205+
let auth_data: &[u8] = &message[..37];
206+
let client_data_hash: &[u8; 32] = &message[37..].try_into().unwrap();
207+
assert!(
208+
verify_secp256r1_instruction_data(&ix_data, &pubkey, auth_data, client_data_hash)
209+
.is_ok()
210+
);
211+
}
212+
213+
#[test]
214+
fn test_verify_rejects_wrong_pubkey() {
215+
let pubkey = [0x02; 33];
216+
let wrong_pubkey = [0x03; 33];
217+
let signature = [0xAB; 64];
218+
let message = [0x11; 32];
219+
220+
let ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
221+
let err =
222+
verify_secp256r1_instruction_data(&ix_data, &wrong_pubkey, &[], &message).unwrap_err();
223+
assert_eq!(err, AuthError::InvalidPubkey.into());
224+
}
225+
226+
#[test]
227+
fn test_verify_rejects_wrong_message() {
228+
let pubkey = [0x02; 33];
229+
let signature = [0xAB; 64];
230+
let message = [0x11; 32];
231+
let wrong_message = [0x22; 32];
232+
233+
let ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
234+
let err =
235+
verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &wrong_message).unwrap_err();
236+
assert_eq!(err, AuthError::InvalidMessageHash.into());
237+
}
238+
239+
#[test]
240+
fn test_verify_rejects_zero_signatures() {
241+
let pubkey = [0x02; 33];
242+
let signature = [0xAB; 64];
243+
let message = [0x11; 32];
244+
245+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
246+
ix_data[0] = 0; // zero signatures
247+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
248+
}
249+
250+
#[test]
251+
fn test_verify_rejects_multiple_signatures() {
252+
let pubkey = [0x02; 33];
253+
let signature = [0xAB; 64];
254+
let message = [0x11; 32];
255+
256+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
257+
ix_data[0] = 2; // two signatures
258+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
259+
}
260+
261+
#[test]
262+
fn test_verify_rejects_cross_instruction_sig_index() {
263+
let pubkey = [0x02; 33];
264+
let signature = [0xAB; 64];
265+
let message = [0x11; 32];
266+
267+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
268+
// Set signature_instruction_index to 0 instead of 0xFFFF
269+
ix_data[4..6].copy_from_slice(&0u16.to_le_bytes());
270+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
271+
}
272+
273+
#[test]
274+
fn test_verify_rejects_cross_instruction_pubkey_index() {
275+
let pubkey = [0x02; 33];
276+
let signature = [0xAB; 64];
277+
let message = [0x11; 32];
278+
279+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
280+
// Set public_key_instruction_index to 1 instead of 0xFFFF
281+
ix_data[8..10].copy_from_slice(&1u16.to_le_bytes());
282+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
283+
}
284+
285+
#[test]
286+
fn test_verify_rejects_cross_instruction_msg_index() {
287+
let pubkey = [0x02; 33];
288+
let signature = [0xAB; 64];
289+
let message = [0x11; 32];
290+
291+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
292+
// Set message_instruction_index to 0 instead of 0xFFFF
293+
ix_data[14..16].copy_from_slice(&0u16.to_le_bytes());
294+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
295+
}
296+
297+
#[test]
298+
fn test_verify_rejects_wrong_pubkey_offset() {
299+
let pubkey = [0x02; 33];
300+
let signature = [0xAB; 64];
301+
let message = [0x11; 32];
302+
303+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
304+
// Tamper pubkey_offset to point elsewhere
305+
ix_data[6..8].copy_from_slice(&200u16.to_le_bytes());
306+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
307+
}
308+
309+
#[test]
310+
fn test_verify_rejects_wrong_message_offset() {
311+
let pubkey = [0x02; 33];
312+
let signature = [0xAB; 64];
313+
let message = [0x11; 32];
314+
315+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
316+
// Tamper message_data_offset
317+
ix_data[10..12].copy_from_slice(&50u16.to_le_bytes());
318+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
319+
}
320+
321+
#[test]
322+
fn test_verify_rejects_wrong_signature_offset() {
323+
let pubkey = [0x02; 33];
324+
let signature = [0xAB; 64];
325+
let message = [0x11; 32];
326+
327+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
328+
// Tamper signature_offset
329+
ix_data[2..4].copy_from_slice(&100u16.to_le_bytes());
330+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
331+
}
332+
333+
#[test]
334+
fn test_verify_rejects_message_size_mismatch() {
335+
let pubkey = [0x02; 33];
336+
let signature = [0xAB; 64];
337+
let message = [0x11; 32];
338+
339+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
340+
// Set message_data_size to wrong value
341+
ix_data[12..14].copy_from_slice(&64u16.to_le_bytes());
342+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
343+
}
344+
345+
#[test]
346+
fn test_verify_rejects_too_short_data() {
347+
let pubkey = [0x02; 33];
348+
let message = [0x11; 32];
349+
350+
// Only 2 bytes — way too short
351+
assert!(verify_secp256r1_instruction_data(&[0x01, 0x00], &pubkey, &[], &message).is_err());
352+
}
353+
354+
#[test]
355+
fn test_verify_rejects_truncated_message_area() {
356+
let pubkey = [0x02; 33];
357+
let signature = [0xAB; 64];
358+
let message = [0x11; 32];
359+
360+
let mut ix_data = build_precompile_ix_data(&pubkey, &signature, &message);
361+
// Truncate — remove last 10 bytes so message area is incomplete
362+
ix_data.truncate(ix_data.len() - 10);
363+
assert!(verify_secp256r1_instruction_data(&ix_data, &pubkey, &[], &message).is_err());
364+
}
365+
366+
#[test]
367+
fn test_offsets_constants_are_consistent() {
368+
assert_eq!(DATA_START, 16); // 2 header + 14 offsets
369+
assert_eq!(SIGNATURE_DATA_OFFSET, 16);
370+
assert_eq!(PUBKEY_DATA_OFFSET, 16 + 64); // 80
371+
assert_eq!(MESSAGE_DATA_OFFSET, 80 + 33 + 1); // 114
372+
}
373+
}

0 commit comments

Comments
 (0)