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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(tag = "status", rename_all = "camelCase")]
75pub enum ReleaseState {
76 Released {
78 content: AnyHash,
80 },
81 Yanked {
83 by: signing::KeyID,
85 #[serde(with = "crate::timestamp")]
87 timestamp: SystemTime,
88 },
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct Release {
95 pub record_id: RecordId,
97 pub version: Version,
99 pub by: signing::KeyID,
101 #[serde(with = "crate::timestamp")]
103 pub timestamp: SystemTime,
104 pub state: ReleaseState,
106}
107
108impl Release {
109 pub fn yanked(&self) -> bool {
111 matches!(self.state, ReleaseState::Yanked { .. })
112 }
113
114 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#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "camelCase")]
130pub struct Head {
131 pub digest: RecordId,
133 #[serde(with = "crate::timestamp")]
135 pub timestamp: SystemTime,
136}
137
138#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
140#[serde(default, rename_all = "camelCase")]
141pub struct LogState {
142 #[serde(skip_serializing_if = "Option::is_none")]
145 algorithm: Option<HashAlgorithm>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 head: Option<Head>,
149 #[serde(skip_serializing_if = "IndexMap::is_empty")]
151 permissions: IndexMap<signing::KeyID, IndexSet<model::Permission>>,
152 #[serde(skip_serializing_if = "IndexMap::is_empty")]
154 releases: IndexMap<Version, Release>,
155 #[serde(skip_serializing_if = "IndexMap::is_empty")]
157 keys: IndexMap<signing::KeyID, signing::PublicKey>,
158}
159
160impl LogState {
161 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn head(&self) -> &Option<Head> {
170 &self.head
171 }
172
173 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 pub fn releases(&self) -> impl Iterator<Item = &Release> {
194 self.releases.values()
195 }
196
197 pub fn release(&self, version: &Version) -> Option<&Release> {
201 self.releases.get(version)
202 }
203
204 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 pub fn public_key(&self, key_id: &signing::KeyID) -> Option<&signing::PublicKey> {
218 self.keys.get(key_id)
219 }
220
221 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 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 self.validate_record_hash(record)?;
242
243 self.validate_record_version(record)?;
245
246 self.validate_record_timestamp(record)?;
248
249 self.validate_record_entries(
251 &record_id,
252 envelope.key_id(),
253 record.timestamp,
254 &record.entries,
255 )?;
256
257 let _algorithm = self
259 .algorithm
260 .ok_or(ValidationError::InitialRecordDoesNotInit)?;
261
262 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 model::PackageRecord::verify(key, envelope.content_bytes(), envelope.signature())?;
271
272 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 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 if !self.initialized() {
353 return Err(ValidationError::FirstEntryIsNotInit);
354 }
355
356 match entry {
357 model::PackageEntry::Init { .. } => unreachable!(), 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 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 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 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 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 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 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 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 model::PackageEntry::GrantFlat {
775 key: bob_pub,
776 permissions: vec![model::Permission::Release],
777 },
778 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 match state.validate(&envelope).unwrap_err() {
791 ValidationError::PermissionNotFoundToRevoke { .. } => {}
792 _ => panic!("expected a different error"),
793 }
794 }
795}