use std::fmt;
use num_bigint::BigUint;
use num_traits::{One, Zero};
use sha1::{Digest, Sha1};
use crate::{Error, Result};
const KEY_HEADER: [u8; 13] = [
0x02, 0x00, 0x80, 0x80, 0x03, 0x00, 0x01, 0x00, 0x01, 0x81, 0x80, 0x80, 0x00,
];
const RSA_ALGORITHM: [u8; 13] = [
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
];
const METADATA_CLAIM: (u16, u16) = (11, 1);
fn u16_at(data: &[u8], at: usize) -> Option<u16> {
Some(u16::from_le_bytes(data.get(at..at + 2)?.try_into().ok()?))
}
pub fn validate_metadata_certificate(certificate: &[u8]) -> Result<Vec<(u16, u16)>> {
if !matches!(certificate.len(), 281 | 285) {
return Err(Error::request(
"Expected an observed 281- or 285-byte public certificate",
));
}
let word = |at| u16_at(certificate, at).unwrap_or_default(); let (size, version, issuer, count) = (word(0), word(2), word(4), word(6));
if usize::from(size) + 128 != certificate.len()
|| version != 1
|| issuer != 1
|| !(1..=2).contains(&count)
{
return Err(Error::request("Unsupported public certificate header"));
}
let claims: Vec<(u16, u16)> = (0..usize::from(count))
.map(|i| (word(8 + 4 * i), word(10 + 4 * i)))
.collect();
if claims.iter().any(|&(_, level)| level != 1) {
return Err(Error::request("Unsupported certificate claim layout"));
}
let key_start = 8 + 4 * claims.len();
if certificate.get(key_start..key_start + 13) != Some(&KEY_HEADER[..]) {
return Err(Error::request("Unsupported certificate public-key layout"));
}
if key_start + 13 + 128 != usize::from(size) {
return Err(Error::request("Certificate public-key length mismatch"));
}
Ok(claims)
}
struct DerReader<'a> {
data: &'a [u8],
position: usize,
}
impl<'a> DerReader<'a> {
fn new(data: &'a [u8]) -> Self {
DerReader { data, position: 0 }
}
fn at_end(&self) -> bool {
self.position >= self.data.len()
}
fn item(&mut self, tag: u8) -> Result<&'a [u8]> {
let data = self.data;
let mut position = self.position;
if position + 2 > data.len() || data[position] != tag {
return Err(Error::request("Invalid RSA private-key DER structure"));
}
let mut length = usize::from(data[position + 1]);
position += 2;
if length & 0x80 != 0 {
let count = length & 0x7F;
if !(1..=2).contains(&count) || position + count > data.len() {
return Err(Error::request("Invalid RSA private-key DER length"));
}
let encoded = &data[position..position + count];
if encoded[0] == 0 {
return Err(Error::request("Noncanonical RSA private-key DER length"));
}
length = encoded.iter().fold(0, |n, &b| (n << 8) | usize::from(b));
if length < 128 {
return Err(Error::request("Noncanonical RSA private-key DER length"));
}
position += count;
}
let end = position + length;
if end > data.len() {
return Err(Error::request("Truncated RSA private-key DER"));
}
self.position = end;
Ok(&data[position..end])
}
fn integer(&mut self) -> Result<BigUint> {
let encoded = self.item(2)?;
match encoded {
[] => Err(Error::request("RSA key integers must be nonnegative")),
[first, ..] if first & 0x80 != 0 => {
Err(Error::request("RSA key integers must be nonnegative"))
}
[0, second, ..] if second & 0x80 == 0 => {
Err(Error::request("Noncanonical RSA key integer"))
}
_ => Ok(BigUint::from_bytes_be(encoded)),
}
}
}
fn gcd(a: &BigUint, b: &BigUint) -> BigUint {
let (mut a, mut b) = (a.clone(), b.clone());
while !b.is_zero() {
let r = &a % &b;
a = b;
b = r;
}
a
}
fn private_numbers(data: &[u8]) -> Result<(BigUint, BigUint, BigUint)> {
if !(1..=2048).contains(&data.len()) {
return Err(Error::request(
"Supply an unencrypted PKCS #1 or PKCS #8 RSA private key in DER format",
));
}
let mut outer = DerReader::new(data);
let mut inner = DerReader::new(outer.item(0x30)?);
if outer.position != data.len() {
return Err(Error::request("Unexpected data after RSA private key"));
}
let mut version = inner.integer()?;
if !inner.at_end() && inner.data[inner.position] == 0x30 {
if !version.is_zero() || inner.item(0x30)? != RSA_ALGORITHM {
return Err(Error::request(
"Unsupported PKCS #8 RSA algorithm or version",
));
}
let wrapped = inner.item(4)?;
if !inner.at_end() {
return Err(Error::request("Unexpected PKCS #8 private-key fields"));
}
let mut wrapper = DerReader::new(wrapped);
inner = DerReader::new(wrapper.item(0x30)?);
if wrapper.position != wrapped.len() {
return Err(Error::request("Unexpected data after PKCS #8 RSA key"));
}
version = inner.integer()?;
}
let mut numbers = Vec::with_capacity(8);
for _ in 0..8 {
numbers.push(inner.integer()?);
}
if !inner.at_end() || !version.is_zero() {
return Err(Error::request(
"Only a two-prime PKCS #1 RSA private key is supported",
));
}
let [n, e, d, p, q, dp, dq, coefficient]: [BigUint; 8] = numbers
.try_into()
.map_err(|_| Error::request("Invalid RSA private-key DER structure"))?;
let one = BigUint::one();
let two = BigUint::from(2u8);
if n.bits() != 1024
|| e != BigUint::from(65537u32)
|| !(one < d && d < n)
|| p <= two
|| q <= two
|| p == q
{
return Err(Error::request("Unsupported RSA private-key parameters"));
}
if &p * &q != n {
return Err(Error::request("Inconsistent RSA private-key modulus"));
}
let (p1, q1) = (&p - &one, &q - &one);
let order = &p1 * &q1 / gcd(&p1, &q1);
if (&e * &d) % &order != one
|| dp != &d % &p1
|| dq != &d % &q1
|| (&coefficient * &q) % &p != one
{
return Err(Error::request("Inconsistent RSA private-key components"));
}
Ok((n, e, d))
}
#[derive(Clone)]
pub struct LogixMetadataCredentials {
certificate: Vec<u8>,
n: BigUint,
e: BigUint,
d: BigUint,
}
impl fmt::Debug for LogixMetadataCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("LogixMetadataCredentials(<redacted>)")
}
}
impl LogixMetadataCredentials {
pub fn new(certificate: &[u8], private_key_der: &[u8]) -> Result<Self> {
let claims = validate_metadata_certificate(certificate)?;
if !claims.contains(&METADATA_CLAIM) {
return Err(Error::request(
"Certificate lacks the observed metadata access claim",
));
}
let (n, e, d) = private_numbers(private_key_der)?;
let key_start = 8 + 4 * claims.len();
let public = BigUint::from_bytes_be(&certificate[key_start + 13..key_start + 141]);
if n != public {
return Err(Error::request(
"Private key does not match the signed certificate",
));
}
Ok(LogixMetadataCredentials {
certificate: certificate.to_vec(),
n,
e,
d,
})
}
pub fn bundled() -> Result<Self> {
super::bundle::bundled_credentials()
}
pub fn certificate(&self) -> &[u8] {
&self.certificate
}
pub fn answer_challenge(&self, data: &[u8]) -> Result<[u8; 20]> {
if data.len() != 130 || data[..2] != [0x80, 0x00] {
return Err(Error::request(
"Expected a length-prefixed 128-byte metadata challenge",
));
}
let ciphertext = BigUint::from_bytes_le(&data[2..]);
if ciphertext >= self.n {
return Err(Error::request(
"Metadata challenge is outside the RSA modulus",
));
}
let (factor, inverse) = self.blinding_factor()?;
let blinded = &ciphertext * factor.modpow(&self.e, &self.n) % &self.n;
let plain = blinded.modpow(&self.d, &self.n) * inverse % &self.n;
if plain.modpow(&self.e, &self.n) != ciphertext {
return Err(Error::request(
"RSA challenge calculation failed verification",
));
}
let mut bytes = plain.to_bytes_le();
bytes.resize(128, 0);
Ok(Sha1::digest(&bytes[..20]).into())
}
fn blinding_factor(&self) -> Result<(BigUint, BigUint)> {
let fail = || Error::request("Could not generate an RSA blinding factor");
let span = &self.n - 3u8;
for _ in 0..128 {
let mut random = [0u8; 136];
getrandom::fill(&mut random).map_err(|_| fail())?;
let factor = BigUint::from_bytes_le(&random) % &span + 2u8;
if let Some(inverse) = factor.modinv(&self.n) {
return Ok((factor, inverse));
}
}
Err(fail())
}
pub fn validate_completion(&self, data: &[u8]) -> Result<()> {
let Some(count) = u16_at(data, 0) else {
return Err(Error::request("Missing metadata authentication grants"));
};
let count = usize::from(count);
if !(1..=64).contains(&count) || data.len() != 2 + count * 6 {
return Err(Error::request("Malformed metadata authentication grants"));
}
let grants: Vec<(u16, u32)> = data[2..]
.chunks_exact(6)
.map(|g| {
(
u16::from_le_bytes([g[0], g[1]]),
u32::from_le_bytes([g[2], g[3], g[4], g[5]]),
)
})
.collect();
let mut claims: Vec<u16> = grants.iter().map(|g| g.0).collect();
claims.sort_unstable();
claims.dedup();
if claims.len() != count {
return Err(Error::request("Duplicate metadata authentication grant"));
}
if !grants.contains(&(METADATA_CLAIM.0, u32::from(METADATA_CLAIM.1))) {
return Err(Error::request("Controller did not grant metadata access"));
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn der_rejects_negative_empty_and_noncanonical_integers() {
for data in [&b"\x02\x01\x80"[..], b"\x02\x02\x00\x01", b"\x02\x00"] {
assert!(DerReader::new(data).integer().is_err(), "{data:?}");
}
assert_eq!(
DerReader::new(b"\x02\x02\x00\x80").integer().unwrap(),
BigUint::from(128u8)
);
}
#[test]
fn der_long_lengths_must_be_canonical() {
let mut long = vec![0x04, 0x81, 0x80];
long.extend([0u8; 128]);
assert_eq!(DerReader::new(&long).item(4).unwrap().len(), 128);
assert!(DerReader::new(b"\x04\x81\x7f").item(4).is_err());
assert!(DerReader::new(b"\x04\x82\x00\x80").item(4).is_err());
assert!(DerReader::new(b"\x04\x83\x00\x00\x01").item(4).is_err());
}
#[test]
fn gcd_matches_euclid() {
assert_eq!(
gcd(&BigUint::from(12u8), &BigUint::from(18u8)),
BigUint::from(6u8)
);
}
}