voa_openpgp/import/
mod.rs1pub mod destructured;
4pub mod error;
5
6use std::{
7 fs::File,
8 io::Read,
9 path::{Path, PathBuf},
10 str::FromStr,
11};
12
13use error::Error;
14use log::debug;
15use pgp::{
16 composed::{ArmorOptions, Deserializable, SignedPublicKey},
17 types::KeyDetails,
18};
19use voa_core::{VerifierWriter, identifiers::Technology};
20
21#[derive(Clone, Debug)]
28pub struct OpenPgpImport(SignedPublicKey);
29
30impl OpenPgpImport {
31 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
42 let path = path.as_ref();
43 debug!("Reading an OpenPGP certificate from file: {path:?}");
44
45 if !path.is_file() {
46 return Err(error::Error::PathIsNotAFile {
47 path: path.to_path_buf(),
48 }
49 .into());
50 }
51
52 let file = File::open(path).map_err(|source| crate::Error::IoPath {
53 path: path.to_path_buf(),
54 context: "reading the file",
55 source,
56 })?;
57
58 let (pubkey, _) = SignedPublicKey::from_reader_single(file).map_err(|source| {
59 crate::Error::OpenPgpPath {
60 path: path.to_path_buf(),
61 context: "parsing an OpenPGP certificate from file",
62 source,
63 }
64 })?;
65
66 Ok(Self(pubkey))
67 }
68
69 pub fn from_reader(reader: impl Read) -> Result<Self, crate::Error> {
78 debug!("Reading an OpenPGP certificate from stdin");
79 let (pubkey, _) = SignedPublicKey::from_reader_single(reader).map_err(|source| {
80 crate::Error::OpenPgp {
81 context: "reading an OpenPGP certificate from stdin",
82 source,
83 }
84 })?;
85
86 Ok(Self(pubkey))
87 }
88}
89
90impl TryFrom<&Path> for OpenPgpImport {
91 type Error = crate::Error;
92
93 fn try_from(value: &Path) -> Result<Self, Self::Error> {
106 Self::from_file(value)
107 }
108}
109
110impl FromStr for OpenPgpImport {
111 type Err = crate::Error;
112
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
123 Self::try_from(Path::new(s))
124 }
125}
126
127impl VerifierWriter for OpenPgpImport {
128 fn to_bytes(&self) -> Result<Vec<u8>, voa_core::Error> {
132 self.0
133 .to_armored_bytes(ArmorOptions::default())
134 .map_err(|source| {
135 voa_core::Error::WriteError(Box::new(crate::Error::OpenPgp {
136 context: "creating an ASCII-armored OpenPGP public key",
137 source,
138 }))
139 })
140 }
141
142 fn file_name(&self) -> PathBuf {
149 let fingerprint = self.0.fingerprint();
150 let technology = self.technology();
151
152 PathBuf::from(format!("{fingerprint}.{technology}"))
153 }
154
155 fn technology(&self) -> Technology {
157 Technology::Openpgp
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use pgp::{
164 composed::{KeyType, SecretKeyParamsBuilder, SubkeyParamsBuilder},
165 ser::Serialize,
166 types::Password,
167 };
168 use rand::thread_rng;
169 use rstest::{fixture, rstest};
170 use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
171 use tempfile::{NamedTempFile, tempdir};
172 use testresult::TestResult;
173
174 use super::*;
175
176 struct TestConfig {
178 pub armor: bool,
180 }
181
182 fn init_logger() {
184 if TermLogger::init(
185 LevelFilter::Trace,
186 Config::default(),
187 TerminalMode::Stderr,
188 ColorChoice::Auto,
189 )
190 .is_err()
191 {
192 debug!("Not initializing another logger, as one is initialized already.");
193 }
194 }
195
196 #[fixture]
198 fn openpgp_cert() -> TestResult<SignedPublicKey> {
199 let mut signkey = SubkeyParamsBuilder::default();
200 signkey
201 .key_type(KeyType::Ed25519Legacy)
202 .can_sign(true)
203 .can_encrypt(false)
204 .can_authenticate(false);
205 let mut key_params = SecretKeyParamsBuilder::default();
206 key_params
207 .key_type(KeyType::Ed25519Legacy)
208 .can_certify(true)
209 .can_sign(false)
210 .can_encrypt(false)
211 .primary_user_id("John Doe <jdoe@example.org>".to_string())
212 .subkeys(vec![signkey.build()?]);
213
214 let secret_key_params = key_params.build()?;
215 let secret_key = secret_key_params.generate(thread_rng())?;
216
217 let signed = secret_key.sign(&mut thread_rng(), &Password::from(""))?;
219
220 let pubkey = SignedPublicKey::from(signed);
221 Ok(pubkey)
222 }
223
224 #[rstest]
230 #[case::single_binary(TestConfig{armor: false})]
231 #[case::single_ascii_armor(TestConfig{armor: true})]
232 fn write_to_hierarchy_succeeds(
233 openpgp_cert: TestResult<SignedPublicKey>,
234 #[case] config: TestConfig,
235 ) -> TestResult {
236 init_logger();
237
238 let input_pubkey = openpgp_cert?;
239 let mut temp_file = NamedTempFile::new()?;
240 if config.armor {
241 input_pubkey.to_armored_writer(&mut temp_file, ArmorOptions::default())?;
242 } else {
243 input_pubkey.to_writer(&mut temp_file)?;
244 }
245 let path = temp_file.path();
246
247 let import = OpenPgpImport::try_from(path)?;
248
249 let temp_dir = tempdir()?;
250 let output_dir = temp_dir.path();
251 import.write_to_hierarchy(
252 output_dir,
253 "os".parse()?,
254 "purpose".parse()?,
255 Some("context".parse()?),
256 )?;
257
258 let output_file = output_dir
259 .join("os")
260 .join("purpose")
261 .join("context")
262 .join(import.technology().to_string())
263 .join(import.file_name());
264
265 let (output_pubkey, _) = SignedPublicKey::from_armor_file(&output_file)?;
266
267 assert_eq!(input_pubkey, output_pubkey);
268
269 Ok(())
270 }
271
272 #[test]
274 #[cfg(target_os = "linux")]
275 fn try_from_path_fails_on_path_not_a_file() -> TestResult {
276 match OpenPgpImport::try_from(PathBuf::from("/dev/null").as_path()) {
277 Ok(verifier) => {
278 return Err(format!(
279 "Should have failed, but succeeded to create a verifier: {verifier:?}"
280 )
281 .into());
282 }
283 Err(error) => match error {
284 crate::Error::Import(error::Error::PathIsNotAFile { .. }) => {}
285 error => {
286 return Err(format!("Did not return the correct error, got: {error}").into());
287 }
288 },
289 }
290 Ok(())
291 }
292}