warg_protocol/package/
mod.rs

1use anyhow::Error;
2use prost::Message;
3use thiserror::Error;
4use warg_crypto::{hash::AnyHash, Decode, Encode, Signable};
5use warg_protobuf::protocol as protobuf;
6
7use crate::{pbjson_to_prost_timestamp, prost_to_pbjson_timestamp, registry::RecordId};
8
9mod model;
10mod state;
11
12pub use model::{PackageEntry, PackageRecord, Permission};
13pub use state::{LogState, Release, ReleaseState, ValidationError};
14
15/// The currently supported package protocol version.
16pub const PACKAGE_RECORD_VERSION: u32 = 0;
17
18impl Decode for model::PackageRecord {
19    fn decode(bytes: &[u8]) -> Result<Self, Error> {
20        protobuf::PackageRecord::decode(bytes)?.try_into()
21    }
22}
23
24impl TryFrom<protobuf::PackageRecord> for model::PackageRecord {
25    type Error = Error;
26
27    fn try_from(record: protobuf::PackageRecord) -> Result<Self, Self::Error> {
28        let prev: Option<RecordId> = match record.prev {
29            Some(hash_string) => {
30                let hash: AnyHash = hash_string.parse()?;
31                Some(hash.into())
32            }
33            None => None,
34        };
35        let version = record.version;
36        let pbjson_timestamp = record.time.ok_or(InvalidTimestampError)?;
37        let prost_timestamp = pbjson_to_prost_timestamp(pbjson_timestamp);
38        let timestamp = prost_timestamp.try_into()?;
39
40        let entries: Result<Vec<model::PackageEntry>, Error> = record
41            .entries
42            .into_iter()
43            .map(|proto_entry| proto_entry.try_into())
44            .collect();
45        let entries = entries?;
46
47        Ok(model::PackageRecord {
48            prev,
49            version,
50            timestamp,
51            entries,
52        })
53    }
54}
55
56#[derive(Error, Debug)]
57#[error("empty or invalid timestamp in record")]
58struct InvalidTimestampError;
59
60impl TryFrom<protobuf::PackageEntry> for model::PackageEntry {
61    type Error = Error;
62
63    fn try_from(entry: protobuf::PackageEntry) -> Result<Self, Self::Error> {
64        use protobuf::package_entry::Contents;
65        let output = match entry.contents.ok_or(EmptyContentError)? {
66            Contents::Init(init) => model::PackageEntry::Init {
67                hash_algorithm: init.hash_algorithm.parse()?,
68                key: init.key.parse()?,
69            },
70            Contents::GrantFlat(grant_flat) => model::PackageEntry::GrantFlat {
71                key: grant_flat.key.parse()?,
72                permissions: grant_flat
73                    .permissions
74                    .into_iter()
75                    .map(TryInto::try_into)
76                    .collect::<Result<_, _>>()?,
77            },
78            Contents::RevokeFlat(revoke_flat) => model::PackageEntry::RevokeFlat {
79                key_id: revoke_flat.key_id.into(),
80                permissions: revoke_flat
81                    .permissions
82                    .into_iter()
83                    .map(TryInto::try_into)
84                    .collect::<Result<_, _>>()?,
85            },
86            Contents::Release(release) => model::PackageEntry::Release {
87                version: release
88                    .version
89                    .parse()
90                    .map_err(|error| Error::new(error) as Error)?,
91                content: release.content_hash.parse()?,
92            },
93            Contents::Yank(yank) => model::PackageEntry::Yank {
94                version: yank.version.parse()?,
95            },
96        };
97        Ok(output)
98    }
99}
100
101#[derive(Error, Debug)]
102#[error("no content in entry")]
103struct EmptyContentError;
104
105impl TryFrom<i32> for model::Permission {
106    type Error = Error;
107
108    fn try_from(permission: i32) -> Result<Self, Self::Error> {
109        let proto_perm = protobuf::PackagePermission::try_from(permission)
110            .map_err(|_| PermissionParseError { value: permission })?;
111        match proto_perm {
112            protobuf::PackagePermission::Unspecified => {
113                Err(Error::new(PermissionParseError { value: permission }))
114            }
115            protobuf::PackagePermission::Release => Ok(model::Permission::Release),
116            protobuf::PackagePermission::Yank => Ok(model::Permission::Yank),
117        }
118    }
119}
120
121#[derive(Error, Debug)]
122#[error("the value {value} could not be parsed as a permission")]
123struct PermissionParseError {
124    value: i32,
125}
126
127// Serialization
128
129impl Signable for model::PackageRecord {
130    const PREFIX: &'static [u8] = b"WARG-PACKAGE-RECORD-SIGNATURE-V0";
131}
132
133impl Encode for model::PackageRecord {
134    fn encode(&self) -> Vec<u8> {
135        let proto_record: protobuf::PackageRecord = self.into();
136        proto_record.encode_to_vec()
137    }
138}
139
140impl<'a> From<&'a model::PackageRecord> for protobuf::PackageRecord {
141    fn from(record: &'a model::PackageRecord) -> Self {
142        protobuf::PackageRecord {
143            prev: record.prev.as_ref().map(|hash| hash.to_string()),
144            version: record.version,
145            time: Some(prost_to_pbjson_timestamp(record.timestamp.into())),
146            entries: record.entries.iter().map(|entry| entry.into()).collect(),
147        }
148    }
149}
150
151impl<'a> From<&'a model::PackageEntry> for protobuf::PackageEntry {
152    fn from(entry: &'a model::PackageEntry) -> Self {
153        use protobuf::package_entry::Contents;
154        let contents = match entry {
155            model::PackageEntry::Init {
156                hash_algorithm,
157                key,
158            } => Contents::Init(protobuf::PackageInit {
159                key: key.to_string(),
160                hash_algorithm: hash_algorithm.to_string(),
161            }),
162            model::PackageEntry::GrantFlat { key, permissions } => {
163                Contents::GrantFlat(protobuf::PackageGrantFlat {
164                    key: key.to_string(),
165                    permissions: permissions.iter().map(Into::into).collect(),
166                })
167            }
168            model::PackageEntry::RevokeFlat {
169                key_id,
170                permissions,
171            } => Contents::RevokeFlat(protobuf::PackageRevokeFlat {
172                key_id: key_id.to_string(),
173                permissions: permissions.iter().map(Into::into).collect(),
174            }),
175            model::PackageEntry::Release { version, content } => {
176                Contents::Release(protobuf::PackageRelease {
177                    version: version.to_string(),
178                    content_hash: content.to_string(),
179                })
180            }
181            model::PackageEntry::Yank { version } => Contents::Yank(protobuf::PackageYank {
182                version: version.to_string(),
183            }),
184        };
185        let contents = Some(contents);
186        protobuf::PackageEntry { contents }
187    }
188}
189
190impl<'a> From<&'a model::Permission> for i32 {
191    fn from(permission: &'a model::Permission) -> Self {
192        let proto_perm = match permission {
193            model::Permission::Release => protobuf::PackagePermission::Release,
194            model::Permission::Yank => protobuf::PackagePermission::Yank,
195        };
196        proto_perm.into()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    use std::time::SystemTime;
205
206    use semver::Version;
207
208    use warg_crypto::hash::HashAlgorithm;
209
210    use crate::ProtoEnvelope;
211    use warg_crypto::signing::generate_p256_pair;
212
213    #[test]
214    fn test_envelope_roundtrip() {
215        let (alice_pub, alice_priv) = generate_p256_pair();
216        let (bob_pub, _bob_priv) = generate_p256_pair();
217
218        let record = model::PackageRecord {
219            prev: None,
220            version: PACKAGE_RECORD_VERSION,
221            timestamp: SystemTime::now(),
222            entries: vec![
223                model::PackageEntry::Init {
224                    hash_algorithm: HashAlgorithm::Sha256,
225                    key: alice_pub,
226                },
227                model::PackageEntry::GrantFlat {
228                    key: bob_pub.clone(),
229                    permissions: vec![model::Permission::Release, model::Permission::Yank],
230                },
231                model::PackageEntry::RevokeFlat {
232                    key_id: bob_pub.fingerprint(),
233                    permissions: vec![model::Permission::Release],
234                },
235                model::PackageEntry::Release {
236                    version: Version::new(1, 0, 0),
237                    content: HashAlgorithm::Sha256.digest(&[0, 1, 2, 3]),
238                },
239            ],
240        };
241
242        let first_envelope = match ProtoEnvelope::signed_contents(&alice_priv, record) {
243            Ok(value) => value,
244            Err(error) => panic!("Failed to sign envelope 1: {:?}", error),
245        };
246
247        let bytes = first_envelope.to_protobuf();
248
249        let second_envelope: ProtoEnvelope<model::PackageRecord> =
250            match ProtoEnvelope::from_protobuf(&bytes) {
251                Ok(value) => value,
252                Err(error) => panic!("Failed to create envelope 2: {:?}", error),
253            };
254
255        assert_eq!(first_envelope, second_envelope);
256    }
257}