use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct KeyId(String);
impl KeyId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for KeyId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for KeyId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for KeyId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyMetadata {
pub key_id: KeyId,
pub algorithm: KeyAlgorithm,
pub label: String,
pub public_key: Option<Vec<u8>>,
pub attestation: Option<Attestation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
#[non_exhaustive]
pub enum KeyAlgorithm {
Rsa2048,
Rsa4096,
EcdsaP256,
EcdsaP384,
Ed25519,
Aes128,
Aes256,
}
impl fmt::Display for KeyAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyAlgorithm::Rsa2048 => write!(f, "RSA-2048"),
KeyAlgorithm::Rsa4096 => write!(f, "RSA-4096"),
KeyAlgorithm::EcdsaP256 => write!(f, "ECDSA-P256"),
KeyAlgorithm::EcdsaP384 => write!(f, "ECDSA-P384"),
KeyAlgorithm::Ed25519 => write!(f, "Ed25519"),
KeyAlgorithm::Aes128 => write!(f, "AES-128"),
KeyAlgorithm::Aes256 => write!(f, "AES-256"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown value: {0:?}")]
pub struct ParseError(String);
impl std::str::FromStr for KeyAlgorithm {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"RSA-2048" => Ok(Self::Rsa2048),
"RSA-4096" => Ok(Self::Rsa4096),
"ECDSA-P256" => Ok(Self::EcdsaP256),
"ECDSA-P384" => Ok(Self::EcdsaP384),
"Ed25519" => Ok(Self::Ed25519),
"AES-128" => Ok(Self::Aes128),
"AES-256" => Ok(Self::Aes256),
_ => Err(ParseError(s.to_owned())),
}
}
}
impl From<KeyAlgorithm> for String {
fn from(a: KeyAlgorithm) -> String {
a.to_string()
}
}
impl TryFrom<String> for KeyAlgorithm {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyUsages: u8 {
const SIGN = 0b0000_0001;
const VERIFY = 0b0000_0010;
const ENCRYPT = 0b0000_0100;
const DECRYPT = 0b0000_1000;
const WRAP = 0b0001_0000;
const UNWRAP = 0b0010_0000;
const DERIVE = 0b0100_0000;
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyPolicy {
pub persistent: bool,
pub sensitive: bool,
pub extractable: bool,
pub wrap_with_trusted_only: bool,
pub usages: KeyUsages,
}
impl Default for KeyPolicy {
fn default() -> Self {
Self {
persistent: true,
sensitive: true,
extractable: false,
wrap_with_trusted_only: false,
usages: KeyUsages::SIGN | KeyUsages::VERIFY,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeySpec {
pub algorithm: KeyAlgorithm,
pub label: String,
pub policy: KeyPolicy,
pub location_hint: Option<String>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeySecurityAttributes {
pub always_sensitive: bool,
pub never_extractable: bool,
pub sensitive: bool,
pub extractable: bool,
pub wrap_with_trusted_only: bool,
pub usages: KeyUsages,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SignAlgorithm {
RsaPkcs1Sha256,
RsaPssSha256,
EcdsaSha256,
EcdsaSha384,
Ed25519,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
#[non_exhaustive]
pub enum WrapAlgorithm {
CmsRsaCbc,
CmsRsaGcm,
AesKeyWrap,
AesKeyWrapPad,
RsaOaepSha256,
}
impl fmt::Display for WrapAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WrapAlgorithm::CmsRsaCbc => write!(f, "CMS-RSA-CBC"),
WrapAlgorithm::CmsRsaGcm => write!(f, "CMS-RSA-GCM"),
WrapAlgorithm::AesKeyWrap => write!(f, "AES-KW"),
WrapAlgorithm::AesKeyWrapPad => write!(f, "AES-KWP"),
WrapAlgorithm::RsaOaepSha256 => write!(f, "RSA-OAEP-SHA256"),
}
}
}
impl std::str::FromStr for WrapAlgorithm {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"CMS-RSA-CBC" => Ok(Self::CmsRsaCbc),
"CMS-RSA-GCM" => Ok(Self::CmsRsaGcm),
"AES-KW" => Ok(Self::AesKeyWrap),
"AES-KWP" => Ok(Self::AesKeyWrapPad),
"RSA-OAEP-SHA256" => Ok(Self::RsaOaepSha256),
_ => Err(ParseError(s.to_owned())),
}
}
}
impl From<WrapAlgorithm> for String {
fn from(a: WrapAlgorithm) -> String {
a.to_string()
}
}
impl TryFrom<String> for WrapAlgorithm {
type Error = ParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum WrappedKeyFormat {
Cms,
AesKeyWrap,
RsaOaep,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrappedKey {
pub algorithm: WrapAlgorithm,
pub data: Vec<u8>,
pub recipient_hint: Option<String>,
}
impl WrappedKey {
pub fn format(&self) -> WrappedKeyFormat {
match self.algorithm {
WrapAlgorithm::CmsRsaCbc | WrapAlgorithm::CmsRsaGcm => WrappedKeyFormat::Cms,
WrapAlgorithm::AesKeyWrap | WrapAlgorithm::AesKeyWrapPad => {
WrappedKeyFormat::AesKeyWrap
}
WrapAlgorithm::RsaOaepSha256 => WrappedKeyFormat::RsaOaep,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum AttestationKind {
HardwareCertChain,
TpmQuote,
Pkcs11Attributes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attestation {
pub kind: AttestationKind,
pub certificates: Vec<Vec<u8>>,
pub signature: Option<Vec<u8>>,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
pub hostname: String,
pub machine_id: Option<String>,
pub cpu_model: Option<String>,
pub os_name: Option<String>,
pub os_version: Option<String>,
pub kernel_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PivSlot {
Authentication,
Signature,
KeyManagement,
CardAuthentication,
Retired(u8),
}
impl PivSlot {
pub fn retired(index: u8) -> Option<Self> {
(index <= 19).then_some(Self::Retired(index))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CertRef {
PivSlot(PivSlot),
Label(String),
RawId(Vec<u8>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PivPinPolicy {
Default,
Never,
Once,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PivTouchPolicy {
Default,
Never,
Always,
Cached,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PivSlotInfo {
pub slot: PivSlot,
pub algorithm: Option<KeyAlgorithm>,
pub has_certificate: bool,
pub origin: PivKeyOrigin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PivKeyOrigin {
Generated,
Imported,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PivDeviceInfo {
pub serial: Option<String>,
pub firmware_version: Option<String>,
pub form_factor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YubikeySlotMetadata {
pub pin_policy: PivPinPolicy,
pub touch_policy: PivTouchPolicy,
pub origin: PivKeyOrigin,
pub public_key: Option<Vec<u8>>,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pkcs11TokenFlags: u8 {
const LOGIN_REQUIRED = 0b0000_0001;
const USER_PIN_INITIALIZED = 0b0000_0010;
const TOKEN_INITIALIZED = 0b0000_0100;
const WRITE_PROTECTED = 0b0000_1000;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pkcs11TokenInfo {
pub label: String,
pub manufacturer: String,
pub model: String,
pub serial: String,
pub firmware_version: String,
pub flags: Pkcs11TokenFlags,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Pkcs11Mechanism(String);
impl Pkcs11Mechanism {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Pkcs11Mechanism {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TpmInfo {
pub version: String,
pub manufacturer: String,
pub firmware_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PcrValue {
pub index: u8,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendConfig {
pub provider: String,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_algorithm_serde_roundtrip() {
let cases: &[(KeyAlgorithm, &str)] = &[
(KeyAlgorithm::Rsa2048, "\"RSA-2048\""),
(KeyAlgorithm::Rsa4096, "\"RSA-4096\""),
(KeyAlgorithm::EcdsaP256, "\"ECDSA-P256\""),
(KeyAlgorithm::EcdsaP384, "\"ECDSA-P384\""),
(KeyAlgorithm::Ed25519, "\"Ed25519\""),
(KeyAlgorithm::Aes128, "\"AES-128\""),
(KeyAlgorithm::Aes256, "\"AES-256\""),
];
for &(variant, expected) in cases {
let serialized = serde_json::to_string(&variant).unwrap();
assert_eq!(serialized, expected, "serialize {variant:?}");
let deserialized: KeyAlgorithm = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, variant, "deserialize {expected}");
}
}
#[test]
fn wrap_algorithm_serde_roundtrip() {
let cases: &[(WrapAlgorithm, &str)] = &[
(WrapAlgorithm::CmsRsaCbc, "\"CMS-RSA-CBC\""),
(WrapAlgorithm::CmsRsaGcm, "\"CMS-RSA-GCM\""),
(WrapAlgorithm::AesKeyWrap, "\"AES-KW\""),
(WrapAlgorithm::AesKeyWrapPad, "\"AES-KWP\""),
(WrapAlgorithm::RsaOaepSha256, "\"RSA-OAEP-SHA256\""),
];
for &(variant, expected) in cases {
let serialized = serde_json::to_string(&variant).unwrap();
assert_eq!(serialized, expected, "serialize {variant:?}");
let deserialized: WrapAlgorithm = serde_json::from_str(expected).unwrap();
assert_eq!(deserialized, variant, "deserialize {expected}");
}
}
#[test]
fn wrap_algorithm_from_str_rejects_unknown() {
assert!("CMS-RSA-XTS".parse::<WrapAlgorithm>().is_err());
assert!("".parse::<WrapAlgorithm>().is_err());
assert!(
"cms-rsa-gcm".parse::<WrapAlgorithm>().is_err(),
"must be case-sensitive"
);
}
#[test]
fn piv_slot_retired_enforces_valid_range() {
assert!(PivSlot::retired(0).is_some(), "index 0 must be valid");
assert!(
PivSlot::retired(19).is_some(),
"index 19 must be valid (last slot 0x95)"
);
assert!(
PivSlot::retired(20).is_none(),
"index 20 must be rejected (out of range)"
);
assert!(
PivSlot::retired(255).is_none(),
"index 255 must be rejected"
);
}
#[test]
fn key_policy_default_is_secure() {
let policy = KeyPolicy::default();
assert!(
policy.persistent,
"ceremony keys must persist across sessions"
);
assert!(policy.sensitive, "ceremony keys must be marked sensitive");
assert!(
!policy.extractable,
"ceremony keys must not be extractable by default"
);
assert!(policy.usages.contains(KeyUsages::SIGN));
assert!(policy.usages.contains(KeyUsages::VERIFY));
assert!(
!policy.usages.contains(KeyUsages::ENCRYPT),
"signing-only default must not permit encryption"
);
assert!(
!policy.usages.contains(KeyUsages::WRAP),
"signing-only default must not permit wrapping"
);
}
}