Keep the colleague's microbu-esp32c5 tree in this repository

obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but
that tree was gitignored, so a clone of this repository could not build the
firmware it ships. It is now committed here as ordinary files in its own
folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP
bridge's signature verification (--trust) used on the bench. Nothing is
fetched from or pushed to the colleague's repository; this repository and
its remotes carry everything. The folder's own .gitignore keeps build output,
downloaded components and private key material out, as it did there; the
committed file set is identical to that repository's tracked files.

The ESP32-C5 is still flashed from obu-firmware/, which only takes
vanetza-idf from microbu-esp32c5/, so the two stay separate folders.
FLASHING.md says how to take a newer version of the colleague's tree (copy
it over the folder, rebuild, test, commit).
This commit is contained in:
Ashin Walpola
2026-09-23 17:46:40 +02:00
parent 2f60623e18
commit 0e9525162d
9881 changed files with 1582523 additions and 17 deletions
@@ -0,0 +1,179 @@
use std::io::{self, Read};
use std::path::PathBuf;
use sha2::{Digest, Sha256};
const EXPECTED_TEMPLATE_SHA256: &str =
"7168E341764188E140EF591634A9D2488D7FDACDA667EE53AFD566A8034ED5DB";
fn err(msg: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.into())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let template_path = PathBuf::from(
std::env::var("UML_TEMPLATE")
.unwrap_or_else(|_| "registered_rca.oer".to_string()),
);
let output_dir = PathBuf::from(
std::env::var("UML_OUTPUT_DIR").unwrap_or_else(|_| "uml_output".to_string()),
);
let template_bytes = std::fs::read(&template_path)?;
let template_sha256 = hex::encode_upper(Sha256::digest(&template_bytes));
if template_sha256 != EXPECTED_TEMPLATE_SHA256 {
return Err(err(format!(
"Template SHA-256 mismatch. Expected {}, got {}",
EXPECTED_TEMPLATE_SHA256, template_sha256
))
.into());
}
// Decode the registered certificate only as the reviewed profile template.
let template: rasn_its::ts103097::EtsiTs103097Certificate =
rasn::oer::decode(&template_bytes)?;
// The wrapper sends a newly generated P-256 scalar over stdin. It is never
// written to disk unencrypted.
let mut scalar_hex = String::new();
std::io::stdin().read_to_string(&mut scalar_hex)?;
let scalar = hex::decode(scalar_hex.trim())?;
if scalar.len() != 32 {
return Err(err(format!(
"Expected a 32-byte NIST P-256 private scalar, received {} bytes",
scalar.len()
))
.into());
}
let secret_key = p256::SecretKey::from_slice(&scalar)
.map_err(|_| err("Invalid NIST P-256 private scalar"))?;
let private_key =
c_its::security::crypto::PrivateKey::P256(secret_key.into());
let mut tbs_cert = template.to_be_signed.clone();
tbs_cert.verify_key_indicator =
rasn_its::ieee1609dot2::VerificationKeyIndicator::VerificationKey(
private_key.public_key().encode(),
);
// This is deliberately the exact encoding/signing route introduced by
// TheEnbyperor/c-its commit e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4.
let tbs_bytes = rasn::oer::encode(&tbs_cert)?;
let hash_alg = c_its::security::crypto::HashAlgorithm::Sha256;
let signature = private_key
.sign(hash_alg, &tbs_bytes, None)
.ok_or_else(|| err("c-its signing failed"))?;
let (r_hex, s_hex) = match &signature {
c_its::security::crypto::Signature::P256(sig) => (
hex::encode_upper(sig.r().to_bytes()),
hex::encode_upper(sig.s().to_bytes()),
),
_ => return Err(err("Unexpected signature algorithm").into()),
};
// Construct the certificate exactly like the upstream root generator.
let raw_cert = rasn_its::ts103097::EtsiTs103097Certificate::try_from(
rasn_its::ieee1609dot2::Certificate::from(
rasn_its::ieee1609dot2::ExplicitCertificate::new(
rasn_its::ieee1609dot2::CertificateBase {
version: 3,
r#type: rasn_its::ieee1609dot2::CertificateType::Explicit,
issuer: rasn_its::ieee1609dot2::IssuerIdentifier::VSelf(
hash_alg.into(),
),
to_be_signed: tbs_cert,
signature: Some(signature.as_signature()),
},
)
.map_err(|_| err("Could not construct ExplicitCertificate"))?,
),
)
.map_err(|_| err("Certificate does not satisfy EtsiTs103097Certificate constraints"))?;
let cert = c_its::security::certs::Certificate::new(&raw_cert);
let encoded = cert.to_bytes();
// Parse the actual final bytes from scratch and make the upstream c-its verifier
// validate the self-signature.
let reparsed = c_its::security::certs::Certificate::parse(&encoded)
.map_err(err)?;
let report = reparsed.report().map_err(err)?;
match report.signature() {
c_its::security::certs::CertificateSignature::SelfSigned { verifies: true } => {}
other => {
return Err(err(format!(
"FINAL CERTIFICATE SELF-SIGNATURE DID NOT VERIFY: {:?}",
other
))
.into())
}
}
let reencoded = reparsed.to_bytes();
if reencoded != encoded {
return Err(err("Final certificate is not stable across parse/re-encode").into());
}
let id8 = hex::encode_upper(report.id8());
let cert_sha256 = hex::encode_upper(Sha256::digest(&encoded));
if &cert_sha256[cert_sha256.len() - 16..] != id8 {
return Err(err("HashedID8 does not match low-order 8 bytes of SHA-256").into());
}
// Compute the IEEE/ETSI self-signed certificate signing prehash independently
// from the signature object:
// SHA256( SHA256(COER/OER-TBS) || SHA256(empty signer certificate) )
let tbs_hash = Sha256::digest(&tbs_bytes);
let empty_hash = Sha256::digest([]);
let mut outer = Sha256::new();
outer.update(&tbs_hash);
outer.update(&empty_hash);
let signing_prehash = outer.finalize();
std::fs::create_dir_all(&output_dir)?;
std::fs::write(output_dir.join(format!("{}.oer", id8)), &encoded)?;
std::fs::write(output_dir.join("tbs.oer"), &tbs_bytes)?;
std::fs::write(
output_dir.join("certificate_report.json"),
serde_json::to_vec_pretty(&report)?,
)?;
let signing_details = format!(
concat!(
"UPSTREAM_COMMIT=e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4\n",
"TEMPLATE_SHA256={}\n",
"CERTIFICATE_SHA256={}\n",
"HASHEDID8={}\n",
"TBS_LENGTH={}\n",
"TBS_SHA256={}\n",
"EMPTY_SHA256={}\n",
"SIGNING_PREHASH_SHA256={}\n",
"ECDSA_R={}\n",
"ECDSA_S={}\n",
"CITS_SELF_SIGNATURE_VERIFIES=true\n",
"PARSE_REENCODE_STABLE=true\n"
),
template_sha256,
cert_sha256,
id8,
tbs_bytes.len(),
hex::encode_upper(tbs_hash),
hex::encode_upper(empty_hash),
hex::encode_upper(signing_prehash),
r_hex,
s_hex,
);
std::fs::write(output_dir.join("signing_details.txt"), signing_details)?;
println!("RESULT_ID8={}", id8);
println!(
"RESULT_CERT={}",
output_dir.join(format!("{}.oer", id8)).display()
);
println!("CITS_SELF_SIGNATURE_VERIFIES=true");
Ok(())
}
@@ -0,0 +1,176 @@
use std::io::{self, Read};
use std::path::PathBuf;
use sha2::{Digest, Sha256};
const EXPECTED_TEMPLATE_SHA256: &str =
"7168E341764188E140EF591634A9D2488D7FDACDA667EE53AFD566A8034ED5DB";
fn err(msg: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.into())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let template_path = PathBuf::from(
std::env::var("UML_TEMPLATE")
.unwrap_or_else(|_| "registered_rca.oer".to_string()),
);
let output_dir = PathBuf::from(
std::env::var("UML_OUTPUT_DIR").unwrap_or_else(|_| "uml_output".to_string()),
);
let template_bytes = std::fs::read(&template_path)?;
let template_sha256 = hex::encode_upper(Sha256::digest(&template_bytes));
if template_sha256 != EXPECTED_TEMPLATE_SHA256 {
return Err(err(format!(
"Template SHA-256 mismatch. Expected {}, got {}",
EXPECTED_TEMPLATE_SHA256, template_sha256
))
.into());
}
// Decode the registered certificate. Its TBS, including its public key, is kept
// unchanged so this tool cannot silently create a different root identity.
let template: rasn_its::ts103097::EtsiTs103097Certificate =
rasn::oer::decode(&template_bytes)?;
// The wrapper unlocks the registered P-256 key and sends its scalar over stdin.
// It is never written to disk unencrypted.
let mut scalar_hex = String::new();
std::io::stdin().read_to_string(&mut scalar_hex)?;
let scalar = hex::decode(scalar_hex.trim())?;
if scalar.len() != 32 {
return Err(err(format!(
"Expected a 32-byte NIST P-256 private scalar, received {} bytes",
scalar.len()
))
.into());
}
let secret_key = p256::SecretKey::from_slice(&scalar)
.map_err(|_| err("Invalid NIST P-256 private scalar"))?;
let private_key =
c_its::security::crypto::PrivateKey::P256(secret_key.into());
let tbs_cert = template.to_be_signed.clone();
// This is deliberately the exact encoding/signing route introduced by
// TheEnbyperor/c-its commit e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4.
let tbs_bytes = rasn::oer::encode(&tbs_cert)?;
let hash_alg = c_its::security::crypto::HashAlgorithm::Sha256;
let signature = private_key
.sign(hash_alg, &tbs_bytes, None)
.ok_or_else(|| err("c-its signing failed"))?;
let (r_hex, s_hex) = match &signature {
c_its::security::crypto::Signature::P256(sig) => (
hex::encode_upper(sig.r().to_bytes()),
hex::encode_upper(sig.s().to_bytes()),
),
_ => return Err(err("Unexpected signature algorithm").into()),
};
// Construct the certificate exactly like the upstream root generator.
let raw_cert = rasn_its::ts103097::EtsiTs103097Certificate::try_from(
rasn_its::ieee1609dot2::Certificate::from(
rasn_its::ieee1609dot2::ExplicitCertificate::new(
rasn_its::ieee1609dot2::CertificateBase {
version: 3,
r#type: rasn_its::ieee1609dot2::CertificateType::Explicit,
issuer: rasn_its::ieee1609dot2::IssuerIdentifier::VSelf(
hash_alg.into(),
),
to_be_signed: tbs_cert,
signature: Some(signature.as_signature()),
},
)
.map_err(|_| err("Could not construct ExplicitCertificate"))?,
),
)
.map_err(|_| err("Certificate does not satisfy EtsiTs103097Certificate constraints"))?;
let cert = c_its::security::certs::Certificate::new(&raw_cert);
let encoded = cert.to_bytes();
// Parse the actual final bytes from scratch and make the upstream c-its verifier
// validate the self-signature.
let reparsed = c_its::security::certs::Certificate::parse(&encoded)
.map_err(err)?;
let report = reparsed.report().map_err(err)?;
match report.signature() {
c_its::security::certs::CertificateSignature::SelfSigned { verifies: true } => {}
other => {
return Err(err(format!(
"FINAL CERTIFICATE SELF-SIGNATURE DID NOT VERIFY: {:?}",
other
))
.into())
}
}
let reencoded = reparsed.to_bytes();
if reencoded != encoded {
return Err(err("Final certificate is not stable across parse/re-encode").into());
}
let id8 = hex::encode_upper(report.id8());
let cert_sha256 = hex::encode_upper(Sha256::digest(&encoded));
if &cert_sha256[cert_sha256.len() - 16..] != id8 {
return Err(err("HashedID8 does not match low-order 8 bytes of SHA-256").into());
}
// Compute the IEEE/ETSI self-signed certificate signing prehash independently
// from the signature object:
// SHA256( SHA256(COER/OER-TBS) || SHA256(empty signer certificate) )
let tbs_hash = Sha256::digest(&tbs_bytes);
let empty_hash = Sha256::digest([]);
let mut outer = Sha256::new();
outer.update(&tbs_hash);
outer.update(&empty_hash);
let signing_prehash = outer.finalize();
std::fs::create_dir_all(&output_dir)?;
std::fs::write(output_dir.join(format!("{}.oer", id8)), &encoded)?;
std::fs::write(output_dir.join("tbs.oer"), &tbs_bytes)?;
std::fs::write(
output_dir.join("certificate_report.json"),
serde_json::to_vec_pretty(&report)?,
)?;
let signing_details = format!(
concat!(
"UPSTREAM_COMMIT=e3bb3b82480d6df4237e2a8c35ea0dd7eade25b4\n",
"TEMPLATE_SHA256={}\n",
"CERTIFICATE_SHA256={}\n",
"HASHEDID8={}\n",
"TBS_LENGTH={}\n",
"TBS_SHA256={}\n",
"EMPTY_SHA256={}\n",
"SIGNING_PREHASH_SHA256={}\n",
"ECDSA_R={}\n",
"ECDSA_S={}\n",
"CITS_SELF_SIGNATURE_VERIFIES=true\n",
"PARSE_REENCODE_STABLE=true\n"
),
template_sha256,
cert_sha256,
id8,
tbs_bytes.len(),
hex::encode_upper(tbs_hash),
hex::encode_upper(empty_hash),
hex::encode_upper(signing_prehash),
r_hex,
s_hex,
);
std::fs::write(output_dir.join("signing_details.txt"), signing_details)?;
println!("RESULT_ID8={}", id8);
println!(
"RESULT_CERT={}",
output_dir.join(format!("{}.oer", id8)).display()
);
println!("CITS_SELF_SIGNATURE_VERIFIES=true");
Ok(())
}