///|
/// SRI 1.0 algorithms supported by this library.
pub(all) enum Algorithm {
Sha256
Sha384
Sha512
}
///|
/// A token with a recognized SRI hash algorithm.
pub(all) struct Metadata {
algorithm : Algorithm
digest : String
}
///| NoSupportedMetadata is deliberately distinct from a digest mismatch.
///|
/// An offline checker should not silently accept an empty or unknown-only list.
pub(all) enum Verdict {
Match
Mismatch
NoSupportedMetadata
}
///|
fn Algorithm::label(self : Algorithm) -> String {
match self {
Sha256 => "sha256"
Sha384 => "sha384"
Sha512 => "sha512"
}
}
///|
fn Algorithm::strength(self : Algorithm) -> Int32 {
match self {
Sha256 => 1
Sha384 => 2
Sha512 => 3
}
}
///|
/// Generate one SRI metadata token for exact resource bytes.
pub fn generate(data : BytesView, algorithm : Algorithm) -> String {
algorithm.label() + "-" + encoded_digest(data, algorithm)
}
///|
fn encoded_digest(data : BytesView, algorithm : Algorithm) -> String {
let digest = @sha2.hash(data, kind=algorithm.kind())
@base64.encode(digest[:])
}
///|
fn Algorithm::kind(self : Algorithm) -> @sha2.Kind {
match self {
Sha256 => @sha2.Sha256
Sha384 => @sha2.Sha384
Sha512 => @sha2.Sha512
}
}
///|
fn is_metadata_space(ch : Char) -> Bool {
ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\u{000c}'
}
///|
fn parse_token(token : String) -> Metadata? {
// SRI reads the expression before '?' and the digest in its second field.
let expression = match token.split_once("?") {
Some((expression, _)) => expression
None => token[:]
}
let (label, tail) = match expression.split_once("-") {
Some(parts) => parts
None => (expression, ""[:])
}
let algorithm = if label.compare_ignore_ascii_case("sha256") == 0 {
Sha256
} else if label.compare_ignore_ascii_case("sha384") == 0 {
Sha384
} else if label.compare_ignore_ascii_case("sha512") == 0 {
Sha512
} else {
return None
}
let digest = match tail.split_once("-") {
Some((digest, _)) => digest.to_owned()
None => tail.to_owned()
}
Some({ algorithm, digest, })
}
///|
/// Parse SRI tokens using the algorithm in SRI 1.0, ignoring unknown algorithms.
pub fn parse_metadata(input : String) -> Array[Metadata] {
let result : Array[Metadata] = []
let token = StringBuilder()
for ch in input {
if is_metadata_space(ch) {
if !token.is_empty() {
match parse_token(token.to_string()) {
Some(item) => result.push(item)
None => ()
}
token.reset()
}
} else {
token.write_char(ch)
}
}
if !token.is_empty() {
match parse_token(token.to_string()) {
Some(item) => result.push(item)
None => ()
}
}
result
}
///| Compare resource bytes with only the strongest supported hash family.
///|
/// Multiple digests of that strength are alternatives, as in SRI 1.0.
pub fn verify_bytes(data : BytesView, input : String) -> Verdict {
let items = parse_metadata(input)
guard strongest_algorithm(items) is Some(strongest) else {
return NoSupportedMetadata
}
verify_strongest(data, items, strongest)
}
///|
/// Verify with a minimum algorithm strength. Err contains the strongest
/// advertised algorithm when it falls below the minimum.
pub fn verify_bytes_at_least(
data : BytesView,
input : String,
minimum : Algorithm,
) -> Result[Verdict, Algorithm] {
let items = parse_metadata(input)
guard strongest_algorithm(items) is Some(strongest) else {
return Ok(NoSupportedMetadata)
}
if strongest.strength() < minimum.strength() {
return Err(strongest)
}
Ok(verify_strongest(data, items, strongest))
}
///|
fn strongest_algorithm(items : Array[Metadata]) -> Algorithm? {
guard items.get(0) is Some(first) else { return None }
let mut strongest = first.algorithm
for item in items {
if item.algorithm.strength() > strongest.strength() {
strongest = item.algorithm
}
}
Some(strongest)
}
///|
fn verify_strongest(
data : BytesView,
items : Array[Metadata],
strongest : Algorithm,
) -> Verdict {
// The expected digest depends on the algorithm, not the number of candidates.
let expected = encoded_digest(data, strongest)
for item in items {
if item.algorithm.strength() == strongest.strength() &&
expected == item.digest {
return Match
}
}
Mismatch
}