underhill_attestation/igvm_attest/
wrapped_key.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The module for `WRAPPED_KEY_REQUEST` request type that supports parsing the
5//! response in JSON format defined by Azure CVM Provisioning Service (CPS).
6
7use crate::igvm_attest::Error as CommonError;
8use crate::igvm_attest::parse_response_header;
9use openhcl_attestation_protocol::igvm_attest::cps;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub(crate) enum WrappedKeyError {
14    #[error("failed to deserialize the response payload into JSON: {json_data}")]
15    WrappedKeyResponsePayloadToJson {
16        #[source]
17        json_err: serde_json::Error,
18        json_data: String,
19    },
20    #[error("the response payload size is too small to parse")]
21    PayloadSizeTooSmall,
22    #[error("error in response header)")]
23    ParseHeader(#[source] CommonError),
24    #[error("invalid response header version: {0}")]
25    InvalidResponseVersion(u32),
26}
27
28/// Return value of the [`parse_response`].
29pub struct IgvmWrappedKeyParsedResponse {
30    /// Wrapped DiskEncryptionSettings key.
31    pub wrapped_key: Vec<u8>,
32    /// Key reference in JSON string.
33    pub key_reference: Vec<u8>,
34}
35
36/// Parse a `WRAPPED_KEY_REQUEST` response and return a wrapped key blob.
37///
38/// Returns `Ok(IgvmWrappedKeyParsedResponse)` on successfully extracting a wrapped DiskEncryptionSettings
39/// key from `response`, otherwise returns an error.
40pub fn parse_response(response: &[u8]) -> Result<IgvmWrappedKeyParsedResponse, WrappedKeyError> {
41    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader;
42    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion;
43    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestWrappedKeyResponseHeader;
44
45    // Minimum acceptable payload would look like {"ciphertext":"base64URL wrapped key"}
46    const CIPHER_TEXT_KEY: &str = r#"{"ciphertext":""}"#;
47    const MINIMUM_WRAPPED_KEY_SIZE: usize = 256;
48    const MINIMUM_WRAPPED_KEY_BASE64_URL_SIZE: usize = MINIMUM_WRAPPED_KEY_SIZE / 3 * 4;
49    const MINIMUM_PAYLOAD_SIZE: usize = CIPHER_TEXT_KEY.len() + MINIMUM_WRAPPED_KEY_BASE64_URL_SIZE;
50
51    let header = parse_response_header(response).map_err(WrappedKeyError::ParseHeader)?;
52
53    // Extract payload as per header version
54    let header_size = match header.version {
55        IgvmAttestResponseVersion::VERSION_1 => size_of::<IgvmAttestCommonResponseHeader>(),
56        IgvmAttestResponseVersion::VERSION_2 => size_of::<IgvmAttestWrappedKeyResponseHeader>(),
57        invalid_version => return Err(WrappedKeyError::InvalidResponseVersion(invalid_version.0)),
58    };
59    let payload = &response[header_size..header.data_size as usize];
60
61    if payload.len() < MINIMUM_PAYLOAD_SIZE {
62        Err(WrappedKeyError::PayloadSizeTooSmall)?
63    }
64    let payload = String::from_utf8_lossy(payload);
65    let payload: cps::VmmdBlob = serde_json::from_str(&payload).map_err(|json_err| {
66        WrappedKeyError::WrappedKeyResponsePayloadToJson {
67            json_err,
68            json_data: payload.to_string(),
69        }
70    })?;
71    let wrapped_key = payload
72        .disk_encryption_settings
73        .encryption_info
74        .aes_info
75        .ciphertext;
76
77    let key_reference = payload
78        .disk_encryption_settings
79        .encryption_info
80        .key_reference
81        .to_string()
82        .as_bytes()
83        .to_vec();
84
85    Ok(IgvmWrappedKeyParsedResponse {
86        wrapped_key,
87        key_reference,
88    })
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use openhcl_attestation_protocol::igvm_attest::get::IgvmErrorInfo;
95    use zerocopy::IntoBytes;
96
97    const KEY_REFERENCE: &str = r#"{
98    "key_info": {
99        "host": "name"
100    },
101    "attestation_info": {
102        "host": "attestation_name"
103    }
104}"#;
105
106    #[test]
107    fn test_response() {
108        const JSON_DATA: &str = r#"
109{
110  "version": "1.0",
111  "DiskEncryptionSettings": {
112    "encryption_info": {
113      "aes_info": {
114        "ciphertext": "Q0lQSEVSVEVYVA==",
115        "algorithm": "AES_256_WRAP_PAD",
116        "creation_time": "2023-11-03T22:58:59.7967119Z"
117      },
118      "key_reference": {
119        "key_info": {
120          "auth_method": "msi",
121          "host": "HOST",
122          "key_name": "cvmps-pmk-key",
123          "key_version": "58bf696275cd4b6d8150bb3376981076",
124          "aad_msi_res_id": "<identity resource id>",
125          "tenant_id": "33e01921-4d64-4f8c-a055-5bdaffd5e33d"
126        },
127        "attestation_info": {
128          "host": "HOST"
129        }
130      }
131    },
132    "recoverykey_info": {
133      "wrapped_key": "WRAPPEDKEY",
134      "os_type": "Windows",
135      "encryption_scheme": "WindowsBitLocker",
136      "algorithm_type": "RSA-OAEP-256",
137      "key_id": "KEYID"
138    }
139  }
140}"#;
141
142        let result = serde_json::from_str(JSON_DATA);
143        assert!(result.is_ok());
144        let payload: cps::VmmdBlob = result.unwrap();
145        assert_eq!(
146            payload
147                .disk_encryption_settings
148                .encryption_info
149                .aes_info
150                .ciphertext,
151            b"CIPHERTEXT"
152        );
153    }
154
155    #[test]
156    fn test_response_without_key_reference() {
157        const JSON_DATA: &str = r#"
158{
159  "DiskEncryptionSettings": {
160    "encryption_info": {
161      "aes_info": {
162        "ciphertext": "TESTKEY",
163        "algorithm": "AES_256_WRAP_PAD",
164        "creation_time": "2023-11-03T22:58:59.7967119Z"
165      }
166    }
167  }
168}"#;
169        let result: Result<cps::VmmdBlob, _> = serde_json::from_str(JSON_DATA);
170        // Expect to fail
171        assert!(result.is_err());
172    }
173
174    fn mock_response() -> Vec<u8> {
175        use openhcl_attestation_protocol::igvm_attest::get::IGVM_ATTEST_RESPONSE_CURRENT_VERSION;
176        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestWrappedKeyResponseHeader;
177
178        const WRAPPED_KEY: [u8; 256] = [
179            0x9d, 0x72, 0x81, 0xbc, 0x6d, 0x0c, 0xeb, 0x8f, 0x32, 0xb9, 0xc3, 0xd0, 0xd2, 0x58,
180            0x89, 0x2f, 0x49, 0xb4, 0x40, 0xb1, 0x3d, 0xb1, 0x2f, 0x1e, 0x9c, 0xb5, 0x46, 0x4a,
181            0x4a, 0x87, 0xbe, 0x97, 0xf5, 0xa2, 0x90, 0x7a, 0xd1, 0x7d, 0x6c, 0x91, 0x8a, 0x46,
182            0x9e, 0xc1, 0x87, 0x9c, 0xa9, 0xb2, 0xcd, 0xc2, 0x6e, 0x6c, 0xdc, 0xda, 0xdd, 0x79,
183            0x64, 0x25, 0x7a, 0xd7, 0xb9, 0x5d, 0xd3, 0xc7, 0x82, 0x0d, 0x4a, 0xb1, 0x86, 0xe2,
184            0x78, 0xc1, 0x94, 0xe4, 0x81, 0x9b, 0x48, 0xba, 0x90, 0xcb, 0x79, 0x51, 0x0c, 0xda,
185            0x98, 0x69, 0xed, 0xc7, 0xc9, 0x0b, 0xde, 0xb5, 0x9a, 0xcb, 0xcc, 0x16, 0x06, 0xa7,
186            0x66, 0xfe, 0xd7, 0x41, 0xe6, 0x71, 0xcb, 0x16, 0xb1, 0x16, 0xf8, 0x05, 0x41, 0x9a,
187            0x6b, 0x99, 0xa3, 0xc9, 0x3c, 0x7c, 0xa3, 0x26, 0x37, 0x0c, 0xb0, 0x87, 0x6b, 0x2a,
188            0xde, 0x9c, 0xce, 0x1a, 0xe8, 0x71, 0xe9, 0xce, 0xf8, 0x53, 0x75, 0xfd, 0x95, 0x47,
189            0xf8, 0x60, 0x21, 0xd5, 0xce, 0x33, 0xca, 0x9b, 0x6b, 0x7c, 0xa9, 0x73, 0xe8, 0x5a,
190            0x6e, 0x91, 0x57, 0x9c, 0xb1, 0xa1, 0x02, 0xce, 0x67, 0x0e, 0x8f, 0xac, 0x14, 0x0f,
191            0xa7, 0x08, 0x7e, 0xa8, 0xb3, 0xb9, 0x25, 0x36, 0x41, 0xae, 0x37, 0x59, 0xf8, 0x0d,
192            0x11, 0xc0, 0x81, 0xd9, 0x6f, 0x6b, 0xb1, 0xc3, 0xd1, 0xe3, 0xdd, 0xa9, 0x6d, 0x16,
193            0xb2, 0x34, 0xe1, 0xf3, 0xa1, 0xa2, 0x86, 0x83, 0x65, 0x3d, 0x48, 0x9e, 0xa0, 0x50,
194            0x15, 0xce, 0x0b, 0x06, 0x0a, 0x87, 0x89, 0x97, 0x42, 0x3d, 0x92, 0x1e, 0xab, 0x91,
195            0x62, 0x47, 0x31, 0xfb, 0xca, 0x43, 0xa5, 0x12, 0x2a, 0x2c, 0xde, 0x4a, 0xdc, 0x7a,
196            0x7f, 0x38, 0x18, 0xe0, 0x4d, 0xbe, 0xf3, 0xf2, 0xc3, 0xb9, 0x22, 0x22, 0x43, 0x19,
197            0xdb, 0x0b, 0x47, 0xc7,
198        ];
199
200        let aes_info = cps::AesInfo {
201            ciphertext: WRAPPED_KEY.to_vec(),
202        };
203
204        let result = serde_json::from_str(KEY_REFERENCE);
205        assert!(result.is_ok());
206        let key_reference = result.unwrap();
207
208        let encryption_info = cps::EncryptionInfo {
209            aes_info,
210            key_reference,
211        };
212        let disk_encryption_settings = cps::DiskEncryptionSettings { encryption_info };
213        let payload = cps::VmmdBlob {
214            disk_encryption_settings,
215        };
216
217        let result = serde_json::to_string(&payload);
218        assert!(result.is_ok());
219        let payload = result.unwrap();
220
221        let header = IgvmAttestWrappedKeyResponseHeader {
222            data_size: (payload.len() + size_of::<IgvmAttestWrappedKeyResponseHeader>()) as u32,
223            version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION,
224            error_info: IgvmErrorInfo::default(),
225        };
226        [header.as_bytes(), payload.as_bytes()].concat()
227    }
228
229    #[test]
230    fn test_mock_response() {
231        let response = mock_response();
232        let result = parse_response(&response);
233        assert!(result.is_ok());
234        let igvm_wrapped_key = result.unwrap();
235        assert!(!igvm_wrapped_key.wrapped_key.is_empty());
236
237        let result = serde_json::from_str(KEY_REFERENCE);
238        assert!(result.is_ok());
239        let expected_key_reference: serde_json::Value = result.unwrap();
240        assert_eq!(
241            igvm_wrapped_key.key_reference,
242            expected_key_reference.to_string().as_bytes()
243        );
244    }
245}