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, Deserialize, Eq, PartialEq, Serialize)]
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, Deserialize, Eq, PartialEq, Serialize)]
89pub enum SignatureType {
90 #[serde(rename = "OpenPGPv4")]
92 OpenPgpV4,
93}
94
95#[derive(Debug, Deserialize, Serialize)]
97pub struct SignatureRequestInput {
98 #[serde(rename = "type")]
99 hash_type: HashType,
100 content: Vec<u8>,
101}
102
103#[derive(Debug, Deserialize, Serialize)]
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, Deserialize, Serialize)]
158pub struct Required {
159 pub input: SignatureRequestInput,
161
162 pub output: SignatureRequestOutput,
164}
165
166struct IoWrapper(sha2::Sha512);
168
169impl std::io::Write for IoWrapper {
170 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
176 self.0.update(buf);
177 Ok(buf.len())
178 }
179
180 fn flush(&mut self) -> std::io::Result<()> {
186 Ok(())
187 }
188}
189
190#[derive(Debug, Deserialize, Serialize)]
192pub struct Request {
193 pub version: Version,
195
196 pub required: Required,
201
202 pub optional: HashMap<String, Value>,
208}
209
210impl Request {
211 pub fn from_reader(reader: impl std::io::Read) -> Result<Self, Error> {
218 let req: Request = serde_json::from_reader(reader)?;
219 Ok(req)
220 }
221
222 pub fn to_writer(&self, writer: impl std::io::Write) -> Result<(), Error> {
229 serde_json::to_writer(writer, &self)?;
230 Ok(())
231 }
232
233 pub fn for_file(input: impl AsRef<Path>) -> Result<Self, Error> {
255 let input = input.as_ref();
256 let pack_err = |source| Error::Io {
257 file: input.into(),
258 source,
259 };
260 let hasher = {
261 let mut hasher = IoWrapper(sha2::Sha512::new());
262 std::io::copy(
263 &mut std::fs::File::open(input).map_err(pack_err)?,
264 &mut hasher,
265 )
266 .map_err(pack_err)?;
267 hasher.0
268 };
269 let required = Required {
270 input: hasher.into(),
271 output: SignatureRequestOutput::new_openpgp_v4(),
272 };
273
274 let grease: String = rand::thread_rng()
278 .sample_iter(&rand::distributions::Alphanumeric)
279 .take(7)
280 .map(char::from)
281 .collect();
282
283 Ok(Self {
284 version: semver::Version::new(1, 0, 0),
285 required,
286 optional: vec![
287 (
288 grease,
289 Value::String(
290 "https://gitlab.archlinux.org/archlinux/signstar/-/merge_requests/43"
291 .to_string(),
292 ),
293 ),
294 (
295 "request-time".into(),
296 Value::Number(
297 SystemTime::now()
298 .duration_since(SystemTime::UNIX_EPOCH)
299 .map_err(|source| crate::Error::CurrentTimeBeforeReference {
300 reference_time: SystemTime::UNIX_EPOCH,
301 source,
302 })?
303 .as_secs()
304 .into(),
305 ),
306 ),
307 (
308 "file-name".into(),
309 input
310 .file_name()
311 .and_then(|s| s.to_str())
312 .map(Into::into)
313 .unwrap_or(Value::Null),
314 ),
315 ]
316 .into_iter()
317 .collect(),
318 })
319 }
320}
321
322#[derive(Debug, Deserialize, Serialize)]
330pub struct Response {
331 pub version: Version,
333
334 signature: String,
336}
337
338impl Response {
339 pub fn v1(signature: String) -> Self {
341 Self {
342 version: Version::new(1, 0, 0),
343 signature,
344 }
345 }
346
347 pub fn from_reader(reader: impl std::io::Read) -> Result<Self, Error> {
353 let resp: Self = serde_json::from_reader(reader)?;
354 Ok(resp)
355 }
356
357 pub fn to_writer(&self, writer: impl std::io::Write) -> Result<(), Error> {
363 serde_json::to_writer(writer, &self)?;
364 Ok(())
365 }
366
367 pub fn signature_to_writer(&self, mut writer: impl std::io::Write) -> Result<(), Error> {
373 writer
374 .write_all(self.signature.as_bytes())
375 .map_err(|source| Error::Io {
376 file: PathBuf::new(),
377 source,
378 })?;
379 Ok(())
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use std::{fs::File, path::PathBuf};
386
387 use rstest::rstest;
388 use sha2::Digest;
389 use testresult::TestResult;
390
391 use super::*;
392
393 #[test]
394 fn hash_values_are_predictable() -> testresult::TestResult {
395 let mut hasher = IoWrapper(sha2::Sha512::new());
396 let mut bytes = std::io::Cursor::new(b"this is sample text");
397 std::io::copy(&mut bytes, &mut hasher)?;
398 let result: &[u8] = &hasher.0.serialize();
399
400 let expected_state = [
401 8, 201, 188, 243, 103, 230, 9, 106, 59, 167, 202, 132, 133, 174, 103, 187, 43, 248,
402 148, 254, 114, 243, 110, 60, 241, 54, 29, 95, 58, 245, 79, 165, 209, 130, 230, 173,
403 127, 82, 14, 81, 31, 108, 62, 43, 140, 104, 5, 155, 107, 189, 65, 251, 171, 217, 131,
404 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,
405 64, 19, 116, 104, 105, 115, 32, 105, 115, 32, 115, 97, 109, 112, 108, 101, 32, 116,
406 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,
407 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,
408 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,
409 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,
410 ];
411
412 assert_eq!(result, expected_state);
413
414 let expected_digest = [
415 20, 253, 69, 133, 146, 76, 11, 4, 191, 13, 150, 196, 9, 97, 21, 35, 186, 95, 254, 59,
416 148, 60, 88, 155, 127, 203, 151, 216, 11, 16, 228, 73, 113, 23, 115, 110, 198, 42, 109,
417 92, 23, 33, 70, 71, 136, 219, 73, 238, 135, 13, 223, 117, 215, 69, 243, 33, 125, 109,
418 95, 121, 213, 44, 212, 166,
419 ];
420
421 let hasher = sha2::Sha512::deserialize(&expected_state.into())?;
422 let hash = &hasher.finalize()[..];
423 assert_eq!(hash, expected_digest);
424
425 Ok(())
430 }
431
432 #[test]
433 fn sample_request_is_ok() -> TestResult {
434 let reader = File::open("tests/sample-request.json")?;
435 let reader = Request::from_reader(reader)?;
436 let hasher: sha2::Sha512 = reader.required.input.try_into()?;
437 assert_eq!(
438 hasher.finalize(),
439 [
440 85, 185, 86, 249, 187, 64, 117, 47, 163, 40, 201, 53, 35, 169, 119, 90, 168, 78,
441 29, 32, 20, 55, 39, 121, 253, 203, 159, 82, 85, 40, 233, 26, 208, 13, 111, 61, 93,
442 100, 199, 31, 185, 140, 195, 114, 92, 118, 108, 237, 100, 152, 212, 177, 189, 56,
443 146, 204, 137, 76, 235, 31, 101, 1, 19, 55
444 ]
445 );
446 Ok(())
447 }
448
449 #[rstest]
450 fn sample_request_is_bad(#[files("tests/bad-*.json")] request_file: PathBuf) -> TestResult {
451 let reader = File::open(request_file)?;
452 assert!(
453 Request::from_reader(reader).is_err(),
454 "parsing of the request file should fail"
455 );
456 Ok(())
457 }
458}