From e92d236f5d49bfac51620d8a29854d53849cc525 Mon Sep 17 00:00:00 2001 From: Suvam Mukherjee Date: Thu, 27 Jun 2024 23:38:31 -0400 Subject: [PATCH] Updating common annotated security key generation and validation in Rust (#57) * adding an end-to-end test * adding constants for long form and standard * adding validation * fixing failing test * adding long-form support * fixing generation * adding support for long-form, normalizing rust and c# common key generation and validation * fixes and cleanups * adding common key generator test * adding validation test * adding test * adding test * adding test --- .../IdentifiableSecrets.cs | 17 +- .../.vscode/launch.json | 24 -- .../src/end_to_end_tests.rs | 36 +++ .../src/identifiable_secrets_tests.rs | 178 ++++++++++++- src/security_utilities_rust/src/lib.rs | 1 + .../cross_company_correlating_id.rs | 1 - .../identifiable_secrets.rs | 236 +++++++++++++----- .../marvin.rs | 4 +- 8 files changed, 392 insertions(+), 105 deletions(-) delete mode 100644 src/security_utilities_rust/.vscode/launch.json create mode 100644 src/security_utilities_rust/src/end_to_end_tests.rs diff --git a/src/Microsoft.Security.Utilities.Core/IdentifiableSecrets.cs b/src/Microsoft.Security.Utilities.Core/IdentifiableSecrets.cs index fcb6c48..79c7e52 100644 --- a/src/Microsoft.Security.Utilities.Core/IdentifiableSecrets.cs +++ b/src/Microsoft.Security.Utilities.Core/IdentifiableSecrets.cs @@ -83,12 +83,6 @@ public static class IdentifiableSecrets bool longForm = key.Length == LongFormCommonAnnotatedKeySize; - string signature = key.Substring(CommonAnnotatedKey.ProviderFixedSignatureOffset, CommonAnnotatedKey.ProviderFixedSignatureLength); - - string alternate = char.IsUpper(signature[0]) - ? signature.ToLowerInvariant() - : signature.ToUpperInvariant(); - ulong checksumSeed = VersionTwoChecksumSeed; string componentToChecksum = key.Substring(0, CommonAnnotatedKey.ChecksumOffset); @@ -238,12 +232,17 @@ public static class IdentifiableSecrets throw new InvalidOperationException($"The key length is less than 86 characters: {key}"); } - key = key.Substring(0, 86); - key = $"{key}=="; + key = key.Substring(0, 85); + + // We use Q== as the suffix to keep the key format consistent with the usage in Rust. + // In C#, the base64 decoder can handle illegal base64 strings, but not in Rust. + key = $"{key}Q=="; } else { - key = $"{new string(testChar!.Value, 86)}=="; + // We use Q== as the suffix to keep the key format consistent with the usage in Rust. + // In C#, the base64 decoder can handle illegal base64 strings, but not in Rust. + key = $"{new string(testChar!.Value, 85)}Q=="; } try diff --git a/src/security_utilities_rust/.vscode/launch.json b/src/security_utilities_rust/.vscode/launch.json deleted file mode 100644 index efcdce3..0000000 --- a/src/security_utilities_rust/.vscode/launch.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'microsoft_security_utilities'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=microsoft_security_utilities" - ], - "filter": { - "name": "microsoft_security_utilities", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/src/security_utilities_rust/src/end_to_end_tests.rs b/src/security_utilities_rust/src/end_to_end_tests.rs new file mode 100644 index 0000000..23aa02b --- /dev/null +++ b/src/security_utilities_rust/src/end_to_end_tests.rs @@ -0,0 +1,36 @@ +#[cfg(test)] +use super::*; + +#[test] +fn generate_and_detect_common_annotated_key_test() { + let options = microsoft_security_utilities_core::identifiable_scans::ScanOptions::default(); + let mut scan = microsoft_security_utilities_core::identifiable_scans::Scan::new(options); + + let input = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_test_key( + microsoft_security_utilities_core::identifiable_secrets::VERSION_TWO_CHECKSUM_SEED.clone(), + "TEST", + true, + None, + None, + false, + Some('a') + ); + + let generated_input = input.clone().unwrap(); + let input_as_bytes = generated_input.as_bytes(); + scan.parse_bytes(input_as_bytes); + + let check = scan.possible_matches().first().unwrap(); + let scan_match = check.matches_bytes(input_as_bytes, true); + assert!(scan_match.is_some(), "identifiable_scan: at least one match found"); + + let scan_match = scan_match.unwrap(); + assert_eq!(generated_input, scan_match.text(), "identifiable_scan: matched string equals generated input"); + + let validation_result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key( + &generated_input, + "TEST" + ); + + assert_eq!(validation_result, true, "checksum validation of generated input passes"); +} \ No newline at end of file diff --git a/src/security_utilities_rust/src/identifiable_secrets_tests.rs b/src/security_utilities_rust/src/identifiable_secrets_tests.rs index ae2ccd4..be4330f 100644 --- a/src/security_utilities_rust/src/identifiable_secrets_tests.rs +++ b/src/security_utilities_rust/src/identifiable_secrets_tests.rs @@ -3,14 +3,165 @@ #[cfg(test)] use super::*; -use std::{collections::HashSet, hash::Hash}; -use base64::alphabet; +use std::{collections::HashSet}; use rand::prelude::*; use rand_chacha::ChaCha20Rng; use uuid::Uuid; static S_BASE62_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; +#[test] +fn identifiable_secrets_try_validate_common_annotated_key_generate_common_annotated_key_long_form() { + for &long_form in &[true, false] { + let valid_signature = "ABCD"; + let valid_key = microsoft_security_utilities_core::identifiable_secrets:: + generate_common_annotated_key( + valid_signature, + true, + Some(&vec![0; 9]), + Some(&vec![0; 3]), + long_form.clone(), + None + ); + + let valid_key = valid_key.unwrap().clone(); + let valid_key_len = valid_key.len(); + + let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key( + &valid_key, + valid_signature, + ); + assert!(result, "a generated key should validate"); + + let expected_length = if long_form { + microsoft_security_utilities_core::identifiable_secrets::LONG_FORM_COMMON_ANNOTATED_KEY_SIZE + } else { + microsoft_security_utilities_core::identifiable_secrets::STANDARD_COMMON_ANNOTATED_KEY_SIZE + }; + + assert_eq!( + valid_key_len, + expected_length + ); + } +} + +#[test] +fn identifiable_secrets_try_validate_common_annotated_key_reject_null_empty_and_whitespace_arguments() { + let valid_signature = "ABCD"; + let valid_key = microsoft_security_utilities_core::identifiable_secrets:: + generate_common_annotated_key( + valid_signature, + true, + Some(&vec![0; 9]), + Some(&vec![0; 3]), + true, + None + ); + + let valid_key = valid_key.unwrap().clone(); + + let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&valid_key, valid_signature); + assert!(result, "a generated key should validate"); + + let args = vec![String::new(), String::from(" ")]; + for arg in args { + let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&arg, valid_signature); + assert!(!result, "{}", format!("the key {} is not a valid argument", arg)); + + let result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(&valid_key, &arg); + assert!(!result, "{}", format!("the signature '{}' is not a valid argument", arg.to_string())); + } +} + +#[test] +fn identifiable_secrets_try_validate_common_annotated_key_reject_invalid_signatures() { + let valid_signature = "ABCD"; + let valid_key = microsoft_security_utilities_core::identifiable_secrets:: + generate_common_annotated_key( + valid_signature, + true, + Some(&vec![0; 9]), + Some(&vec![0; 3]), + true, + None + ); + + let valid_key = valid_key.unwrap().clone(); + + let result = microsoft_security_utilities_core::identifiable_secrets:: + try_validate_common_annotated_key(&valid_key, valid_signature); + assert!(result, "a generated key should validate"); + + let signatures = vec!["Z", "YY", "XXX", "WWWWW", "1AAA"]; + + for signature in signatures { + for &long_form in &[true, false] { + let action = + microsoft_security_utilities_core::identifiable_secrets:: + generate_common_annotated_key( + signature, + true, + Some(&vec![0; 9]), + Some(&vec![0; 3]), + long_form, + None + ); + + + assert!(action.is_err(), "{}", format!("the signature '{}' is not valid", signature)); + + let result = microsoft_security_utilities_core::identifiable_secrets:: + try_validate_common_annotated_key(&valid_key, signature); + assert!(!result, "{}", format!("'{}' is not a valid signature argument", signature)); + } + } +} + +#[test] +fn identifiable_secrets_try_validate_common_annotated_key_reject_invalid_key() { + let valid_signature = "Z123"; + let valid_key = microsoft_security_utilities_core::identifiable_secrets:: + generate_common_annotated_key( + valid_signature, + true, + Some(&vec![0; 9]), + Some(&vec![0; 3]), + true, + None + ); + + let valid_key = valid_key.unwrap().clone(); + + let result = microsoft_security_utilities_core::identifiable_secrets:: + try_validate_common_annotated_key(&valid_key, valid_signature); + assert!(result, "a generated key should validate"); + + let result = microsoft_security_utilities_core::identifiable_secrets:: + try_validate_common_annotated_key( + &format!("{}a", valid_key), + valid_signature, + ); + assert!(!result, "a key with an invalid length should not validate"); + + let result = microsoft_security_utilities_core::identifiable_secrets:: + try_validate_common_annotated_key( + &valid_key[1..], + valid_signature, + ); + assert!(!result, "a key with an invalid length should not validate"); +} + +#[test] +fn identifiable_secrets_validate_common_annotated_key_signature() { + for invalid_signature in ["AbAA", "aaaB", "1AAA"] { + assert!(matches!( + microsoft_security_utilities_core::identifiable_secrets::validate_common_annotated_key_signature(invalid_signature), + Err(_) + )); + } +} + #[test] fn identifiable_secrets_compute_checksum_seed_enforces_length_requirement() { @@ -42,7 +193,7 @@ fn identifiable_secrets_platform_annotated_security_keys() { { for k in 0..iterations { - let mut signature = format!("{:?}", Uuid::new_v4().simple()).chars().skip(1).take(4).collect::(); + let mut signature: String = format!("{:?}", Uuid::new_v4().simple()).chars().skip(1).take(4).collect::(); signature = format!("{}{}", alphabet.chars().nth(((keys_generated as i32) % (alphabet.len() as i32)) as usize).unwrap().to_string(), &signature[1..]); @@ -86,15 +237,24 @@ fn identifiable_secrets_platform_annotated_security_keys() { let provider_reserved_vec = provider_reserved.to_vec(); for &customer_managed in &[true, false] { - let key = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_key(&signature, customer_managed, Some(&platform_reserved_vec), Some(&provider_reserved_vec), None).unwrap(); + for &long_form in &[true, false] { + let mut cased_signature = signature.clone(); + if customer_managed { + cased_signature = cased_signature.to_uppercase(); + } else { + cased_signature = cased_signature.to_lowercase(); + } - let mut result = microsoft_security_utilities_core::identifiable_secrets::COMMON_ANNOTATED_KEY_REGEX.is_match(key.as_str()); - assert!(result, "the key '{}' should match the common annotated key regex", key); + let key = microsoft_security_utilities_core::identifiable_secrets::generate_common_annotated_key(&cased_signature, customer_managed, Some(&platform_reserved_vec), Some(&provider_reserved_vec), long_form, None).unwrap(); - result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(key.as_str(), &signature); - assert!(result, "the key '{}' should comprise an HIS v2-conformant pattern", key); + let mut result = microsoft_security_utilities_core::identifiable_secrets::COMMON_ANNOTATED_KEY_REGEX.is_match(key.as_str()); + assert!(result, "the key '{}' should match the common annotated key regex", key); - keys_generated += 1; + result = microsoft_security_utilities_core::identifiable_secrets::try_validate_common_annotated_key(key.as_str(), &cased_signature); + assert!(result, "the key '{}' should comprise an HIS v2-conformant pattern", key); + + keys_generated += 1; + } } } } diff --git a/src/security_utilities_rust/src/lib.rs b/src/security_utilities_rust/src/lib.rs index 53681f8..2dd5fab 100644 --- a/src/security_utilities_rust/src/lib.rs +++ b/src/security_utilities_rust/src/lib.rs @@ -3,6 +3,7 @@ pub mod microsoft_security_utilities_core; +mod end_to_end_tests; mod marvin_tests; mod identifiable_secrets_tests; mod cross_company_correlating_id_tests; diff --git a/src/security_utilities_rust/src/microsoft_security_utilities_core/cross_company_correlating_id.rs b/src/security_utilities_rust/src/microsoft_security_utilities_core/cross_company_correlating_id.rs index bf42e87..d05029a 100644 --- a/src/security_utilities_rust/src/microsoft_security_utilities_core/cross_company_correlating_id.rs +++ b/src/security_utilities_rust/src/microsoft_security_utilities_core/cross_company_correlating_id.rs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. use base64::{engine::general_purpose::STANDARD, Engine as _}; -use lazy_static::lazy_static; use sha2::{Sha256, Digest}; use std::fmt; use std::cell::RefCell; diff --git a/src/security_utilities_rust/src/microsoft_security_utilities_core/identifiable_secrets.rs b/src/security_utilities_rust/src/microsoft_security_utilities_core/identifiable_secrets.rs index 2bb44b0..988a70b 100644 --- a/src/security_utilities_rust/src/microsoft_security_utilities_core/identifiable_secrets.rs +++ b/src/security_utilities_rust/src/microsoft_security_utilities_core/identifiable_secrets.rs @@ -9,6 +9,7 @@ use lazy_static::lazy_static; use std::{mem}; use super::*; use rand::prelude::*; +use rand::RngCore; use rand_chacha::ChaCha20Rng; use regex::Regex; use substring::Substring; @@ -25,17 +26,54 @@ lazy_static! { pub static MAXIMUM_GENERATED_KEY_SIZE: u32 = 4096; pub static MINIMUM_GENERATED_KEY_SIZE: u32 = 24; +pub static STANDARD_COMMON_ANNOTATED_KEY_SIZE: usize = 84; +pub static LONG_FORM_COMMON_ANNOTATED_KEY_SIZE: usize = 88; +pub static COMMON_ANNOTATED_KEY_SIGNATURE: &str = "JQQJ99"; +pub static COMMON_ANNOTATED_DERIVED_KEY_SIGNATURE: &str = "JQQJ9D"; static BITS_IN_BYTES: i32 = 8; static BITS_IN_BASE64_CHARACTER: i32 = 6; static SIZE_OF_CHECKSUM_IN_BYTES: i32 = mem::size_of::() as i32; static COMMON_ANNOTATED_KEY_SIZE_IN_BYTES: usize = 63; -pub fn is_base62_encoding_char(ch: char) -> bool -{ - return (ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9'); +/// The offset to the encoded standard fixed signature ('JQQJ99' or 'JQQJ9D'). +pub static STANDARD_FIXED_SIGNATURE_OFFSET: usize = 52; + +/// The encoded length of the standard fixed signature ('JQQJ99' or 'JQQJ9D'). +pub static STANDARD_FIXED_SIGNATURE_LENGTH: usize = 6; + +/// The offset to the encoded character that denotes a derived ('D') +/// or standard ('9') common annotated security key. +pub static DERIVED_KEY_CHARACTER_OFFSET: usize = STANDARD_FIXED_SIGNATURE_OFFSET + STANDARD_FIXED_SIGNATURE_LENGTH - 1; + +/// The offset to the two-character encoded key creation date. +pub static DATE_OFFSET: usize = STANDARD_FIXED_SIGNATURE_OFFSET + STANDARD_FIXED_SIGNATURE_LENGTH; + +/// The encoded length of the creation date (a value such as 'AE'). +pub static DATE_LENGTH: usize = 2; + +/// The offset to the 12-character encoded platform-reserved data. +pub static PLATFORM_RESERVED_OFFSET: usize = DATE_OFFSET + DATE_LENGTH; + +/// The encoded length of the platform-reserved bytes. +pub static PLATFORM_RESERVED_LENGTH: usize = 12; + +/// The offset to the 4-character encoded provider-reserved data. +pub static PROVIDER_RESERVED_OFFSET: usize = PLATFORM_RESERVED_OFFSET + PLATFORM_RESERVED_LENGTH; + +/// The encoded length of the provider-reserved bytes. +pub static PROVIDER_RESERVED_LENGTH: usize = 4; + +/// The offset to the 4-character encoded provider fixed signature. +pub static PROVIDER_FIXED_SIGNATURE_OFFSET: usize = PROVIDER_RESERVED_OFFSET + PROVIDER_RESERVED_LENGTH; + +/// The encoded length of the provider fixed signature, e.g., 'AZEG'. +pub static PROVIDER_FIXED_SIGNATURE_LENGTH: usize = 4; + +pub static CHECKSUM_OFFSET: usize = PROVIDER_FIXED_SIGNATURE_OFFSET + PROVIDER_FIXED_SIGNATURE_LENGTH; + +pub fn is_base62_encoding_char(ch: char) -> bool { + ch.is_ascii_alphanumeric() } pub fn is_base64_encoding_char(ch: char) -> bool @@ -52,6 +90,46 @@ pub fn is_base64_url_encoding_char(ch: char) -> bool ch == '_'; } +pub fn try_validate_common_annotated_key(key: &str, base64_encoded_signature: &str) -> bool { + if key.is_empty() || key.trim().is_empty() { + return false; + } + + match validate_common_annotated_key_signature(base64_encoded_signature) { + Ok(_) => (), + Err(s) => { + println!("{}", s); + return false; + }, + }; + + if key.len() != STANDARD_COMMON_ANNOTATED_KEY_SIZE && key.len() != LONG_FORM_COMMON_ANNOTATED_KEY_SIZE { + return false; + } + + let long_form = key.len() == LONG_FORM_COMMON_ANNOTATED_KEY_SIZE; + + let checksum_seed = VERSION_TWO_CHECKSUM_SEED.clone(); + + let component_to_checksum = &key[..CHECKSUM_OFFSET]; + let checksum_text = &key[CHECKSUM_OFFSET..]; + + let key_bytes = general_purpose::STANDARD.decode(component_to_checksum).unwrap(); + + let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, key_bytes.len() as i32); + + let checksum_bytes = checksum.to_ne_bytes(); + + // A long-form has a full 4-byte checksum, while a standard form has only 3. + let encoded = general_purpose::STANDARD.encode(if long_form { + &checksum_bytes[..4] + } else { + &checksum_bytes[..3] + }); + + encoded == checksum_text +} + /// Generate a u64 an HIS v1 compliant checksum seed from a string literal /// that is 8 characters long and ends with at least one digit, e.g., 'ReadKey0', 'RWSeed00', /// etc. The checksum seed is used to initialize the Marvin32 algorithm to watermark a @@ -80,37 +158,18 @@ pub fn compute_his_v1_checksum_seed(versioned_key_kind: &str) -> u64 { result } -pub fn try_validate_common_annotated_key(key: &str, base64_encoded_signature: &str) -> bool { - let checksum_seed: u64 = VERSION_TWO_CHECKSUM_SEED.clone(); - - let key_bytes = general_purpose::STANDARD.decode(&key).unwrap(); - - assert_eq!(key_bytes.len(), COMMON_ANNOTATED_KEY_SIZE_IN_BYTES); - - let key_bytes_length = key_bytes.len(); - - let bytes_for_checksum = &key_bytes[..key_bytes_length - 3]; - let actual_checksum_bytes = &key_bytes[key_bytes_length - 3..key_bytes_length]; - - let computed_marvin = marvin::compute_hash32(bytes_for_checksum, checksum_seed, 0, bytes_for_checksum.len() as i32); - let computed_marvin_bytes = computed_marvin.to_ne_bytes(); - - // The HIS v2 standard requires a match for the first 3-bytes (24 bits) of the Marvin checksum. - actual_checksum_bytes[0] == computed_marvin_bytes[0] - && actual_checksum_bytes[1] == computed_marvin_bytes[1] - && actual_checksum_bytes[2] == computed_marvin_bytes[2] -} - pub fn generate_common_annotated_key(base64_encoded_signature: &str, customer_managed_key: bool, platform_reserved: Option<&[u8]>, provider_reserved: Option<&[u8]>, + long_form: bool, test_char: Option) -> Result { generate_common_annotated_test_key(VERSION_TWO_CHECKSUM_SEED.clone(), base64_encoded_signature, customer_managed_key, platform_reserved, provider_reserved, + long_form, test_char) } @@ -120,11 +179,17 @@ pub fn generate_common_annotated_test_key( customer_managed_key: bool, platform_reserved: Option<&[u8]>, provider_reserved: Option<&[u8]>, + long_form: bool, test_char: Option, ) -> Result { const PLATFORM_RESERVED_LENGTH: usize = 9; const PROVIDER_RESERVED_LENGTH: usize = 3; + match validate_common_annotated_key_signature(base64_encoded_signature) { + Ok(_) => base64_encoded_signature, + Err(s) => return Err(format!("Common Annotated Key generation failed due to: {}", s)), + }; + let platform_reserved = match platform_reserved { Some(reserved) if reserved.len() != PLATFORM_RESERVED_LENGTH => { return Err(format!( @@ -153,21 +218,33 @@ pub fn generate_common_annotated_test_key( base64_encoded_signature.to_lowercase() }; - let mut key = String::new(); + let mut key: String; loop { let key_length_in_bytes = 66; let mut key_bytes = vec![0; key_length_in_bytes]; if let Some(test_char) = test_char { - key = format!("{:86}==", test_char.to_string().repeat(86)); - } else { + key = format!("{:85}Q==", test_char.to_string().repeat(85)); + } + else { let mut rng = rand::thread_rng(); - rng.fill_bytes(&mut key_bytes); + rng.try_fill_bytes(&mut key_bytes).expect("Failed to generate random bytes."); + + key = base_62::encode(&key_bytes); + + if key.len() < 86 { + return Err(format!("The key length is less than 86 characters: {}", key)); + } + + key = key.substring(0, 85).to_string(); + key = format!("{}Q==", key); } - let key_string = general_purpose::STANDARD.encode(&key_bytes); - key_bytes = general_purpose::STANDARD.decode(&key_string).unwrap(); + key_bytes = match general_purpose::STANDARD.decode(&key) { + Ok(bytes) => bytes, + Err(_) => return Err(format!("Key could not be decoded: {}.", key)), + }; let j_bits = b'J' - b'A'; let q_bits = b'Q' - b'A'; @@ -177,61 +254,66 @@ pub fn generate_common_annotated_test_key( let key_bytes_length = key_bytes.len(); - key_bytes[key_bytes_length - 27] = reserved_bytes[2]; - key_bytes[key_bytes_length - 26] = reserved_bytes[1]; - key_bytes[key_bytes_length - 25] = reserved_bytes[0]; + key_bytes[key_bytes_length - 25] = reserved_bytes[2]; + key_bytes[key_bytes_length - 24] = reserved_bytes[1]; + key_bytes[key_bytes_length - 23] = reserved_bytes[0]; + // Simplistic timestamp computation. let years_since_2024 = (chrono::Utc::now().year() - 2024) as u8; let zero_indexed_month = (chrono::Utc::now().month() - 1) as u8; let metadata: i32 = (61 << 18) | (61 << 12) | (years_since_2024 << 6) as i32 | zero_indexed_month as i32; let metadata_bytes = metadata.to_ne_bytes(); - key_bytes[key_bytes_length - 24] = metadata_bytes[2]; - key_bytes[key_bytes_length - 23] = metadata_bytes[1]; - key_bytes[key_bytes_length - 22] = metadata_bytes[0]; + key_bytes[key_bytes_length - 22] = metadata_bytes[2]; + key_bytes[key_bytes_length - 21] = metadata_bytes[1]; + key_bytes[key_bytes_length - 20] = metadata_bytes[0]; - key_bytes[key_bytes_length - 21] = platform_reserved[0]; - key_bytes[key_bytes_length - 20] = platform_reserved[1]; - key_bytes[key_bytes_length - 19] = platform_reserved[2]; - key_bytes[key_bytes_length - 18] = platform_reserved[3]; - key_bytes[key_bytes_length - 17] = platform_reserved[4]; - key_bytes[key_bytes_length - 16] = platform_reserved[5]; - key_bytes[key_bytes_length - 15] = platform_reserved[6]; - key_bytes[key_bytes_length - 14] = platform_reserved[7]; - key_bytes[key_bytes_length - 13] = platform_reserved[8]; + key_bytes[key_bytes_length - 19] = platform_reserved[0]; + key_bytes[key_bytes_length - 18] = platform_reserved[1]; + key_bytes[key_bytes_length - 17] = platform_reserved[2]; + key_bytes[key_bytes_length - 16] = platform_reserved[3]; + key_bytes[key_bytes_length - 15] = platform_reserved[4]; + key_bytes[key_bytes_length - 14] = platform_reserved[5]; + key_bytes[key_bytes_length - 13] = platform_reserved[6]; + key_bytes[key_bytes_length - 12] = platform_reserved[7]; + key_bytes[key_bytes_length - 11] = platform_reserved[8]; - key_bytes[key_bytes_length - 12] = provider_reserved[0]; - key_bytes[key_bytes_length - 11] = provider_reserved[1]; - key_bytes[key_bytes_length - 10] = provider_reserved[2]; + key_bytes[key_bytes_length - 10] = provider_reserved[0]; + key_bytes[key_bytes_length - 9] = provider_reserved[1]; + key_bytes[key_bytes_length - 8] = provider_reserved[2]; - let signature_offset = key_bytes_length - 9; + let signature_offset = key_bytes_length - 7; let sig_bytes = general_purpose::STANDARD.decode(&base64_encoded_signature).unwrap(); for i in 0..sig_bytes.len() { key_bytes[signature_offset + i] = sig_bytes[i]; } - let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, (key_bytes_length - 6) as i32); + let checksum = marvin::compute_hash32(&key_bytes, checksum_seed, 0, (key_bytes_length - 4) as i32); let checksum_bytes = checksum.to_ne_bytes(); - key_bytes[key_bytes_length - 6] = checksum_bytes[0]; - key_bytes[key_bytes_length - 5] = checksum_bytes[1]; - key_bytes[key_bytes_length - 4] = checksum_bytes[2]; - key_bytes[key_bytes_length - 3] = checksum_bytes[3]; + key_bytes[key_bytes_length - 4] = checksum_bytes[0]; + key_bytes[key_bytes_length - 3] = checksum_bytes[1]; + key_bytes[key_bytes_length - 2] = checksum_bytes[2]; + key_bytes[key_bytes_length - 1] = checksum_bytes[3]; - key = general_purpose::STANDARD.encode(&key_bytes[..COMMON_ANNOTATED_KEY_SIZE_IN_BYTES]); + key = general_purpose::STANDARD.encode(&key_bytes); // The HIS v2 standard requires that there be no special characters in the generated key. if !key.contains('+') && !key.contains('/') { - break; + if !long_form { + key = key.substring(0, key.len() - 4).to_string(); + } + return Ok(key); } else if test_char.is_some() { + // We could not produce a valid test key given the current signature, + // checksum seed, reserved bits and specified test character. key = String::new(); break; } } - Ok(key) } @@ -474,6 +556,40 @@ fn generate_base64_key_helper(checksum_seed: u64, encode_for_url); } +pub fn validate_common_annotated_key_signature(base64_encoded_signature: &str) -> Result { + const REQUIRED_ENCODED_SIGNATURE_LENGTH: usize = 4; + + if base64_encoded_signature.len() != REQUIRED_ENCODED_SIGNATURE_LENGTH { + return Err(format!("Base64-encoded signature {} must be 4 characters long.", + base64_encoded_signature)); + } + + if base64_encoded_signature.chars().next().unwrap().is_digit(10) { + return Err(format!("The first character of the signature {} must not be a digit.", + base64_encoded_signature)); + } + + for ch in base64_encoded_signature.chars() { + if !is_base62_encoding_char(ch) { + return Err(format!("Signature {} can only contain alphabetic or numeric values.", + base64_encoded_signature)); + } + } + + let all_upper = base64_encoded_signature.to_uppercase(); + if base64_encoded_signature == all_upper { + return Ok(format!("Valid signature {}", base64_encoded_signature)); + } + + let all_lower = base64_encoded_signature.to_lowercase(); + if base64_encoded_signature == all_lower { + return Ok(format!("Valid signature {}", base64_encoded_signature)); + } + + return Err(format!("Signature {} characters must all upper- or all lower-case.", + base64_encoded_signature)); +} + fn validate_base64_encoded_signature(base64_encoded_signature: &String, encode_for_url: bool) { let required_encoded_signature_length = 4; @@ -514,7 +630,7 @@ fn get_csrandom_bytes(key_length_in_bytes: u32) -> Vec let mut rng = ChaCha20Rng::from_entropy(); let mut random_bytes: Vec = Vec::new(); - for i in 0..key_length_in_bytes + for _i in 0..key_length_in_bytes { random_bytes.push(rng.gen::()); } diff --git a/src/security_utilities_rust/src/microsoft_security_utilities_core/marvin.rs b/src/security_utilities_rust/src/microsoft_security_utilities_core/marvin.rs index 28a09a4..6fbd764 100644 --- a/src/security_utilities_rust/src/microsoft_security_utilities_core/marvin.rs +++ b/src/security_utilities_rust/src/microsoft_security_utilities_core/marvin.rs @@ -68,13 +68,13 @@ pub fn compute_hash(data: &[u8], seed: u64, offset: i32, mut length: i32) -> i64 1 => p0 = p0.wrapping_add(0x8000 | (data[remaining_data_offset as usize] as u32)), 2 => { let d1 = (data[(remaining_data_offset as usize) + 1] as u32) << 8; - let d0: u32 = data[(remaining_data_offset as usize)] as u32; + let d0: u32 = data[remaining_data_offset as usize] as u32; p0 = p0.wrapping_add(0x800000 | d1 | d0); }, 3 => { let d2 = (data[(remaining_data_offset as usize) + 2] as u32) << 16; let d1 = (data[(remaining_data_offset as usize) + 1] as u32) << 8; - let d0: u32 = data[(remaining_data_offset as usize)] as u32; + let d0: u32 = data[remaining_data_offset as usize] as u32; p0 = p0.wrapping_add(0x80000000 | d2 | d1 | d0); }, _ => panic!("Hash computation reached an invalid state")