-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This addresses #201 without breaking semver by adding opt-in methods on Descriptor to access the digest as a parsed, validated content (according to the grammar) as well as directly as a validated SHA-256. For example in some cases (e.g. ocidir-rs) I want to write a descriptor to the filesystem, and I don't want any possibility of path traversal attacks from someone including a `/` in a descriptor. Signed-off-by: Colin Walters <[email protected]>
- Loading branch information
Showing
3 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
//! Functionality corresponding to <https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests>. | ||
|
||
/// The well-known identifier for a SHA-256 digest. | ||
/// At this point, no one is really using alternative digests like sha512, so we | ||
/// don't yet try to expose them here in a higher level way. | ||
const ALG_SHA256: &str = "sha256"; | ||
|
||
fn char_is_lowercase_ascii_hex(c: char) -> bool { | ||
matches!(c, '0'..='9' | 'a'..='f') | ||
} | ||
|
||
/// algorithm-component ::= [a-z0-9]+ | ||
fn char_is_algorithm_component(c: char) -> bool { | ||
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9') | ||
} | ||
|
||
/// encoded ::= [a-zA-Z0-9=_-]+ | ||
fn char_is_encoded(c: char) -> bool { | ||
char_is_algorithm_component(c) || matches!(c, '=' | '_' | '-') | ||
} | ||
|
||
/// A parsed pair of algorithm:digest as defined | ||
/// by <https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests> | ||
pub struct Digest<'a> { | ||
/// The algorithm name (e.g. sha256, sha512) | ||
pub algorithm: &'a str, | ||
/// The algorithm component (lowercase hexadecimal) | ||
pub value: &'a str, | ||
} | ||
|
||
impl<'a> Digest<'a> { | ||
const ALGORITHM_SEPARATOR: &'static [char] = &['+', '.', '_', '-']; | ||
/// Parse a digest instance. | ||
pub fn new(s: &'a str) -> crate::Result<Self> { | ||
let (algorithm, value) = s | ||
.split_once(':') | ||
.ok_or_else(|| crate::OciSpecError::Other("Missing : in digest".into()))?; | ||
// algorithm ::= algorithm-component (algorithm-separator algorithm-component)* | ||
let algorithm_parts = algorithm.split(Self::ALGORITHM_SEPARATOR); | ||
for part in algorithm_parts { | ||
if part.is_empty() { | ||
return Err(crate::OciSpecError::Other( | ||
"Empty algorithm component".into(), | ||
)); | ||
} | ||
if !part.chars().all(char_is_algorithm_component) { | ||
return Err(crate::OciSpecError::Other(format!( | ||
"Invalid algorithm component: {part}" | ||
))); | ||
} | ||
} | ||
|
||
if value.is_empty() { | ||
return Err(crate::OciSpecError::Other("Empty algorithm value".into())); | ||
} | ||
if !value.chars().all(char_is_encoded) { | ||
return Err(crate::OciSpecError::Other(format!( | ||
"Invalid encoded value {value}" | ||
))); | ||
} | ||
|
||
Ok(Self { algorithm, value }) | ||
} | ||
|
||
/// View this instance as a validated SHA256 digest. | ||
pub fn to_sha256(self) -> crate::Result<Sha256Digest<'a>> { | ||
self.try_into() | ||
} | ||
} | ||
|
||
/// A SHA-256 digest. | ||
pub struct Sha256Digest<'a> { | ||
/// The underlying SHA-256 digest, guaranteed to be 64 lowercase hexadecimal ASCII characters. | ||
pub value: &'a str, | ||
} | ||
|
||
impl<'a> TryFrom<Digest<'a>> for Sha256Digest<'a> { | ||
type Error = crate::OciSpecError; | ||
|
||
fn try_from(algdigest: Digest<'a>) -> Result<Self, Self::Error> { | ||
match algdigest.algorithm { | ||
ALG_SHA256 => {} | ||
o => { | ||
return Err(crate::OciSpecError::Other(format!( | ||
"Expected algorithm {ALG_SHA256} but found {o}" | ||
))) | ||
} | ||
} | ||
|
||
let digest = algdigest.value; | ||
let is_all_hex = digest.chars().all(char_is_lowercase_ascii_hex); | ||
if !(algdigest.value.len() == 64 && is_all_hex) { | ||
return Err(crate::OciSpecError::Other(format!( | ||
"Invalid SHA-256 digest: {digest}" | ||
))); | ||
} | ||
Ok(Self { | ||
value: algdigest.value, | ||
}) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_digest_invalid() { | ||
let invalid = [ | ||
"", | ||
"foo", | ||
":", | ||
"blah+", | ||
"_digest:somevalue", | ||
":blah", | ||
"blah:", | ||
"^:foo", | ||
"bar^baz:blah", | ||
"sha256:123456*78", | ||
]; | ||
for case in invalid { | ||
assert!( | ||
Digest::new(case).is_err(), | ||
"Should have failed to parse: {case}" | ||
) | ||
} | ||
} | ||
|
||
const VALID_DIGEST_SHA256: &str = | ||
"sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"; | ||
|
||
#[test] | ||
fn test_digest_valid() { | ||
let cases = ["foo:bar", "sha256:blah", "sha512:12345"]; | ||
for case in cases { | ||
Digest::new(case).unwrap(); | ||
} | ||
|
||
let d = | ||
Digest::new("multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8").unwrap(); | ||
assert_eq!(d.algorithm, "multihash+base58"); | ||
assert_eq!(d.value, "QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8"); | ||
} | ||
|
||
#[test] | ||
fn test_sha256_invalid() { | ||
let invalid = [ | ||
"sha256:123456=78", | ||
"foo:bar", | ||
"sha256+blah:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b", | ||
]; | ||
for case in invalid { | ||
let d = Digest::new(case).unwrap(); | ||
assert!( | ||
Sha256Digest::try_from(d).is_err(), | ||
"Should have failed to parse: {case}" | ||
) | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_sha256_valid() { | ||
let d = Digest::new(VALID_DIGEST_SHA256).unwrap(); | ||
let d = d.to_sha256().unwrap(); | ||
assert_eq!(d.value, VALID_DIGEST_SHA256.split_once(':').unwrap().1); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters