format/ota: Decouple OTA signature parsing from verification

This way, we can fail hard for parsing errors, but not for verification
errors in `avbroot ota verify`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2025-03-13 20:20:44 -04:00
parent 80c47e9a02
commit e2dc5174b4
2 changed files with 126 additions and 90 deletions
+14 -15
View File
@@ -1505,9 +1505,8 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
info!("Extracting embedded OTA certificate from zip signature");
let ota_sig = ota::parse_ota_sig(&mut raw_reader)?;
let embedded_cert = ota_sig.embedded_cert()?;
crypto::write_pem_cert_file(path, embedded_cert)
crypto::write_pem_cert_file(path, &ota_sig.cert)
.with_context(|| format!("Failed to write OTA certificate: {path:?}"))?;
}
@@ -1706,28 +1705,27 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
info!("Verifying whole-file signature");
let embedded_cert = match ota::verify_ota(&mut reader, cancel_signal)
let ota_sig = ota::parse_ota_sig(&mut reader).context("Failed to parse OTA signature")?;
if let Err(e) = ota_sig
.verify_ota(&mut reader, cancel_signal)
.context("Failed to verify OTA against embedded certificate")
{
Ok(cert) => Some(cert),
Err(e) => {
fail_later!("{e:?}");
None
}
};
fail_later!("{e:?}");
}
let (metadata, ota_cert, header, properties) =
ota::parse_zip_ota_info(&mut reader).context("Failed to parse OTA metadata")?;
if embedded_cert.as_ref() != Some(&ota_cert) {
if ota_cert != ota_sig.cert {
fail_later!(
"CMS embedded certificate does not match {}",
ota::PATH_OTACERT
"{} does not match CMS embedded certificate",
ota::PATH_OTACERT,
);
} else if let Some(p) = &cli.cert_ota {
let verify_cert = crypto::read_pem_cert_file(p)
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
if embedded_cert != Some(verify_cert) {
if ota_sig.cert != verify_cert {
fail_later!("OTA has a valid signature, but was not signed with: {p:?}");
}
} else {
@@ -1756,8 +1754,9 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)
.context("Failed to directly open payload section")?;
if let Err(e) = payload::verify_payload(section_reader, &ota_cert, &properties, cancel_signal)
.context("Failed to verify payload signatures and digests")
if let Err(e) =
payload::verify_payload(section_reader, &ota_sig.cert, &properties, cancel_signal)
.context("Failed to verify payload signatures and digests")
{
fail_later!("{e:?}");
}
+112 -75
View File
@@ -10,7 +10,7 @@ use std::{
sync::atomic::AtomicBool,
};
use aws_lc_rs::digest::Context;
use aws_lc_rs::digest::{Algorithm, Context};
use clap::ValueEnum;
use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
@@ -566,14 +566,14 @@ pub fn verify_metadata(
}
#[derive(Clone, Debug)]
pub struct OtaSignature {
struct RawOtaSignature {
/// Decoded CMS structure.
pub signed_data: SignedData,
signed_data: SignedData,
/// Length of the file (from the beginning) that's covered by the signature.
pub hashed_size: u64,
hashed_size: u64,
}
impl OtaSignature {
impl RawOtaSignature {
pub fn embedded_cert(&self) -> Result<&Certificate> {
let mut iter = crypto::iter_cms_certs(&self.signed_data);
@@ -589,9 +589,110 @@ impl OtaSignature {
}
}
#[derive(Clone, Debug)]
pub struct OtaSignature {
pub cert: Certificate,
pub digest_algo: &'static Algorithm,
pub sig_algo: SignatureAlgorithm,
pub sig: Vec<u8>,
pub data_size: u64,
}
impl TryFrom<RawOtaSignature> for OtaSignature {
type Error = Error;
fn try_from(raw_ota_sig: RawOtaSignature) -> Result<Self> {
let cert = raw_ota_sig.embedded_cert()?;
// Make sure this is a signature scheme we can handle. There's currently
// no Rust library to verify arbitrary CMS signatures for large files
// without fully reading them into memory.
let signers_len = raw_ota_sig.signed_data.signer_infos.0.len();
if signers_len != 1 {
return Err(Error::NotOneCmsSignerInfo(signers_len));
}
let signer = raw_ota_sig.signed_data.signer_infos.0.get(0).unwrap();
if signer.digest_alg.oid != rfc5912::ID_SHA_256
&& signer.digest_alg.oid != rfc5912::ID_SHA_1
{
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
{
return Err(Error::UnsupportedSignatureAlgorithm(
signer.signature_algorithm.oid,
));
}
// We support SHA1 for verification only.
let (digest_algo, sig_algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(
&aws_lc_rs::digest::SHA256,
SignatureAlgorithm::Sha256WithRsa,
)
} else {
(
&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
SignatureAlgorithm::Sha1WithRsa,
)
};
Ok(Self {
cert: cert.clone(),
digest_algo,
sig_algo,
sig: signer.signature.as_bytes().to_vec(),
data_size: raw_ota_sig.hashed_size,
})
}
}
impl OtaSignature {
/// Verify an OTA zip against its embedded certificate. This function makes
/// no assertion about whether the certificate is actually trusted.
///
/// CMS signed attributes are intentionally not supported because AOSP
/// recovery does not support them either. It expects the CMS [`SignedData`]
/// structure to be used for nothing more than a raw signature transport
/// mechanism.
pub fn verify_ota(
&self,
mut reader: impl Read + Seek,
cancel_signal: &AtomicBool,
) -> Result<()> {
let public_key = crypto::get_public_key(&self.cert).map_err(Error::OtaCertExtractPubKey)?;
// Manually hash the parts of the file covered by the signature.
reader
.seek(SeekFrom::Start(0))
.map_err(|e| Error::DataRead("raw_data", e))?;
let mut hashing_reader = HashingReader::new(reader, Context::new(self.digest_algo));
stream::copy_n(
&mut hashing_reader,
io::sink(),
self.data_size,
cancel_signal,
)
.map_err(|e| Error::DataRead("raw_data", e))?;
let (_, context) = hashing_reader.finish();
let digest = context.finish();
// Verify the signature against the public key.
public_key
.verify_sig(self.sig_algo, digest.as_ref(), &self.sig)
.map_err(Error::CmsVerify)?;
Ok(())
}
}
/// Parse the CMS signature from the OTA zip comment. This does not perform any
/// parsing of zip data structures.
pub fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<OtaSignature> {
fn parse_raw_ota_sig(mut reader: impl Read + Seek) -> Result<RawOtaSignature> {
let file_size = reader
.seek(SeekFrom::End(0))
.map_err(|e| Error::DataRead("file_size", e))?;
@@ -642,80 +743,16 @@ pub fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<OtaSignature> {
// length field.
let hashed_size = file_size - 2 - u64::from(comment_size);
Ok(OtaSignature {
Ok(RawOtaSignature {
signed_data,
hashed_size,
})
}
/// Verify an OTA zip against its embedded certificates. This function makes no
/// assertion about whether the certificate is actually trusted. Returns the
/// embedded certificate.
///
/// CMS signed attributes are intentionally not supported because AOSP recovery
/// does not support them either. It expects the CMS [`SignedData`] structure to
/// be used for nothing more than a raw signature transport mechanism.
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
let ota_sig = parse_ota_sig(&mut reader)?;
let cert = ota_sig.embedded_cert()?;
let public_key = crypto::get_public_key(cert).map_err(Error::OtaCertExtractPubKey)?;
// Make sure this is a signature scheme we can handle. There's currently no
// Rust library to verify arbitrary CMS signatures for large files without
// fully reading them into memory.
let signers_len = ota_sig.signed_data.signer_infos.0.len();
if signers_len != 1 {
return Err(Error::NotOneCmsSignerInfo(signers_len));
}
let signer = ota_sig.signed_data.signer_infos.0.get(0).unwrap();
if signer.digest_alg.oid != rfc5912::ID_SHA_256 && signer.digest_alg.oid != rfc5912::ID_SHA_1 {
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
{
return Err(Error::UnsupportedSignatureAlgorithm(
signer.signature_algorithm.oid,
));
}
// Manually hash the parts of the file covered by the signature.
reader
.seek(SeekFrom::Start(0))
.map_err(|e| Error::DataRead("raw_data", e))?;
// We support SHA1 for verification only.
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(
&aws_lc_rs::digest::SHA256,
SignatureAlgorithm::Sha256WithRsa,
)
} else {
(
&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
SignatureAlgorithm::Sha1WithRsa,
)
};
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
stream::copy_n(
&mut hashing_reader,
io::sink(),
ota_sig.hashed_size,
cancel_signal,
)
.map_err(|e| Error::DataRead("raw_data", e))?;
let (_, context) = hashing_reader.finish();
let digest = context.finish();
// Verify the signature against the public key.
public_key
.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())
.map_err(Error::CmsVerify)?;
Ok(cert.clone())
/// Parse the signature information from the CMS signature embedded in the OTA
/// zip archive comment.
pub fn parse_ota_sig(reader: impl Read + Seek) -> Result<OtaSignature> {
parse_raw_ota_sig(reader)?.try_into()
}
/// Get and parse the protobuf-encoded OTA metadata, the PEM-encoded otacert,