1 use crate::cbor::field_value::FieldValue; 2 use crate::cbor::value_from_bytes; 3 use crate::dice::Chain; 4 use crate::rkp::{Csr, DeviceInfo, ProtectedData}; 5 use crate::session::Session; 6 use anyhow::{anyhow, bail, ensure, Context, Result}; 7 use base64::{prelude::BASE64_STANDARD, Engine}; 8 use ciborium::value::Value; 9 10 const VERSION_OR_DEVICE_INFO_INDEX: usize = 0; 11 12 impl Csr { 13 /// Parse base64-encoded CBOR data as a Certificate Signing Request. from_base64_cbor<S: AsRef<[u8]>>(session: &Session, base64: &S) -> Result<Self>14 pub fn from_base64_cbor<S: AsRef<[u8]>>(session: &Session, base64: &S) -> Result<Self> { 15 let cbor: Vec<u8> = BASE64_STANDARD.decode(base64).context("invalid base64 CSR")?; 16 Self::from_cbor(session, cbor.as_slice()) 17 } 18 19 /// Read and parse CBOR data as a Certificate Signing Request. from_cbor<S: std::io::Read>(session: &Session, cbor: S) -> Result<Self>20 pub fn from_cbor<S: std::io::Read>(session: &Session, cbor: S) -> Result<Self> { 21 let value: Value = ciborium::de::from_reader(cbor).context("invalid CBOR")?; 22 let mut array = match value { 23 Value::Array(a) if a.is_empty() => bail!("CSR CBOR is an empty array"), 24 Value::Array(a) => a, 25 other => bail!("expected array, found {other:?}"), 26 }; 27 let version_or_device_info = 28 std::mem::replace(&mut array[VERSION_OR_DEVICE_INFO_INDEX], Value::Null); 29 match version_or_device_info { 30 Value::Array(device_info) => Self::v2_from_cbor_values(session, array, device_info), 31 Value::Integer(i) => Self::v3_from_authenticated_request(session, array, i.into()), 32 other => Err(anyhow!( 33 "Expected integer or array at index {VERSION_OR_DEVICE_INFO_INDEX}, \ 34 found {other:?}" 35 )), 36 } 37 } 38 v2_from_cbor_values( session: &Session, mut csr: Vec<Value>, mut device_info: Vec<Value>, ) -> Result<Self>39 fn v2_from_cbor_values( 40 session: &Session, 41 mut csr: Vec<Value>, 42 mut device_info: Vec<Value>, 43 ) -> Result<Self> { 44 let maced_keys_to_sign = 45 FieldValue::from_optional_value("MacedKeysToSign", csr.pop()).into_cose_mac0()?; 46 let encrypted_protected_data = 47 FieldValue::from_optional_value("ProtectedData", csr.pop()).into_cose_encrypt()?; 48 let challenge = FieldValue::from_optional_value("Challenge", csr.pop()).into_bytes()?; 49 50 ensure!(device_info.len() == 2, "Device info should contain exactly 2 entries"); 51 device_info.pop(); // ignore unverified info 52 let verified_device_info = match device_info.pop() { 53 Some(Value::Map(d)) => Value::Map(d), 54 other => bail!("Expected a map for verified device info, found '{:?}'", other), 55 }; 56 57 let protected_data = ProtectedData::from_cose_encrypt( 58 session, 59 encrypted_protected_data, 60 &challenge, 61 &verified_device_info, 62 &maced_keys_to_sign.tag, 63 )?; 64 65 let verified_device_info = match verified_device_info { 66 Value::Map(m) => m, 67 _ => unreachable!("verified device info is always a map"), 68 }; 69 70 Ok(Self::V2 { 71 device_info: DeviceInfo::from_cbor_values(verified_device_info, None)?, 72 challenge, 73 protected_data, 74 }) 75 } 76 v3_from_authenticated_request( session: &Session, mut csr: Vec<Value>, version: i128, ) -> Result<Self>77 fn v3_from_authenticated_request( 78 session: &Session, 79 mut csr: Vec<Value>, 80 version: i128, 81 ) -> Result<Self> { 82 if version != 1 { 83 bail!("Invalid CSR version. Only '1' is supported, found '{}", version); 84 } 85 86 // CSRs that are uploaded to the backend have an additional unverified info field tacked 87 // onto them. We just ignore that, so if it's there pop it and move on. 88 if csr.len() == 5 { 89 FieldValue::from_optional_value("UnverifiedDeviceInfo", csr.pop()); 90 } 91 92 let signed_data = 93 FieldValue::from_optional_value("SignedData", csr.pop()).into_cose_sign1()?; 94 let dice_chain = 95 Chain::from_value(session, csr.pop().ok_or(anyhow!("Missing DiceCertChain"))?)?; 96 97 let signed_data_payload = signed_data.payload.context("missing payload in SignedData")?; 98 let csr_payload_value = value_from_bytes(&signed_data_payload) 99 .context("SignedData payload is not valid CBOR")? 100 .as_array_mut() 101 .context("SignedData payload is not a CBOR array")? 102 .pop() 103 .context("Missing CsrPayload in SignedData")?; 104 let csr_payload_bytes = csr_payload_value 105 .as_bytes() 106 .context("CsrPayload (in SignedData) is expected to be encoded CBOR")? 107 .as_slice(); 108 let mut csr_payload = match value_from_bytes(csr_payload_bytes)? { 109 Value::Array(a) => a, 110 other => bail!("CsrPayload is expected to be an array, found {other:?}"), 111 }; 112 113 let _keys_to_sign = FieldValue::from_optional_value("KeysToSign", csr_payload.pop()); 114 let device_info = FieldValue::from_optional_value("DeviceInfo", csr_payload.pop()); 115 let _certificate_type = 116 FieldValue::from_optional_value("CertificateType", csr_payload.pop()); 117 118 let device_info = DeviceInfo::from_cbor_values(device_info.into_map()?, Some(3))?; 119 Ok(Self::V3 { device_info, dice_chain }) 120 } 121 } 122 123 #[cfg(test)] 124 mod tests { 125 // More complete testing happens in the factorycsr module, as the test data 126 // generation spits out full JSON files, not just a CSR. Therefore, only a 127 // minimal number of smoke tests are here. 128 use super::*; 129 use crate::cbor::rkp::csr::testutil::{parse_pem_public_key_or_panic, test_device_info}; 130 use crate::dice::{ChainForm, DegenerateChain, DiceMode}; 131 use crate::rkp::DeviceInfoVersion; 132 use std::fs; 133 134 #[test] from_base64_valid_v2()135 fn from_base64_valid_v2() { 136 let input = fs::read_to_string("testdata/csr/v2_csr.base64").unwrap().trim().to_owned(); 137 let csr = Csr::from_base64_cbor(&Session::default(), &input).unwrap(); 138 139 let device_info = testutil::test_device_info(DeviceInfoVersion::V2); 140 let challenge = 141 b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10".to_vec(); 142 let pem = "-----BEGIN PUBLIC KEY-----\n\ 143 MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERd9pHZbUJ/b4IleUGDN8fs8+LDxE\n\ 144 vG6VX1dkw0sClFs4imbzfXGbocEq74S7TQiyZkd1LhY6HRZnTC51KoGDIA==\n\ 145 -----END PUBLIC KEY-----\n"; 146 let subject_public_key = testutil::parse_pem_public_key_or_panic(pem); 147 let degenerate = ChainForm::Degenerate( 148 DegenerateChain::new("self-signed", "self-signed", subject_public_key).unwrap(), 149 ); 150 let protected_data = ProtectedData::new(vec![0; 32], degenerate, None); 151 assert_eq!(csr, Csr::V2 { device_info, challenge, protected_data }); 152 } 153 154 #[test] from_base64_valid_v3()155 fn from_base64_valid_v3() { 156 let input = fs::read_to_string("testdata/csr/v3_csr.base64").unwrap().trim().to_owned(); 157 let csr = Csr::from_base64_cbor(&Session::default(), &input).unwrap(); 158 if let Csr::V3 { device_info, dice_chain } = csr { 159 assert_eq!(device_info, test_device_info(DeviceInfoVersion::V3)); 160 let root_public_key = parse_pem_public_key_or_panic( 161 "-----BEGIN PUBLIC KEY-----\n\ 162 MCowBQYDK2VwAyEA3FEn/nhqoGOKNok1AJaLfTKI+aFXHf4TfC42vUyPU6s=\n\ 163 -----END PUBLIC KEY-----\n", 164 ); 165 assert_eq!(dice_chain.root_public_key(), &root_public_key); 166 let payloads = dice_chain.payloads(); 167 assert_eq!(payloads.len(), 1); 168 assert_eq!(payloads[0].issuer(), "issuer"); 169 assert_eq!(payloads[0].subject(), "subject"); 170 assert_eq!(payloads[0].mode(), DiceMode::Normal); 171 assert_eq!(payloads[0].code_hash(), &[0x55; 32]); 172 let expected_config_hash: &[u8] = 173 b"\xb8\x96\x54\xe2\x2c\xa4\xd2\x4a\x9c\x0e\x45\x11\xc8\xf2\x63\xf0\ 174 \x66\x0d\x2e\x20\x48\x96\x90\x14\xf4\x54\x63\xc4\xf4\x39\x30\x38"; 175 assert_eq!(payloads[0].config_hash(), Some(expected_config_hash)); 176 assert_eq!(payloads[0].authority_hash(), &[0x55; 32]); 177 assert_eq!(payloads[0].config_desc().component_name(), Some("component_name")); 178 assert_eq!(payloads[0].config_desc().component_version(), None); 179 assert!(!payloads[0].config_desc().resettable()); 180 assert_eq!(payloads[0].config_desc().security_version(), None); 181 assert_eq!(payloads[0].config_desc().extensions(), []); 182 } else { 183 panic!("Parsed CSR was not V3: {:?}", csr); 184 } 185 } 186 187 #[test] from_empty_string()188 fn from_empty_string() { 189 let err = Csr::from_base64_cbor(&Session::default(), &"").unwrap_err(); 190 assert!(err.to_string().contains("invalid CBOR")); 191 } 192 193 #[test] from_garbage()194 fn from_garbage() { 195 let err = Csr::from_base64_cbor(&Session::default(), &"cnViYmlzaAo=").unwrap_err(); 196 assert!(err.to_string().contains("invalid CBOR")); 197 } 198 199 #[test] from_invalid_base64()200 fn from_invalid_base64() { 201 let err = Csr::from_base64_cbor(&Session::default(), &"not base64").unwrap_err(); 202 assert!(err.to_string().contains("invalid base64")); 203 } 204 } 205 206 #[cfg(test)] 207 pub(crate) mod testutil { 208 use crate::publickey::PublicKey; 209 use crate::rkp::{ 210 DeviceInfo, DeviceInfoBootloaderState, DeviceInfoSecurityLevel, DeviceInfoVbState, 211 DeviceInfoVersion, 212 }; 213 use openssl::pkey::PKey; 214 215 // Parse the given PEM-encoded public key into a PublicKey object, panicking on failure. parse_pem_public_key_or_panic(pem: &str) -> PublicKey216 pub fn parse_pem_public_key_or_panic(pem: &str) -> PublicKey { 217 PKey::public_key_from_pem(pem.as_bytes()).unwrap().try_into().unwrap() 218 } 219 220 // The test data uses mostly common DeviceInfo fields test_device_info(version: DeviceInfoVersion) -> DeviceInfo221 pub fn test_device_info(version: DeviceInfoVersion) -> DeviceInfo { 222 DeviceInfo { 223 version, 224 brand: "Google".to_string(), 225 manufacturer: "Google".to_string(), 226 product: "pixel".to_string(), 227 model: "model".to_string(), 228 device: "device".to_string(), 229 vb_state: DeviceInfoVbState::Green, 230 bootloader_state: DeviceInfoBootloaderState::Locked, 231 vbmeta_digest: b"\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff".to_vec(), 232 os_version: Some("12".to_string()), 233 system_patch_level: 20221025, 234 boot_patch_level: 20221026, 235 vendor_patch_level: 20221027, 236 security_level: Some(DeviceInfoSecurityLevel::Tee), 237 fused: true, 238 } 239 } 240 } 241