use super::{model, OPERATOR_RECORD_VERSION};
use crate::registry::PackageName;
use crate::registry::RecordId;
use crate::ProtoEnvelope;
use indexmap::{IndexMap, IndexSet};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
use thiserror::Error;
use warg_crypto::hash::{HashAlgorithm, Sha256};
use warg_crypto::{signing, Signable};
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("the first entry of the log is not \"init\"")]
FirstEntryIsNotInit,
#[error("the initial record is empty and does not \"init\"")]
InitialRecordDoesNotInit,
#[error("the Key ID used to sign this envelope is not known to this operator log")]
KeyIDNotRecognized { key_id: signing::KeyID },
#[error("a second \"init\" entry was found")]
InitialEntryAfterBeginning,
#[error("the key with ID {key_id} did not have required permission {needed_permission}")]
UnauthorizedAction {
key_id: signing::KeyID,
needed_permission: model::Permission,
},
#[error("attempted to remove permission {permission} from key {key_id} which did not have it")]
PermissionNotFoundToRevoke {
permission: model::Permission,
key_id: signing::KeyID,
},
#[error("unable to verify signature: {0}")]
SignatureError(#[from] signing::SignatureError),
#[error("record hash uses {found} algorithm but {expected} was expected")]
IncorrectHashAlgorithm {
found: HashAlgorithm,
expected: HashAlgorithm,
},
#[error("previous record hash does not match")]
RecordHashDoesNotMatch,
#[error("the first record contained a previous hash value")]
PreviousHashOnFirstRecord,
#[error("non-initial record contained no previous hash")]
NoPreviousHashAfterInit,
#[error("protocol version {version} not allowed")]
ProtocolVersionNotAllowed { version: u32 },
#[error("record has lower timestamp than previous")]
TimestampLowerThanPrevious,
#[error("the namespace `{namespace}` is invalid; namespace must be a kebab case string")]
InvalidNamespace { namespace: String },
#[error("the namespace `{namespace}` conflicts with the existing namespace `{existing}`; namespace must be unique in a case insensitive way")]
NamespaceConflict { namespace: String, existing: String },
#[error("the namespace `{namespace}` is already defined and cannot be redefined")]
NamespaceAlreadyDefined { namespace: String },
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct NamespaceDefinition {
namespace: String,
state: NamespaceState,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum NamespaceState {
Defined,
#[serde(rename_all = "camelCase")]
Imported {
registry: String,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Head {
pub digest: RecordId,
#[serde(with = "crate::timestamp")]
pub timestamp: SystemTime,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default, rename_all = "camelCase")]
pub struct LogState {
#[serde(skip_serializing_if = "Option::is_none")]
algorithm: Option<HashAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
head: Option<Head>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
permissions: IndexMap<signing::KeyID, IndexSet<model::Permission>>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
keys: IndexMap<signing::KeyID, signing::PublicKey>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
namespaces: IndexMap<String, NamespaceDefinition>,
}
impl LogState {
pub fn new() -> Self {
Self::default()
}
pub fn head(&self) -> &Option<Head> {
&self.head
}
pub fn validate(
mut self,
record: &ProtoEnvelope<model::OperatorRecord>,
) -> Result<Self, ValidationError> {
self.validate_record(record)?;
Ok(self)
}
pub fn public_key(&self, key_id: &signing::KeyID) -> Option<&signing::PublicKey> {
self.keys.get(key_id)
}
pub fn namespace_state(&self, namespace: &str) -> Result<Option<&NamespaceState>, &str> {
if let Some(def) = self.namespaces.get(&namespace.to_ascii_lowercase()) {
if def.namespace == namespace {
Ok(Some(&def.state))
} else {
Err(&def.namespace)
}
} else {
Ok(None)
}
}
pub fn key_has_permission_to_sign_checkpoints(&self, key_id: &signing::KeyID) -> bool {
self.check_key_permissions(key_id, &[model::Permission::Commit])
.is_ok()
}
fn initialized(&self) -> bool {
self.algorithm.is_some()
}
fn validate_record(
&mut self,
envelope: &ProtoEnvelope<model::OperatorRecord>,
) -> Result<(), ValidationError> {
let record = envelope.as_ref();
self.validate_record_hash(record)?;
self.validate_record_version(record)?;
self.validate_record_timestamp(record)?;
self.validate_record_entries(envelope.key_id(), &record.entries)?;
let _algorithm = self
.algorithm
.ok_or(ValidationError::InitialRecordDoesNotInit)?;
let key = self.keys.get(envelope.key_id()).ok_or_else(|| {
ValidationError::KeyIDNotRecognized {
key_id: envelope.key_id().clone(),
}
})?;
model::OperatorRecord::verify(key, envelope.content_bytes(), envelope.signature())?;
self.head = Some(Head {
digest: RecordId::operator_record::<Sha256>(envelope),
timestamp: record.timestamp,
});
Ok(())
}
fn validate_record_hash(&self, record: &model::OperatorRecord) -> Result<(), ValidationError> {
match (&self.head, &record.prev) {
(None, Some(_)) => Err(ValidationError::PreviousHashOnFirstRecord),
(Some(_), None) => Err(ValidationError::NoPreviousHashAfterInit),
(None, None) => Ok(()),
(Some(expected), Some(found)) => {
if found.algorithm() != expected.digest.algorithm() {
return Err(ValidationError::IncorrectHashAlgorithm {
found: found.algorithm(),
expected: expected.digest.algorithm(),
});
}
if found != &expected.digest {
return Err(ValidationError::RecordHashDoesNotMatch);
}
Ok(())
}
}
}
fn validate_record_version(
&self,
record: &model::OperatorRecord,
) -> Result<(), ValidationError> {
if record.version == OPERATOR_RECORD_VERSION {
Ok(())
} else {
Err(ValidationError::ProtocolVersionNotAllowed {
version: record.version,
})
}
}
fn validate_record_timestamp(
&self,
record: &model::OperatorRecord,
) -> Result<(), ValidationError> {
if let Some(head) = &self.head {
if record.timestamp < head.timestamp {
return Err(ValidationError::TimestampLowerThanPrevious);
}
}
Ok(())
}
fn validate_record_entries(
&mut self,
signer_key_id: &signing::KeyID,
entries: &[model::OperatorEntry],
) -> Result<(), ValidationError> {
for entry in entries {
if let Some(permission) = entry.required_permission() {
self.check_key_permissions(signer_key_id, &[permission])?;
}
if let model::OperatorEntry::Init {
hash_algorithm,
key,
} = entry
{
self.validate_init_entry(signer_key_id, *hash_algorithm, key)?;
continue;
}
if !self.initialized() {
return Err(ValidationError::FirstEntryIsNotInit);
}
match entry {
model::OperatorEntry::Init { .. } => unreachable!(), model::OperatorEntry::GrantFlat { key, permissions } => {
self.validate_grant_entry(signer_key_id, key, permissions)?
}
model::OperatorEntry::RevokeFlat {
key_id,
permissions,
} => self.validate_revoke_entry(signer_key_id, key_id, permissions)?,
model::OperatorEntry::DefineNamespace { namespace } => {
self.validate_namespace(namespace, NamespaceState::Defined)?
}
model::OperatorEntry::ImportNamespace {
namespace,
registry,
} => self.validate_namespace(
namespace,
NamespaceState::Imported {
registry: registry.to_string(),
},
)?,
}
}
Ok(())
}
fn validate_init_entry(
&mut self,
signer_key_id: &signing::KeyID,
algorithm: HashAlgorithm,
init_key: &signing::PublicKey,
) -> Result<(), ValidationError> {
if self.initialized() {
return Err(ValidationError::InitialEntryAfterBeginning);
}
assert!(self.permissions.is_empty());
assert!(self.keys.is_empty());
self.algorithm = Some(algorithm);
self.permissions.insert(
signer_key_id.clone(),
IndexSet::from(model::Permission::all()),
);
self.keys.insert(init_key.fingerprint(), init_key.clone());
Ok(())
}
fn validate_grant_entry(
&mut self,
signer_key_id: &signing::KeyID,
key: &signing::PublicKey,
permissions: &[model::Permission],
) -> Result<(), ValidationError> {
self.check_key_permissions(signer_key_id, permissions)?;
let grant_key_id = key.fingerprint();
self.keys.insert(grant_key_id.clone(), key.clone());
self.permissions
.entry(grant_key_id)
.or_default()
.extend(permissions);
Ok(())
}
fn validate_revoke_entry(
&mut self,
signer_key_id: &signing::KeyID,
key_id: &signing::KeyID,
permissions: &[model::Permission],
) -> Result<(), ValidationError> {
self.check_key_permissions(signer_key_id, permissions)?;
for permission in permissions {
if !self
.permissions
.get_mut(key_id)
.map(|set| set.swap_remove(permission))
.unwrap_or(false)
{
return Err(ValidationError::PermissionNotFoundToRevoke {
permission: *permission,
key_id: key_id.clone(),
});
}
}
Ok(())
}
fn validate_namespace(
&mut self,
namespace: &str,
state: NamespaceState,
) -> Result<(), ValidationError> {
if !PackageName::is_valid_namespace(namespace) {
return Err(ValidationError::InvalidNamespace {
namespace: namespace.to_string(),
});
}
let namespace_lowercase = namespace.to_ascii_lowercase();
if let Some(def) = self.namespaces.get(&namespace_lowercase) {
if namespace == def.namespace {
Err(ValidationError::NamespaceAlreadyDefined {
namespace: namespace.to_string(),
})
} else {
Err(ValidationError::NamespaceConflict {
namespace: namespace.to_string(),
existing: def.namespace.to_string(),
})
}
} else {
self.namespaces.insert(
namespace_lowercase,
NamespaceDefinition {
namespace: namespace.to_string(),
state,
},
);
Ok(())
}
}
fn check_key_permissions(
&self,
key_id: &signing::KeyID,
permissions: &[model::Permission],
) -> Result<(), ValidationError> {
for permission in permissions {
if !self
.permissions
.get(key_id)
.map(|p| p.contains(permission))
.unwrap_or(false)
{
return Err(ValidationError::UnauthorizedAction {
key_id: key_id.clone(),
needed_permission: *permission,
});
}
}
Ok(())
}
}
impl crate::Validator for LogState {
type Record = model::OperatorRecord;
type Error = ValidationError;
fn validate(self, record: &ProtoEnvelope<Self::Record>) -> Result<Self, Self::Error> {
self.validate(record)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use warg_crypto::signing::generate_p256_pair;
use std::time::SystemTime;
use warg_crypto::hash::HashAlgorithm;
#[test]
fn test_validate_base_log() {
let (alice_pub, alice_priv) = generate_p256_pair();
let alice_id = alice_pub.fingerprint();
let timestamp = SystemTime::now();
let record = model::OperatorRecord {
prev: None,
version: 0,
timestamp,
entries: vec![model::OperatorEntry::Init {
hash_algorithm: HashAlgorithm::Sha256,
key: alice_pub.clone(),
}],
};
let envelope =
ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
let state = LogState::default();
let state = state.validate(&envelope).unwrap();
assert_eq!(
state,
LogState {
head: Some(Head {
digest: RecordId::operator_record::<Sha256>(&envelope),
timestamp,
}),
algorithm: Some(HashAlgorithm::Sha256),
permissions: IndexMap::from([(
alice_id.clone(),
IndexSet::from([
model::Permission::Commit,
model::Permission::DefineNamespace,
model::Permission::ImportNamespace
]),
)]),
keys: IndexMap::from([(alice_id, alice_pub)]),
namespaces: IndexMap::new(),
}
);
}
#[test]
fn test_rollback() {
let (alice_pub, alice_priv) = generate_p256_pair();
let alice_id = alice_pub.fingerprint();
let (bob_pub, _) = generate_p256_pair();
let timestamp = SystemTime::now();
let record = model::OperatorRecord {
prev: None,
version: 0,
timestamp,
entries: vec![model::OperatorEntry::Init {
hash_algorithm: HashAlgorithm::Sha256,
key: alice_pub.clone(),
}],
};
let envelope =
ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
let state = LogState::default();
let state = state.validate(&envelope).unwrap();
let expected = LogState {
head: Some(Head {
digest: RecordId::operator_record::<Sha256>(&envelope),
timestamp,
}),
algorithm: Some(HashAlgorithm::Sha256),
permissions: IndexMap::from([(
alice_id.clone(),
IndexSet::from([
model::Permission::Commit,
model::Permission::DefineNamespace,
model::Permission::ImportNamespace,
]),
)]),
keys: IndexMap::from([(alice_id, alice_pub)]),
namespaces: IndexMap::new(),
};
assert_eq!(state, expected);
let record = model::OperatorRecord {
prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
version: 0,
timestamp: SystemTime::now(),
entries: vec![
model::OperatorEntry::GrantFlat {
key: bob_pub,
permissions: vec![model::Permission::Commit],
},
model::OperatorEntry::RevokeFlat {
key_id: "not-valid".to_string().into(),
permissions: vec![model::Permission::Commit],
},
model::OperatorEntry::DefineNamespace {
namespace: "example-namespace".to_string(),
},
],
};
let envelope =
ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
match state.validate(&envelope).unwrap_err() {
ValidationError::PermissionNotFoundToRevoke { .. } => {}
_ => panic!("expected a different error"),
}
}
#[test]
fn test_namespaces() {
let (alice_pub, alice_priv) = generate_p256_pair();
let alice_id = alice_pub.fingerprint();
let timestamp = SystemTime::now();
let record = model::OperatorRecord {
prev: None,
version: 0,
timestamp,
entries: vec![
model::OperatorEntry::Init {
hash_algorithm: HashAlgorithm::Sha256,
key: alice_pub.clone(),
},
model::OperatorEntry::DefineNamespace {
namespace: "my-namespace".to_string(),
},
model::OperatorEntry::ImportNamespace {
namespace: "imported-namespace".to_string(),
registry: "registry.example.com".to_string(),
},
],
};
let envelope =
ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
let state = LogState::default();
let state = state.validate(&envelope).unwrap();
let expected = LogState {
head: Some(Head {
digest: RecordId::operator_record::<Sha256>(&envelope),
timestamp,
}),
algorithm: Some(HashAlgorithm::Sha256),
permissions: IndexMap::from([(
alice_id.clone(),
IndexSet::from([
model::Permission::Commit,
model::Permission::DefineNamespace,
model::Permission::ImportNamespace,
]),
)]),
keys: IndexMap::from([(alice_id, alice_pub)]),
namespaces: IndexMap::from([
(
"my-namespace".to_string(),
NamespaceDefinition {
namespace: "my-namespace".to_string(),
state: NamespaceState::Defined,
},
),
(
"imported-namespace".to_string(),
NamespaceDefinition {
namespace: "imported-namespace".to_string(),
state: NamespaceState::Imported {
registry: "registry.example.com".to_string(),
},
},
),
]),
};
assert_eq!(state, expected);
{
let record = model::OperatorRecord {
prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
version: 0,
timestamp: SystemTime::now(),
entries: vec![
model::OperatorEntry::DefineNamespace {
namespace: "other-namespace".to_string(),
},
model::OperatorEntry::ImportNamespace {
namespace: "my-namespace".to_string(),
registry: "registry.alternative.com".to_string(),
},
],
};
let envelope = ProtoEnvelope::signed_contents(&alice_priv, record)
.expect("failed to sign envelope");
match state.clone().validate(&envelope).unwrap_err() {
ValidationError::NamespaceAlreadyDefined { .. } => {}
_ => panic!("expected a different error"),
}
}
{
let record = model::OperatorRecord {
prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
version: 0,
timestamp: SystemTime::now(),
entries: vec![
model::OperatorEntry::DefineNamespace {
namespace: "other-namespace".to_string(),
},
model::OperatorEntry::ImportNamespace {
namespace: "my-NAMESPACE".to_string(),
registry: "registry.alternative.com".to_string(),
},
],
};
let envelope = ProtoEnvelope::signed_contents(&alice_priv, record)
.expect("failed to sign envelope");
match state.validate(&envelope).unwrap_err() {
ValidationError::NamespaceConflict { .. } => {}
_ => panic!("expected a different error"),
}
}
}
}