warg_protocol/package/
state.rs

1use super::{model, PACKAGE_RECORD_VERSION};
2use crate::registry::RecordId;
3use crate::ProtoEnvelope;
4use indexmap::{map::Entry, IndexMap, IndexSet};
5use semver::{Version, VersionReq};
6use serde::{Deserialize, Serialize};
7use std::time::SystemTime;
8use thiserror::Error;
9use warg_crypto::hash::{AnyHash, 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 package 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("an entry attempted to release version {version} which is already released")]
39    ReleaseOfReleased { version: Version },
40
41    #[error("an entry attempted to yank version {version} which had not yet been released")]
42    YankOfUnreleased { version: Version },
43
44    #[error("an entry attempted to yank version {version} which is already yanked")]
45    YankOfYanked { version: Version },
46
47    #[error("unable to verify signature")]
48    SignatureError(#[from] signing::SignatureError),
49
50    #[error("record hash uses {found} algorithm but {expected} was expected")]
51    IncorrectHashAlgorithm {
52        found: HashAlgorithm,
53        expected: HashAlgorithm,
54    },
55
56    #[error("previous record hash does not match")]
57    RecordHashDoesNotMatch,
58
59    #[error("the first record contained a previous hash value")]
60    PreviousHashOnFirstRecord,
61
62    #[error("non-initial record contained no previous hash")]
63    NoPreviousHashAfterInit,
64
65    #[error("protocol version {version} not allowed")]
66    ProtocolVersionNotAllowed { version: u32 },
67
68    #[error("record has lower timestamp than previous")]
69    TimestampLowerThanPrevious,
70}
71
72/// Represents the current state of a release.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(tag = "status", rename_all = "camelCase")]
75pub enum ReleaseState {
76    /// The release is currently available.
77    Released {
78        /// The content digest associated with the release.
79        content: AnyHash,
80    },
81    /// The release has been yanked.
82    Yanked {
83        /// The key id that yanked the package.
84        by: signing::KeyID,
85        /// The timestamp of the yank.
86        #[serde(with = "crate::timestamp")]
87        timestamp: SystemTime,
88    },
89}
90
91/// Represents information about a release.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Release {
95    /// The id of the record that released the package.
96    pub record_id: RecordId,
97    /// The version of the release.
98    pub version: Version,
99    /// The key id that released the package.
100    pub by: signing::KeyID,
101    /// The timestamp of the release.
102    #[serde(with = "crate::timestamp")]
103    pub timestamp: SystemTime,
104    /// The current state of the release.
105    pub state: ReleaseState,
106}
107
108impl Release {
109    /// Determines if the release has been yanked.
110    pub fn yanked(&self) -> bool {
111        matches!(self.state, ReleaseState::Yanked { .. })
112    }
113
114    /// Gets the content associated with the release.
115    ///
116    /// Returns `None` if the release has been yanked.
117    pub fn content(&self) -> Option<&AnyHash> {
118        match &self.state {
119            ReleaseState::Released { content } => Some(content),
120            ReleaseState::Yanked { .. } => None,
121        }
122    }
123}
124
125/// Information about the current head of the package log.
126///
127/// A head is the last validated record digest and timestamp.
128#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "camelCase")]
130pub struct Head {
131    /// The digest of the last validated record.
132    pub digest: RecordId,
133    /// The timestamp of the last validated record.
134    #[serde(with = "crate::timestamp")]
135    pub timestamp: SystemTime,
136}
137
138/// Calculated state for a package log.
139#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
140#[serde(default, rename_all = "camelCase")]
141pub struct LogState {
142    /// The hash algorithm used by the package log.
143    /// This is `None` until the first (i.e. init) record is validated.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    algorithm: Option<HashAlgorithm>,
146    /// The current head of the state.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    head: Option<Head>,
149    /// The permissions of each key.
150    #[serde(skip_serializing_if = "IndexMap::is_empty")]
151    permissions: IndexMap<signing::KeyID, IndexSet<model::Permission>>,
152    /// The releases in the package log.
153    #[serde(skip_serializing_if = "IndexMap::is_empty")]
154    releases: IndexMap<Version, Release>,
155    /// The keys known to the state.
156    #[serde(skip_serializing_if = "IndexMap::is_empty")]
157    keys: IndexMap<signing::KeyID, signing::PublicKey>,
158}
159
160impl LogState {
161    /// Create a new package log state.
162    pub fn new() -> Self {
163        Self::default()
164    }
165
166    /// Gets the current head of the state.
167    ///
168    /// Returns `None` if no records have been validated yet.
169    pub fn head(&self) -> &Option<Head> {
170        &self.head
171    }
172
173    /// Validates an individual package record.
174    ///
175    /// It is expected that `validate` is called in order of the
176    /// records in the log.
177    ///
178    /// Note that on failure, the log state is consumed to prevent
179    /// invalid state from being used in future validations.
180    pub fn validate(
181        mut self,
182        record: &ProtoEnvelope<model::PackageRecord>,
183    ) -> Result<Self, ValidationError> {
184        self.validate_record(record)?;
185        Ok(self)
186    }
187
188    /// Gets the releases known to the state.
189    ///
190    /// The releases are returned in package log order.
191    ///
192    /// Yanked releases are included.
193    pub fn releases(&self) -> impl Iterator<Item = &Release> {
194        self.releases.values()
195    }
196
197    /// Gets the release with the given version.
198    ///
199    /// Returns `None` if a release with the given version does not exist.
200    pub fn release(&self, version: &Version) -> Option<&Release> {
201        self.releases.get(version)
202    }
203
204    /// Finds the latest release matching the given version requirement.
205    ///
206    /// Releases that have been yanked are not considered.
207    pub fn find_latest_release(&self, req: &VersionReq) -> Option<&Release> {
208        self.releases
209            .values()
210            .filter(|release| !release.yanked() && req.matches(&release.version))
211            .max_by(|a, b| a.version.cmp(&b.version))
212    }
213
214    /// Gets the public key of the given key id.
215    ///
216    /// Returns `None` if the key id is not recognized.
217    pub fn public_key(&self, key_id: &signing::KeyID) -> Option<&signing::PublicKey> {
218        self.keys.get(key_id)
219    }
220
221    /// Gets the key permissions.
222    ///
223    /// Returns `None` if the key id is not recognized.
224    pub fn key_permissions(&self, key_id: &signing::KeyID) -> Option<&IndexSet<model::Permission>> {
225        self.permissions.get(key_id)
226    }
227
228    fn initialized(&self) -> bool {
229        // The package log is initialized if the hash algorithm is set
230        self.algorithm.is_some()
231    }
232
233    fn validate_record(
234        &mut self,
235        envelope: &ProtoEnvelope<model::PackageRecord>,
236    ) -> Result<(), ValidationError> {
237        let record = envelope.as_ref();
238        let record_id = RecordId::package_record::<Sha256>(envelope);
239
240        // Validate previous hash
241        self.validate_record_hash(record)?;
242
243        // Validate version
244        self.validate_record_version(record)?;
245
246        // Validate timestamp
247        self.validate_record_timestamp(record)?;
248
249        // Validate entries
250        self.validate_record_entries(
251            &record_id,
252            envelope.key_id(),
253            record.timestamp,
254            &record.entries,
255        )?;
256
257        // At this point the digest algorithm must be set via an init entry
258        let _algorithm = self
259            .algorithm
260            .ok_or(ValidationError::InitialRecordDoesNotInit)?;
261
262        // Validate the envelope key id
263        let key = self.keys.get(envelope.key_id()).ok_or_else(|| {
264            ValidationError::KeyIDNotRecognized {
265                key_id: envelope.key_id().clone(),
266            }
267        })?;
268
269        // Validate the envelope signature
270        model::PackageRecord::verify(key, envelope.content_bytes(), envelope.signature())?;
271
272        // Update the state head
273        self.head = Some(Head {
274            digest: record_id,
275            timestamp: record.timestamp,
276        });
277
278        Ok(())
279    }
280
281    fn validate_record_hash(&self, record: &model::PackageRecord) -> Result<(), ValidationError> {
282        match (&self.head, &record.prev) {
283            (None, Some(_)) => Err(ValidationError::PreviousHashOnFirstRecord),
284            (Some(_), None) => Err(ValidationError::NoPreviousHashAfterInit),
285            (None, None) => Ok(()),
286            (Some(expected), Some(found)) => {
287                if found.algorithm() != expected.digest.algorithm() {
288                    return Err(ValidationError::IncorrectHashAlgorithm {
289                        found: found.algorithm(),
290                        expected: expected.digest.algorithm(),
291                    });
292                }
293
294                if found != &expected.digest {
295                    return Err(ValidationError::RecordHashDoesNotMatch);
296                }
297
298                Ok(())
299            }
300        }
301    }
302
303    fn validate_record_version(
304        &self,
305        record: &model::PackageRecord,
306    ) -> Result<(), ValidationError> {
307        if record.version == PACKAGE_RECORD_VERSION {
308            Ok(())
309        } else {
310            Err(ValidationError::ProtocolVersionNotAllowed {
311                version: record.version,
312            })
313        }
314    }
315
316    fn validate_record_timestamp(
317        &self,
318        record: &model::PackageRecord,
319    ) -> Result<(), ValidationError> {
320        if let Some(head) = &self.head {
321            if record.timestamp < head.timestamp {
322                return Err(ValidationError::TimestampLowerThanPrevious);
323            }
324        }
325
326        Ok(())
327    }
328
329    fn validate_record_entries(
330        &mut self,
331        record_id: &RecordId,
332        signer_key_id: &signing::KeyID,
333        timestamp: SystemTime,
334        entries: &[model::PackageEntry],
335    ) -> Result<(), ValidationError> {
336        for entry in entries {
337            if let Some(permission) = entry.required_permission() {
338                self.check_key_permissions(signer_key_id, &[permission])?;
339            }
340
341            // Process an init entry specially
342            if let model::PackageEntry::Init {
343                hash_algorithm,
344                key,
345            } = entry
346            {
347                self.validate_init_entry(signer_key_id, *hash_algorithm, key)?;
348                continue;
349            }
350
351            // Must have seen an init entry by now
352            if !self.initialized() {
353                return Err(ValidationError::FirstEntryIsNotInit);
354            }
355
356            match entry {
357                model::PackageEntry::Init { .. } => unreachable!(), // handled above
358                model::PackageEntry::GrantFlat { key, permissions } => {
359                    self.validate_grant_entry(signer_key_id, key, permissions)?
360                }
361                model::PackageEntry::RevokeFlat {
362                    key_id,
363                    permissions,
364                } => self.validate_revoke_entry(signer_key_id, key_id, permissions)?,
365                model::PackageEntry::Release { version, content } => self.validate_release_entry(
366                    record_id,
367                    signer_key_id,
368                    timestamp,
369                    version,
370                    content,
371                )?,
372                model::PackageEntry::Yank { version } => {
373                    self.validate_yank_entry(signer_key_id, timestamp, version)?
374                }
375            }
376        }
377
378        Ok(())
379    }
380
381    fn validate_init_entry(
382        &mut self,
383        signer_key_id: &signing::KeyID,
384        algorithm: HashAlgorithm,
385        init_key: &signing::PublicKey,
386    ) -> Result<(), ValidationError> {
387        if self.initialized() {
388            return Err(ValidationError::InitialEntryAfterBeginning);
389        }
390
391        if signer_key_id != &init_key.fingerprint() {
392            return Err(ValidationError::KeyIDNotRecognized {
393                key_id: signer_key_id.clone(),
394            });
395        }
396
397        assert!(self.permissions.is_empty());
398        assert!(self.releases.is_empty());
399        assert!(self.keys.is_empty());
400
401        self.algorithm = Some(algorithm);
402        self.permissions.insert(
403            signer_key_id.clone(),
404            IndexSet::from(model::Permission::all()),
405        );
406        self.keys.insert(init_key.fingerprint(), init_key.clone());
407
408        Ok(())
409    }
410
411    fn validate_grant_entry(
412        &mut self,
413        signer_key_id: &signing::KeyID,
414        key: &signing::PublicKey,
415        permissions: &[model::Permission],
416    ) -> Result<(), ValidationError> {
417        // Check that the current key has the permission they're trying to grant
418        self.check_key_permissions(signer_key_id, permissions)?;
419
420        let grant_key_id = key.fingerprint();
421        self.keys.insert(grant_key_id.clone(), key.clone());
422        self.permissions
423            .entry(grant_key_id)
424            .or_default()
425            .extend(permissions);
426
427        Ok(())
428    }
429
430    fn validate_revoke_entry(
431        &mut self,
432        signer_key_id: &signing::KeyID,
433        key_id: &signing::KeyID,
434        permissions: &[model::Permission],
435    ) -> Result<(), ValidationError> {
436        // Check that the current key has the permission they're trying to revoke
437        self.check_key_permissions(signer_key_id, permissions)?;
438
439        for permission in permissions {
440            if !self
441                .permissions
442                .get_mut(key_id)
443                .map(|set| set.swap_remove(permission))
444                .unwrap_or(false)
445            {
446                return Err(ValidationError::PermissionNotFoundToRevoke {
447                    permission: *permission,
448                    key_id: key_id.clone(),
449                });
450            }
451        }
452        Ok(())
453    }
454
455    fn validate_release_entry(
456        &mut self,
457        record_id: &RecordId,
458        signer_key_id: &signing::KeyID,
459        timestamp: SystemTime,
460        version: &Version,
461        content: &AnyHash,
462    ) -> Result<(), ValidationError> {
463        match self.releases.entry(version.clone()) {
464            Entry::Occupied(e) => {
465                return Err(ValidationError::ReleaseOfReleased {
466                    version: e.key().clone(),
467                })
468            }
469            Entry::Vacant(e) => {
470                let version = e.key().clone();
471                e.insert(Release {
472                    record_id: record_id.clone(),
473                    version,
474                    by: signer_key_id.clone(),
475                    timestamp,
476                    state: ReleaseState::Released {
477                        content: content.clone(),
478                    },
479                });
480            }
481        }
482
483        Ok(())
484    }
485
486    fn validate_yank_entry(
487        &mut self,
488        signer_key_id: &signing::KeyID,
489        timestamp: SystemTime,
490        version: &Version,
491    ) -> Result<(), ValidationError> {
492        match self.releases.get_mut(version) {
493            Some(e) => match e.state {
494                ReleaseState::Yanked { .. } => Err(ValidationError::YankOfYanked {
495                    version: version.clone(),
496                }),
497                ReleaseState::Released { .. } => {
498                    e.state = ReleaseState::Yanked {
499                        by: signer_key_id.clone(),
500                        timestamp,
501                    };
502                    Ok(())
503                }
504            },
505            None => Err(ValidationError::YankOfUnreleased {
506                version: version.clone(),
507            }),
508        }
509    }
510
511    fn check_key_permissions(
512        &self,
513        key_id: &signing::KeyID,
514        permissions: &[model::Permission],
515    ) -> Result<(), ValidationError> {
516        for permission in permissions {
517            if !self
518                .permissions
519                .get(key_id)
520                .map(|p| p.contains(permission))
521                .unwrap_or(false)
522            {
523                return Err(ValidationError::UnauthorizedAction {
524                    key_id: key_id.clone(),
525                    needed_permission: *permission,
526                });
527            }
528        }
529        Ok(())
530    }
531}
532
533impl crate::Validator for LogState {
534    type Record = model::PackageRecord;
535    type Error = ValidationError;
536
537    fn validate(self, record: &ProtoEnvelope<Self::Record>) -> Result<Self, Self::Error> {
538        self.validate(record)
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use pretty_assertions::assert_eq;
546    use std::time::{Duration, SystemTime};
547    use warg_crypto::hash::HashAlgorithm;
548    use warg_crypto::signing::generate_p256_pair;
549
550    #[test]
551    fn test_validate_base_log() {
552        let (alice_pub, alice_priv) = generate_p256_pair();
553        let alice_id = alice_pub.fingerprint();
554
555        let timestamp = SystemTime::now();
556        let record = model::PackageRecord {
557            prev: None,
558            version: PACKAGE_RECORD_VERSION,
559            timestamp,
560            entries: vec![model::PackageEntry::Init {
561                hash_algorithm: HashAlgorithm::Sha256,
562                key: alice_pub.clone(),
563            }],
564        };
565
566        let envelope = ProtoEnvelope::signed_contents(&alice_priv, record).unwrap();
567        let state = LogState::default();
568        let state = state.validate(&envelope).unwrap();
569
570        assert_eq!(
571            state,
572            LogState {
573                head: Some(Head {
574                    digest: RecordId::package_record::<Sha256>(&envelope),
575                    timestamp,
576                }),
577                algorithm: Some(HashAlgorithm::Sha256),
578                permissions: IndexMap::from([(
579                    alice_id.clone(),
580                    IndexSet::from([model::Permission::Release, model::Permission::Yank]),
581                )]),
582                releases: IndexMap::default(),
583                keys: IndexMap::from([(alice_id, alice_pub)]),
584            }
585        );
586    }
587
588    #[test]
589    fn test_validate_larger_log() {
590        let (alice_pub, alice_priv) = generate_p256_pair();
591        let (bob_pub, bob_priv) = generate_p256_pair();
592        let alice_id = alice_pub.fingerprint();
593        let bob_id = bob_pub.fingerprint();
594
595        let hash_algo = HashAlgorithm::Sha256;
596        let state = LogState::default();
597
598        // In envelope 0: alice inits and grants bob release
599        let timestamp0 = SystemTime::now();
600        let record0 = model::PackageRecord {
601            prev: None,
602            version: PACKAGE_RECORD_VERSION,
603            timestamp: timestamp0,
604            entries: vec![
605                model::PackageEntry::Init {
606                    hash_algorithm: hash_algo,
607                    key: alice_pub.clone(),
608                },
609                model::PackageEntry::GrantFlat {
610                    key: bob_pub.clone(),
611                    permissions: model::Permission::all().into(),
612                },
613            ],
614        };
615        let envelope0 = ProtoEnvelope::signed_contents(&alice_priv, record0).unwrap();
616        let state = state.validate(&envelope0).unwrap();
617
618        // In envelope 1: bob releases 1.1.0
619        let timestamp1 = timestamp0 + Duration::from_secs(1);
620        let content = hash_algo.digest(&[0, 1, 2, 3]);
621        let record1 = model::PackageRecord {
622            prev: Some(RecordId::package_record::<Sha256>(&envelope0)),
623            version: PACKAGE_RECORD_VERSION,
624            timestamp: timestamp1,
625            entries: vec![model::PackageEntry::Release {
626                version: Version::new(1, 1, 0),
627                content: content.clone(),
628            }],
629        };
630
631        let envelope1 = ProtoEnvelope::signed_contents(&bob_priv, record1).unwrap();
632        let record_id1 = RecordId::package_record::<Sha256>(&envelope1);
633        let state = state.validate(&envelope1).unwrap();
634
635        // At this point, the state should consider 1.1.0 released
636        assert_eq!(
637            state.find_latest_release(&"~1".parse().unwrap()),
638            Some(&Release {
639                record_id: record_id1.clone(),
640                version: Version::new(1, 1, 0),
641                by: bob_id.clone(),
642                timestamp: timestamp1,
643                state: ReleaseState::Released {
644                    content: content.clone()
645                }
646            })
647        );
648        assert!(state
649            .find_latest_release(&"~1.2".parse().unwrap())
650            .is_none());
651        assert_eq!(
652            state.releases().collect::<Vec<_>>(),
653            vec![&Release {
654                record_id: record_id1.clone(),
655                version: Version::new(1, 1, 0),
656                by: bob_id.clone(),
657                timestamp: timestamp1,
658                state: ReleaseState::Released { content }
659            }]
660        );
661
662        // In envelope 2: alice revokes bobs access and yanks 1.1.0
663        let timestamp2 = timestamp1 + Duration::from_secs(1);
664        let record2 = model::PackageRecord {
665            prev: Some(RecordId::package_record::<Sha256>(&envelope1)),
666            version: PACKAGE_RECORD_VERSION,
667            timestamp: timestamp2,
668            entries: vec![
669                model::PackageEntry::RevokeFlat {
670                    key_id: bob_id.clone(),
671                    permissions: model::Permission::all().into(),
672                },
673                model::PackageEntry::Yank {
674                    version: Version::new(1, 1, 0),
675                },
676            ],
677        };
678        let envelope2 = ProtoEnvelope::signed_contents(&alice_priv, record2).unwrap();
679        let state = state.validate(&envelope2).unwrap();
680
681        // At this point, the state should consider 1.1.0 yanked
682        assert!(state.find_latest_release(&"~1".parse().unwrap()).is_none());
683        assert_eq!(
684            state.releases().collect::<Vec<_>>(),
685            vec![&Release {
686                record_id: record_id1.clone(),
687                version: Version::new(1, 1, 0),
688                by: bob_id.clone(),
689                timestamp: timestamp1,
690                state: ReleaseState::Yanked {
691                    by: alice_id.clone(),
692                    timestamp: timestamp2
693                }
694            }]
695        );
696
697        assert_eq!(
698            state,
699            LogState {
700                algorithm: Some(HashAlgorithm::Sha256),
701                head: Some(Head {
702                    digest: RecordId::package_record::<Sha256>(&envelope2),
703                    timestamp: timestamp2,
704                }),
705                permissions: IndexMap::from([
706                    (
707                        alice_id.clone(),
708                        IndexSet::from([model::Permission::Release, model::Permission::Yank]),
709                    ),
710                    (bob_id.clone(), IndexSet::default()),
711                ]),
712                releases: IndexMap::from([(
713                    Version::new(1, 1, 0),
714                    Release {
715                        record_id: record_id1,
716                        version: Version::new(1, 1, 0),
717                        by: bob_id.clone(),
718                        timestamp: timestamp1,
719                        state: ReleaseState::Yanked {
720                            by: alice_id.clone(),
721                            timestamp: timestamp2
722                        }
723                    }
724                )]),
725                keys: IndexMap::from([(alice_id, alice_pub), (bob_id, bob_pub),]),
726            }
727        );
728    }
729
730    #[test]
731    fn test_rollback() {
732        let (alice_pub, alice_priv) = generate_p256_pair();
733        let alice_id = alice_pub.fingerprint();
734        let (bob_pub, _) = generate_p256_pair();
735
736        let timestamp = SystemTime::now();
737        let record = model::PackageRecord {
738            prev: None,
739            version: 0,
740            timestamp,
741            entries: vec![model::PackageEntry::Init {
742                hash_algorithm: HashAlgorithm::Sha256,
743                key: alice_pub.clone(),
744            }],
745        };
746
747        let envelope =
748            ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
749        let state = LogState::default();
750        let state = state.validate(&envelope).unwrap();
751
752        let expected = LogState {
753            head: Some(Head {
754                digest: RecordId::package_record::<Sha256>(&envelope),
755                timestamp,
756            }),
757            algorithm: Some(HashAlgorithm::Sha256),
758            releases: IndexMap::new(),
759            permissions: IndexMap::from([(
760                alice_id.clone(),
761                IndexSet::from([model::Permission::Release, model::Permission::Yank]),
762            )]),
763            keys: IndexMap::from([(alice_id, alice_pub)]),
764        };
765
766        assert_eq!(state, expected);
767
768        let record = model::PackageRecord {
769            prev: Some(RecordId::package_record::<Sha256>(&envelope)),
770            version: 0,
771            timestamp: SystemTime::now(),
772            entries: vec![
773                // This entry is valid
774                model::PackageEntry::GrantFlat {
775                    key: bob_pub,
776                    permissions: vec![model::Permission::Release],
777                },
778                // This entry is not valid
779                model::PackageEntry::RevokeFlat {
780                    key_id: "not-valid".to_string().into(),
781                    permissions: vec![model::Permission::Release],
782                },
783            ],
784        };
785
786        let envelope =
787            ProtoEnvelope::signed_contents(&alice_priv, record).expect("failed to sign envelope");
788
789        // This validation should fail
790        match state.validate(&envelope).unwrap_err() {
791            ValidationError::PermissionNotFoundToRevoke { .. } => {}
792            _ => panic!("expected a different error"),
793        }
794    }
795}