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