v0.2.5-pre.003

This commit is contained in:
2026-08-19 10:52:21 +02:00
parent bcda6db11f
commit c6794af05b
20 changed files with 2721 additions and 37 deletions

View File

@@ -0,0 +1,71 @@
// file: crates/ksp-wallet-lib/unit_tests/transcript.rs
// version: 1
use base64::Engine as _;
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
#[test]
fn state_transcript_matches_the_frozen_v1_wire_only_canary() -> ksp_core_lib::Result<()> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(envelope) => envelope,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.state_transcript());
assert_eq!(
encoded,
"S1NQV0FMTEVULVYxLVNUQVRFAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwAQAAAAAAAAAAEBABEAAAAAAAAABHZpZXcAEgAAAAAAAAAQICEiIyQlJicoKSorLC0uLwEAAAAAAAAAABAQERITFBUWFxgZGhscHR4fAQEAAAAAAAAABW93bmVyAQIAAAAAAAAACGFyZ29uMmlkAQMAAAAAAAAABAAAABMBBAAAAAAAAAAEAAEAAAEFAAAAAAAAAAQAAAADAQYAAAAAAAAABAAAAAEBBwAAAAAAAAAQMDEyMzQ1Njc4OTo7PD0-PwEIAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDUBCQAAAAAAAAAYUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnAQoAAAAAAAAAIICBgoOEhYaHiImKi4yNjo-QkZKTlJWWl5iZmpucnZ6fAgAAAAAAAAAADW93bmVyLWNvbnRyb2wCAQAAAAAAAAAEAAAAAQICAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDUCAwAAAAAAAAAYkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanAgQAAAAAAAAAMMDBwsPExcbHyMnKy8zNzs_Q0dLT1NXW19jZ2tvc3d7f4OHi4-Tl5ufo6err7O3u7wIAAAAAAAAAAAhtZXRhZGF0YQIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQIDAAAAAAAAABioqaqrrK2ur7CxsrO0tba3uLm6u7y9vr8CBAAAAAAAAAAwEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4_AgAAAAAAAAAABnNlY3JldAIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQIDAAAAAAAAABjAwcLDxMXGx8jJysvMzc7P0NHS09TV1tcCBAAAAAAAAABQQEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8FAAAAAAAAAAAHZWQyNTUxOQ"
);
return std::result::Result::Ok(());
}
#[test]
fn slot_aad_matches_owner_and_view_canaries() -> ksp_core_lib::Result<()> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(envelope) => envelope,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.owner_slot_aad());
assert_eq!(
owner,
"S1NQV0FMTEVULVYxLUFBRC1PV05FUi1TTE9UAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwEAAAAAAAAAABAQERITFBUWFxgZGhscHR4fAQEAAAAAAAAABW93bmVyAQIAAAAAAAAACGFyZ29uMmlkAQMAAAAAAAAABAAAABMBBAAAAAAAAAAEAAEAAAEFAAAAAAAAAAQAAAADAQYAAAAAAAAABAAAAAEBBwAAAAAAAAAQMDEyMzQ1Njc4OTo7PD0-PwEIAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDU"
);
let view_aad_result = envelope.view_slot_aad();
assert!(view_aad_result.is_some());
let view_aad = match view_aad_result {
std::option::Option::Some(view_aad) => view_aad,
std::option::Option::None => return std::result::Result::Ok(()),
};
let view = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(view_aad);
assert_eq!(
view,
"S1NQV0FMTEVULVYxLUFBRC1WSUVXLVNMT1QAAAEAAAAAAAAACUtTUFdBTExFVAACAAAAAAAAAAQAAAABAAMAAAAAAAAAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fAQAAAAAAAAAAECAhIiMkJSYnKCkqKywtLi8BAQAAAAAAAAAEdmlldwECAAAAAAAAAAhhcmdvbjJpZAEDAAAAAAAAAAQAAAATAQQAAAAAAAAABAAAgAABBQAAAAAAAAAEAAAABAEGAAAAAAAAAAQAAAABAQcAAAAAAAAAEEBBQkNERUZHSElKS0xNTk8BCAAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1"
);
return std::result::Result::Ok(());
}
#[test]
fn compartment_aad_is_domain_separated_by_kind() -> ksp_core_lib::Result<()> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(envelope) => envelope,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_control = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl));
let metadata = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata));
let secret = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret));
assert_eq!(
owner_control,
"S1NQV0FMTEVULVYxLUFBRC1PV05FUi1DT05UUk9MAAABAAAAAAAAAAlLU1BXQUxMRVQAAgAAAAAAAAAEAAAAAQADAAAAAAAAACAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwIAAAAAAAAAAA1vd25lci1jb250cm9sAgEAAAAAAAAABAAAAAECAgAAAAAAAAASeGNoYWNoYTIwLXBvbHkxMzA1"
);
assert_eq!(
metadata,
"S1NQV0FMTEVULVYxLUFBRC1NRVRBREFUQQAAAQAAAAAAAAAJS1NQV0FMTEVUAAIAAAAAAAAABAAAAAEAAwAAAAAAAAAgAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8CAAAAAAAAAAAIbWV0YWRhdGECAQAAAAAAAAAEAAAAAQICAAAAAAAAABJ4Y2hhY2hhMjAtcG9seTEzMDU"
);
assert_eq!(
secret,
"S1NQV0FMTEVULVYxLUFBRC1TRUNSRVQAAAEAAAAAAAAACUtTUFdBTExFVAACAAAAAAAAAAQAAAABAAMAAAAAAAAAIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fAgAAAAAAAAAABnNlY3JldAIBAAAAAAAAAAQAAAABAgIAAAAAAAAAEnhjaGFjaGEyMC1wb2x5MTMwNQ"
);
assert_ne!(owner_control, metadata);
assert_ne!(metadata, secret);
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,126 @@
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
// version: 1
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
#[test]
fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Result<()> {
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(parsed.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
assert!(parsed.view_descriptor().enabled());
assert_eq!(parsed.owner_slot().role(), super::WalletKeySlotRoleV1::Owner);
assert_eq!(parsed.view_slot().map(super::WalletKeySlotV1::role), std::option::Option::Some(super::WalletKeySlotRoleV1::View));
assert_eq!(parsed.owner_control().payload_version(), 1);
assert_eq!(parsed.metadata().payload_version(), 1);
assert_eq!(parsed.secret().payload_version(), 1);
let serialized = match parsed.to_json_bytes() {
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let reparsed = match super::KspWalletEnvelopeV1::parse_json(serialized.as_slice()) {
std::result::Result::Ok(reparsed) => reparsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(parsed, reparsed);
return std::result::Result::Ok(());
}
#[test]
fn unknown_format_version_is_rejected_before_v1_shape_validation() {
let source = std::string::String::from_utf8_lossy(FIXTURE).replace("\"format_version\": 1", "\"format_version\": 2");
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED);
}
#[test]
fn unknown_top_level_field_is_rejected() {
let source = std::string::String::from_utf8_lossy(FIXTURE).replacen("{", "{\n \"unexpected\": true,", 1);
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
}
#[test]
fn padded_or_noncanonical_base64url_is_rejected() {
let source = std::string::String::from_utf8_lossy(FIXTURE)
.replace("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\"", "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\"");
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
}
#[test]
fn enabled_view_descriptor_must_match_the_view_slot() {
let source =
std::string::String::from_utf8_lossy(FIXTURE).replacen("\"slot_id\": \"ICEiIyQlJicoKSorLC0uLw\"", "\"slot_id\": \"EBESExQVFhcYGRobHB0eHw\"", 1);
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
}
#[test]
fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
let zero_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 0", 1);
let zero_result = super::KspWalletEnvelopeV1::parse_json(zero_source.as_bytes());
assert!(zero_result.is_err());
let zero_error = match zero_result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(zero_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
let high_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 1048577", 1);
let high_result = super::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
assert!(high_result.is_err());
let high_error = match high_result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(high_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
}
#[test]
fn oversized_document_is_rejected_before_json_or_crypto() {
let oversized = std::vec![b' '; crate::KSPWALLET_MAX_FILE_BYTES + 1];
let result = super::KspWalletEnvelopeV1::parse_json(oversized.as_slice());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
}
#[test]
fn envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> {
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rendered = format!("{parsed:?}");
assert!(rendered.contains("ciphertext_bytes"));
assert!(!rendered.contains("gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8"));
assert!(!rendered.contains("wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v"));
return std::result::Result::Ok(());
}