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