1use super::{model, OPERATOR_RECORD_VERSION};
2use crate::registry::PackageName;
3use crate::registry::RecordId;
4use crate::ProtoEnvelope;
5use indexmap::{IndexMap, IndexSet};
6use serde::{Deserialize, Serialize};
7use std::time::SystemTime;
8use thiserror::Error;
9use warg_crypto::hash::{HashAlgorithm, Sha256};
10use warg_crypto::{signing, Signable};
11
12#[derive(Error, Debug)]
13pub enum ValidationError {
14 #[error("the first entry of the log is not \"init\"")]
15 FirstEntryIsNotInit,
16
17 #[error("the initial record is empty and does not \"init\"")]
18 InitialRecordDoesNotInit,
19
20 #[error("the Key ID used to sign this envelope is not known to this operator log")]
21 KeyIDNotRecognized { key_id: signing::KeyID },
22
23 #[error("a second \"init\" entry was found")]
24 InitialEntryAfterBeginning,
25
26 #[error("the key with ID {key_id} did not have required permission {needed_permission}")]
27 UnauthorizedAction {
28 key_id: signing::KeyID,
29 needed_permission: model::Permission,
30 },
31
32 #[error("attempted to remove permission {permission} from key {key_id} which did not have it")]
33 PermissionNotFoundToRevoke {
34 permission: model::Permission,
35 key_id: signing::KeyID,
36 },
37
38 #[error("unable to verify signature: {0}")]
39 SignatureError(#[from] signing::SignatureError),
40
41 #[error("record hash uses {found} algorithm but {expected} was expected")]
42 IncorrectHashAlgorithm {
43 found: HashAlgorithm,
44 expected: HashAlgorithm,
45 },
46
47 #[error("previous record hash does not match")]
48 RecordHashDoesNotMatch,
49
50 #[error("the first record contained a previous hash value")]
51 PreviousHashOnFirstRecord,
52
53 #[error("non-initial record contained no previous hash")]
54 NoPreviousHashAfterInit,
55
56 #[error("protocol version {version} not allowed")]
57 ProtocolVersionNotAllowed { version: u32 },
58
59 #[error("record has lower timestamp than previous")]
60 TimestampLowerThanPrevious,
61
62 #[error(
63 "the namespace `{namespace}` is invalid; namespace must be a lowercased kebab case string"
64 )]
65 InvalidNamespace { namespace: String },
66
67 #[error("the namespace `{namespace}` is already defined and cannot be redefined")]
68 NamespaceAlreadyDefined { namespace: String },
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(rename_all = "camelCase")]
74struct NamespaceDefinition {
75 state: NamespaceState,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(rename_all = "camelCase")]
82pub enum NamespaceState {
83 Defined,
85 #[serde(rename_all = "camelCase")]
87 Imported {
88 registry: String,
90 },
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "camelCase")]
98pub struct Head {
99 pub digest: RecordId,
101 #[serde(with = "crate::timestamp")]
103 pub timestamp: SystemTime,
104}
105
106#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(default, rename_all = "camelCase")]
109pub struct LogState {
110 #[serde(skip_serializing_if = "Option::is_none")]
113 algorithm: Option<HashAlgorithm>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 head: Option<Head>,
117 #[serde(skip_serializing_if = "IndexMap::is_empty")]
119 permissions: IndexMap<signing::KeyID, IndexSet<model::Permission>>,
120 #[serde(skip_serializing_if = "IndexMap::is_empty")]
122 keys: IndexMap<signing::KeyID, signing::PublicKey>,
123 #[serde(skip_serializing_if = "IndexMap::is_empty")]
125 namespaces: IndexMap<String, NamespaceDefinition>,
126}
127
128impl LogState {
129 pub fn new() -> Self {
131 Self::default()
132 }
133
134 pub fn head(&self) -> &Option<Head> {
138 &self.head
139 }
140
141 pub fn validate(
149 mut self,
150 record: &ProtoEnvelope<model::OperatorRecord>,
151 ) -> Result<Self, ValidationError> {
152 self.validate_record(record)?;
153 Ok(self)
154 }
155
156 pub fn public_key(&self, key_id: &signing::KeyID) -> Option<&signing::PublicKey> {
160 self.keys.get(key_id)
161 }
162
163 pub fn namespace_state(&self, namespace: &str) -> Option<&NamespaceState> {
165 self.namespaces.get(namespace).map(|def| &def.state)
166 }
167
168 pub fn key_has_permission_to_sign_checkpoints(&self, key_id: &signing::KeyID) -> bool {
170 self.check_key_permissions(key_id, &[model::Permission::Commit])
171 .is_ok()
172 }
173
174 fn initialized(&self) -> bool {
175 self.algorithm.is_some()
177 }
178
179 fn validate_record(
180 &mut self,
181 envelope: &ProtoEnvelope<model::OperatorRecord>,
182 ) -> Result<(), ValidationError> {
183 let record = envelope.as_ref();
184
185 self.validate_record_hash(record)?;
187
188 self.validate_record_version(record)?;
190
191 self.validate_record_timestamp(record)?;
193
194 self.validate_record_entries(envelope.key_id(), &record.entries)?;
196
197 let _algorithm = self
199 .algorithm
200 .ok_or(ValidationError::InitialRecordDoesNotInit)?;
201
202 let key = self.keys.get(envelope.key_id()).ok_or_else(|| {
204 ValidationError::KeyIDNotRecognized {
205 key_id: envelope.key_id().clone(),
206 }
207 })?;
208
209 model::OperatorRecord::verify(key, envelope.content_bytes(), envelope.signature())?;
211
212 self.head = Some(Head {
214 digest: RecordId::operator_record::<Sha256>(envelope),
215 timestamp: record.timestamp,
216 });
217
218 Ok(())
219 }
220
221 fn validate_record_hash(&self, record: &model::OperatorRecord) -> Result<(), ValidationError> {
222 match (&self.head, &record.prev) {
223 (None, Some(_)) => Err(ValidationError::PreviousHashOnFirstRecord),
224 (Some(_), None) => Err(ValidationError::NoPreviousHashAfterInit),
225 (None, None) => Ok(()),
226 (Some(expected), Some(found)) => {
227 if found.algorithm() != expected.digest.algorithm() {
228 return Err(ValidationError::IncorrectHashAlgorithm {
229 found: found.algorithm(),
230 expected: expected.digest.algorithm(),
231 });
232 }
233
234 if found != &expected.digest {
235 return Err(ValidationError::RecordHashDoesNotMatch);
236 }
237
238 Ok(())
239 }
240 }
241 }
242
243 fn validate_record_version(
244 &self,
245 record: &model::OperatorRecord,
246 ) -> Result<(), ValidationError> {
247 if record.version == OPERATOR_RECORD_VERSION {
248 Ok(())
249 } else {
250 Err(ValidationError::ProtocolVersionNotAllowed {
251 version: record.version,
252 })
253 }
254 }
255
256 fn validate_record_timestamp(
257 &self,
258 record: &model::OperatorRecord,
259 ) -> Result<(), ValidationError> {
260 if let Some(head) = &self.head {
261 if record.timestamp < head.timestamp {
262 return Err(ValidationError::TimestampLowerThanPrevious);
263 }
264 }
265
266 Ok(())
267 }
268
269 fn validate_record_entries(
270 &mut self,
271 signer_key_id: &signing::KeyID,
272 entries: &[model::OperatorEntry],
273 ) -> Result<(), ValidationError> {
274 for entry in entries {
275 if let Some(permission) = entry.required_permission() {
276 self.check_key_permissions(signer_key_id, &[permission])?;
277 }
278
279 if let model::OperatorEntry::Init {
281 hash_algorithm,
282 key,
283 } = entry
284 {
285 self.validate_init_entry(signer_key_id, *hash_algorithm, key)?;
286 continue;
287 }
288
289 if !self.initialized() {
291 return Err(ValidationError::FirstEntryIsNotInit);
292 }
293
294 match entry {
295 model::OperatorEntry::Init { .. } => unreachable!(), model::OperatorEntry::GrantFlat { key, permissions } => {
297 self.validate_grant_entry(signer_key_id, key, permissions)?
298 }
299 model::OperatorEntry::RevokeFlat {
300 key_id,
301 permissions,
302 } => self.validate_revoke_entry(signer_key_id, key_id, permissions)?,
303 model::OperatorEntry::DefineNamespace { namespace } => {
304 self.validate_namespace(namespace, NamespaceState::Defined)?
305 }
306 model::OperatorEntry::ImportNamespace {
307 namespace,
308 registry,
309 } => self.validate_namespace(
310 namespace,
311 NamespaceState::Imported {
312 registry: registry.to_string(),
313 },
314 )?,
315 }
316 }
317
318 Ok(())
319 }
320
321 fn validate_init_entry(
322 &mut self,
323 signer_key_id: &signing::KeyID,
324 algorithm: HashAlgorithm,
325 init_key: &signing::PublicKey,
326 ) -> Result<(), ValidationError> {
327 if self.initialized() {
328 return Err(ValidationError::InitialEntryAfterBeginning);
329 }
330
331 assert!(self.permissions.is_empty());
332 assert!(self.keys.is_empty());
333
334 self.algorithm = Some(algorithm);
335 self.permissions.insert(
336 signer_key_id.clone(),
337 IndexSet::from(model::Permission::all()),
338 );
339 self.keys.insert(init_key.fingerprint(), init_key.clone());
340
341 Ok(())
342 }
343
344 fn validate_grant_entry(
345 &mut self,
346 signer_key_id: &signing::KeyID,
347 key: &signing::PublicKey,
348 permissions: &[model::Permission],
349 ) -> Result<(), ValidationError> {
350 self.check_key_permissions(signer_key_id, permissions)?;
352
353 let grant_key_id = key.fingerprint();
354 self.keys.insert(grant_key_id.clone(), key.clone());
355 self.permissions
356 .entry(grant_key_id)
357 .or_default()
358 .extend(permissions);
359
360 Ok(())
361 }
362
363 fn validate_revoke_entry(
364 &mut self,
365 signer_key_id: &signing::KeyID,
366 key_id: &signing::KeyID,
367 permissions: &[model::Permission],
368 ) -> Result<(), ValidationError> {
369 self.check_key_permissions(signer_key_id, permissions)?;
371
372 for permission in permissions {
373 if !self
374 .permissions
375 .get_mut(key_id)
376 .map(|set| set.swap_remove(permission))
377 .unwrap_or(false)
378 {
379 return Err(ValidationError::PermissionNotFoundToRevoke {
380 permission: *permission,
381 key_id: key_id.clone(),
382 });
383 }
384 }
385 Ok(())
386 }
387
388 fn validate_namespace(
389 &mut self,
390 namespace: &str,
391 state: NamespaceState,
392 ) -> Result<(), ValidationError> {
393 if !PackageName::is_valid_namespace(namespace) {
394 return Err(ValidationError::InvalidNamespace {
395 namespace: namespace.to_string(),
396 });
397 }
398
399 if self.namespaces.contains_key(namespace) {
400 Err(ValidationError::NamespaceAlreadyDefined {
402 namespace: namespace.to_string(),
403 })
404 } else {
405 self.namespaces
407 .insert(namespace.to_string(), NamespaceDefinition { state });
408
409 Ok(())
410 }
411 }
412
413 fn check_key_permissions(
414 &self,
415 key_id: &signing::KeyID,
416 permissions: &[model::Permission],
417 ) -> Result<(), ValidationError> {
418 for permission in permissions {
419 if !self
420 .permissions
421 .get(key_id)
422 .map(|p| p.contains(permission))
423 .unwrap_or(false)
424 {
425 return Err(ValidationError::UnauthorizedAction {
426 key_id: key_id.clone(),
427 needed_permission: *permission,
428 });
429 }
430 }
431 Ok(())
432 }
433}
434
435impl crate::Validator for LogState {
436 type Record = model::OperatorRecord;
437 type Error = ValidationError;
438
439 fn validate(self, record: &ProtoEnvelope<Self::Record>) -> Result<Self, Self::Error> {
440 self.validate(record)
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use pretty_assertions::assert_eq;
447
448 use super::*;
449 use warg_crypto::signing::generate_p256_pair;
450
451 use std::time::SystemTime;
452 use warg_crypto::hash::HashAlgorithm;
453
454 #[test]
455 fn test_validate_base_log() {
456 let (alice_pub, alice_priv) = generate_p256_pair();
457 let alice_id = alice_pub.fingerprint();
458
459 let timestamp = SystemTime::now();
460 let record = model::OperatorRecord {
461 prev: None,
462 version: 0,
463 timestamp,
464 entries: vec![model::OperatorEntry::Init {
465 hash_algorithm: HashAlgorithm::Sha256,
466 key: alice_pub.clone(),
467 }],
468 };
469
470 let envelope =
471 ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
472 let state = LogState::default();
473 let state = state.validate(&envelope).unwrap();
474
475 assert_eq!(
476 state,
477 LogState {
478 head: Some(Head {
479 digest: RecordId::operator_record::<Sha256>(&envelope),
480 timestamp,
481 }),
482 algorithm: Some(HashAlgorithm::Sha256),
483 permissions: IndexMap::from([(
484 alice_id.clone(),
485 IndexSet::from([
486 model::Permission::Commit,
487 model::Permission::DefineNamespace,
488 model::Permission::ImportNamespace
489 ]),
490 )]),
491 keys: IndexMap::from([(alice_id, alice_pub)]),
492 namespaces: IndexMap::new(),
493 }
494 );
495 }
496
497 #[test]
498 fn test_rollback() {
499 let (alice_pub, alice_priv) = generate_p256_pair();
500 let alice_id = alice_pub.fingerprint();
501 let (bob_pub, _) = generate_p256_pair();
502
503 let timestamp = SystemTime::now();
504 let record = model::OperatorRecord {
505 prev: None,
506 version: 0,
507 timestamp,
508 entries: vec![model::OperatorEntry::Init {
509 hash_algorithm: HashAlgorithm::Sha256,
510 key: alice_pub.clone(),
511 }],
512 };
513
514 let envelope =
515 ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
516 let state = LogState::default();
517 let state = state.validate(&envelope).unwrap();
518
519 let expected = LogState {
520 head: Some(Head {
521 digest: RecordId::operator_record::<Sha256>(&envelope),
522 timestamp,
523 }),
524 algorithm: Some(HashAlgorithm::Sha256),
525 permissions: IndexMap::from([(
526 alice_id.clone(),
527 IndexSet::from([
528 model::Permission::Commit,
529 model::Permission::DefineNamespace,
530 model::Permission::ImportNamespace,
531 ]),
532 )]),
533 keys: IndexMap::from([(alice_id, alice_pub)]),
534 namespaces: IndexMap::new(),
535 };
536
537 assert_eq!(state, expected);
538
539 let record = model::OperatorRecord {
540 prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
541 version: 0,
542 timestamp: SystemTime::now(),
543 entries: vec![
544 model::OperatorEntry::GrantFlat {
546 key: bob_pub,
547 permissions: vec![model::Permission::Commit],
548 },
549 model::OperatorEntry::RevokeFlat {
551 key_id: "not-valid".to_string().into(),
552 permissions: vec![model::Permission::Commit],
553 },
554 model::OperatorEntry::DefineNamespace {
556 namespace: "example-namespace".to_string(),
557 },
558 ],
559 };
560
561 let envelope =
562 ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
563
564 match state.validate(&envelope).unwrap_err() {
566 ValidationError::PermissionNotFoundToRevoke { .. } => {}
567 _ => panic!("expected a different error"),
568 }
569 }
570
571 #[test]
572 fn test_namespaces() {
573 let (alice_pub, alice_priv) = generate_p256_pair();
574 let alice_id = alice_pub.fingerprint();
575
576 let timestamp = SystemTime::now();
577 let record = model::OperatorRecord {
578 prev: None,
579 version: 0,
580 timestamp,
581 entries: vec![
582 model::OperatorEntry::Init {
583 hash_algorithm: HashAlgorithm::Sha256,
584 key: alice_pub.clone(),
585 },
586 model::OperatorEntry::DefineNamespace {
587 namespace: "my-namespace".to_string(),
588 },
589 model::OperatorEntry::ImportNamespace {
590 namespace: "imported-namespace".to_string(),
591 registry: "registry.example.com".to_string(),
592 },
593 ],
594 };
595
596 let envelope =
597 ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
598 let state = LogState::default();
599 let state = state.validate(&envelope).unwrap();
600
601 let expected = LogState {
602 head: Some(Head {
603 digest: RecordId::operator_record::<Sha256>(&envelope),
604 timestamp,
605 }),
606 algorithm: Some(HashAlgorithm::Sha256),
607 permissions: IndexMap::from([(
608 alice_id.clone(),
609 IndexSet::from([
610 model::Permission::Commit,
611 model::Permission::DefineNamespace,
612 model::Permission::ImportNamespace,
613 ]),
614 )]),
615 keys: IndexMap::from([(alice_id, alice_pub)]),
616 namespaces: IndexMap::from([
617 (
618 "my-namespace".to_string(),
619 NamespaceDefinition {
620 state: NamespaceState::Defined,
621 },
622 ),
623 (
624 "imported-namespace".to_string(),
625 NamespaceDefinition {
626 state: NamespaceState::Imported {
627 registry: "registry.example.com".to_string(),
628 },
629 },
630 ),
631 ]),
632 };
633
634 assert_eq!(state, expected);
635
636 {
637 let record = model::OperatorRecord {
638 prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
639 version: 0,
640 timestamp: SystemTime::now(),
641 entries: vec![
642 model::OperatorEntry::DefineNamespace {
644 namespace: "other-namespace".to_string(),
645 },
646 model::OperatorEntry::ImportNamespace {
648 namespace: "my-namespace".to_string(),
649 registry: "registry.alternative.com".to_string(),
650 },
651 ],
652 };
653
654 let envelope = ProtoEnvelope::signed_contents(&alice_priv, record)
655 .expect("failed to sign envelope");
656
657 match state.clone().validate(&envelope).unwrap_err() {
659 ValidationError::NamespaceAlreadyDefined { .. } => {}
660 _ => panic!("expected a different error"),
661 }
662 }
663
664 {
665 let record = model::OperatorRecord {
666 prev: Some(RecordId::operator_record::<Sha256>(&envelope)),
667 version: 0,
668 timestamp: SystemTime::now(),
669 entries: vec![
670 model::OperatorEntry::DefineNamespace {
672 namespace: "other-namespace".to_string(),
673 },
674 model::OperatorEntry::ImportNamespace {
676 namespace: "my-NAMESPACE".to_string(),
677 registry: "registry.alternative.com".to_string(),
678 },
679 ],
680 };
681
682 let envelope = ProtoEnvelope::signed_contents(&alice_priv, record)
683 .expect("failed to sign envelope");
684
685 match state.validate(&envelope).unwrap_err() {
687 ValidationError::InvalidNamespace { .. } => {}
688 _ => panic!("expected a different error"),
689 }
690 }
691 }
692}