1use std::io::Read;
5use std::net::SocketAddr;
6use std::path::PathBuf;
7pub mod dns;
8mod key;
9use chrono::DateTime;
10use chrono::Utc;
11use clap::Parser;
12use openpgp_cert_d::CertD;
13use openpgp_cert_d::MergeResult;
14use sequoia_cert_store::store::StoreError;
15use sequoia_cert_store::CertStore;
16use sequoia_openpgp::cert::prelude::*;
17use sequoia_openpgp::crypto::mpi::PublicKey as SequoiaPublicKey;
18use sequoia_openpgp::crypto::Signer;
19use sequoia_openpgp::packet::signature::subpacket::NotationDataFlags;
20use sequoia_openpgp::packet::signature::SignatureBuilder;
21use sequoia_openpgp::packet::UserID;
22use sequoia_openpgp::parse::Parse;
23use sequoia_openpgp::policy::StandardPolicy;
24use sequoia_openpgp::serialize::SerializeInto;
25use sequoia_openpgp::types::Curve;
26use sequoia_openpgp::types::RevocationStatus;
27use sequoia_wot::store::CertStore as WotCertStore;
28use sequoia_wot::{
29 Network as WotNetwork, QueryBuilder as WotQueryBuilder, Roots as WotRoots, FULLY_TRUSTED,
30};
31
32#[derive(Debug, Parser)]
33pub struct Authenticate {
34 pub host: String,
36
37 #[clap(short, long)]
40 pub time: Option<DateTime<Utc>>,
41
42 #[clap(short, long, env = "PGP_CERT_D")]
44 pub cert_store: Option<PathBuf>,
45
46 #[clap(long, default_value = "8.8.8.8:53")]
48 pub nameserver: SocketAddr,
49
50 #[clap(long)]
52 pub verify_dns_proof: bool,
53
54 #[clap(long)]
56 pub verify_wot: bool,
57
58 #[clap(long)]
60 pub verbose: bool,
61
62 #[clap(long)]
64 pub store_verifications: bool,
65}
66
67impl Authenticate {
68 fn verification_string(&self) -> String {
69 let mut verifications = vec![];
70 if self.verify_dns_proof {
71 verifications.push("dns");
72 }
73 if self.verify_wot {
74 verifications.push("wot");
75 }
76 verifications.join(" ")
77 }
78}
79
80impl Default for Authenticate {
81 fn default() -> Self {
82 Self {
83 host: "example.com".into(),
84 time: None,
85 cert_store: None,
86 nameserver: "8.8.8.8:53".parse().expect("static value to be parseable"),
87 verify_dns_proof: false,
88 verify_wot: false,
89 verbose: false,
90 store_verifications: false,
91 }
92 }
93}
94
95#[derive(Debug, Parser)]
96pub enum Commands {
97 Authenticate(Authenticate),
99}
100
101#[derive(Debug, thiserror::Error)]
102pub enum Error {
103 #[error("OpenPGP error: {0}")]
104 OpenPGP(#[from] anyhow::Error),
105
106 #[error("I/O error: {0}")]
107 IO(#[from] std::io::Error),
108
109 #[error("Network error: {0}")]
110 Network(#[from] reqwest::Error),
111
112 #[error("SSH error: {0}")]
113 Ssh(#[from] ssh_key::Error),
114
115 #[error("Cert store error: {0}")]
116 Store(#[from] openpgp_cert_d::Error),
117
118 #[error("DNS error: {0}")]
119 DnsProto(#[from] hickory_client::proto::error::ProtoError),
120
121 #[error("DNS error: {0}")]
122 DnsClient(#[from] hickory_client::error::ClientError),
123
124 #[error("Cert store error: {0}")]
125 CertStore(#[from] StoreError),
126
127 #[error("Unknown curve: {0} in subkey: {1}")]
128 UnknownCurve(Curve, sequoia_openpgp::Fingerprint),
129
130 #[error("Unknown algorithm: {0:?} in subkey: {1}")]
131 UnknownAlgorithm(SequoiaPublicKey, sequoia_openpgp::Fingerprint),
132
133 #[error("Other error: {0}")]
134 Other(Box<dyn std::error::Error>),
135}
136
137fn is_cert_expired(cert: ValidCert<'_>) -> bool {
138 if let Some(expiry) = cert.primary_key().key_expiration_time() {
139 cert.time() > expiry
140 } else {
141 false
143 }
144}
145
146pub trait Effects {
147 fn get(&mut self, url: String) -> Result<Vec<u8>, Error>;
148 fn dns_query(
149 &mut self,
150 nameserver: SocketAddr,
151 name: &str,
152 ) -> Result<Vec<String>, crate::Error>;
153}
154
155pub fn authenticate(
156 auth: Authenticate,
157 effects: &mut impl Effects,
158 writer: &mut impl std::io::Write,
159) -> Result<(), Error> {
160 let p = StandardPolicy::new();
161 let store = if let Some(cert_store) = &auth.cert_store {
162 CertD::with_base_dir(cert_store)?
163 } else {
164 CertD::new()?
165 };
166 let trust_root = if let Ok(Some((_, bytes))) = store.get("trust-root") {
167 let trust_root_fpr = Cert::from_bytes(&bytes)?.fingerprint();
168 if auth.verbose {
169 writeln!(writer, "# Found trust root: {}", trust_root_fpr)?;
170 }
171 Some(trust_root_fpr)
172 } else {
173 None
174 };
175 let email = format!("ssh-openpgp-auth@{}", auth.host);
176 let mut certs = vec![];
177 for file in store.iter_files() {
178 for cert in file.into_iter().flat_map(|(_, mut f)| {
179 let mut v = Vec::new();
180 f.read_to_end(&mut v)?;
181 Cert::from_bytes(&v)
182 }) {
183 if let Ok(valid_cert) = cert.with_policy(&p, auth.time.map(Into::into)) {
184 if valid_cert
185 .userids()
186 .any(|ui| ui.email2().unwrap_or_default() == Some(&email))
187 && !is_cert_expired(valid_cert)
188 {
189 if auth.verbose {
190 writeln!(writer, "# Found local cert: {}", cert.fingerprint())?;
191 }
192 certs.push(cert);
193 }
194 }
195 }
196 }
197
198 if certs.is_empty() {
199 let f = |new: Cert, old: Option<&[u8]>| {
200 if let Some(old) = old {
201 let old = Cert::from_bytes(&old)?;
202 let merged = old.merge_public(new)?.to_vec()?;
203
204 Ok(MergeResult::Data(merged))
205 } else {
206 Ok(MergeResult::Data(new.to_vec()?))
207 }
208 };
209
210 let bytes = effects.get(format!(
211 "https://{}/.well-known/openpgpkey/hu/w1bjhwjfd8nqsw4ug3kn81sny45zimkq?l=ssh-openpgp-auth",
212 auth.host
213 ))?;
214
215 let parser = CertParser::from_bytes(&*bytes)?;
216
217 for cert in parser.flatten() {
218 let fingerprint = &cert.fingerprint().to_string();
219 if auth.verbose {
220 writeln!(writer, "# Downloaded certificate: {}", fingerprint)?;
221 }
222 store.insert(fingerprint, cert.clone(), false, f)?;
223 certs.push(cert);
224 }
225 }
226
227 if auth.verify_dns_proof {
228 let txts = effects.dns_query(auth.nameserver, &auth.host)?;
229
230 let certs_and_validations = certs.into_iter().map(|cert| {
231 (
232 txts.contains(&format!("openpgp4fpr:{:x}", cert.fingerprint())),
233 cert,
234 )
235 });
236
237 let mut ok_certs = vec![];
238
239 for (cert_in_dns, cert) in certs_and_validations {
240 if auth.verbose {
241 writeln!(
242 writer,
243 "# Certificate {}{} found in the DNS zone",
244 cert.fingerprint(),
245 if cert_in_dns { "" } else { " NOT" }
246 )?;
247 }
248 if cert_in_dns {
249 ok_certs.push(cert);
250 }
251 }
252
253 certs = ok_certs;
254 }
255
256 if auth.verify_wot {
257 let mut store = CertStore::empty();
258 if let Some(cert_store) = &auth.cert_store {
259 if auth.verbose {
260 writeln!(writer, "# Using cert store: {}", cert_store.display())?;
261 }
262 store.add_certd(cert_store)?;
263 } else {
264 if auth.verbose {
265 writeln!(writer, "# Using default cert store")?;
266 }
267 store.add_default_certd()?;
268 };
269 let store = WotCertStore::from_store(&store, &p, auth.time.map(Into::into));
270 let network = WotNetwork::new(&store)?;
271 let mut builder = WotQueryBuilder::new(&network);
272 if let Some(fingerprint) = trust_root {
273 builder.roots(WotRoots::new([fingerprint]));
274 }
275 let query = builder.build();
276 let user_id = UserID::from(format!("<ssh-openpgp-auth@{}>", auth.host));
277 let valid_certs = certs.into_iter().map(|cert| {
278 let paths = query.authenticate(&user_id, cert.fingerprint(), FULLY_TRUSTED);
279 (cert, paths.amount() > 0)
280 });
281 let mut new_certs = vec![];
282 for (cert, valid) in valid_certs {
283 if auth.verbose {
284 writeln!(
285 writer,
286 "# Web of Trust verification of {} {}",
287 cert.fingerprint(),
288 if valid { "succeeded" } else { "failed" }
289 )?;
290 }
291 if valid {
292 new_certs.push(cert);
293 }
294 }
295 certs = new_certs;
296 }
297
298 let mut persistence_cert: Option<Box<dyn Signer>> = if auth.store_verifications {
299 Some(
300 if let Some((_, bytes)) = store.get("_metacode_ssh_openpgp_auth_persistence.pgp")? {
301 Cert::from_bytes(&bytes)
302 } else {
303 Ok(create_new_certifying_key(&store)?)
304 }
305 .and_then(|cert| {
306 Ok(Box::new(
307 cert.primary_key()
308 .key()
309 .clone()
310 .parts_into_secret()?
311 .into_keypair()?,
312 ) as Box<dyn Signer>)
313 })?,
314 )
315 } else {
316 None
317 };
318
319 for cert in certs {
320 let cert = cert.with_policy(&p, auth.time.map(Into::into))?;
321 if cert.alive().is_err() {
322 if auth.verbose {
323 writeln!(
324 writer,
325 "# Certificate {} is already expired at {}",
326 cert.fingerprint(),
327 auth.time.unwrap_or_default()
328 )?;
329 }
330 } else if cert.revocation_status() != RevocationStatus::NotAsFarAsWeKnow {
331 if auth.verbose {
332 writeln!(
333 writer,
334 "# Certificate {} is skipped because it is revoked",
335 cert.fingerprint(),
336 )?;
337 }
338 } else {
339 for key in cert.keys().for_authentication() {
340 match key.revocation_status() {
341 RevocationStatus::NotAsFarAsWeKnow => {
342 if auth.verbose {
343 writeln!(
344 writer,
345 "# Certificate {}, exporting subkey {}",
346 cert.fingerprint(),
347 key.fingerprint()
348 )?;
349 }
350 match key::PublicKey::try_from(key.key()) {
351 Ok(key) => writeln!(writer, "{} {}", auth.host, key)?,
352 Err(error) => writeln!(writer, "# {}", error)?,
353 }
354 }
355 RevocationStatus::Revoked(_revocations)
356 | RevocationStatus::CouldBe(_revocations) => {
357 if auth.verbose {
358 writeln!(
359 writer,
360 "# Certificate {}, skipping subkey {} as it is revoked",
361 cert.fingerprint(),
362 key.fingerprint()
363 )?;
364 }
365 }
366 }
367 }
368 }
369
370 if let Some(ref mut persistence_cert) = persistence_cert {
371 let signature =
372 SignatureBuilder::new(sequoia_openpgp::types::SignatureType::PositiveCertification)
373 .add_notation(
374 "ssh-openpgp-auth-verification@metacode.biz",
375 auth.verification_string(),
376 NotationDataFlags::empty().set_human_readable(),
377 false,
378 )?
379 .set_exportable_certification(false)?
380 .sign_userid_binding(
381 persistence_cert,
382 cert.primary_key().key(),
383 &UserID::from(format!("<ssh-openpgp-auth@{}>", auth.host)),
384 )?;
385
386 let new_cert = cert.cert().clone().insert_packets2(signature)?.0;
387 store.insert(
388 &cert.fingerprint().to_string(),
389 new_cert.clone(),
390 false,
391 |new, old| {
392 Ok(match old {
393 Some(old) => {
394 let old = Cert::from_bytes(&old)?;
395 MergeResult::Data(old.merge_public(new)?.as_tsk().to_vec()?)
396 }
397 None => MergeResult::Data(new.as_tsk().to_vec()?),
398 })
399 },
400 )?;
401 }
402 }
403 Ok(())
404}
405
406fn create_new_certifying_key(store: &CertD) -> sequoia_openpgp::Result<Cert> {
412 let cert = CertBuilder::new().generate()?.0;
413 let cert_data = store.insert_special(
414 "_metacode_ssh_openpgp_auth_persistence.pgp",
415 cert.clone(),
416 true,
417 |new, old| {
421 Ok(match old {
422 Some(old) => MergeResult::Data(old.into()),
423 None => MergeResult::Data(new.as_tsk().to_vec()?),
424 })
425 },
426 )?;
427 if let Some(cert_data) = cert_data.1 {
428 Cert::from_bytes(&cert_data)
429 } else {
430 Ok(cert)
431 }
432}