Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
516 changes: 198 additions & 318 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@ bitfield = { version = "0.14.0", default-features = false }
byteorder = { version = "1.4.3", default-features = false, features = ["std"] }
clap = { version = "4", default-features = false, features = ["std", "derive", "default"] }
colored = { version = "2.0", default-features = false }
const-oid = { version = "0.9.2", default-features = false }
crc-any = { version = "2.4.3", default-features = false }
der = { version = "0.7.3", default-features = false, features = ["std"] }
env_logger = { version = "0.10", default-features = false, features = ["auto-color"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
log = { version = "0.4", default-features = false }
num-derive = { version = "0.3.3", default-features = false, features = ["full-syntax"] }
num-traits = { version = "0.2.15", default-features = false }
packed_struct = { version = "0.10.1", default-features = false, features = ["std"] }
parse_int = { version = "0.6.0", default-features = false }
rsa = { version = "0.8.1", default-features = false, features = ["std", "pem", "sha2"] }
pem-rfc7468 = { version = "0.7.0", features = ["std"] }
rsa = { version = "0.9.0-pre.0", default-features = false, features = ["std", "pem", "sha2"] }
serde = { version = "1", default-features = false, features = ["derive"] }
serialport = { git = "https://github.com/jgallagher/serialport-rs", branch = "illumos-support", default-features = false }
sha2 = { version = "0.10", default-features = false }
strum = { version = "0.24", default-features = false, features = ["std"] }
strum_macros = { version = "0.24", default-features = false }
thiserror = { version = "1", default-features = false }
toml = { version = "0.7.3", default-features = false }
x509-parser = { version = "0.12.0", default-features = false, features = ["verify"] }

x509-cert = { version = "0.2.1", default-features = false, features = ["std"] }
lpc55_areas = { path = "lpc55_areas", default-features = false }
lpc55_sign = { path = "lpc55_sign", default-features = false }
5 changes: 4 additions & 1 deletion lpc55_sign/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ edition = "2021"
byteorder.workspace = true
clap = { workspace = true, optional = true }
crc-any.workspace = true
const-oid = { workspace = true, features = ["db"]}
der.workspace = true
env_logger.workspace = true
hex.workspace = true
log.workspace = true
lpc55_areas.workspace = true
packed_struct.workspace = true
pem-rfc7468.workspace = true
rsa.workspace = true
serde.workspace = true
sha2.workspace = true
thiserror.workspace = true
x509-parser.workspace = true
x509-cert.workspace = true

[features]
clap = ["dep:clap"]
69 changes: 56 additions & 13 deletions lpc55_sign/src/cert.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,60 @@
use x509_parser::{
certificate::X509Certificate,
oid_registry::{self},
};
use crate::Error;
use const_oid;
use der::{Decode as _, Encode as _, Reader as _};
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::RsaPublicKey;
use std::path::PathBuf;
use x509_cert::Certificate;

pub fn uses_supported_signature_algorithm(cert: &X509Certificate) -> bool {
cert.signature_algorithm.algorithm == oid_registry::OID_PKCS1_SHA256WITHRSA
/// Read and parse X.509 certificates from DER or PEM encoded files.
pub fn read_certs(paths: &[PathBuf]) -> Result<Vec<Certificate>, Error> {
let mut certs = Vec::with_capacity(paths.len());
for path in paths {
let bytes = std::fs::read(path)?;
let der = if bytes.starts_with("-----BEGIN CERTIFICATE-----\n".as_bytes()) {
let (label, der) = pem_rfc7468::decode_vec(&bytes)?;
if label != "CERTIFICATE" {
return Err(Error::PemLabel(label.to_string()));
}
der
} else {
bytes
};
let cert = Certificate::from_der(&der)?;
certs.push(cert);
}
Ok(certs)
}

pub fn signature_algorithm_name(cert: &X509Certificate) -> String {
let oid_registry = oid_registry::OidRegistry::default().with_crypto();
if let Some(x) = oid_registry.get(&cert.signature_algorithm.algorithm) {
x.sn().into()
} else {
cert.signature_algorithm.algorithm.to_string()
}
/// `Certificate::from_der` uses a `der::SliceReader`, which returns
/// an error if the slice is larger than the DER message it contains.
/// This is a problem for certs in the LPC55 certificate table, because
/// they are padded to a 4-byte boundary. But we can work around it by
/// manually computing the actual length from the DER header.
pub fn read_from_slice(bytes: &[u8]) -> Result<Certificate, Error> {
let reader = der::SliceReader::new(bytes)?;
let header = reader.peek_header()?;
let length = (header.encoded_len()? + header.length)?.try_into()?;
Ok(Certificate::from_der(&bytes[0..length])?)
}

/// Extract the RSA public key from a certificate.
pub fn public_key(cert: &Certificate) -> Result<RsaPublicKey, Error> {
Ok(RsaPublicKey::from_pkcs1_der(
cert.tbs_certificate
.subject_public_key_info
.subject_public_key
.raw_bytes(),
)?)
}

pub fn uses_supported_signature_algorithm(cert: &Certificate) -> bool {
cert.signature_algorithm.oid == const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION
}

pub fn signature_algorithm_name(cert: &Certificate) -> String {
const_oid::db::DB
.by_oid(&cert.signature_algorithm.oid)
.map(|x| x.to_string())
.unwrap_or_else(|| format!("{:?}", cert.signature_algorithm.oid))
}
16 changes: 14 additions & 2 deletions lpc55_sign/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ pub enum Error {
#[error("struct packing error: {0}")]
PackingError(#[from] packed_struct::PackingError),

#[error("x509 parsing error: {0}")]
X509Error(#[from] x509_parser::nom::Err<x509_parser::error::X509Error>),
#[error("certificate decoding error: {0}")]
DerError(#[from] der::Error),

#[error("error decoding PEM: {0}")]
Pem(#[from] pem_rfc7468::Error),

#[error("unexpected PEM label: {0}")]
PemLabel(String),

#[error("io error: {0}")]
IoError(#[from] std::io::Error),
Expand All @@ -56,6 +62,12 @@ pub enum Error {
#[error("RSA PKCS#8 error: {0}")]
RsaPkcs8Error(#[from] rsa::pkcs8::Error),

#[error("RSA signature error: {0}")]
RsaSignatureError(#[from] rsa::signature::Error),

#[error("SPKI error: {0}")]
SpkiError(#[from] rsa::pkcs8::spki::Error),

#[error("RSA error while signing: {0}")]
SigningError(rsa::errors::Error),

Expand Down
112 changes: 59 additions & 53 deletions lpc55_sign/src/signed_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ use std::{convert::TryInto, path::PathBuf};

use crate::{cert, Error};
use byteorder::{ByteOrder, LittleEndian};
use der::Encode as _;
use lpc55_areas::*;
use packed_struct::prelude::*;
use rsa::{
pkcs1::DecodeRsaPrivateKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePrivateKey, PublicKeyParts,
RsaPrivateKey, RsaPublicKey,
};
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey, PublicKeyParts, RsaPrivateKey};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use x509_parser::parse_x509_certificate;
use x509_cert::Certificate;

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
Expand Down Expand Up @@ -50,18 +48,22 @@ pub struct DiceArgs {
with_dice_inc_sec_epoch: bool,
}

fn get_pad(val: usize) -> usize {
match val.checked_rem(4) {
Some(s) if s > 0 => 4 - s,
_ => 0,
}
}
/// One of:
/// - a SHA2-256 of the modulus (`n`) and exponent (`e`) of an RSA public key,
/// - all zeros to indicate a missing root,
/// - a SHA2-256 of four such "hashes".
pub type Hash = [u8; 32];

/// Four root certificates, any subset of which may be missing (`None`).
pub type RootCerts = [Option<Certificate>; 4];

fn pad_roots(mut roots: Vec<Vec<u8>>) -> Result<[Vec<u8>; 4], Error> {
/// Ensure that there are exactly four root certificates.
pub fn pad_roots(roots: Vec<Certificate>) -> Result<RootCerts, Error> {
if roots.len() > 4 {
return Err(Error::TooManyRoots(roots.len()));
}
roots.resize_with(4, Vec::new);
let mut roots = roots.into_iter().map(Option::Some).collect::<Vec<_>>();
roots.resize_with(4, || None);
Ok(roots.try_into().unwrap())
}

Expand All @@ -71,10 +73,19 @@ fn pad_roots(mut roots: Vec<Vec<u8>>) -> Result<[Vec<u8>; 4], Error> {
/// and the root-key-table hash.
pub fn stamp_image(
mut image_bytes: Vec<u8>,
signing_certs: Vec<Vec<u8>>,
root_certs: Vec<Vec<u8>>,
signing_certs: Vec<Certificate>,
root_certs: Vec<Certificate>,
execution_address: u32,
) -> Result<Vec<u8>, Error> {
// Pad to a 4-byte boundary.
fn pad(val: usize) -> usize {
match val.checked_rem(4) {
Some(s) if s > 0 => 4 - s,
_ => 0,
}
}

// Check the certificates.
if signing_certs.is_empty() {
return Err(Error::NoSigningCertificate);
}
Expand All @@ -89,10 +100,11 @@ pub fn stamp_image(
// of each certificate.
let mut cert_table = Vec::new();
for cert in &signing_certs {
let cert_pad = get_pad(cert.len());
let padded_len = cert.len() + cert_pad;
let cert_bytes = cert.to_der()?;
let cert_pad = pad(cert_bytes.len());
let padded_len = cert_bytes.len() + cert_pad;
cert_table.extend_from_slice(&(padded_len as u32).to_le_bytes());
cert_table.extend_from_slice(cert);
cert_table.extend_from_slice(&cert_bytes);
cert_table.resize(cert_table.len() + cert_pad, 0);
}
let cert_table_len = cert_table.len();
Expand All @@ -103,15 +115,15 @@ pub fn stamp_image(

// How many bytes we sign, including image, cert table, and root key hashes.
let image_len = image_bytes.len();
let image_pad = get_pad(image_len);
let image_pad = pad(image_len);
let signed_len = image_len + image_pad + cert_header_len + cert_table_len + 4 * 32;
cert_header.total_image_len = signed_len
.try_into()
.map_err(|_| Error::SignedLengthOverflow)?;

// Total image length includes the length of the eventual signature.
let (_, leaf) = parse_x509_certificate(signing_certs.last().unwrap())?;
let pub_key = RsaPublicKey::from_pkcs1_der(leaf.public_key().subject_public_key.as_ref())?;
let leaf = signing_certs.last().unwrap();
let pub_key = cert::public_key(leaf)?;
let sig_len = pub_key.n().bits() / 8;
let total_len = signed_len + sig_len;

Expand Down Expand Up @@ -139,7 +151,7 @@ pub fn stamp_image(
// goes into the image and _must_ match the hash-of-hashes programmed in
// the CMPA!
for root in pad_roots(root_certs)? {
image_bytes.extend_from_slice(&root_key_hash(&root)?);
image_bytes.extend_from_slice(&root_key_hash(root.as_ref())?);
}
Ok(image_bytes)
}
Expand All @@ -164,35 +176,10 @@ pub fn sign_image(binary: &[u8], private_key: &str) -> Result<Vec<u8>, Error> {
Ok(signed)
}

pub fn root_key_hash(root: &[u8]) -> Result<[u8; 32], Error> {
if root.is_empty() {
Ok([0; 32])
} else {
let (_, root_cert) = parse_x509_certificate(root)?;

if !cert::uses_supported_signature_algorithm(&root_cert) {
return Err(Error::UnsupportedCertificateSignatureAlgorithm {
subject: root_cert.subject().to_string(),
algorithm: cert::signature_algorithm_name(&root_cert),
});
}

let root_key = root_cert.public_key().subject_public_key.as_ref();
let root_key = RsaPublicKey::from_pkcs1_der(root_key)?;
let mut hash = Sha256::new();
hash.update(&root_key.n().to_bytes_be());
hash.update(&root_key.e().to_bytes_be());
Ok(hash.finalize().into())
}
}

pub fn required_key_size(root_certs: &[Vec<u8>]) -> Result<Option<usize>, Error> {
pub fn required_key_size(root_certs: &RootCerts) -> Result<Option<usize>, Error> {
let mut required_key_size = None;
for cert in root_certs {
let (_, cert) = x509_parser::parse_x509_certificate(cert)?;
let public_key = rsa::RsaPublicKey::from_pkcs1_der(
cert.tbs_certificate.subject_pki.subject_public_key.as_ref(),
)?;
for cert in root_certs.iter().flatten() {
let public_key = cert::public_key(cert)?;
let public_key_bits = public_key.size() * 8;
if let Some(x) = required_key_size {
if x != public_key_bits {
Expand All @@ -205,10 +192,29 @@ pub fn required_key_size(root_certs: &[Vec<u8>]) -> Result<Option<usize>, Error>
Ok(required_key_size)
}

pub fn root_key_table_hash(root_certs: Vec<Vec<u8>>) -> Result<[u8; 32], Error> {
pub fn root_key_hash(root: Option<&Certificate>) -> Result<Hash, Error> {
match root {
None => Ok([0; 32]),
Some(root) => {
if !cert::uses_supported_signature_algorithm(root) {
return Err(Error::UnsupportedCertificateSignatureAlgorithm {
subject: root.tbs_certificate.subject.to_string(),
algorithm: cert::signature_algorithm_name(root),
});
}
let root_key = cert::public_key(root)?;
let mut hash = Sha256::new();
hash.update(&root_key.n().to_bytes_be());
hash.update(&root_key.e().to_bytes_be());
Ok(hash.finalize().into())
}
}
}

pub fn root_key_table_hash(root_certs: &RootCerts) -> Result<Hash, Error> {
let mut rkth = Sha256::new();
for root in pad_roots(root_certs)? {
rkth.update(root_key_hash(&root)?);
for root in root_certs {
rkth.update(root_key_hash(root.as_ref())?);
}
Ok(rkth.finalize().into())
}
Expand Down
Loading