warg_protocol/operator/
state.rs

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/// The namespace definition.
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(rename_all = "camelCase")]
74struct NamespaceDefinition {
75    /// Namespace state.
76    state: NamespaceState,
77}
78
79/// The namespace state for defining or importing from other registries.
80#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
81#[serde(rename_all = "camelCase")]
82pub enum NamespaceState {
83    /// The namespace is defined for the registry to use for its own package logs.
84    Defined,
85    /// The namespace is imported from another registry.
86    #[serde(rename_all = "camelCase")]
87    Imported {
88        /// The imported registry.
89        registry: String,
90    },
91}
92
93/// Information about the current head of the operator log.
94///
95/// A head is the last validated record digest and timestamp.
96#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "camelCase")]
98pub struct Head {
99    /// The digest of the last validated record.
100    pub digest: RecordId,
101    /// The timestamp of the last validated record.
102    #[serde(with = "crate::timestamp")]
103    pub timestamp: SystemTime,
104}
105
106/// Calculated state for an operator log.
107#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(default, rename_all = "camelCase")]
109pub struct LogState {
110    /// The hash algorithm used by the operator log.
111    /// This is `None` until the first (i.e. init) record is validated.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    algorithm: Option<HashAlgorithm>,
114    /// The current head of the state.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    head: Option<Head>,
117    /// The permissions of each key.
118    #[serde(skip_serializing_if = "IndexMap::is_empty")]
119    permissions: IndexMap<signing::KeyID, IndexSet<model::Permission>>,
120    /// The keys known to the state.
121    #[serde(skip_serializing_if = "IndexMap::is_empty")]
122    keys: IndexMap<signing::KeyID, signing::PublicKey>,
123    /// The namespaces known to the state. The key is the namespace.
124    #[serde(skip_serializing_if = "IndexMap::is_empty")]
125    namespaces: IndexMap<String, NamespaceDefinition>,
126}
127
128impl LogState {
129    /// Create a new operator log state.
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Gets the current head of the log.
135    ///
136    /// Returns `None` if no records have been validated yet.
137    pub fn head(&self) -> &Option<Head> {
138        &self.head
139    }
140
141    /// Validates an individual operator record.
142    ///
143    /// It is expected that `validate` is called in order of the
144    /// records in the log.
145    ///
146    /// Note that on failure, the log state is consumed to prevent
147    /// invalid state from being used in future validations.
148    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    /// Gets the public key of the given key id.
157    ///
158    /// Returns `None` if the key id is not recognized.
159    pub fn public_key(&self, key_id: &signing::KeyID) -> Option<&signing::PublicKey> {
160        self.keys.get(key_id)
161    }
162
163    /// Gets the namespace state.
164    pub fn namespace_state(&self, namespace: &str) -> Option<&NamespaceState> {
165        self.namespaces.get(namespace).map(|def| &def.state)
166    }
167
168    /// Checks the key has permission to sign checkpoints.
169    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        // The package log is initialized if the hash algorithm is set
176        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        // Validate previous hash
186        self.validate_record_hash(record)?;
187
188        // Validate version
189        self.validate_record_version(record)?;
190
191        // Validate timestamp
192        self.validate_record_timestamp(record)?;
193
194        // Validate entries
195        self.validate_record_entries(envelope.key_id(), &record.entries)?;
196
197        // At this point the digest algorithm must be set via an init entry
198        let _algorithm = self
199            .algorithm
200            .ok_or(ValidationError::InitialRecordDoesNotInit)?;
201
202        // Validate the envelope key id
203        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        // Validate the envelope signature
210        model::OperatorRecord::verify(key, envelope.content_bytes(), envelope.signature())?;
211
212        // Update the state head
213        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            // Process an init entry specially
280            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            // Must have seen an init entry by now
290            if !self.initialized() {
291                return Err(ValidationError::FirstEntryIsNotInit);
292            }
293
294            match entry {
295                model::OperatorEntry::Init { .. } => unreachable!(), // handled above
296                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        // Check that the current key has the permission they're trying to grant
351        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        // Check that the current key has the permission they're trying to revoke
370        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            // namespace is already defined
401            Err(ValidationError::NamespaceAlreadyDefined {
402                namespace: namespace.to_string(),
403            })
404        } else {
405            // namespace is not defined
406            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                // This entry is valid
545                model::OperatorEntry::GrantFlat {
546                    key: bob_pub,
547                    permissions: vec![model::Permission::Commit],
548                },
549                // This entry is not valid
550                model::OperatorEntry::RevokeFlat {
551                    key_id: "not-valid".to_string().into(),
552                    permissions: vec![model::Permission::Commit],
553                },
554                // This entry is valid but should be rolled back since there is an invalid entry
555                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        // This validation should fail
565        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                    // This entry is valid
643                    model::OperatorEntry::DefineNamespace {
644                        namespace: "other-namespace".to_string(),
645                    },
646                    // This entry is not valid
647                    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            // This validation should fail
658            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                    // This entry is valid
671                    model::OperatorEntry::DefineNamespace {
672                        namespace: "other-namespace".to_string(),
673                    },
674                    // This entry is not valid
675                    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            // This validation should fail
686            match state.validate(&envelope).unwrap_err() {
687                ValidationError::InvalidNamespace { .. } => {}
688                _ => panic!("expected a different error"),
689            }
690        }
691    }
692}