use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WitnessClass {
SignedLedgerChainHead,
ExternalAnchorCrossing,
RemoteCiConclusion,
ReproducibleBuildProvenance,
}
impl WitnessClass {
#[must_use]
pub const fn required_authority_domain(self) -> AuthorityDomain {
match self {
Self::SignedLedgerChainHead => AuthorityDomain::LocalSignedLedger,
Self::ExternalAnchorCrossing => AuthorityDomain::ExternalAnchorSink,
Self::RemoteCiConclusion => AuthorityDomain::RemoteCiProvider,
Self::ReproducibleBuildProvenance => AuthorityDomain::ReproducibleBuildProvider,
}
}
#[must_use]
pub const fn wire_str(self) -> &'static str {
match self {
Self::SignedLedgerChainHead => "signed_ledger_chain_head",
Self::ExternalAnchorCrossing => "external_anchor_crossing",
Self::RemoteCiConclusion => "remote_ci_conclusion",
Self::ReproducibleBuildProvenance => "reproducible_build_provenance",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityDomain {
LocalSignedLedger,
ExternalAnchorSink,
RemoteCiProvider,
ReproducibleBuildProvider,
}
impl AuthorityDomain {
#[must_use]
pub const fn wire_str(self) -> &'static str {
match self {
Self::LocalSignedLedger => "local_signed_ledger",
Self::ExternalAnchorSink => "external_anchor_sink",
Self::RemoteCiProvider => "remote_ci_provider",
Self::ReproducibleBuildProvider => "reproducible_build_provider",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WitnessTier {
Local,
OperatorOwned,
ThirdParty,
}
impl WitnessTier {
#[must_use]
pub const fn wire_str(self) -> &'static str {
match self {
Self::Local => "local",
Self::OperatorOwned => "operator_owned",
Self::ThirdParty => "third_party",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WitnessSignature {
Ed25519 {
#[serde(with = "hex_bytes_32")]
public_key_bytes: [u8; 32],
#[serde(with = "hex_bytes_64")]
signature_bytes: [u8; 64],
#[serde(default, skip_serializing_if = "Option::is_none")]
signer_id: Option<String>,
},
EcdsaP256 {
#[serde(with = "hex_vec")]
public_key_der: Vec<u8>,
#[serde(with = "hex_vec")]
signature_der: Vec<u8>,
},
SelfSigned {
key_id: String,
#[serde(with = "hex_vec")]
signature_bytes: Vec<u8>,
},
}
impl WitnessSignature {
#[must_use]
pub fn signer_id(&self) -> Option<&str> {
match self {
Self::Ed25519 { signer_id, .. } => signer_id.as_deref(),
Self::EcdsaP256 { .. } | Self::SelfSigned { .. } => None,
}
}
}
impl<'de> Deserialize<'de> for WitnessSignature {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(deserializer)?;
if raw.get("type").is_some() {
let inner: WitnessSignatureInner =
serde_json::from_value(raw).map_err(serde::de::Error::custom)?;
return Ok(Self::from(inner));
}
let legacy: LegacyEd25519Shape =
serde_json::from_value(raw).map_err(serde::de::Error::custom)?;
Ok(Self::Ed25519 {
public_key_bytes: legacy.verifying_key,
signature_bytes: legacy.signature,
signer_id: legacy.signer_id,
})
}
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum WitnessSignatureInner {
Ed25519 {
#[serde(with = "hex_bytes_32")]
public_key_bytes: [u8; 32],
#[serde(with = "hex_bytes_64")]
signature_bytes: [u8; 64],
#[serde(default)]
signer_id: Option<String>,
},
EcdsaP256 {
#[serde(with = "hex_vec")]
public_key_der: Vec<u8>,
#[serde(with = "hex_vec")]
signature_der: Vec<u8>,
},
SelfSigned {
key_id: String,
#[serde(with = "hex_vec")]
signature_bytes: Vec<u8>,
},
}
impl From<WitnessSignatureInner> for WitnessSignature {
fn from(inner: WitnessSignatureInner) -> Self {
match inner {
WitnessSignatureInner::Ed25519 {
public_key_bytes,
signature_bytes,
signer_id,
} => Self::Ed25519 {
public_key_bytes,
signature_bytes,
signer_id,
},
WitnessSignatureInner::EcdsaP256 {
public_key_der,
signature_der,
} => Self::EcdsaP256 {
public_key_der,
signature_der,
},
WitnessSignatureInner::SelfSigned {
key_id,
signature_bytes,
} => Self::SelfSigned {
key_id,
signature_bytes,
},
}
}
}
#[derive(Deserialize)]
struct LegacyEd25519Shape {
#[serde(with = "hex_bytes_32")]
verifying_key: [u8; 32],
#[serde(with = "hex_bytes_64")]
signature: [u8; 64],
#[serde(default)]
signer_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WitnessPayload {
SignedLedgerChainHead {
chain_head_hash: String,
event_count: u64,
},
ExternalAnchorCrossing {
chain_head_hash: String,
event_count: u64,
sink_kind: String,
},
RemoteCiConclusion {
workflow_run_id: String,
commit_sha: String,
conclusion_timestamp: DateTime<Utc>,
},
ReproducibleBuildProvenance {
builder_id: String,
source_digest: String,
artifact_digest: String,
},
}
impl WitnessPayload {
#[must_use]
pub const fn class(&self) -> WitnessClass {
match self {
Self::SignedLedgerChainHead { .. } => WitnessClass::SignedLedgerChainHead,
Self::ExternalAnchorCrossing { .. } => WitnessClass::ExternalAnchorCrossing,
Self::RemoteCiConclusion { .. } => WitnessClass::RemoteCiConclusion,
Self::ReproducibleBuildProvenance { .. } => WitnessClass::ReproducibleBuildProvenance,
}
}
#[must_use]
pub fn canonical_preimage(
&self,
domain: AuthorityDomain,
asserted_subject_blake3: &str,
asserted_at: DateTime<Utc>,
) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
out.extend_from_slice(b"cortex.verifier.witness.v1\n");
out.extend_from_slice(b"class=");
out.extend_from_slice(self.class().wire_str().as_bytes());
out.push(b'\n');
out.extend_from_slice(b"authority_domain=");
out.extend_from_slice(domain.wire_str().as_bytes());
out.push(b'\n');
out.extend_from_slice(b"asserted_subject_blake3=");
out.extend_from_slice(asserted_subject_blake3.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"asserted_at=");
out.extend_from_slice(asserted_at.to_rfc3339().as_bytes());
out.push(b'\n');
match self {
Self::SignedLedgerChainHead {
chain_head_hash,
event_count,
} => {
out.extend_from_slice(b"chain_head_hash=");
out.extend_from_slice(chain_head_hash.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"event_count=");
out.extend_from_slice(event_count.to_string().as_bytes());
out.push(b'\n');
}
Self::ExternalAnchorCrossing {
chain_head_hash,
event_count,
sink_kind,
} => {
out.extend_from_slice(b"chain_head_hash=");
out.extend_from_slice(chain_head_hash.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"event_count=");
out.extend_from_slice(event_count.to_string().as_bytes());
out.push(b'\n');
out.extend_from_slice(b"sink_kind=");
out.extend_from_slice(sink_kind.as_bytes());
out.push(b'\n');
}
Self::RemoteCiConclusion {
workflow_run_id,
commit_sha,
conclusion_timestamp,
} => {
out.extend_from_slice(b"workflow_run_id=");
out.extend_from_slice(workflow_run_id.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"commit_sha=");
out.extend_from_slice(commit_sha.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"conclusion_timestamp=");
out.extend_from_slice(conclusion_timestamp.to_rfc3339().as_bytes());
out.push(b'\n');
}
Self::ReproducibleBuildProvenance {
builder_id,
source_digest,
artifact_digest,
} => {
out.extend_from_slice(b"builder_id=");
out.extend_from_slice(builder_id.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"source_digest=");
out.extend_from_slice(source_digest.as_bytes());
out.push(b'\n');
out.extend_from_slice(b"artifact_digest=");
out.extend_from_slice(artifact_digest.as_bytes());
out.push(b'\n');
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndependentWitness {
pub class: WitnessClass,
pub authority_domain: AuthorityDomain,
pub tier: WitnessTier,
pub asserted_at: DateTime<Utc>,
pub asserted_subject_blake3: String,
pub signature: WitnessSignature,
pub payload: WitnessPayload,
}
impl IndependentWitness {
#[must_use]
pub fn canonical_preimage(&self) -> Vec<u8> {
self.payload.canonical_preimage(
self.authority_domain,
&self.asserted_subject_blake3,
self.asserted_at,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WitnessSummary {
pub class: String,
pub authority_domain: String,
pub tier: String,
pub signer_id: Option<String>,
pub asserted_at: DateTime<Utc>,
}
impl WitnessSummary {
#[must_use]
pub fn from_witness(witness: &IndependentWitness) -> Self {
let signer_id = match &witness.signature {
WitnessSignature::Ed25519 { signer_id, .. } => signer_id.clone(),
WitnessSignature::SelfSigned { key_id, .. } => Some(key_id.clone()),
WitnessSignature::EcdsaP256 { .. } => None,
};
Self {
class: witness.class.wire_str().to_string(),
authority_domain: witness.authority_domain.wire_str().to_string(),
tier: witness.tier.wire_str().to_string(),
signer_id,
asserted_at: witness.asserted_at,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SelfSignedAlgorithm {
Ed25519,
EcdsaP256,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct SelfSignedKeyEntry {
pub key_id: String,
pub algorithm: SelfSignedAlgorithm,
pub key_bytes_hex: String,
}
impl SelfSignedKeyEntry {
pub fn key_bytes(&self) -> Result<Vec<u8>, String> {
hex_decode(&self.key_bytes_hex)
}
}
#[derive(serde::Deserialize)]
struct KeyRegistryFile {
keys: Vec<SelfSignedKeyEntry>,
}
#[derive(Debug, Clone)]
pub struct SelfSignedKeyRegistry {
entries: Vec<SelfSignedKeyEntry>,
}
impl SelfSignedKeyRegistry {
#[must_use]
pub fn empty() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn load(path: &std::path::Path) -> Result<Self, String> {
let raw = std::fs::read_to_string(path).map_err(|e| {
format!(
"witness-key-registry: cannot read `{}`: {e}",
path.display()
)
})?;
let file: KeyRegistryFile = toml::from_str(&raw).map_err(|e| {
format!(
"witness-key-registry: `{}` did not parse as expected TOML: {e}",
path.display()
)
})?;
Ok(Self { entries: file.keys })
}
#[must_use]
pub fn get(&self, key_id: &str) -> Option<&SelfSignedKeyEntry> {
self.entries.iter().find(|e| e.key_id == key_id)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub(crate) mod hex_bytes_32 {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&hex_encode(value))
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 32], D::Error> {
let text = <String as Deserialize>::deserialize(deserializer)?;
let bytes = super::hex_decode(&text).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(format!(
"expected 32-byte hex string, got {}",
bytes.len()
)));
}
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
Ok(out)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(super::hex_nibble(b >> 4));
out.push(super::hex_nibble(b & 0x0F));
}
out
}
}
pub(crate) mod hex_bytes_64 {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&hex_encode(value))
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 64], D::Error> {
let text = <String as Deserialize>::deserialize(deserializer)?;
let bytes = super::hex_decode(&text).map_err(serde::de::Error::custom)?;
if bytes.len() != 64 {
return Err(serde::de::Error::custom(format!(
"expected 64-byte hex string, got {}",
bytes.len()
)));
}
let mut out = [0u8; 64];
out.copy_from_slice(&bytes);
Ok(out)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(super::hex_nibble(b >> 4));
out.push(super::hex_nibble(b & 0x0F));
}
out
}
}
pub(crate) mod hex_vec {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
let mut out = String::with_capacity(value.len() * 2);
for b in value {
out.push(super::hex_nibble(b >> 4));
out.push(super::hex_nibble(b & 0x0F));
}
serializer.serialize_str(&out)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
let text = <String as Deserialize>::deserialize(deserializer)?;
super::hex_decode(&text).map_err(serde::de::Error::custom)
}
}
fn hex_nibble(nib: u8) -> char {
match nib {
0..=9 => (b'0' + nib) as char,
10..=15 => (b'a' + nib - 10) as char,
_ => unreachable!("nibble fits in 4 bits"),
}
}
fn hex_decode(text: &str) -> Result<Vec<u8>, String> {
if !text.len().is_multiple_of(2) {
return Err(format!("hex string length {} is not even", text.len()));
}
let mut out = Vec::with_capacity(text.len() / 2);
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let high = hex_value(bytes[i])?;
let low = hex_value(bytes[i + 1])?;
out.push((high << 4) | low);
i += 2;
}
Ok(out)
}
fn hex_value(byte: u8) -> Result<u8, String> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(format!("non-hex character {byte:#x?}")),
}
}