1 // Copyright 2024, The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! libavb_rs certificate tests.
16
17 use crate::{
18 build_test_ops_one_image_one_vbmeta,
19 test_data::*,
20 test_ops::{FakeVbmetaKey, TestOps},
21 verify_one_image_one_vbmeta,
22 };
23 use avb::{
24 cert_generate_unlock_challenge, cert_validate_unlock_credential, CertPermanentAttributes,
25 CertUnlockChallenge, CertUnlockCredential, IoError, SlotVerifyError, CERT_PIK_VERSION_LOCATION,
26 CERT_PSK_VERSION_LOCATION,
27 };
28 use hex::decode;
29 use std::{collections::HashMap, fs, mem::size_of};
30 use zerocopy::{AsBytes, FromBytes};
31
32 /// Initializes a `TestOps` object such that cert verification will succeed on
33 /// `TEST_PARTITION_NAME`.
34 ///
35 /// The returned `TestOps` also contains RNG configured to return the contents of
36 /// `TEST_CERT_UNLOCK_CHALLENGE_RNG_PATH`, so that the pre-signed contents of
37 /// `TEST_CERT_UNLOCK_CREDENTIAL_PATH` will successfully validate by default.
build_test_cert_ops_one_image_one_vbmeta<'a>() -> TestOps<'a>38 fn build_test_cert_ops_one_image_one_vbmeta<'a>() -> TestOps<'a> {
39 let mut ops = build_test_ops_one_image_one_vbmeta();
40
41 // Replace vbmeta with the cert-signed version.
42 ops.add_partition("vbmeta", fs::read(TEST_CERT_VBMETA_PATH).unwrap());
43
44 // Tell `ops` to use cert APIs and to route the default key through cert validation.
45 ops.use_cert = true;
46 ops.default_vbmeta_key = Some(FakeVbmetaKey::Cert);
47
48 // Add the libavb_cert permanent attributes.
49 let perm_attr_bytes = fs::read(TEST_CERT_PERMANENT_ATTRIBUTES_PATH).unwrap();
50 ops.cert_permanent_attributes =
51 Some(CertPermanentAttributes::read_from(&perm_attr_bytes[..]).unwrap());
52 ops.cert_permanent_attributes_hash = Some(
53 decode(TEST_CERT_PERMANENT_ATTRIBUTES_HASH_HEX)
54 .unwrap()
55 .try_into()
56 .unwrap(),
57 );
58
59 // Add the rollbacks for the cert keys.
60 ops.rollbacks
61 .insert(CERT_PIK_VERSION_LOCATION, Ok(TEST_CERT_PIK_VERSION));
62 ops.rollbacks
63 .insert(CERT_PSK_VERSION_LOCATION, Ok(TEST_CERT_PSK_VERSION));
64
65 // It's non-trivial to sign a challenge without `avbtool.py`, so instead we inject the exact RNG
66 // used by the pre-generated challenge so that we can use the pre-signed credential.
67 ops.cert_fake_rng = fs::read(TEST_CERT_UNLOCK_CHALLENGE_RNG_PATH).unwrap();
68
69 ops
70 }
71
72 /// Returns the contents of `TEST_CERT_UNLOCK_CREDENTIAL_PATH` as a `CertUnlockCredential`.
test_unlock_credential() -> CertUnlockCredential73 fn test_unlock_credential() -> CertUnlockCredential {
74 let credential_bytes = fs::read(TEST_CERT_UNLOCK_CREDENTIAL_PATH).unwrap();
75 CertUnlockCredential::read_from(&credential_bytes[..]).unwrap()
76 }
77
78 /// Enough fake RNG data to generate a single unlock challenge.
79 const UNLOCK_CHALLENGE_FAKE_RNG: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
80
81 /// Returns a `TestOps` with only enough configuration to generate a single unlock challenge.
82 ///
83 /// The generated unlock challenge will have:
84 /// * permanent attributes sourced from the contents of `TEST_CERT_PERMANENT_ATTRIBUTES_PATH`.
85 /// * RNG sourced from `UNLOCK_CHALLENGE_FAKE_RNG`.
build_test_cert_ops_unlock_challenge_only<'a>() -> TestOps<'a>86 fn build_test_cert_ops_unlock_challenge_only<'a>() -> TestOps<'a> {
87 let mut ops = TestOps::default();
88
89 // Permanent attributes are needed for the embedded product ID.
90 let perm_attr_bytes = fs::read(TEST_CERT_PERMANENT_ATTRIBUTES_PATH).unwrap();
91 ops.cert_permanent_attributes =
92 Some(CertPermanentAttributes::read_from(&perm_attr_bytes[..]).unwrap());
93
94 // Fake RNG for unlock challenge generation.
95 ops.cert_fake_rng = UNLOCK_CHALLENGE_FAKE_RNG.into();
96
97 ops
98 }
99
100 #[test]
cert_verify_succeeds()101 fn cert_verify_succeeds() {
102 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
103
104 let result = verify_one_image_one_vbmeta(&mut ops);
105
106 assert!(result.is_ok());
107 }
108
109 #[test]
cert_verify_sets_key_rollbacks()110 fn cert_verify_sets_key_rollbacks() {
111 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
112
113 // `cert_key_versions` should start empty and be filled by the `set_key_version()` callback
114 // during cert key validation.
115 assert!(ops.cert_key_versions.is_empty());
116
117 let result = verify_one_image_one_vbmeta(&mut ops);
118 assert!(result.is_ok());
119
120 assert_eq!(
121 ops.cert_key_versions,
122 HashMap::from([
123 (CERT_PIK_VERSION_LOCATION, TEST_CERT_PIK_VERSION),
124 (CERT_PSK_VERSION_LOCATION, TEST_CERT_PSK_VERSION)
125 ])
126 );
127 }
128
129 #[test]
cert_verify_fails_with_pik_rollback_violation()130 fn cert_verify_fails_with_pik_rollback_violation() {
131 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
132 // If the image is signed with a lower key version than our rollback, it should fail to verify.
133 *ops.rollbacks
134 .get_mut(&CERT_PIK_VERSION_LOCATION)
135 .unwrap()
136 .as_mut()
137 .unwrap() += 1;
138
139 let result = verify_one_image_one_vbmeta(&mut ops);
140
141 assert_eq!(result.unwrap_err(), SlotVerifyError::PublicKeyRejected);
142 }
143
144 #[test]
cert_verify_fails_with_psk_rollback_violation()145 fn cert_verify_fails_with_psk_rollback_violation() {
146 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
147 // If the image is signed with a lower key version than our rollback, it should fail to verify.
148 *ops.rollbacks
149 .get_mut(&CERT_PSK_VERSION_LOCATION)
150 .unwrap()
151 .as_mut()
152 .unwrap() += 1;
153
154 let result = verify_one_image_one_vbmeta(&mut ops);
155
156 assert_eq!(result.unwrap_err(), SlotVerifyError::PublicKeyRejected);
157 }
158
159 #[test]
cert_verify_fails_with_wrong_vbmeta_key()160 fn cert_verify_fails_with_wrong_vbmeta_key() {
161 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
162 // The default non-cert vbmeta image should fail to verify.
163 ops.add_partition("vbmeta", fs::read(TEST_VBMETA_PATH).unwrap());
164
165 let result = verify_one_image_one_vbmeta(&mut ops);
166
167 assert_eq!(result.unwrap_err(), SlotVerifyError::PublicKeyRejected);
168 }
169
170 #[test]
cert_verify_fails_with_bad_permanent_attributes_hash()171 fn cert_verify_fails_with_bad_permanent_attributes_hash() {
172 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
173 // The permanent attributes must match their hash.
174 ops.cert_permanent_attributes_hash.as_mut().unwrap()[0] ^= 0x01;
175
176 let result = verify_one_image_one_vbmeta(&mut ops);
177
178 assert_eq!(result.unwrap_err(), SlotVerifyError::PublicKeyRejected);
179 }
180
181 #[test]
cert_generate_unlock_challenge_succeeds()182 fn cert_generate_unlock_challenge_succeeds() {
183 let mut ops = build_test_cert_ops_unlock_challenge_only();
184
185 let challenge = cert_generate_unlock_challenge(&mut ops).unwrap();
186
187 // Make sure the challenge token used our cert callback data correctly.
188 assert_eq!(
189 challenge.product_id_hash,
190 &decode(TEST_CERT_PRODUCT_ID_HASH_HEX).unwrap()[..]
191 );
192 assert_eq!(challenge.challenge, UNLOCK_CHALLENGE_FAKE_RNG);
193 }
194
195 #[test]
cert_generate_unlock_challenge_fails_without_permanent_attributes()196 fn cert_generate_unlock_challenge_fails_without_permanent_attributes() {
197 let mut ops = build_test_cert_ops_unlock_challenge_only();
198
199 // Challenge generation should fail without the product ID provided by the permanent attributes.
200 ops.cert_permanent_attributes = None;
201
202 assert_eq!(
203 cert_generate_unlock_challenge(&mut ops).unwrap_err(),
204 IoError::Io
205 );
206 }
207
208 #[test]
cert_generate_unlock_challenge_fails_insufficient_rng()209 fn cert_generate_unlock_challenge_fails_insufficient_rng() {
210 let mut ops = build_test_cert_ops_unlock_challenge_only();
211
212 // Remove a byte of RNG so there isn't enough.
213 ops.cert_fake_rng.pop();
214
215 assert_eq!(
216 cert_generate_unlock_challenge(&mut ops).unwrap_err(),
217 IoError::Io
218 );
219 }
220
221 #[test]
cert_validate_unlock_credential_success()222 fn cert_validate_unlock_credential_success() {
223 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
224
225 // We don't actually need the challenge here since we've pre-signed it, but we still need to
226 // call this function so the libavb_cert internal state is ready for the unlock cred.
227 let _ = cert_generate_unlock_challenge(&mut ops).unwrap();
228
229 assert_eq!(
230 cert_validate_unlock_credential(&mut ops, &test_unlock_credential()),
231 Ok(true)
232 );
233 }
234
235 #[test]
cert_validate_unlock_credential_fails_wrong_rng()236 fn cert_validate_unlock_credential_fails_wrong_rng() {
237 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
238 // Modify the RNG slightly, the cerificate should now fail to validate.
239 ops.cert_fake_rng[0] ^= 0x01;
240
241 let _ = cert_generate_unlock_challenge(&mut ops).unwrap();
242
243 assert_eq!(
244 cert_validate_unlock_credential(&mut ops, &test_unlock_credential()),
245 Ok(false)
246 );
247 }
248
249 #[test]
cert_validate_unlock_credential_fails_with_pik_rollback_violation()250 fn cert_validate_unlock_credential_fails_with_pik_rollback_violation() {
251 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
252 // Rotating the PIK should invalidate all existing unlock keys, which includes our pre-signed
253 // certificate.
254 *ops.rollbacks
255 .get_mut(&CERT_PIK_VERSION_LOCATION)
256 .unwrap()
257 .as_mut()
258 .unwrap() += 1;
259
260 let _ = cert_generate_unlock_challenge(&mut ops).unwrap();
261
262 assert_eq!(
263 cert_validate_unlock_credential(&mut ops, &test_unlock_credential()),
264 Ok(false)
265 );
266 }
267
268 #[test]
cert_validate_unlock_credential_fails_no_challenge()269 fn cert_validate_unlock_credential_fails_no_challenge() {
270 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
271
272 // We never called `cert_generate_unlock_challenge()`, so no credentials should validate.
273 assert_eq!(
274 cert_validate_unlock_credential(&mut ops, &test_unlock_credential()),
275 Ok(false)
276 );
277 }
278
279 // In practice, devices will usually be passing unlock challenges and credentials over fastboot as
280 // raw bytes. This test ensures that there are some reasonable APIs available to convert between
281 // `CertUnlockChallenge`/`CertUnlockCredential` and byte slices.
282 #[test]
cert_validate_unlock_credential_bytes_api()283 fn cert_validate_unlock_credential_bytes_api() {
284 let mut ops = build_test_cert_ops_one_image_one_vbmeta();
285
286 // Write an unlock challenge to a byte buffer for TX over fastboot.
287 let challenge = cert_generate_unlock_challenge(&mut ops).unwrap();
288 let mut buffer = vec![0u8; size_of::<CertUnlockChallenge>()];
289 assert_eq!(challenge.write_to(&mut buffer[..]), Some(())); // zerocopy::AsBytes.
290
291 // Read an unlock credential from a byte buffer for RX from fastboot.
292 let buffer = vec![0u8; size_of::<CertUnlockCredential>()];
293 let credential = CertUnlockCredential::ref_from(&buffer[..]).unwrap(); // zerocopy::FromBytes.
294
295 // It shouldn't actually validate since the credential is just zeroes, the important thing
296 // is that it compiles.
297 assert_eq!(
298 cert_validate_unlock_credential(&mut ops, credential),
299 Ok(false)
300 );
301 }
302