1#![doc = include_str!("../README.md")]
2#![deny(missing_debug_implementations)]
3#![deny(missing_docs)]
4#![deny(clippy::unwrap_used)]
5#![deny(clippy::expect_used)]
6
7use std::path::Path;
8use std::time::SystemTime;
9use std::{collections::HashMap, path::PathBuf};
10
11use digest_io::IoWrapper;
12use rand::Rng;
13use semver::Version;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use sha2::Digest;
17pub use sha2::Sha512;
18use sha2::digest::common::hazmat::{DeserializeStateError, SerializableState};
19
20#[cfg(feature = "cli")]
21pub mod cli;
22pub mod ssh;
23
24#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum Error {
28 #[error("Invalid content type. Found {actual:?} but expected {expected:?}.")]
30 InvalidContentType {
31 actual: HashType,
33
34 expected: HashType,
36 },
37
38 #[error("Malformed content size")]
40 InvalidContentSize,
41
42 #[error("Deserialization of the hasher's state failed: {0}")]
44 HasherDeserialization(#[from] DeserializeStateError),
45
46 #[error("Could not deserialize request: {0}")]
48 RequestDeserialization(#[from] serde_json::Error),
49
50 #[error("I/O error: {source} when processing {file}")]
52 Io {
53 file: PathBuf,
58
59 source: std::io::Error,
61 },
62
63 #[error("Current time is before reference time {reference_time:?}: {source}")]
65 CurrentTimeBeforeReference {
66 reference_time: SystemTime,
68 source: std::time::SystemTimeError,
70 },
71
72 #[error("SSH client error: {0}")]
74 SshClient(#[from] crate::ssh::client::Error),
75}
76
77#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
79#[serde(rename_all = "lowercase")]
80pub enum HashType {
81 #[serde(rename = "sha2-0.11-SHA512-state")]
85 #[allow(non_camel_case_types)]
86 Sha2_0_11_Sha512_State,
87}
88
89#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub enum SignatureType {
92 #[serde(rename = "OpenPGPv4")]
94 OpenPgpV4,
95}
96
97#[derive(Debug, Deserialize, Serialize)]
99pub struct SignatureRequestInput {
100 #[serde(rename = "type")]
101 hash_type: HashType,
102 content: Vec<u8>,
103}
104
105#[derive(Debug, Deserialize, Serialize)]
107pub struct SignatureRequestOutput {
108 #[serde(rename = "type")]
110 sig_type: SignatureType,
111}
112
113impl SignatureRequestOutput {
114 pub fn new_openpgp_v4() -> Self {
117 Self {
118 sig_type: SignatureType::OpenPgpV4,
119 }
120 }
121
122 pub fn is_openpgp_v4(&self) -> bool {
125 self.sig_type == SignatureType::OpenPgpV4
126 }
127}
128
129impl From<sha2::Sha512> for SignatureRequestInput {
130 fn from(value: sha2::Sha512) -> Self {
131 Self {
132 hash_type: HashType::Sha2_0_11_Sha512_State,
133 content: value.serialize().to_vec(),
134 }
135 }
136}
137
138impl TryFrom<SignatureRequestInput> for sha2::Sha512 {
139 type Error = Error;
140 fn try_from(value: SignatureRequestInput) -> Result<Self, Self::Error> {
141 if value.hash_type != HashType::Sha2_0_11_Sha512_State {
142 return Err(Error::InvalidContentType {
143 actual: value.hash_type,
144 expected: HashType::Sha2_0_11_Sha512_State,
145 });
146 }
147
148 let hasher = sha2::Sha512::deserialize(
149 value.content[..]
150 .try_into()
151 .map_err(|_| Error::InvalidContentSize)?,
152 )?;
153
154 Ok(hasher)
155 }
156}
157
158#[derive(Debug, Deserialize, Serialize)]
160pub struct Required {
161 pub input: SignatureRequestInput,
163
164 pub output: SignatureRequestOutput,
166}
167
168#[derive(Debug, Deserialize, Serialize)]
170pub struct Request {
171 pub version: Version,
173
174 pub required: Required,
179
180 pub optional: HashMap<String, Value>,
186}
187
188impl Request {
189 pub fn from_reader(reader: impl std::io::Read) -> Result<Self, Error> {
196 let req: Request = serde_json::from_reader(reader)?;
197 Ok(req)
198 }
199
200 pub fn to_writer(&self, writer: impl std::io::Write) -> Result<(), Error> {
207 serde_json::to_writer(writer, &self)?;
208 Ok(())
209 }
210
211 pub fn for_file(input: impl AsRef<Path>) -> Result<Self, Error> {
233 let input = input.as_ref();
234 let pack_err = |source| Error::Io {
235 file: input.into(),
236 source,
237 };
238 let hasher = {
239 let mut hasher = IoWrapper(sha2::Sha512::new());
240 std::io::copy(
241 &mut std::fs::File::open(input).map_err(pack_err)?,
242 &mut hasher,
243 )
244 .map_err(pack_err)?;
245 hasher.0
246 };
247 let required = Required {
248 input: hasher.into(),
249 output: SignatureRequestOutput::new_openpgp_v4(),
250 };
251
252 let grease: String = rand::thread_rng()
256 .sample_iter(&rand::distributions::Alphanumeric)
257 .take(7)
258 .map(char::from)
259 .collect();
260
261 Ok(Self {
262 version: semver::Version::new(1, 0, 0),
263 required,
264 optional: vec![
265 (
266 grease,
267 Value::String(
268 "https://gitlab.archlinux.org/archlinux/signstar/-/merge_requests/43"
269 .to_string(),
270 ),
271 ),
272 (
273 "request-time".into(),
274 Value::Number(
275 SystemTime::now()
276 .duration_since(SystemTime::UNIX_EPOCH)
277 .map_err(|source| crate::Error::CurrentTimeBeforeReference {
278 reference_time: SystemTime::UNIX_EPOCH,
279 source,
280 })?
281 .as_secs()
282 .into(),
283 ),
284 ),
285 (
286 "file-name".into(),
287 input
288 .file_name()
289 .and_then(|s| s.to_str())
290 .map(Into::into)
291 .unwrap_or(Value::Null),
292 ),
293 ]
294 .into_iter()
295 .collect(),
296 })
297 }
298}
299
300#[derive(Debug, Deserialize, Serialize)]
308pub struct Response {
309 pub version: Version,
311
312 signature: String,
314}
315
316impl Response {
317 pub fn v1(signature: String) -> Self {
319 Self {
320 version: Version::new(1, 0, 0),
321 signature,
322 }
323 }
324
325 pub fn from_reader(reader: impl std::io::Read) -> Result<Self, Error> {
331 let resp: Self = serde_json::from_reader(reader)?;
332 Ok(resp)
333 }
334
335 pub fn to_writer(&self, writer: impl std::io::Write) -> Result<(), Error> {
341 serde_json::to_writer(writer, &self)?;
342 Ok(())
343 }
344
345 pub fn signature_to_writer(&self, mut writer: impl std::io::Write) -> Result<(), Error> {
351 writer
352 .write_all(self.signature.as_bytes())
353 .map_err(|source| Error::Io {
354 file: PathBuf::new(),
355 source,
356 })?;
357 Ok(())
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use std::{fs::File, path::PathBuf};
364
365 use rstest::rstest;
366 use sha2::Digest;
367 use testresult::TestResult;
368
369 use super::*;
370
371 #[test]
372 fn hash_values_are_predictable() -> testresult::TestResult {
373 let mut hasher = IoWrapper(sha2::Sha512::new());
374 let mut bytes = std::io::Cursor::new(b"this is sample text");
375 std::io::copy(&mut bytes, &mut hasher)?;
376 let result: &[u8] = &hasher.0.serialize();
377
378 let expected_state = [
379 8, 201, 188, 243, 103, 230, 9, 106, 59, 167, 202, 132, 133, 174, 103, 187, 43, 248,
380 148, 254, 114, 243, 110, 60, 241, 54, 29, 95, 58, 245, 79, 165, 209, 130, 230, 173,
381 127, 82, 14, 81, 31, 108, 62, 43, 140, 104, 5, 155, 107, 189, 65, 251, 171, 217, 131,
382 31, 121, 33, 126, 19, 25, 205, 224, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
383 19, 116, 104, 105, 115, 32, 105, 115, 32, 115, 97, 109, 112, 108, 101, 32, 116, 101,
384 120, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
385 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
386 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
387 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
388 ];
389
390 assert_eq!(result, expected_state);
391
392 let expected_digest = [
393 20, 253, 69, 133, 146, 76, 11, 4, 191, 13, 150, 196, 9, 97, 21, 35, 186, 95, 254, 59,
394 148, 60, 88, 155, 127, 203, 151, 216, 11, 16, 228, 73, 113, 23, 115, 110, 198, 42, 109,
395 92, 23, 33, 70, 71, 136, 219, 73, 238, 135, 13, 223, 117, 215, 69, 243, 33, 125, 109,
396 95, 121, 213, 44, 212, 166,
397 ];
398
399 let hasher = sha2::Sha512::deserialize(&expected_state.into())?;
400 let hash = &hasher.finalize()[..];
401 assert_eq!(hash, expected_digest);
402
403 Ok(())
408 }
409
410 #[test]
411 fn sample_request_is_ok() -> TestResult {
412 let reader = File::open("tests/sample-request.json")?;
413 let reader = Request::from_reader(reader)?;
414 let hasher: sha2::Sha512 = reader.required.input.try_into()?;
415 assert_eq!(
416 hasher.finalize(),
417 [
418 20, 253, 69, 133, 146, 76, 11, 4, 191, 13, 150, 196, 9, 97, 21, 35, 186, 95, 254,
419 59, 148, 60, 88, 155, 127, 203, 151, 216, 11, 16, 228, 73, 113, 23, 115, 110, 198,
420 42, 109, 92, 23, 33, 70, 71, 136, 219, 73, 238, 135, 13, 223, 117, 215, 69, 243,
421 33, 125, 109, 95, 121, 213, 44, 212, 166
422 ]
423 );
424 Ok(())
425 }
426
427 #[rstest]
428 fn sample_request_is_bad(#[files("tests/bad-*.json")] request_file: PathBuf) -> TestResult {
429 let reader = File::open(request_file)?;
430 assert!(
431 Request::from_reader(reader).is_err(),
432 "parsing of the request file should fail"
433 );
434 Ok(())
435 }
436}