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::{EncryptionCaps, KeyType, SecretKeyParamsBuilder, SubkeyParamsBuilder},
165 ser::Serialize,
166 };
167 use rand::thread_rng;
168 use rstest::{fixture, rstest};
169 use simplelog::{ColorChoice, Config, LevelFilter, TermLogger, TerminalMode};
170 use tempfile::{NamedTempFile, tempdir};
171 use testresult::TestResult;
172
173 use super::*;
174
175 struct TestConfig {
177 pub armor: bool,
179 }
180
181 fn init_logger() {
183 if TermLogger::init(
184 LevelFilter::Trace,
185 Config::default(),
186 TerminalMode::Stderr,
187 ColorChoice::Auto,
188 )
189 .is_err()
190 {
191 debug!("Not initializing another logger, as one is initialized already.");
192 }
193 }
194
195 #[fixture]
197 fn openpgp_cert() -> TestResult<SignedPublicKey> {
198 let mut signkey = SubkeyParamsBuilder::default();
199 signkey
200 .key_type(KeyType::Ed25519Legacy)
201 .can_sign(true)
202 .can_encrypt(EncryptionCaps::None)
203 .can_authenticate(false);
204 let mut key_params = SecretKeyParamsBuilder::default();
205 key_params
206 .key_type(KeyType::Ed25519Legacy)
207 .can_certify(true)
208 .can_sign(false)
209 .can_encrypt(EncryptionCaps::None)
210 .primary_user_id("John Doe <jdoe@example.org>".to_string())
211 .subkeys(vec![signkey.build()?]);
212
213 let secret_key_params = key_params.build()?;
214 let secret_key = secret_key_params.generate(thread_rng())?;
215 let pubkey = SignedPublicKey::from(secret_key);
216 Ok(pubkey)
217 }
218
219 #[rstest]
225 #[case::single_binary(TestConfig{armor: false})]
226 #[case::single_ascii_armor(TestConfig{armor: true})]
227 fn write_to_hierarchy_succeeds(
228 openpgp_cert: TestResult<SignedPublicKey>,
229 #[case] config: TestConfig,
230 ) -> TestResult {
231 init_logger();
232
233 let input_pubkey = openpgp_cert?;
234 let mut temp_file = NamedTempFile::new()?;
235 if config.armor {
236 input_pubkey.to_armored_writer(&mut temp_file, ArmorOptions::default())?;
237 } else {
238 input_pubkey.to_writer(&mut temp_file)?;
239 }
240 let path = temp_file.path();
241
242 let import = OpenPgpImport::try_from(path)?;
243
244 let temp_dir = tempdir()?;
245 let output_dir = temp_dir.path();
246 import.write_to_hierarchy(
247 output_dir,
248 "os".parse()?,
249 "purpose".parse()?,
250 Some("context".parse()?),
251 )?;
252
253 let output_file = output_dir
254 .join("os")
255 .join("purpose")
256 .join("context")
257 .join(import.technology().to_string())
258 .join(import.file_name());
259
260 let (output_pubkey, _) = SignedPublicKey::from_armor_file(&output_file)?;
261
262 assert_eq!(input_pubkey, output_pubkey);
263
264 Ok(())
265 }
266
267 #[test]
269 #[cfg(target_os = "linux")]
270 fn try_from_path_fails_on_path_not_a_file() -> TestResult {
271 match OpenPgpImport::try_from(PathBuf::from("/dev/null").as_path()) {
272 Ok(verifier) => {
273 panic!("Should have failed, but succeeded to create a verifier: {verifier:?}");
274 }
275 Err(error) => match error {
276 crate::Error::Import(error::Error::PathIsNotAFile { .. }) => {}
277 error => {
278 panic!("Did not return the correct error, got: {error}");
279 }
280 },
281 }
282 Ok(())
283 }
284}