Commit
Author: Dirkjan Ochtman [dirkjan@ochtman.nl]
Hash: e559e8262f29249f3297ad217336d2e3ba274002
Timestamp: Fri, 09 Dec 2022 10:23:42 +0000 (2 years ago)

+109 -164 +/-5 browse
Use object-safe VerifyingKey trait instead of PublicKey enum
1diff --git a/src/common/crypto.rs b/src/common/crypto.rs
2index 2d4a4d1..b443c7d 100644
3--- a/src/common/crypto.rs
4+++ b/src/common/crypto.rs
5 @@ -4,7 +4,7 @@ use ed25519_dalek::Signer;
6 use rsa::{
7 pkcs1::DecodeRsaPrivateKey,
8 pkcs8::{AssociatedOid, ObjectIdentifier},
9- PaddingScheme, RsaPrivateKey,
10+ PaddingScheme, PublicKey as _, RsaPrivateKey,
11 };
12 use sha1::{digest::Output, Sha1};
13 use sha2::{digest::Digest, Sha256};
14 @@ -111,6 +111,78 @@ impl SigningKey for Ed25519Key {
15 }
16 }
17
18+ pub trait VerifyingKey {
19+ fn verify(&self, hash: &[u8], signature: &[u8], algorithm: Algorithm) -> Result<()>;
20+ }
21+
22+ pub(crate) enum VerifyingKeyType {
23+ Rsa,
24+ Ed25519,
25+ }
26+
27+ impl VerifyingKeyType {
28+ pub(crate) fn new(&self, bytes: &[u8]) -> Result<Box<dyn VerifyingKey>> {
29+ Ok(match self {
30+ Self::Rsa => {
31+ let inner =
32+ <rsa::RsaPublicKey as rsa::pkcs8::DecodePublicKey>::from_public_key_der(bytes)
33+ .or_else(|_| rsa::pkcs1::DecodeRsaPublicKey::from_pkcs1_der(bytes))
34+ .map_err(|err| Error::CryptoError(err.to_string()))?;
35+
36+ Box::new(RsaPublicKey { inner }) as Box<dyn VerifyingKey>
37+ }
38+ Self::Ed25519 => Box::new(Ed25519PublicKey {
39+ inner: ed25519_dalek::PublicKey::from_bytes(bytes)
40+ .map_err(|err| Error::CryptoError(err.to_string()))?,
41+ }),
42+ })
43+ }
44+ }
45+
46+ pub(crate) struct RsaPublicKey {
47+ inner: rsa::RsaPublicKey,
48+ }
49+
50+ impl VerifyingKey for RsaPublicKey {
51+ fn verify(&self, hash: &[u8], signature: &[u8], algorithm: Algorithm) -> Result<()> {
52+ match algorithm {
53+ Algorithm::RsaSha1 => self
54+ .inner
55+ .verify(PaddingScheme::new_pkcs1v15_sign::<Sha1>(), hash, signature)
56+ .map_err(|_| Error::FailedVerification),
57+ Algorithm::RsaSha256 => self
58+ .inner
59+ .verify(
60+ PaddingScheme::new_pkcs1v15_sign::<Sha256>(),
61+ hash,
62+ signature,
63+ )
64+ .map_err(|_| Error::FailedVerification),
65+ _ => Err(Error::IncompatibleAlgorithms),
66+ }
67+ }
68+ }
69+
70+ pub(crate) struct Ed25519PublicKey {
71+ inner: ed25519_dalek::PublicKey,
72+ }
73+
74+ impl VerifyingKey for Ed25519PublicKey {
75+ fn verify(&self, hash: &[u8], signature: &[u8], algorithm: Algorithm) -> Result<()> {
76+ if !matches!(algorithm, Algorithm::Ed25519Sha256) {
77+ return Err(Error::IncompatibleAlgorithms);
78+ }
79+
80+ self.inner
81+ .verify_strict(
82+ hash,
83+ &ed25519_dalek::Signature::from_bytes(signature)
84+ .map_err(|err| Error::CryptoError(err.to_string()))?,
85+ )
86+ .map_err(|_| Error::FailedVerification)
87+ }
88+ }
89+
90 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
91 #[repr(u64)]
92 pub enum HashAlgorithm {
93 diff --git a/src/common/verify.rs b/src/common/verify.rs
94index b88f716..25fdac5 100644
95--- a/src/common/verify.rs
96+++ b/src/common/verify.rs
97 @@ -8,15 +8,7 @@
98 * except according to those terms.
99 */
100
101- use rsa::PaddingScheme;
102- use sha1::Sha1;
103- use sha2::Sha256;
104-
105- use crate::{
106- common::crypto::Algorithm,
107- dkim::{DomainKey, PublicKey},
108- Error,
109- };
110+ use crate::{common::crypto::Algorithm, dkim::DomainKey};
111
112 pub(crate) trait VerifySignature {
113 fn s(&self) -> &str;
114 @@ -39,34 +31,6 @@ pub(crate) trait VerifySignature {
115 }
116
117 fn verify(&self, record: &DomainKey, hh: &[u8]) -> crate::Result<()> {
118- match (&self.a(), &record.p) {
119- (Algorithm::RsaSha256, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
120- public_key,
121- PaddingScheme::new_pkcs1v15_sign::<Sha256>(),
122- hh,
123- self.b(),
124- )
125- .map_err(|_| Error::FailedVerification),
126-
127- (Algorithm::RsaSha1, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
128- public_key,
129- PaddingScheme::new_pkcs1v15_sign::<Sha1>(),
130- hh,
131- self.b(),
132- )
133- .map_err(|_| Error::FailedVerification),
134-
135- (Algorithm::Ed25519Sha256, PublicKey::Ed25519(public_key)) => public_key
136- .verify_strict(
137- hh,
138- &ed25519_dalek::Signature::from_bytes(self.b())
139- .map_err(|err| Error::CryptoError(err.to_string()))?,
140- )
141- .map_err(|_| Error::FailedVerification),
142-
143- (_, PublicKey::Revoked) => Err(Error::RevokedPublicKey),
144-
145- (_, _) => Err(Error::IncompatibleAlgorithms),
146- }
147+ record.p.verify(hh, self.b(), self.a())
148 }
149 }
150 diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs
151index c1832f1..65374ed 100644
152--- a/src/dkim/mod.rs
153+++ b/src/dkim/mod.rs
154 @@ -10,12 +10,10 @@
155
156 use std::borrow::Cow;
157
158- use rsa::RsaPublicKey;
159-
160 use crate::{
161 arc::Set,
162 common::{
163- crypto::{Algorithm, HashAlgorithm},
164+ crypto::{Algorithm, HashAlgorithm, VerifyingKey},
165 verify::VerifySignature,
166 },
167 ArcOutput, DkimOutput, DkimResult, Error, Version,
168 @@ -66,10 +64,8 @@ impl Default for Canonicalization {
169 }
170 }
171
172- #[derive(Debug, PartialEq, Eq, Clone)]
173 pub struct DomainKey {
174- pub(crate) v: Version,
175- pub(crate) p: PublicKey,
176+ pub(crate) p: Box<dyn VerifyingKey>,
177 pub(crate) f: u64,
178 }
179
180 @@ -132,13 +128,6 @@ impl From<Service> for u64 {
181 }
182 }
183
184- #[derive(Debug, PartialEq, Eq, Clone)]
185- pub(crate) enum PublicKey {
186- Rsa(RsaPublicKey),
187- Ed25519(ed25519_dalek::PublicKey),
188- Revoked,
189- }
190-
191 impl From<Algorithm> for HashAlgorithm {
192 fn from(a: Algorithm) -> Self {
193 match a {
194 diff --git a/src/dkim/parse.rs b/src/dkim/parse.rs
195index ee13d43..56a5634 100644
196--- a/src/dkim/parse.rs
197+++ b/src/dkim/parse.rs
198 @@ -11,17 +11,16 @@
199 use std::slice::Iter;
200
201 use mail_parser::decoders::base64::base64_decode_stream;
202- use rsa::RsaPublicKey;
203
204 use crate::{
205- common::parse::*,
206+ common::{crypto::VerifyingKeyType, parse::*},
207 dkim::{RR_EXPIRATION, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION},
208 Error,
209 };
210
211 use super::{
212- Algorithm, Atps, Canonicalization, DomainKey, DomainKeyReport, Flag, HashAlgorithm, PublicKey,
213- Service, Signature, Version, RR_DNS, RR_OTHER, RR_POLICY,
214+ Algorithm, Atps, Canonicalization, DomainKey, DomainKeyReport, Flag, HashAlgorithm, Service,
215+ Signature, Version, RR_DNS, RR_OTHER, RR_POLICY,
216 };
217
218 const ATPSH: u64 = (b'a' as u64)
219 @@ -241,24 +240,14 @@ impl SignatureParser for Iter<'_, u8> {
220 }
221 }
222
223- enum KeyType {
224- Rsa,
225- Ed25519,
226- None,
227- }
228-
229 impl TxtRecordParser for DomainKey {
230 #[allow(clippy::while_let_on_iterator)]
231 fn parse(header: &[u8]) -> crate::Result<Self> {
232 let header_len = header.len();
233 let mut header = header.iter();
234- let mut record = DomainKey {
235- v: Version::V1,
236- p: PublicKey::Revoked,
237- f: 0,
238- };
239- let mut k = KeyType::None;
240- let mut public_key = Vec::new();
241+ let mut flags = 0;
242+ let mut key_type = VerifyingKeyType::Rsa;
243+ let mut public_key = None;
244
245 while let Some(key) = header.key() {
246 match key {
247 @@ -267,26 +256,27 @@ impl TxtRecordParser for DomainKey {
248 return Err(Error::InvalidRecordType);
249 }
250 }
251- H => record.f |= header.flags::<HashAlgorithm>(),
252+ H => flags |= header.flags::<HashAlgorithm>(),
253 P => {
254- public_key =
255- base64_decode_stream(&mut header, header_len, b';').unwrap_or_default()
256+ if let Some(bytes) = base64_decode_stream(&mut header, header_len, b';') {
257+ public_key = Some(bytes);
258+ }
259 }
260- S => record.f |= header.flags::<Service>(),
261- T => record.f |= header.flags::<Flag>(),
262+ S => flags |= header.flags::<Service>(),
263+ T => flags |= header.flags::<Flag>(),
264 K => {
265 if let Some(ch) = header.next_skip_whitespaces() {
266 match ch {
267 b'r' | b'R' => {
268 if header.match_bytes(b"sa") && header.seek_tag_end() {
269- k = KeyType::Rsa;
270+ key_type = VerifyingKeyType::Rsa;
271 } else {
272 return Err(Error::UnsupportedKeyType);
273 }
274 }
275 b'e' | b'E' => {
276 if header.match_bytes(b"d25519") && header.seek_tag_end() {
277- k = KeyType::Ed25519;
278+ key_type = VerifyingKeyType::Ed25519;
279 } else {
280 return Err(Error::UnsupportedKeyType);
281 }
282 @@ -304,21 +294,13 @@ impl TxtRecordParser for DomainKey {
283 }
284 }
285
286- if !public_key.is_empty() {
287- record.p = match k {
288- KeyType::Rsa | KeyType::None => PublicKey::Rsa(
289- <RsaPublicKey as rsa::pkcs8::DecodePublicKey>::from_public_key_der(&public_key)
290- .or_else(|_| rsa::pkcs1::DecodeRsaPublicKey::from_pkcs1_der(&public_key))
291- .map_err(|err| Error::CryptoError(err.to_string()))?,
292- ),
293- KeyType::Ed25519 => PublicKey::Ed25519(
294- ed25519_dalek::PublicKey::from_bytes(&public_key)
295- .map_err(|err| Error::CryptoError(err.to_string()))?,
296- ),
297- }
298+ match public_key {
299+ Some(public_key) => Ok(DomainKey {
300+ p: key_type.new(&public_key)?,
301+ f: flags,
302+ }),
303+ _ => Err(Error::InvalidRecordType),
304 }
305-
306- Ok(record)
307 }
308 }
309
310 @@ -476,7 +458,6 @@ impl ItemParser for Service {
311 #[cfg(test)]
312 mod test {
313 use mail_parser::decoders::base64::base64_decode;
314- use rsa::{pkcs8::DecodePublicKey, RsaPublicKey};
315
316 use crate::{
317 common::{
318 @@ -484,8 +465,8 @@ mod test {
319 parse::TxtRecordParser,
320 },
321 dkim::{
322- Canonicalization, DomainKey, DomainKeyReport, PublicKey, Signature, Version, RR_DNS,
323- RR_EXPIRATION, RR_OTHER, RR_POLICY, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION,
324+ Canonicalization, DomainKey, DomainKeyReport, Signature, RR_DNS, RR_EXPIRATION,
325+ RR_OTHER, RR_POLICY, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION,
326 R_FLAG_MATCH_DOMAIN, R_FLAG_TESTING, R_SVC_ALL, R_SVC_EMAIL,
327 },
328 };
329 @@ -651,26 +632,7 @@ mod test {
330 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
331 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
332 ),
333- DomainKey {
334- v: Version::V1,
335- p: PublicKey::Rsa(
336- RsaPublicKey::from_public_key_der(
337- &base64_decode(
338- concat!(
339- "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ",
340- "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt",
341- "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v",
342- "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
343- "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
344- )
345- .as_bytes(),
346- )
347- .unwrap(),
348- )
349- .unwrap(),
350- ),
351- f: 0,
352- },
353+ 0,
354 ),
355 (
356 concat!(
357 @@ -685,35 +647,12 @@ mod test {
358 "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;",
359 "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes "
360 ),
361- DomainKey {
362- v: Version::V1,
363- p: PublicKey::Rsa(
364- RsaPublicKey::from_public_key_der(
365- &base64_decode(
366- concat!(
367- "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvz",
368- "wKQIIWzQXv0nihasFTT3+JO23hXCge+ESWNxCJdVLxKL5e",
369- "dxrumEU3DnrPeGD6q6E/vjoXwBabpm8F5o96MEPm7v12O5",
370- "IIK7wx7gIJiQWvexwh+GJvW4aFFa0g13Ai75UdZjGFNKHA",
371- "EGeLmkQYybK/EHW5ymRlSg3g8zydJGEcI/melLCiBoShHjf",
372- "ZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W7qnq6t9puZYXMWj",
373- "6tEaf6ORWgb7DOXZSTJJjAJPBWa2+UrxXX6Ro7L7Xy1zzeY",
374- "FCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9p5wMedWasaPS74",
375- "TZ1b7tI39ncp6QIDAQAB",
376- )
377- .as_bytes(),
378- )
379- .unwrap(),
380- )
381- .unwrap(),
382- ),
383- f: R_HASH_SHA1
384- | R_HASH_SHA256
385- | R_SVC_ALL
386- | R_SVC_EMAIL
387- | R_FLAG_MATCH_DOMAIN
388- | R_FLAG_TESTING,
389- },
390+ R_HASH_SHA1
391+ | R_HASH_SHA256
392+ | R_SVC_ALL
393+ | R_SVC_EMAIL
394+ | R_FLAG_MATCH_DOMAIN
395+ | R_FLAG_TESTING,
396 ),
397 (
398 concat!(
399 @@ -722,29 +661,11 @@ mod test {
400 "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
401 "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
402 ),
403- DomainKey {
404- v: Version::V1,
405- p: PublicKey::Rsa(
406- RsaPublicKey::from_public_key_der(
407- &base64_decode(
408- concat!(
409- "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS",
410- "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT",
411- "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
412- "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
413- )
414- .as_bytes(),
415- )
416- .unwrap(),
417- )
418- .unwrap(),
419- ),
420- f: 0,
421- },
422+ 0,
423 ),
424 ] {
425 assert_eq!(
426- DomainKey::parse(record.as_bytes()).unwrap(),
427+ DomainKey::parse(record.as_bytes()).unwrap().f,
428 expected_result
429 );
430 }
431 diff --git a/src/lib.rs b/src/lib.rs
432index 80f7638..050b94e 100644
433--- a/src/lib.rs
434+++ b/src/lib.rs
435 @@ -275,7 +275,6 @@ pub mod dmarc;
436 pub mod report;
437 pub mod spf;
438
439- #[derive(Debug)]
440 pub struct Resolver {
441 pub(crate) resolver: TokioAsyncResolver,
442 pub(crate) cache_txt: LruCache<String, Txt>,
443 @@ -287,7 +286,7 @@ pub struct Resolver {
444 pub(crate) verify_policy: Policy,
445 }
446
447- #[derive(Debug, Clone, PartialEq, Eq)]
448+ #[derive(Clone)]
449 pub(crate) enum Txt {
450 Spf(Arc<Spf>),
451 SpfMacro(Arc<Macro>),