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