use core::fmt;
use core::str::FromStr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Algorithm {
Ed25519,
Secp256k1,
P256,
#[cfg(feature = "bls-threshold")]
Bls12381Threshold,
}
impl Algorithm {
#[must_use]
pub fn cose_id(self) -> i32 {
match self {
Self::Ed25519 => -19,
Self::Secp256k1 => -47,
Self::P256 => -7,
#[cfg(feature = "bls-threshold")]
Self::Bls12381Threshold => -256,
}
}
#[must_use]
pub fn prefix(self) -> &'static str {
match self {
Self::Ed25519 => "ed25519",
Self::Secp256k1 => "secp256k1",
Self::P256 => "p256",
#[cfg(feature = "bls-threshold")]
Self::Bls12381Threshold => "bls12381-thr",
}
}
#[must_use]
pub fn from_keyid(keyid: &str) -> Option<Self> {
let (prefix, _) = keyid.split_once(':')?;
match prefix {
"ed25519" | "blake3" => Some(Self::Ed25519),
"secp256k1" => Some(Self::Secp256k1),
"p256" => Some(Self::P256),
#[cfg(feature = "bls-threshold")]
"bls12381-thr" => Some(Self::Bls12381Threshold),
_ => None,
}
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.prefix())
}
}
impl FromStr for Algorithm {
type Err = UnknownAlgorithm;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ed25519" => Ok(Self::Ed25519),
"secp256k1" => Ok(Self::Secp256k1),
"p256" => Ok(Self::P256),
#[cfg(feature = "bls-threshold")]
"bls12381-thr" => Ok(Self::Bls12381Threshold),
other => Err(UnknownAlgorithm(other.to_owned())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownAlgorithm(pub String);
impl fmt::Display for UnknownAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown algorithm prefix: {}", self.0)
}
}
impl std::error::Error for UnknownAlgorithm {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cose_ids_match_iana_registry() {
assert_eq!(Algorithm::Ed25519.cose_id(), -19);
assert_eq!(Algorithm::Secp256k1.cose_id(), -47);
assert_eq!(Algorithm::P256.cose_id(), -7);
}
#[test]
fn prefix_strings() {
assert_eq!(Algorithm::Ed25519.prefix(), "ed25519");
assert_eq!(Algorithm::Secp256k1.prefix(), "secp256k1");
assert_eq!(Algorithm::P256.prefix(), "p256");
}
#[test]
fn display_uses_prefix() {
assert_eq!(Algorithm::Ed25519.to_string(), "ed25519");
assert_eq!(Algorithm::Secp256k1.to_string(), "secp256k1");
assert_eq!(Algorithm::P256.to_string(), "p256");
}
#[test]
fn from_str_roundtrip_through_prefix() {
for alg in [Algorithm::Ed25519, Algorithm::Secp256k1, Algorithm::P256] {
let parsed: Algorithm = alg.prefix().parse().expect("round-trip parse");
assert_eq!(parsed, alg);
}
}
#[test]
fn from_str_rejects_unknown() {
let err: Result<Algorithm, _> = "rsa".parse();
assert!(err.is_err());
let err = "".parse::<Algorithm>().unwrap_err();
assert_eq!(err.0, "");
}
#[test]
fn from_keyid_parses_canonical_prefixes() {
assert_eq!(
Algorithm::from_keyid("ed25519:abc"),
Some(Algorithm::Ed25519)
);
assert_eq!(
Algorithm::from_keyid("secp256k1:dead"),
Some(Algorithm::Secp256k1)
);
assert_eq!(Algorithm::from_keyid("p256:feed"), Some(Algorithm::P256));
}
#[test]
fn from_keyid_legacy_blake3_maps_to_ed25519() {
assert_eq!(
Algorithm::from_keyid("blake3:deadbeef"),
Some(Algorithm::Ed25519)
);
}
#[test]
fn from_keyid_unknown_prefix_returns_none() {
assert_eq!(Algorithm::from_keyid("rsa:abc"), None);
assert_eq!(Algorithm::from_keyid("sigstore:https://x"), None);
}
#[test]
fn from_keyid_missing_colon_returns_none() {
assert_eq!(Algorithm::from_keyid("ed25519"), None);
assert_eq!(Algorithm::from_keyid(""), None);
}
#[test]
fn from_keyid_handles_multiple_colons() {
assert_eq!(
Algorithm::from_keyid("sigstore:https://example.com/workflow"),
None
);
assert_eq!(
Algorithm::from_keyid("ed25519:aa:bb"),
Some(Algorithm::Ed25519)
);
}
}