135 lines
4.2 KiB
Rust

use base64::Engine;
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tal {
pub raw: Vec<u8>,
pub comments: Vec<String>,
pub ta_uris: Vec<Url>,
pub subject_public_key_info_der: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum TalDecodeError {
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
InvalidUtf8,
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
CommentAfterHeader,
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
MissingTaUris,
#[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")]
MissingSeparatorEmptyLine,
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
InvalidUri(String),
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
UnsupportedUriScheme(String),
#[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")]
UriIsDirectory(String),
#[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")]
MissingSpki,
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
SpkiBase64Decode,
#[error("TAL SPKI DER is empty (RFC 8630 §2.2)")]
SpkiDerEmpty,
}
impl Tal {
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
let raw = input.to_vec();
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
let lines: Vec<&str> = text
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l))
.collect();
let mut idx = 0usize;
// 1) Leading comments.
let mut comments: Vec<String> = Vec::new();
while idx < lines.len() && lines[idx].starts_with('#') {
comments.push(lines[idx][1..].to_string());
idx += 1;
}
// 2) URI list (one or more non-empty lines).
let mut ta_uris: Vec<Url> = Vec::new();
while idx < lines.len() {
let line = lines[idx].trim();
if line.is_empty() {
break;
}
if line.starts_with('#') {
return Err(TalDecodeError::CommentAfterHeader);
}
let url = match Url::parse(line) {
Ok(u) => u,
Err(_) => {
if !ta_uris.is_empty() {
return Err(TalDecodeError::MissingSeparatorEmptyLine);
}
return Err(TalDecodeError::InvalidUri(line.to_string()));
}
};
match url.scheme() {
"rsync" | "https" => {}
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
}
if url.path().ends_with('/') {
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
}
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
}
ta_uris.push(url);
idx += 1;
}
if ta_uris.is_empty() {
return Err(TalDecodeError::MissingTaUris);
}
// 3) Empty line separator (must exist).
if idx >= lines.len() || !lines[idx].trim().is_empty() {
return Err(TalDecodeError::MissingSeparatorEmptyLine);
}
idx += 1;
// 4) Base64(SPKI DER) remainder; allow line wrapping.
let mut b64 = String::new();
while idx < lines.len() {
let line = lines[idx].trim();
if !line.is_empty() {
b64.push_str(line);
}
idx += 1;
}
if b64.is_empty() {
return Err(TalDecodeError::MissingSpki);
}
let spki_der = base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
if spki_der.is_empty() {
return Err(TalDecodeError::SpkiDerEmpty);
}
Ok(Self {
raw,
comments,
ta_uris,
subject_public_key_info_der: spki_der,
})
}
}