xref: /aosp_15_r20/external/avb/rust/tests/cert_tests.rs (revision d289c2ba6de359471b23d594623b906876bc48a0)
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