Commit
Author: Mauro D [mauro@stalw.art]
Hash: e92d195f5fa943b17a3faf1670e89d7f120a253b
Timestamp: Wed, 09 Nov 2022 18:32:45 +0000 (2 years ago)

+1601 -614 +/-22 browse
ARC verify implementation.
1diff --git a/resources/dkim/001.txt b/resources/dkim/001.txt
2index 08dec3d..da9fb24 100644
3--- a/resources/dkim/001.txt
4+++ b/resources/dkim/001.txt
5 @@ -1,5 +1,5 @@
6- brisbane v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
7- test v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB
8+ brisbane._domainkey.football.example.com v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
9+ test._domainkey.football.example.com v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkHlOQoBTzWRiGs5V6NpP3idY6Wk08a5qhdR6wy5bdOKb2jLQiY/J16JYi0Qvx/byYzCNb3W91y3FutACDfzwQ/BC/e/8uBsCR+yz1Lxj+PL6lHvqMKrM3rG4hstT5QjvHO9PzoxZyVYLzBfO2EeC3Ip3G+2kryOTIKT+l/K4w3QIDAQAB
10
11 DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
12 d=football.example.com; i=@football.example.com;
13 diff --git a/resources/dkim/002.txt b/resources/dkim/002.txt
14index c73dd5a..6766d8a 100644
15--- a/resources/dkim/002.txt
16+++ b/resources/dkim/002.txt
17 @@ -1,4 +1,4 @@
18- newengland v=DKIM1; p=MIGJAoGBALVI635dLK4cJJAH3Lx6upo3X/Lm1tQz3mezcWTA3BUBnyIsdnRf57aD5BtNmhPrYYDlWlzw3UgnKisIxktkk5+iMQMlFtAS10JB8L3YadXNJY+JBcbeSi5TgJe4WFzNgW95FWDAuSTRXSWZfA/8xjflbTLDx0euFZOM7C4T0GwLAgMBAAE=
19+ newengland._domainkey.example.com v=DKIM1; p=MIGJAoGBALVI635dLK4cJJAH3Lx6upo3X/Lm1tQz3mezcWTA3BUBnyIsdnRf57aD5BtNmhPrYYDlWlzw3UgnKisIxktkk5+iMQMlFtAS10JB8L3YadXNJY+JBcbeSi5TgJe4WFzNgW95FWDAuSTRXSWZfA/8xjflbTLDx0euFZOM7C4T0GwLAgMBAAE=
20
21 DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
22 c=simple/simple; d=example.com;
23 diff --git a/resources/dkim/003.txt b/resources/dkim/003.txt
24index 429b64f..9e87ac0 100644
25--- a/resources/dkim/003.txt
26+++ b/resources/dkim/003.txt
27 @@ -1,4 +1,4 @@
28- ietf1 k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNzNnjKTd5cczd2CDzHflCZuv1tMWYwd7zE+deoJ6s/fXR7/n9ZIBnDS5egt7HAHjNjZrmjcoRlfSsNxRJvUQFyYvaU1BT1s8R+mkPgSOqZ4t9HqAVjiczn2B9+dbjdNN+S/zvSyMMuSCSJDKKAXhBpDeQTpeY7/UdP9s6ws0yjQIDAQAB
29+ ietf1._domainkey.ietf.org k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNzNnjKTd5cczd2CDzHflCZuv1tMWYwd7zE+deoJ6s/fXR7/n9ZIBnDS5egt7HAHjNjZrmjcoRlfSsNxRJvUQFyYvaU1BT1s8R+mkPgSOqZ4t9HqAVjiczn2B9+dbjdNN+S/zvSyMMuSCSJDKKAXhBpDeQTpeY7/UdP9s6ws0yjQIDAQAB
30
31 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ietf.org; s=ietf1;
32 t=1667592145; bh=M3BM66+ux2IbqyOhw6XrN0rYwgjbrSbsG7H+29IL9UQ=;
33 diff --git a/resources/dkim/004.txt b/resources/dkim/004.txt
34new file mode 100644
35index 0000000..55ed848
36--- /dev/null
37+++ b/resources/dkim/004.txt
38 @@ -0,0 +1,97 @@
39+ s1024-2013-q3._domainkey.facebookmail.com k=rsa; t=s; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAUemE56fSDRo+H9Cu8u0uEIOXKON0YbB5A10wuWvNc7bUFIjL0tiUIzhktZjhAXc5CWw2/TZnTZaLZtmtJ2MRfd2e+ty7LylkRAiZUWaT3dcDGVVibWn27DIz3+oCnbL7CFiLzxCZnxHx8B7BC/UM7UCCJMrAgaJWJR6tYwz0MwIDAQAB
40+
41+ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=facebookmail.com;
42+ s=s1024-2013-q3; t=1667862801;
43+ bh=WD7cPh9RpkUGmkO18mzurJGvkR3KhuxeMfs8TP7zhXo=;
44+ h=Date:To:Subject:From:MIME-Version:Content-Type;
45+ b=gKG3clziu1BwjHb4J5MWWs9j40JK+uZ9zAZlXyTykDLBxK8+Mh0oN96q8LiL11I9a
46+ zhcUt/wP8Svjg2aaRnFfl1IQvrnmAMfsNQcRRRaiAOadBEVUfPwfFokUKjO5Q8/MmH
47+ JiK9cNz78PAB4X3MR/biV4phSSlRymGUkzmmK9ns=
48+ X-Facebook: from 2401:db00:116c:4513:face:0:1c0:0 ([MTI3LjAuMC4x])
49+ by www.facebook.com with HTTPS (ZuckMail);
50+ Date: Mon, 7 Nov 2022 15:13:21 -0800
51+ To: Mauro DG <mauro@minter.ltd>
52+ Subject: The new Pages experience is replacing classic Pages
53+ X-Priority: 3
54+ X-Mailer: ZuckMail [version 1.00]
55+ From: "Facebook" <notification@facebookmail.com>
56+ Reply-to: noreply <noreply@facebookmail.com>
57+ Errors-To: notification@facebookmail.com
58+ X-Facebook-Notify: aymt_profile_plus_preemptive_transition_tip_notif:aymt_profile_plus_rollback_removal_email_tip; mailid=U1U5ece94830d9aeG29d41ac9G5ece991c6dc80G2dcf
59+ List-Unsubscribe: <https://facebook.com/aymt/unsubscribe/?c=510530525742901&n=1667862800817280&t=4503113996480200>
60+ Feedback-ID: 30:aymt_profile_plus_preemptive_transition_tip_notif:Facebook
61+ X-FACEBOOK-PRIORITY: 0
62+ X-Auto-Response-Suppress: All
63+ MIME-Version: 1.0
64+ Content-Transfer-Encoding: quoted-printable
65+ Content-Type: text/html; charset="UTF-8"
66+ Message-ID: <c578a5be-5ef1-11ed-8284-2b57b60ab540@facebookmail.com>
67+
68+ <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html; =
69+ charset=3DUTF-8" /><meta name=3D"viewport" =
70+ content=3D"width=3Ddevice-width, initial-scale=3D1.0" /><title>Facebook =
71+ for Business</title></head><body><table style=3D"background-color: =
72+ #ffffff" border=3D"0" cellpadding=3D"0" cellspacing=3D"0" =
73+ width=3D"100%"><tr><td align=3D"center"><!--[if (gte mso 9)|(IE)]>
74+ <table
75+ align=3D"center"
76+ border=3D"0"
77+ cellspacing=3D"0"
78+ cellpadding=3D"0"
79+ width=3D"720">
80+ <tr>
81+ <td align=3D"center" valign=3D"top" width=3D"720">
82+ <![endif]--><table align=3D"left" border=3D"0" =
83+ cellpadding=3D"0" cellspacing=3D"0" style=3D"max-width: 720px;" =
84+ width=3D"100%"><tr><td><div style=3D"display:none; font-size:1px; =
85+ color:#333333; line-height:1px; max-height:0px; max-width:0px; opacity:0; =
86+ overflow:hidden;"></div></td></tr><tr><td align=3D"left" =
87+ valign=3D"top"><table bgcolor=3D"#ffffff" style=3D"padding: 5px 0px; =
88+ width: 100%;"><tr><td><table><tr><td><img width=3D"32" height=3D"32" =
89+ src=3D"https://static.xx.fbcdn.net/rsrc.php/v3/yc/r/I92GqZOkKcu.png" =
90+ style=3D"border:0;max-width:100%;" alt=3D"Header" title=3D"Image" =
91+ /></td><td width=3D"2px"></td><td style=3D"font: 22px Helvetica, =
92+ Arial;"><span class=3D"mb_text" style=3D"font-family:Helvetica =
93+ Neue,Helvetica,Lucida Grande,tahoma,verdana,arial,sans-serif;font-size:16p=
94+ x;line-height:21px;font-weight:bold;color:#141823;">Switching back to =
95+ classic Pages will no longer be available for your =
96+ Page(s).</span></td></tr></table></td></tr></table></td></tr><tr><td><div =
97+ align=3D"center"><table cellpadding=3D"0" cellspacing=3D"0" =
98+ align=3D"center" style=3D"background-color: #e5e5e5; max-width: 100%; =
99+ padding: 0px"><tr><td align=3D"center" style=3D"background-color: #e5e5e5; =
100+ max-width: 100%; padding: 0px" height=3D"1px" =
101+ width=3D"720px"></td></tr></table></div></td></tr><tr><td align=3D"center" =
102+ style=3D"padding: 10px 0px 10px 0px; text-align:center; font: 16px =
103+ Helvetica, Arial; " valign=3D"top"><p style=3D"text-align:left; =
104+ color:#000000; font-size:14pt;">Mauro, </p><div style=3D"text-align:left; =
105+ margin-top: 20px">Starting soon, the option to switch back to the classic =
106+ Pages experience will no longer be available for your Page(s). Over the =
107+ coming months, all Pages will be updated to the new Pages experience and =
108+ the classic Pages experience will no longer be available.</div><div =
109+ style=3D"text-align:left;"><p style=3D"text-align:left;">Thanks,<br />The =
110+ Facebook Pages Team</p></div></td></tr><tr><td style=3D"padding: =
111+ 10px"></td></tr><tr><td><div align=3D"center"><table cellpadding=3D"0" =
112+ cellspacing=3D"0" align=3D"center" style=3D"background-color: #e5e5e5; =
113+ max-width: 100%; padding: 0px"><tr><td align=3D"center" =
114+ style=3D"background-color: #e5e5e5; max-width: 100%; padding: 0px" =
115+ height=3D"1px" width=3D"720px"></td></tr></table></div></td></tr><tr><td =
116+ style=3D"padding: 10px"></td></tr><tr><td><table align=3D"left" =
117+ style=3D""><tr><td align=3D"left"><div align=3D"left">This message was =
118+ sent to <a =
119+ style=3D"color:#1b74e4;text-decoration:none;">mauro&#064;minter.ltd</a>. =
120+ If you don&#039;t want to receive these emails from Meta in the future, =
121+ please <a href=3D"https://facebook.com/aymt/unsubscribe/?c=3D5105305257429=
122+ 01&amp;n=3D1667862800817280&amp;t=3D4503113996480200" =
123+ style=3D"color:#1b74e4;text-decoration:none;">unsubscribe here</a>.<br =
124+ />Meta Platforms Ireland Ltd., Attention: Community Operations, 4 Grand =
125+ Canal Square, Dublin 2, =
126+ Ireland</div></td></tr></table></td></tr></table><!--[if (gte mso =
127+ 9)|(IE)]>
128+ </td>
129+ </tr>
130+ </table>
131+ <![endif]--></td></tr><tr><td><span style=3D""><img =
132+ src=3D"https://facebook.com/aymt/aa/?e=3D%7B%22c%22%3A%22f1czoo1iezle1%22%=
133+ 2C%22t%22%3A%224446%3D4z44_atc%22%2C%22n%22%3A%22z%3D9%3D06jfs9ca6%22%7D" =
134+ style=3D"border:0;width:1px;height:1px;" =
135+ /></span></td></tr></table></body></html>
136 diff --git a/resources/dkim/005.txt b/resources/dkim/005.txt
137new file mode 100644
138index 0000000..16f8cd9
139--- /dev/null
140+++ b/resources/dkim/005.txt
141 @@ -0,0 +1,123 @@
142+ sysmsg-1._domainkey.topicbox.com v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC43V3fV5jD8p3isdDRIz5yXSg85VrZzd7fPCz3q5B2Z3Be/yOyumWmG4hX5UHn0HR/Im5cnzcwZeNu6SnlLJwggN0H664JHTjwQp8YiWKfEcDfOdz4K4kL6OrasDwb1nk5JBuJloGRpK5cTNEa0J0SNG+bhBzFfLvG5qp3p86RZwIDAQAB
143+
144+ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; d=topicbox.com; h=from:to
145+ :subject:date:mime-version:content-type
146+ :content-transfer-encoding:message-id; s=sysmsg-1; t=1667843664;
147+ x=1667930064; bh=FuZLEu0Dc6ZvRmafp+d/dAFzxmaVkLWLgzk8S9wR6Ro=; b=
148+ sEM2Pfv1NVwlnGJ5t15xPJdipz4HLJPMMr92LzU2yTrrtcHxiATbyENUZlLwomca
149+ 8MLlGDtVvsVMqPqoTRUipuwICkCbMVnn0fqyv64YQcr74kwuv5pmgUyyHJpK7dzu
150+ BwdcpbBp7yBQ+vi5xwtcCEdsRqALNEhWrGAir6ExzrE=
151+ From: Topicbox <topicbox@topicbox.com>
152+ To: "Mauro D." <mauro@stalw.art>
153+ Subject: Your Topicbox login code: YKPMYE
154+ Date: Mon, 7 Nov 2022 12:54:24 -0500
155+ MIME-Version: 1.0
156+ Content-Type: multipart/alternative; boundary=16678436640.cBEEbcFd.41169
157+ Content-Transfer-Encoding: 7bit
158+ Message-ID: <373C5876-5EC5-11ED-849C-F948292D11B0@lc.jmap.topicbox.com>
159+ Auto-Submitted: auto-generated
160+
161+
162+ --16678436640.cBEEbcFd.41169
163+ Date: Mon, 7 Nov 2022 12:54:24 -0500
164+ MIME-Version: 1.0
165+ Content-Type: text/plain; charset=utf-8
166+ Content-Transfer-Encoding: quoted-printable
167+
168+ # Single-use login code for mauro@stalw.art is:
169+
170+ ### YKPMYE
171+
172+
173+
174+ ---
175+
176+ **This code is valid for 10 minutes.** If more than 10 minutes have passed,
177+ please request a new code at [your organization=E2=80=99s login page](https=
178+ ://jmap.topicbox.com/login).
179+
180+
181+
182+
183+ --16678436640.cBEEbcFd.41169
184+ Date: Mon, 7 Nov 2022 12:54:24 -0500
185+ MIME-Version: 1.0
186+ Content-Type: text/html; charset=utf-8
187+ Content-Transfer-Encoding: quoted-printable
188+
189+ <!DOCTYPE html>
190+ <html lang=3D"en" xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:=
191+ schemas-microsoft-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:o=
192+ ffice:word">
193+ <meta charset=3D"utf-8">
194+ <meta name=3D"viewport" content=3D"width=3Ddevice-width">
195+ <!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPer=
196+ Inch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
197+ <style>
198+ h1,h2,h3,p,pre,ul,ol,hr{margin-top:0;margin-bottom:0;padding-bottom:20px}
199+ h1{font-size:25px;line-height:1.2;color:#078277}
200+ h2{font:inherit}
201+ .main h2 a{text-decoration:none;text-align:center;display:inline-block;bord=
202+ er:1px solid #007c67;border-color:rgba(0,89,74,.62745);border-radius:4px;pa=
203+ dding:12px 30px;line-height:1.4;background-color:#078277;background-image:l=
204+ inear-gradient(#4db5ac,#007c67);background-position:-1px -1px;background-si=
205+ ze:auto 104%;background-size:auto calc(100% + 2px);box-shadow:0 1px 1.5px r=
206+ gba(0,0,0,.25);color:#fff;font-weight:600}
207+ .button:active,.main h2 a:active{background-color:#078277;background-image:=
208+ linear-gradient(#45a59d,#006151);border-color:#006151;border-color:rgba(0,6=
209+ 1,51,.62745)}
210+ h3{text-align:center;font-size:40px;line-height:1.5}
211+ hr{border:0;border-top:1px solid #e0e2e3;height:0;background:transparent}
212+ .main a{color:#3f5db2}
213+ h1,h3,.main a,strong{font-weight:600}
214+ @media(max-width:500px){.main{padding:10px 10px 0 10px!important}.logo{padd=
215+ ing:10px!important} }
216+ @media(max-width:400px){.button,.main h2 a{display:block} }
217+ @media screen{
218+ @import url("https://fonts.googleapis.com/css?family=3DSource+Sans+Pro:400,=
219+ 600");
220+ }
221+ </style>
222+ <body bgcolor=3D"#ebeced" style=3D"padding:0;margin:0;background:#ebeced;co=
223+ lor:#333e48;font:16px/25px 'Source Sans Pro','Helvetica Neue',Arial,sans-se=
224+ rif;text-align:center;-webkit-text-size-adjust:none">
225+
226+ <header>
227+ <a href=3D"https://www.topicbox.com" class=3D"logo" style=3D"border:none;te=
228+ xt-decoration:none;display:inline-block;padding:15px;color:inherit"><img sr=
229+ c=3D"https://www.topicbox.com/_email-logo-v1-1x.png" srcset=3D"https://www.=
230+ topicbox.com/_email-logo-v1-2x.png 2x" width=3D"120" height=3D"30" alt=3D"T=
231+ opicbox" style=3D"font-size:23px;line-height:30px;font-weight:600;vertical-=
232+ align:top"></a>
233+ </header>
234+
235+ <div style=3D"background:#fff;width:600px;max-width:100%;margin:auto;text-a=
236+ lign:left">
237+ <div class=3D"main" style=3D"padding:40px 30px 30px">
238+ <h1>Single-use login code for mauro@stalw.art is:</h1>
239+
240+ <h3>YKPMYE</h3>
241+
242+ <hr />
243+
244+ <p><strong>This code is valid for 10 minutes.</strong> If more than 10 minu=
245+ tes have passed,
246+ please request a new code at <a href=3D"https://jmap.topicbox.com/login">yo=
247+ ur organization&rsquo;s login page</a>.</p>
248+
249+ </div>
250+ <div style=3D"height:6px;background:linear-gradient(90deg,#009688,#3f5db2)"=
251+ ></div>
252+ </div>
253+
254+ <footer>
255+ <p style=3D"padding:25px;line-height:20px;font-size:14px;color:#858b91">
256+ <a href=3D"https://www.topicbox.help/hc/en-us" style=3D"padding:0 4px;color=
257+ :#858b91">Help</a>
258+
259+
260+ <br>Powered by <a href=3D"https://www.topicbox.com" style=3D"color:#858b91;=
261+ font-weight:600">Topicbox</a></p>
262+ </footer>
263+
264+ --16678436640.cBEEbcFd.41169--
265 diff --git a/resources/dkim/006.txt b/resources/dkim/006.txt
266new file mode 100644
267index 0000000..cf82ac3
268--- /dev/null
269+++ b/resources/dkim/006.txt
270 @@ -0,0 +1,404 @@
271+ dk2016._domainkey.github.com v=DKIM1; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDn7EiK3r/vRRde/oD9XAsACz44UTrt2j+hGKdqQ093/QBbPZS99TKxBkcKeWEnu+TzV+WigS8eD424pZVNP2Y4Ta5qbWdtJa+jtoc9953m7WOkTYMM4/iiDxPzhg2yxWdxu3VvuyiZBLhPXzX54mj8rXaTyXXWry2+CRQqDds9pwIDAQAB; t=s
272+
273+ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=dk2016; d=github.com;
274+ h=Message-ID:List-Unsubscribe:From:Reply-To:To:Subject:Date:MIME-Version:
275+ Content-Type; i=github@github.com;
276+ bh=c7fP0xI1KdPdyzII89SvuYNAYaMYAxyGuTNxEPFBYOU=;
277+ b=wLrCCki4f2qSp5+WGt8FyNuminCbDu3HoGeh+oNCbyk+hGkX8hVAa+F5s0+81k3f+NedPc8sAKci
278+ 3C5mHVcPB5x5iaDjtqTMyHvkZZ2bEzTJsndr66IjRBmncNSgHft7djWdwPRoUWePzZzklzqPAN/2
279+ 7zvPBJdZ5FPkvbW0xxU=
280+ Received: from [10.34.27.159] (10.34.140.5) by mail01.resources.github.com id hcatd42tlo8p for <mauro@stalw.art>; Wed, 2 Nov 2022 14:45:38 -0400 (envelope-from <bounce@resources.github.com>)
281+ Message-ID: <d4522d31ac474a07bb3cbc49dc2985a1@88570519>
282+ X-Binding: 88570519
283+ X-elqSiteID: 88570519
284+ X-elqPod: 0xF1B94DB93BE5675EF0A64610BCE5945D8F0CC1F9D072032A60E02D51CEF3F9E5
285+ X-cid: 2806-2866
286+ List-Unsubscribe: =?utf-8?q?=3Cmailto=3Aspamproc=40fbl=2Een25=2Ecom=3Fsubject=3DListUnsub=5F88570519=5Fd452?=
287+ =?utf-8?q?2d31ac474a07bb3cbc49dc2985a1=3E=2C?=
288+ =?utf-8?q?_=3Chttp=3A=2F=2Fapp=2Egithub=2Emedia=2Fe=2Fu=3Fs=3D88570519&elq=3Dd4522d31ac474a07b?=
289+ =?utf-8?q?b3cbc49dc2985a1&t=3D17=3E?=
290+ From: GitHub <github@github.com>
291+ Reply-To: GitHub <github@github.com>
292+ To: mauro@stalw.art
293+ Subject: Copilot: One More Try =?utf-8?b?8J+agA==?=
294+ Date: Wed, 02 Nov 2022 14:45:38 -0400
295+ MIME-Version: 1.0
296+ Content-Type: multipart/alternative; boundary="=-Z1XVp+ho2orUDYPPOxt0Ag=="
297+
298+ --=-Z1XVp+ho2orUDYPPOxt0Ag==
299+ Content-Type: text/plain; charset=utf-8
300+ Content-Transfer-Encoding: base64
301+ Content-Id: <292482a5-7095-483c-81d7-9314f808ecbe>
302+
303+ R2V0IG1vcmUgcHJvZHVjdGl2ZSB3aXRoIEdpdEh1YiBDb3BpbG90IHRvZGF5LiAgIA0KDQrN
304+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
305+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
306+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
307+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
308+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
309+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCAgICDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
310+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
311+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
312+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
313+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
314+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
315+ jCDNj+KAjCAgICDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
316+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
317+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
318+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
319+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDN
320+ j+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCAgICDNj+KAjCDNj+KA
321+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
322+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
323+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
324+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
325+ jCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KAjCDNj+KA
326+ jCDNj+KAjCDNj+KAjCDNj+KAjCAgIA0KDQoiR2l0SHViIiAgICAgICAgICAgICAgICAgICJH
327+ aXRIdWIiICAgICAgICAgICAgICAgIDxodHRwczovL2FwcC5naXRodWIubWVkaWEvZS9lcj9z
328+ PTg4NTcwNTE5JmxpZD0zNzk1JmVscVRyYWNrSWQ9ZTg1YThmNTM3ODJjNGMyZTk2NzdiMDNm
329+ ZDQ5YTFjMmYmZWxxPWQ0NTIyZDMxYWM0NzRhMDdiYjNjYmM0OWRjMjk4NWExJmVscWFpZD0y
330+ ODY2JmVscWF0PTE+ICAgICAgICAgICAgICAgICAgICAgICAgICAiIiAgICAgICAgIDxodHRw
331+ czovL2FwcC5naXRodWIubWVkaWEvZS9lcj9zPTg4NTcwNTE5JmxpZD0zNzkyJmVscVRyYWNr
332+ SWQ9ZWJjNzZjMWFjOTEzNDViZmExNmIxYTVlNmNhZTBjMmEmZWxxPWQ0NTIyZDMxYWM0NzRh
333+ MDdiYjNjYmM0OWRjMjk4NWExJmVscWFpZD0yODY2JmVscWF0PTE+ICAgICAgIA0KU2luY2Ug
334+ d2UgbGFzdCBzYXcgeW91LCBvdmVyIDEgTWlsbGlvbiBkZXZlbG9wZXJzIGhhdmUgdHJpZWQg
335+ Q29waWxvdCAgICAgICANCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
336+ LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
337+ LS0tLS0NCkFuZCBvdXIgdXNlciByZXNlYXJjaCA8aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlh
338+ L2UvZXI/cz04ODU3MDUxOSZsaWQ9Mzc5NCZlbHFUcmFja0lkPWQ3NjVlYmE4NTI5ODQzYWNi
339+ MDVlODM4YTVhYzk0NjA1JmVscT1kNDUyMmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZl
340+ bHFhaWQ9Mjg2NiZlbHFhdD0xPiBoYXMgc2hvd24gdGhhdCA4OCUgb2YgZGV2ZWxvcGVycyBm
341+ ZWx0IG1vcmUgcHJvZHVjdGl2ZSwgd2l0aCBDb3BpbG90IGhlbHBpbmcgdGhlbSBzdGF5IGlu
342+ IHRoZSBmbG93IGFuZCBjb21wbGV0ZSB0YXNrcyBmYXN0ZXIuICAgICAgIA0KSGVyZSdzIHdo
343+ YXQgZGV2ZWxvcGVycyBsaWtlIHlvdSBhcmUgc2F5aW5nOiAgICAgICANCuKAnFdlJ3ZlIGJl
344+ ZW4gdXNpbmcgR2l0SHViICNDb3BpbG90IGZvciAzIG1vbnRocyBub3cuIEl0IGlzIG9uZSBv
345+ ZiB0aG9zZSBwcm9kdWN0cyB0aGF0IHlvdSBkb24ndCB0aGluayB5b3UgbmVlZCAoZXNwZWNp
346+ YWxseSBpZiB5b3UgYXJlIGFuIGV4cGVyaWVuY2VkIGVuZ2luZWVyKSwgYnV0IG9uY2UgeW91
347+ J3ZlIHN0YXJ0ZWQgdXNpbmcgaXQsIGl0IGlzIGhhcmQgdG8gaW1hZ2luZSBjb2Rpbmcgd2l0
348+ aG91dCBpdC4gJDEwIHBlciBtb250aCBwcmljaW5nIGlzIGFsc28gZW50aWNpbmcu4oCdICAg
349+ ICAgIA0KLSBLaW50YW4gQnJhaG1iaGF0dCAgICAgICANCuKAnFRoaXMgaXMgdGhlIHNpbmds
350+ ZSBtb3N0IG1pbmQtYmxvd2luZyBhcHBsaWNhdGlvbiBvZiBNTCBJ4oCZdmUgZXZlciBzZWVu
351+ LuKAnSAgICAgICANCi0gTWlrZSBLcmllZ2VyLCBjby1mb3VuZGVyLCBJbnN0YWdyYW0gICAg
352+ ICAgDQrigJxUcnlpbmcgdG8gY29kZSBpbiBhbiB1bmZhbWlsaWFyIGxhbmd1YWdlIGJ5IGdv
353+ b2dsaW5nIGV2ZXJ5dGhpbmcgaXMgbGlrZSBuYXZpZ2F0aW5nIGEgZm9yZWlnbiBjb3VudHJ5
354+ IHdpdGgganVzdCBhIHBocmFzZSBib29rLiBVc2luZyBHaXRIdWIgQ29waWxvdCBpcyBsaWtl
355+ IGhpcmluZyBhbiBpbnRlcnByZXRlciA8aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvZXI/
356+ cz04ODU3MDUxOSZsaWQ9Mzc5MyZlbHFUcmFja0lkPTRhOGM0NTI2NTY1OTQ3MmI4N2MxNTg3
357+ ZGIxZGIwN2U2JmVscT1kNDUyMmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZlbHFhaWQ9
358+ Mjg2NiZlbHFhdD0xPi7igJ0gICAgICAgDQotIEhhcnJpIEVkd2FyZHMsIHNjaWVudGlzdCwg
359+ T3BlbkFJICAgICAgIA0KV2FudCB0byBnZXQgbW9yZSBwcm9kdWN0aXZlIHdpdGggR2l0SHVi
360+ IENvcGlsb3Q/ICAgICAgIA0KU2lnbiB1cCB0b2RheSAgICAgICAgIDxodHRwczovL2FwcC5n
361+ aXRodWIubWVkaWEvZS9lcj9zPTg4NTcwNTE5JmxpZD0zNzkyJmVscVRyYWNrSWQ9MThlMzE2
362+ MmMyYmM5NGY2YWE0ODlkODVmNDkyMDJlZWEmZWxxPWQ0NTIyZDMxYWM0NzRhMDdiYjNjYmM0
363+ OWRjMjk4NWExJmVscWFpZD0yODY2JmVscWF0PTE+ICAgICAgIA0KDQoNCg0KDQpVbnN1YnNj
364+ cmliZSA8aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvdT9zPTg4NTcwNTE5JmVscT1kNDUy
365+ MmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMT4gwrcgRW1haWwgcHJlZmVyZW5jZXMgPGh0
366+ dHBzOi8vYXBwLmdpdGh1Yi5tZWRpYS9lL2VyP3M9ODg1NzA1MTkmbGlkPTMwMzUmZWxxVHJh
367+ Y2tJZD05MWFhMGQ2Nzg5NGE0NWFmODJiZGNlZTFlZWQyZDBiNyZlbHE9ZDQ1MjJkMzFhYzQ3
368+ NGEwN2JiM2NiYzQ5ZGMyOTg1YTEmZWxxYWlkPTI4NjYmZWxxYXQ9MT4gwrcgVGVybXMgPGh0
369+ dHBzOi8vYXBwLmdpdGh1Yi5tZWRpYS9lL2VyP3M9ODg1NzA1MTkmbGlkPTMwMzQmZWxxVHJh
370+ Y2tJZD1jNDdjM2E2OWJlN2U0OTIxYjYwNzUwMDFhM2E0N2U5ZCZlbHE9ZDQ1MjJkMzFhYzQ3
371+ NGEwN2JiM2NiYzQ5ZGMyOTg1YTEmZWxxYWlkPTI4NjYmZWxxYXQ9MT4gwrcgUHJpdmFjeSA8
372+ aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvZXI/cz04ODU3MDUxOSZsaWQ9MzAzNiZlbHFU
373+ cmFja0lkPTY0NTU0NmExNTlkMTRmNmRiZmUxM2U4NjAzOThlODFkJmVscT1kNDUyMmQzMWFj
374+ NDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0xPiDCtyBTaWduIGlu
375+ IHRvIEdpdEh1YiA8aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvZXI/cz04ODU3MDUxOSZs
376+ aWQ9MzAzMyZlbHFUcmFja0lkPWE0NjQ3OWZjYTBmYTQxYmRhNjA2ZjAyYmNjODBiYmVjJmVs
377+ cT1kNDUyMmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0x
378+ PiAgICAgDQpHaXRIdWIsIEluYy4NCjg4IENvbGluIFAgS2VsbHkgSnIgU3QuDQpTYW4gRnJh
379+ bmNpc2NvLCBDQSA5NDEwNyAgICAgDQoNCg==
380+
381+ --=-Z1XVp+ho2orUDYPPOxt0Ag==
382+ Content-Type: text/html; charset=utf-8
383+ Content-Transfer-Encoding: base64
384+ Content-Id: <3e08a47e-e741-4db6-bd39-f762c12f6e97>
385+
386+ PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJlbiIgZGlyPSJsdHIiIHhtbG5zOnY9InVy
387+ bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206dm1sIiB4bWxuczpvPSJ1cm46c2NoZW1hcy1taWNy
388+ b3NvZnQtY29tOm9mZmljZTpvZmZpY2UiPg0KPGhlYWQ+DQogICAgPG1ldGEgY2hhcnNldD0i
389+ dXRmLTgiPg0KICAgIDxtZXRhIGh0dHAtZXF1aXY9IlgtVUEtQ29tcGF0aWJsZSIgY29udGVu
390+ dD0iSUU9ZWRnZSI+DQogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRo
391+ PWRldmljZS13aWR0aCxpbml0aWFsLXNjYWxlPTEiPg0KICAgIDxtZXRhIG5hbWU9ImZvcm1h
392+ dC1kZXRlY3Rpb24iIGNvbnRlbnQ9InRlbGVwaG9uZT1ubywgZGF0ZT1ubywgYWRkcmVzcz1u
393+ bywgZW1haWw9bm8iPg0KICAgIDxtZXRhIG5hbWU9IngtYXBwbGUtZGlzYWJsZS1tZXNzYWdl
394+ LXJlZm9ybWF0dGluZyI+DQogICAgPG1ldGEgbmFtZT0iY29sb3Itc2NoZW1lIiBjb250ZW50
395+ PSJsaWdodCBkYXJrIj4NCiAgICA8bWV0YSBuYW1lPSJzdXBwb3J0ZWQtY29sb3Itc2NoZW1l
396+ cyIgY29udGVudD0ibGlnaHQgZGFyayI+DQogICAgPHRpdGxlPjwvdGl0bGU+DQogICAgPCEt
397+ LVtpZiBtc29dPg0KICAgIDx4bWw+DQogICAgPG86T2ZmaWNlRG9jdW1lbnRTZXR0aW5ncz4N
398+ CiAgICA8bzpBbGxvd1BORy8+DQogICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQ
399+ ZXJJbmNoPg0KICAgIDwvbzpPZmZpY2VEb2N1bWVudFNldHRpbmdzPg0KICAgIDwveG1sPg0K
400+ ICAgIDwhW2VuZGlmXS0tPg0KICAgIDxzdHlsZT4NCiAgICAgICAgLyogR2VuZXJhbCByZXNl
401+ dCBzdHlsZXMuICovDQogICAgICAgIGJvZHksIHRhYmxlLCB0ZCwgYSB7IC13ZWJraXQtdGV4
402+ dC1zaXplLWFkanVzdDogMTAwJTsgLW1zLXRleHQtc2l6ZS1hZGp1c3Q6IDEwMCU7IH0NCiAg
403+ ICAgICAgdGFibGUsIHRkIHsgbXNvLXRhYmxlLWxzcGFjZTogMHB0OyBtc28tdGFibGUtcnNw
404+ YWNlOiAwcHQ7IH0NCiAgICAgICAgaW1nIHsgLW1zLWludGVycG9sYXRpb24tbW9kZTogYmlj
405+ dWJpYzsgfQ0KICAgICAgICBpbWcgeyBib3JkZXI6IDA7IGhlaWdodDogYXV0bzsgbGluZS1o
406+ ZWlnaHQ6IDEwMCU7IG91dGxpbmU6IG5vbmU7IHRleHQtZGVjb3JhdGlvbjogbm9uZTsgfQ0K
407+ ICAgICAgICB0YWJsZSB7IGJvcmRlci1jb2xsYXBzZTogY29sbGFwc2UgIWltcG9ydGFudDsg
408+ fQ0KICAgICAgICBib2R5IHsgaGVpZ2h0OiAxMDAlICFpbXBvcnRhbnQ7IG1hcmdpbjogMCAh
409+ aW1wb3J0YW50OyBwYWRkaW5nOiAwICFpbXBvcnRhbnQ7IHdpZHRoOiAxMDAlICFpbXBvcnRh
410+ bnQ7IH0NCiAgICAgICAgYVt4LWFwcGxlLWRhdGEtZGV0ZWN0b3JzXSB7IGNvbG9yOiBpbmhl
411+ cml0ICFpbXBvcnRhbnQ7IHRleHQtZGVjb3JhdGlvbjogbm9uZSAhaW1wb3J0YW50OyBmb250
412+ LXNpemU6IGluaGVyaXQgIWltcG9ydGFudDsgZm9udC1mYW1pbHk6IGluaGVyaXQgIWltcG9y
413+ dGFudDsgZm9udC13ZWlnaHQ6IGluaGVyaXQgIWltcG9ydGFudDsgbGluZS1oZWlnaHQ6IGlu
414+ aGVyaXQgIWltcG9ydGFudDsgfQ0KICAgICAgICB1KyNib2R5IGEgeyBjb2xvcjogaW5oZXJp
415+ dDsgdGV4dC1kZWNvcmF0aW9uOiBub25lOyBmb250LXNpemU6IGluaGVyaXQ7IGZvbnQtZmFt
416+ aWx5OiBpbmhlcml0OyBmb250LXdlaWdodDogaW5oZXJpdDsgbGluZS1oZWlnaHQ6IGluaGVy
417+ aXQ7IH0NCiAgICAgICAgI01lc3NhZ2VWaWV3Qm9keSBhIHsgY29sb3I6IGluaGVyaXQ7IHRl
418+ eHQtZGVjb3JhdGlvbjogbm9uZTsgZm9udC1zaXplOiBpbmhlcml0OyBmb250LWZhbWlseTog
419+ aW5oZXJpdDsgZm9udC13ZWlnaHQ6IGluaGVyaXQ7IGxpbmUtaGVpZ2h0OiBpbmhlcml0OyB9
420+ DQogICAgICAgIHUrLmJvZHkgLmdsaXN0IHsgbWFyZ2luLWxlZnQ6IDAgIWltcG9ydGFudDsg
421+ fQ0KDQogICAgICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDogNjQwcHgp
422+ IHsNCiAgICAgICAgICAgIHUrLmJvZHkgLmdsaXN0IHsNCiAgICAgICAgICAgICAgICBtYXJn
423+ aW4tbGVmdDogNDBweCAhaW1wb3J0YW50Ow0KICAgICAgICAgICAgfQ0KICAgICAgICB9DQoN
424+ CiAgICAgICAgLyogU2V0IGRlZmF1bHQgbGluayBzdHlsZXMgaGVyZS4gT3ZlcnJpZGUgdGhl
425+ bSBvbiBob3ZlciB0byBzaG93IHVzZXJzIHRoZXkgYXJlIGludGVyYWN0aXZlLiAqLw0KICAg
426+ ICAgICBhIHsgY29sb3I6ICMwOTY5ZGE7IGZvbnQtd2VpZ2h0OiBub3JtYWw7IHRleHQtZGVj
427+ b3JhdGlvbjogbm9uZTsgfQ0KICAgICAgICBhOmhvdmVyIHsgY29sb3I6ICMwOTY5ZGEgIWlt
428+ cG9ydGFudDsgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmUgIWltcG9ydGFudDsgfQ0KICAg
429+ ICAgICBhLmJ1dHRvbiB7IHRleHQtYWxpZ246IGNlbnRlcjsgfQ0KICAgICAgICBhLmJ1dHRv
430+ bjpob3ZlciB7IGJhY2tncm91bmQtY29sb3I6ICMyYzk3NGIgIWltcG9ydGFudDsgYm9yZGVy
431+ LWNvbG9yOiAjMmM5NzRiICFpbXBvcnRhbnQ7IGNvbG9yOiAjZmZmZmZmICFpbXBvcnRhbnQ7
432+ IHRleHQtZGVjb3JhdGlvbjogbm9uZSAhaW1wb3J0YW50OyB9DQoNCiAgICAgICAgQG1lZGlh
433+ IHNjcmVlbiBhbmQgKG1heC13aWR0aDogNTgwcHgpIHsNCiAgICAgICAgICAgIGEuYnV0dG9u
434+ IHsgZGlzcGxheTogYmxvY2sgIWltcG9ydGFudDsgfQ0KICAgICAgICB9DQogICAgPC9zdHls
435+ ZT4NCiAgICA8IS0tIFNldCB5b3VyIGRhcmsgbW9kZSBzdHlsZXMgYmVsb3cuIC0tPg0KICAg
436+ IDxzdHlsZT4NCiAgICAgICAgOnJvb3Qgew0KICAgICAgICAgICAgY29sb3Itc2NoZW1lOiBs
437+ aWdodCBkYXJrOw0KICAgICAgICAgICAgc3VwcG9ydGVkLWNvbG9yLXNjaGVtZXM6IGxpZ2h0
438+ IGRhcms7DQogICAgICAgIH0NCg0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2No
439+ ZW1lOiBkYXJrICkgew0KICAgICAgICAgICAgYm9keSwgLmVtYWlsLWNvbnRhaW5lciwgLmNv
440+ bnRlbnQtY29udGFpbmVyIHsgYmFja2dyb3VuZC1jb2xvcjogIzAwMDAwMCAhaW1wb3J0YW50
441+ OyBjb2xvcjogI2ZmZmZmZiAhaW1wb3J0YW50OyB9DQogICAgICAgICAgICBoMSwgaDIsIGgz
442+ LCBzdHJvbmcgeyBjb2xvcjogI2ZmZmZmZiAhaW1wb3J0YW50OyB9DQogICAgICAgICAgICBh
443+ IHsgY29sb3I6ICNmZmZmZmYgIWltcG9ydGFudDsgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxp
444+ bmUgIWltcG9ydGFudDsgfQ0KICAgICAgICAgICAgLmZvb3RlciBhIHsgY29sb3I6ICM5OTk5
445+ OTkgIWltcG9ydGFudDsgdGV4dC1kZWNvcmF0aW9uOiBub25lICFpbXBvcnRhbnQ7IH0NCiAg
446+ ICAgICAgICAgIGE6aG92ZXIgeyBjb2xvcjogI2ZmZmZmZiAhaW1wb3J0YW50OyB0ZXh0LWRl
447+ Y29yYXRpb246IG5vbmUgIWltcG9ydGFudDsgfQ0KICAgICAgICAgICAgLmZvb3RlciBhOmhv
448+ dmVyIHsgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmUgIWltcG9ydGFudDsgfQ0KICAgICAg
449+ ICAgICAgYS5idXR0b24geyBjb2xvcjogI2ZmZmZmZiAhaW1wb3J0YW50OyB0ZXh0LWRlY29y
450+ YXRpb246IG5vbmUgIWltcG9ydGFudDsgfQ0KICAgICAgICAgICAgLmRhcmstbW9kZS1oaWRl
451+ IHsgZGlzcGxheTogbm9uZSAhaW1wb3J0YW50OyB9DQogICAgICAgICAgICAuZGFyay1tb2Rl
452+ LXNob3cgeyBkaXNwbGF5OiBibG9jayAhaW1wb3J0YW50OyB9DQogICAgICAgIH0NCiAgICA8
453+ L3N0eWxlPg0KPC9oZWFkPg0KPGJvZHkgaWQ9ImJvZHkiIGNsYXNzPSJib2R5IiBzdHlsZT0i
454+ YmFja2dyb3VuZC1jb2xvcjogI2ZmZmZmZjsgbWFyZ2luOiAwICFpbXBvcnRhbnQ7IHBhZGRp
455+ bmc6IDAgIWltcG9ydGFudDsiPg0KDQoNCg0KDQoNCiAgICA8ZGl2IHN0eWxlPSJkaXNwbGF5
456+ OiBub25lOyBtYXgtaGVpZ2h0OiAwOyBvdmVyZmxvdzogaGlkZGVuOyI+DQogICAgICAgIEdl
457+ dCBtb3JlIHByb2R1Y3RpdmUgd2l0aCBHaXRIdWIgQ29waWxvdCB0b2RheS4NCiAgICA8L2Rp
458+ dj4NCiAgICA8ZGl2IHN0eWxlPSJkaXNwbGF5OiBub25lOyBtYXgtaGVpZ2h0OiAwcHg7IG92
459+ ZXJmbG93OiBoaWRkZW47Ij4NCiAgICANCiAgICANCiYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
460+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
461+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
462+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
463+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
464+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
465+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
466+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
467+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
468+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
469+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
470+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
471+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
472+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
473+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
474+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
475+ OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3
476+ OyZ6d25qOyZuYnNwOw0KICAgIA0KJiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
477+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
478+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
479+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
480+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
481+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
482+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
483+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
484+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
485+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
486+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
487+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
488+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
489+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
490+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
491+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
492+ c3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5ic3A7JiM4NDc7Jnp3bmo7Jm5i
493+ c3A7DQogICAgDQomIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
494+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
495+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
496+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
497+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
498+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
499+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
500+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
501+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
502+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
503+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
504+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
505+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
506+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
507+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
508+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0Nzsm
509+ enduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsmIzg0NzsmenduajsmbmJzcDsNCiAgICAN
510+ CiYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
511+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
512+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
513+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
514+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
515+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
516+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
517+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
518+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
519+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
520+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
521+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
522+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
523+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
524+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
525+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNw
526+ OyYjODQ3OyZ6d25qOyZuYnNwOyYjODQ3OyZ6d25qOyZuYnNwOw0KICAgIDwvZGl2Pg0KDQog
527+ ICAgPCEtLSBUaGlzIGlzIHRoZSBtYWluIGNvbnRhaW5lciBkaXYsIHdoZXJlIHdlIHNldCBz
528+ b21lIGFjY2Vzc2liaWxpdHkgYmFzaWNzLiAtLT4NCiAgICA8ZGl2IGNsYXNzPSJlbWFpbC1j
529+ b250YWluZXIiIHJvbGU9ImFydGljbGUiIGFyaWEtcm9sZWRlc2NyaXB0aW9uPSJlbWFpbCIg
530+ YXJpYS1sYWJlbD0iIiBsYW5nPSJlbiIgZGlyPSJsdHIiIHN0eWxlPSJiYWNrZ3JvdW5kLWNv
531+ bG9yOiAjZmZmZmZmOyBmb250LXNpemU6IG1lZGl1bTsgZm9udC1zaXplOiBtYXgoMTZweCwg
532+ MXJlbSk7Ij4NCiAgICAgICAgPCEtLVtpZiAoZ3RlIG1zbyA5KXwoSUUpXT4NCiAgICAgICAg
533+ PHRhYmxlIGNlbGxzcGFjaW5nPSIwIiBjZWxscGFkZGluZz0iMCIgYm9yZGVyPSIwIiB3aWR0
534+ aD0iNTgwIiBhbGlnbj0iY2VudGVyIiByb2xlPSJwcmVzZW50YXRpb24iPjx0cj48dGQ+DQog
535+ ICAgICAgIDwhW2VuZGlmXS0tPg0KICAgICAgICA8ZGl2IGNsYXNzPSJjb250ZW50LWNvbnRh
536+ aW5lciIgc3R5bGU9ImNvbG9yOiAjMDAwMDAwOyBmb250LWZhbWlseTogLWFwcGxlLXN5c3Rl
537+ bSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAnU2Vnb2UgVUknLCBIZWx2ZXRpY2EsIEFyaWFsLCBz
538+ YW5zLXNlcmlmLCAnQXBwbGUgQ29sb3IgRW1vamknLCAnU2Vnb2UgVUkgRW1vamknLCAnU2Vn
539+ b2UgVUkgU3ltYm9sJzsgbGluZS1oZWlnaHQ6IDEuNTsgbWFyZ2luOiAxcmVtIGF1dG87IG1h
540+ eC13aWR0aDogNTgwcHg7IHBhZGRpbmc6IDFlbTsiPg0KDQogICAgICAgICAgICA8YSBocmVm
541+ PSJodHRwczovL2FwcC5naXRodWIubWVkaWEvZS9lcj9zPTg4NTcwNTE5JmxpZD0zNzk1JmVs
542+ cVRyYWNrSWQ9ZTg1YThmNTM3ODJjNGMyZTk2NzdiMDNmZDQ5YTFjMmYmZWxxPWQ0NTIyZDMx
543+ YWM0NzRhMDdiYjNjYmM0OWRjMjk4NWExJmVscWFpZD0yODY2JmVscWF0PTEiIHN0eWxlPSJk
544+ aXNwbGF5OiBibG9jazsgbWFyZ2luOiAwIDAgMmVtIDA7Ij4NCiAgICAgICAgICAgICAgICA8
545+ aW1nIGFsdD0iR2l0SHViIiBzcmM9Imh0dHBzOi8vaW1hZ2VzLmdpdGh1Yi5tZWRpYS9FbG9x
546+ dWFJbWFnZXMvY2xpZW50cy9HaXRIdWJJbmMvJTdCMDY5NTg1NTUtYjE1OC00M2Y5LTlmNGMt
547+ ZjdjYzEwYTMwNWEwJTdEX2dpdGh1Yi1sb2dvLWVtYWlsLnBuZyIgd2lkdGg9IjEwMCIgYm9y
548+ ZGVyPSIwIiBzdHlsZT0iZGlzcGxheTogYmxvY2s7IiBjbGFzcz0iZGFyay1tb2RlLWhpZGUi
549+ Pg0KDQogICAgICAgICAgICAgICAgPCEtLVtpZiAhbXNvXT48IS0tPg0KICAgICAgICAgICAg
550+ ICAgIDxpbWcgYWx0PSJHaXRIdWIiIHNyYz0iaHR0cHM6Ly9pbWFnZXMuZ2l0aHViLm1lZGlh
551+ L0Vsb3F1YUltYWdlcy9jbGllbnRzL0dpdEh1YkluYy8lN0IzNjljZmYzNS05MjQ0LTQyMGUt
552+ OWE4My0xYTkwNDBkOWUzMTElN0RfZ2l0aHViLWxvZ28tZW1haWwtZGFyay1tb2RlLnBuZyIg
553+ d2lkdGg9IjEwMCIgYm9yZGVyPSIwIiBzdHlsZT0iZGlzcGxheTogbm9uZTsiIGNsYXNzPSJk
554+ YXJrLW1vZGUtc2hvdyI+DQogICAgICAgICAgICAgICAgPCEtLTwhW2VuZGlmXS0tPiANCiAg
555+ ICAgICAgICAgIDwvYT4NCg0KICAgICAgICAgICAgPHAgc3R5bGU9Im1hcmdpbjogMmVtIDAg
556+ MmVtIDA7Ij4NCiAgICAgICAgICAgICAgICA8YSBocmVmPSJodHRwczovL2FwcC5naXRodWIu
557+ bWVkaWEvZS9lcj9zPTg4NTcwNTE5JmxpZD0zNzkyJmVscVRyYWNrSWQ9ZWJjNzZjMWFjOTEz
558+ NDViZmExNmIxYTVlNmNhZTBjMmEmZWxxPWQ0NTIyZDMxYWM0NzRhMDdiYjNjYmM0OWRjMjk4
559+ NWExJmVscWFpZD0yODY2JmVscWF0PTEiPg0KICAgICAgICAgICAgICAgICAgICA8aW1nIGFs
560+ dD0iIiBzcmM9Imh0dHBzOi8vaW1hZ2VzLmdpdGh1Yi5tZWRpYS9FbG9xdWFJbWFnZXMvY2xp
561+ ZW50cy9HaXRIdWJJbmMvJTdCMzMzZGI2NTctNmYwOC00ZTYzLTg3OWQtOTljYmM0MWM2Mjg1
562+ JTdEX0NvcGlsb3QuanBnIiB3aWR0aD0iNTgwIiBib3JkZXI9IjAiIHN0eWxlPSJib3JkZXIt
563+ cmFkaXVzOiA0cHg7IGNvbG9yOiAjMDAwMDAwOyBmb250LXNpemU6IDEuOGVtOyBsaW5lLWhl
564+ aWdodDogMS40OyBkaXNwbGF5OiBibG9jazsgbWF4LXdpZHRoOiAxMDAlOyBtaW4td2lkdGg6
565+ IDEwMHB4OyB3aWR0aDogMTAwJTsiPg0KICAgICAgICAgICAgICAgIDwvYT4NCiAgICAgICAg
566+ ICAgIDwvcD4NCg0KICAgICAgICAgICAgPGgxIHN0eWxlPSJjb2xvcjogIzIyMjIyMjsgZm9u
567+ dC1zaXplOiAxLjhlbTsgZm9udC13ZWlnaHQ6IGJvbGQ7IGxpbmUtaGVpZ2h0OiAxLjI7IG1h
568+ cmdpbjogMCAwIDAgMDsgdGV4dC1hbGlnbjogY2VudGVyOyI+DQogICAgICAgICAgICAgICAg
569+ U2luY2Ugd2UgbGFzdCBzYXcgeW91LCBvdmVyIDEgTWlsbGlvbiBkZXZlbG9wZXJzIGhhdmUg
570+ dHJpZWQgQ29waWxvdA0KICAgICAgICAgICAgPC9oMT4NCg0KICAgICAgICAgICAgPHAgc3R5
571+ bGU9Im1hcmdpbjogMmVtIDAgMmVtIDA7Ij4NCiAgICAgICAgICAgICAgICBBbmQgPGEgaHJl
572+ Zj0iaHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvZXI/cz04ODU3MDUxOSZsaWQ9Mzc5NCZl
573+ bHFUcmFja0lkPWQ3NjVlYmE4NTI5ODQzYWNiMDVlODM4YTVhYzk0NjA1JmVscT1kNDUyMmQz
574+ MWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0xIiBzdHlsZT0i
575+ Y29sb3I6ICMwOTY5ZGE7IGZvbnQtd2VpZ2h0OiBub3JtYWw7IHRleHQtZGVjb3JhdGlvbjog
576+ bm9uZTsiPm91ciB1c2VyIHJlc2VhcmNoPC9hPiBoYXMgc2hvd24gdGhhdCA4OCUgb2YgZGV2
577+ ZWxvcGVycyBmZWx0IG1vcmUgcHJvZHVjdGl2ZSwgd2l0aCBDb3BpbG90IGhlbHBpbmcgdGhl
578+ bSBzdGF5IGluIHRoZSBmbG93IGFuZCBjb21wbGV0ZSB0YXNrcyBmYXN0ZXIuDQogICAgICAg
579+ ICAgICA8L3A+DQoNCiAgICAgICAgICAgIDxwIHN0eWxlPSJtYXJnaW46IDJlbSAwIDJlbSAw
580+ OyI+DQogICAgICAgICAgICAgICAgSGVyZSdzIHdoYXQgZGV2ZWxvcGVycyBsaWtlIHlvdSBh
581+ cmUgc2F5aW5nOg0KICAgICAgICAgICAgPC9wPg0KDQogICAgICAgICAgICA8cCBzdHlsZT0i
582+ bWFyZ2luOiAyZW0gMCAwIDA7Ij4NCiAgICAgICAgICAgICAgICDigJxXZSd2ZSBiZWVuIHVz
583+ aW5nIEdpdEh1YiAjQ29waWxvdCBmb3IgMyBtb250aHMgbm93LiBJdCBpcyBvbmUgb2YgdGhv
584+ c2UgcHJvZHVjdHMgdGhhdCB5b3UgZG9uJ3QgdGhpbmsgeW91IG5lZWQgKGVzcGVjaWFsbHkg
585+ aWYgeW91IGFyZSBhbiBleHBlcmllbmNlZCBlbmdpbmVlciksIGJ1dCBvbmNlIHlvdSd2ZSBz
586+ dGFydGVkIHVzaW5nIGl0LCBpdCBpcyBoYXJkIHRvIGltYWdpbmUgY29kaW5nIHdpdGhvdXQg
587+ aXQuICQxMCBwZXIgbW9udGggcHJpY2luZyBpcyBhbHNvIGVudGljaW5nLuKAnQ0KICAgICAg
588+ ICAgICAgPC9wPg0KDQogICAgICAgICAgICA8cCBzdHlsZT0ibWFyZ2luOiAxZW0gMCAyZW0g
589+ MDsiPg0KICAgICAgICAgICAgICAgIDxzdHJvbmc+LSBLaW50YW4gQnJhaG1iaGF0dDwvc3Ry
590+ b25nPg0KICAgICAgICAgICAgPC9wPg0KDQogICAgICAgICAgICA8cCBzdHlsZT0ibWFyZ2lu
591+ OiAyZW0gMCAwIDA7Ij4NCiAgICAgICAgICAgICAgICDigJxUaGlzIGlzIHRoZSBzaW5nbGUg
592+ bW9zdCBtaW5kLWJsb3dpbmcgYXBwbGljYXRpb24gb2YgTUwgSeKAmXZlIGV2ZXIgc2Vlbi7i
593+ gJ0NCiAgICAgICAgICAgIDwvcD4NCg0KICAgICAgICAgICAgPHAgc3R5bGU9Im1hcmdpbjog
594+ MWVtIDAgMmVtIDA7Ij4NCiAgICAgICAgICAgICAgICA8c3Ryb25nPi0gTWlrZSBLcmllZ2Vy
595+ LCBjby1mb3VuZGVyLCBJbnN0YWdyYW08L3N0cm9uZz4NCiAgICAgICAgICAgIDwvcD4NCg0K
596+ ICAgICAgICAgICAgPHAgc3R5bGU9Im1hcmdpbjogMmVtIDAgMCAwOyI+DQogICAgICAgICAg
597+ ICAgICAg4oCcVHJ5aW5nIHRvIGNvZGUgaW4gYW4gdW5mYW1pbGlhciBsYW5ndWFnZSBieSBn
598+ b29nbGluZyBldmVyeXRoaW5nIGlzIGxpa2UgbmF2aWdhdGluZyBhIGZvcmVpZ24gY291bnRy
599+ eSB3aXRoIGp1c3QgYSBwaHJhc2UgYm9vay4gPGEgaHJlZj0iaHR0cHM6Ly9hcHAuZ2l0aHVi
600+ Lm1lZGlhL2UvZXI/cz04ODU3MDUxOSZsaWQ9Mzc5MyZlbHFUcmFja0lkPTRhOGM0NTI2NTY1
601+ OTQ3MmI4N2MxNTg3ZGIxZGIwN2U2JmVscT1kNDUyMmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5
602+ ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0xIiBzdHlsZT0iY29sb3I6ICMwOTY5ZGE7IGZvbnQt
603+ d2VpZ2h0OiBub3JtYWw7IHRleHQtZGVjb3JhdGlvbjogbm9uZTsiPlVzaW5nIEdpdEh1YiBD
604+ b3BpbG90IGlzIGxpa2UgaGlyaW5nIGFuIGludGVycHJldGVyPC9hPi7igJ0NCiAgICAgICAg
605+ ICAgIDwvcD4NCg0KICAgICAgICAgICAgPHAgc3R5bGU9Im1hcmdpbjogMWVtIDAgMmVtIDA7
606+ Ij4NCiAgICAgICAgICAgICAgICA8c3Ryb25nPi0gSGFycmkgRWR3YXJkcywgc2NpZW50aXN0
607+ LCBPcGVuQUk8L3N0cm9uZz4NCiAgICAgICAgICAgIDwvcD4NCg0KICAgICAgICAgICAgPHAg
608+ c3R5bGU9Im1hcmdpbjogMmVtIDAgMmVtIDA7Ij4NCiAgICAgICAgICAgICAgICBXYW50IHRv
609+ IGdldCBtb3JlIHByb2R1Y3RpdmUgd2l0aCBHaXRIdWIgQ29waWxvdD8NCiAgICAgICAgICAg
610+ IDwvcD4NCg0KICAgICAgICAgICAgPCEtLSBBIGJ1dHRvbiBibG9jay4gLS0+DQogICAgICAg
611+ ICAgICA8cCBzdHlsZT0ibWFyZ2luOiAyZW0gMCA2ZW0gMDsgdGV4dC1hbGlnbjogY2VudGVy
612+ OyI+DQogICAgICAgICAgICAgICAgPGEgY2xhc3M9ImJ1dHRvbiIgaHJlZj0iaHR0cHM6Ly9h
613+ cHAuZ2l0aHViLm1lZGlhL2UvZXI/cz04ODU3MDUxOSZsaWQ9Mzc5MiZlbHFUcmFja0lkPTE4
614+ ZTMxNjJjMmJjOTRmNmFhNDg5ZDg1ZjQ5MjAyZWVhJmVscT1kNDUyMmQzMWFjNDc0YTA3YmIz
615+ Y2JjNDlkYzI5ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0xIiBzdHlsZT0iYmFja2dyb3VuZDog
616+ IzJlYTQ0ZjsgYm9yZGVyOiAycHggc29saWQgIzJlYTQ0ZjsgdGV4dC1kZWNvcmF0aW9uOiBu
617+ b25lOyBwYWRkaW5nOiAxMHB4IDIwcHg7IGNvbG9yOiAjZmZmZmZmOyBib3JkZXItcmFkaXVz
618+ OiA0cHg7IGRpc3BsYXk6IGlubGluZS1ibG9jazsgZm9udC13ZWlnaHQ6IGJvbGQ7IG1zby1w
619+ YWRkaW5nLWFsdDogMDsgdGV4dC11bmRlcmxpbmUtY29sb3I6ICMyZWE0NGYiPjwhLS1baWYg
620+ bXNvXT48aSBzdHlsZT0ibGV0dGVyLXNwYWNpbmc6IDIwcHg7bXNvLWZvbnQtd2lkdGg6LTEw
621+ MCU7bXNvLXRleHQtcmFpc2U6MzBwdCIgaGlkZGVuPiZuYnNwOzwvaT48IVtlbmRpZl0tLT48
622+ c3BhbiBzdHlsZT0ibXNvLXRleHQtcmFpc2U6MTVwdDsiPlNpZ24gdXAgdG9kYXk8L3NwYW4+
623+ PCEtLVtpZiBtc29dPjxpIHN0eWxlPSJsZXR0ZXItc3BhY2luZzogMjVweDttc28tZm9udC13
624+ aWR0aDotMTAwJSIgaGlkZGVuPiZuYnNwOzwvaT48IVtlbmRpZl0tLT4NCiAgICAgICAgICAg
625+ ICAgICA8L2E+DQogICAgICAgICAgICA8L3A+DQoNCiAgICAgICAgPC9kaXY+DQogICAgICAg
626+ IDwhLS1baWYgKGd0ZSBtc28gOSl8KElFKV0+DQogICAgICAgIDwvdGQ+PC90cj48L3RhYmxl
627+ Pg0KICAgICAgICA8IVtlbmRpZl0tLT4NCiAgICA8L2Rpdj4NCg0KDQoNCjwhLS1baWYgKGd0
628+ ZSBtc28gOSl8KElFKV0+DQo8dGFibGUgY2VsbHNwYWNpbmc9IjAiIGNlbGxwYWRkaW5nPSIw
629+ IiBib3JkZXI9IjAiIHdpZHRoPSI1ODAiIGFsaWduPSJjZW50ZXIiIHJvbGU9InByZXNlbnRh
630+ dGlvbiI+PHRyPjx0ZD4NCjwhW2VuZGlmXS0tPg0KPGRpdiBjbGFzcz0iY29udGVudC1jb250
631+ YWluZXIiIHN0eWxlPSJjb2xvcjogIzAwMDAwMDsgZm9udC1mYW1pbHk6IC1hcHBsZS1zeXN0
632+ ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJ1NlZ29lIFVJJywgSGVsdmV0aWNhLCBBcmlhbCwg
633+ c2Fucy1zZXJpZiwgJ0FwcGxlIENvbG9yIEVtb2ppJywgJ1NlZ29lIFVJIEVtb2ppJywgJ1Nl
634+ Z29lIFVJIFN5bWJvbCc7IGxpbmUtaGVpZ2h0OiAxLjU7IG1hcmdpbjogMXJlbSBhdXRvOyBt
635+ YXgtd2lkdGg6IDU4MHB4OyBwYWRkaW5nOiAxZW07Ij4NCiAgICA8ZGl2IGNsYXNzPSJmb290
636+ ZXIiPg0KICAgICAgICA8aHIgc3R5bGU9ImJvcmRlci13aWR0aDogMDsgYmFja2dyb3VuZDog
637+ I2VhZWFlYTsgY29sb3I6ICNlYWVhZWE7IGhlaWdodDogMXB4Ij4NCiAgICAgICAgPHAgc3R5
638+ bGU9ImNvbG9yOiAjOTk5OTk5OyBmb250LXNpemU6IDAuODc1cmVtOyBtYXJnaW46IDJlbSAw
639+ IDFlbSAwOyI+DQogICAgICAgICAgICA8YSBkYXRhLXRhcmdldHR5cGU9InN5c2FjdGlvbiIg
640+ aHJlZj0iaHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvdT9zPTg4NTcwNTE5JmVscT1kNDUy
641+ MmQzMWFjNDc0YTA3YmIzY2JjNDlkYzI5ODVhMSIgY2xhc3M9ImZvb3Rlci1saW5rIiBzdHls
642+ ZT0iY29sb3I6ICM5OTk5OTk7IGZvbnQtd2VpZ2h0OiBub3JtYWw7IHRleHQtZGVjb3JhdGlv
643+ bjogbm9uZTsiPlVuc3Vic2NyaWJlPC9hPiAmbWlkZG90OyA8YSBocmVmPSJodHRwczovL2Fw
644+ cC5naXRodWIubWVkaWEvZS9lcj9zPTg4NTcwNTE5JmxpZD0zMDM1JmVscVRyYWNrSWQ9OTFh
645+ YTBkNjc4OTRhNDVhZjgyYmRjZWUxZWVkMmQwYjcmZWxxPWQ0NTIyZDMxYWM0NzRhMDdiYjNj
646+ YmM0OWRjMjk4NWExJmVscWFpZD0yODY2JmVscWF0PTEiIGNsYXNzPSJmb290ZXItbGluayIg
647+ c3R5bGU9ImNvbG9yOiAjOTk5OTk5OyBmb250LXdlaWdodDogbm9ybWFsOyB0ZXh0LWRlY29y
648+ YXRpb246IG5vbmU7Ij5FbWFpbCBwcmVmZXJlbmNlczwvYT4gJm1pZGRvdDsgPGEgaHJlZj0i
649+ aHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlhL2UvZXI/cz04ODU3MDUxOSZsaWQ9MzAzNCZlbHFU
650+ cmFja0lkPWM0N2MzYTY5YmU3ZTQ5MjFiNjA3NTAwMWEzYTQ3ZTlkJmVscT1kNDUyMmQzMWFj
651+ NDc0YTA3YmIzY2JjNDlkYzI5ODVhMSZlbHFhaWQ9Mjg2NiZlbHFhdD0xIiBjbGFzcz0iZm9v
652+ dGVyLWxpbmsiIHN0eWxlPSJjb2xvcjogIzk5OTk5OTsgZm9udC13ZWlnaHQ6IG5vcm1hbDsg
653+ dGV4dC1kZWNvcmF0aW9uOiBub25lOyI+VGVybXM8L2E+ICZtaWRkb3Q7IDxhIGhyZWY9Imh0
654+ dHBzOi8vYXBwLmdpdGh1Yi5tZWRpYS9lL2VyP3M9ODg1NzA1MTkmbGlkPTMwMzYmZWxxVHJh
655+ Y2tJZD02NDU1NDZhMTU5ZDE0ZjZkYmZlMTNlODYwMzk4ZTgxZCZlbHE9ZDQ1MjJkMzFhYzQ3
656+ NGEwN2JiM2NiYzQ5ZGMyOTg1YTEmZWxxYWlkPTI4NjYmZWxxYXQ9MSIgY2xhc3M9ImZvb3Rl
657+ ci1saW5rIiBzdHlsZT0iY29sb3I6ICM5OTk5OTk7IGZvbnQtd2VpZ2h0OiBub3JtYWw7IHRl
658+ eHQtZGVjb3JhdGlvbjogbm9uZTsiPlByaXZhY3k8L2E+ICZtaWRkb3Q7IDxhIGhyZWY9Imh0
659+ dHBzOi8vYXBwLmdpdGh1Yi5tZWRpYS9lL2VyP3M9ODg1NzA1MTkmbGlkPTMwMzMmZWxxVHJh
660+ Y2tJZD1hNDY0NzlmY2EwZmE0MWJkYTYwNmYwMmJjYzgwYmJlYyZlbHE9ZDQ1MjJkMzFhYzQ3
661+ NGEwN2JiM2NiYzQ5ZGMyOTg1YTEmZWxxYWlkPTI4NjYmZWxxYXQ9MSIgY2xhc3M9ImZvb3Rl
662+ ci1saW5rIiBzdHlsZT0iY29sb3I6ICM5OTk5OTk7IGZvbnQtd2VpZ2h0OiBub3JtYWw7IHRl
663+ eHQtZGVjb3JhdGlvbjogbm9uZTsiPlNpZ24gaW4gdG8gR2l0SHViPC9hPg0KICAgICAgICA8
664+ L3A+DQogICAgICAgIDxwIHN0eWxlPSJjb2xvcjogIzk5OTk5OTsgZm9udC1zaXplOiAwLjg3
665+ NXJlbTsgbWFyZ2luOiAwIDAgMCAwOyI+DQogICAgICAgICAgICBHaXRIdWIsIEluYy48YnI+
666+ DQogICAgICAgICAgICA4OCBDb2xpbiBQIEtlbGx5IEpyIFN0Ljxicj4NCiAgICAgICAgICAg
667+ IFNhbiBGcmFuY2lzY28sIENBIDk0MTA3DQogICAgICAgIDwvcD4NCiAgICA8L2Rpdj4NCjwh
668+ LS1baWYgKGd0ZSBtc28gOSl8KElFKV0+DQo8L3RkPjwvdHI+PC90YWJsZT4NCjwhW2VuZGlm
669+ XS0tPg0KPC9kaXY+DQogIA0KDQo8aW1nIHNyYz0naHR0cHM6Ly9hcHAuZ2l0aHViLm1lZGlh
670+ L2UvRm9vdGVySW1hZ2VzL0Zvb3RlckltYWdlMT9lbHE9ZDQ1MjJkMzFhYzQ3NGEwN2JiM2Ni
671+ YzQ5ZGMyOTg1YTEmc2l0ZWlkPTg4NTcwNTE5JyBhbHQ9IiIgYm9yZGVyPTAgd2lkdGg9MXB4
672+ IGhlaWdodD0xcHg+PC9ib2R5PjwvaHRtbD4=
673+
674+ --=-Z1XVp+ho2orUDYPPOxt0Ag==--
675 diff --git a/src/arc/mod.rs b/src/arc/mod.rs
676index 7ce5a3d..4cdf6f9 100644
677--- a/src/arc/mod.rs
678+++ b/src/arc/mod.rs
679 @@ -1,38 +1,37 @@
680 pub mod parse;
681- pub mod verify;
682
683 use std::borrow::Cow;
684
685 use crate::{
686- common::headers::Header,
687+ common::{headers::Header, verify::VerifySignature},
688 dkim::{Algorithm, Canonicalization},
689 };
690
691 #[derive(Debug, PartialEq, Eq, Clone)]
692 pub struct Signature<'x> {
693 pub(crate) i: u32,
694- a: Algorithm,
695- d: Cow<'x, [u8]>,
696- s: Cow<'x, [u8]>,
697- b: Vec<u8>,
698- bh: Vec<u8>,
699- h: Vec<Vec<u8>>,
700- z: Vec<Vec<u8>>,
701- l: u64,
702- x: u64,
703- t: u64,
704- ch: Canonicalization,
705- cb: Canonicalization,
706+ pub(crate) a: Algorithm,
707+ pub(crate) d: Cow<'x, [u8]>,
708+ pub(crate) s: Cow<'x, [u8]>,
709+ pub(crate) b: Vec<u8>,
710+ pub(crate) bh: Vec<u8>,
711+ pub(crate) h: Vec<Vec<u8>>,
712+ pub(crate) z: Vec<Vec<u8>>,
713+ pub(crate) l: u64,
714+ pub(crate) x: u64,
715+ pub(crate) t: u64,
716+ pub(crate) ch: Canonicalization,
717+ pub(crate) cb: Canonicalization,
718 }
719
720 #[derive(Debug, PartialEq, Eq, Clone)]
721 pub struct Seal<'x> {
722 pub(crate) i: u32,
723- a: Algorithm,
724- b: Vec<u8>,
725- d: Cow<'x, [u8]>,
726- s: Cow<'x, [u8]>,
727- t: u64,
728+ pub(crate) a: Algorithm,
729+ pub(crate) b: Vec<u8>,
730+ pub(crate) d: Cow<'x, [u8]>,
731+ pub(crate) s: Cow<'x, [u8]>,
732+ pub(crate) t: u64,
733 pub(crate) cv: ChainValidation,
734 }
735
736 @@ -55,22 +54,22 @@ pub enum ChainValidation {
737 Pass,
738 }
739
740- #[derive(Debug)]
741- pub enum Error {
742- ParseError,
743- InvalidInstance,
744- InvalidChainValidation,
745- MissingParameters,
746- Base64,
747- HasHeaderTag,
748- BrokenArcChain,
749- DKIM(crate::dkim::Error),
750- }
751+ impl<'x> VerifySignature for Signature<'x> {
752+ fn b(&self) -> &[u8] {
753+ &self.b
754+ }
755
756- impl From<crate::dkim::Error> for Error {
757- fn from(err: crate::dkim::Error) -> Self {
758- Error::DKIM(err)
759+ fn a(&self) -> Algorithm {
760+ self.a
761 }
762 }
763
764- pub type Result<T> = std::result::Result<T, Error>;
765+ impl<'x> VerifySignature for Seal<'x> {
766+ fn b(&self) -> &[u8] {
767+ &self.b
768+ }
769+
770+ fn a(&self) -> Algorithm {
771+ self.a
772+ }
773+ }
774 diff --git a/src/arc/parse.rs b/src/arc/parse.rs
775index e995d21..3bd46ee 100644
776--- a/src/arc/parse.rs
777+++ b/src/arc/parse.rs
778 @@ -3,9 +3,10 @@ use mail_parser::decoders::base64::base64_decode_stream;
779 use crate::{
780 common::parse::TagParser,
781 dkim::{parse::SignatureParser, Algorithm, Canonicalization},
782+ Error,
783 };
784
785- use super::{ChainValidation, Error, Results, Seal, Signature};
786+ use super::{ChainValidation, Results, Seal, Signature};
787
788 use crate::common::parse::*;
789
790 @@ -13,7 +14,7 @@ pub(crate) const CV: u16 = (b'c' as u16) | ((b'v' as u16) << 8);
791
792 impl<'x> Signature<'x> {
793 #[allow(clippy::while_let_on_iterator)]
794- pub fn parse(header: &'_ [u8]) -> super::Result<Self> {
795+ pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
796 let mut signature = Signature {
797 a: Algorithm::RsaSha256,
798 d: (b""[..]).into(),
799 @@ -37,7 +38,7 @@ impl<'x> Signature<'x> {
800 I => {
801 signature.i = header.number().unwrap_or(0) as u32;
802 if !(1..=50).contains(&signature.i) {
803- return Err(Error::InvalidInstance);
804+ return Err(Error::ARCInvalidInstance);
805 }
806 }
807 A => {
808 @@ -82,7 +83,7 @@ impl<'x> Signature<'x> {
809
810 impl<'x> Seal<'x> {
811 #[allow(clippy::while_let_on_iterator)]
812- pub fn parse(header: &'_ [u8]) -> super::Result<Self> {
813+ pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
814 let mut seal = Seal {
815 a: Algorithm::RsaSha256,
816 d: (b""[..]).into(),
817 @@ -122,22 +123,22 @@ impl<'x> Seal<'x> {
818 b'p' | b'P' if header.match_bytes(b"ass") => {
819 cv = ChainValidation::Pass.into();
820 }
821- _ => return Err(Error::InvalidChainValidation),
822+ _ => return Err(Error::ARCInvalidCV),
823 }
824 if !header.seek_tag_end() {
825- return Err(Error::InvalidChainValidation);
826+ return Err(Error::ARCInvalidCV);
827 }
828 }
829 H => {
830- return Err(Error::HasHeaderTag);
831+ return Err(Error::ARCHasHeaderTag);
832 }
833 _ => header.ignore(),
834 }
835 }
836- seal.cv = cv.ok_or(Error::InvalidChainValidation)?;
837+ seal.cv = cv.ok_or(Error::ARCInvalidCV)?;
838
839 if !(1..=50).contains(&seal.i) {
840- Err(Error::InvalidInstance)
841+ Err(Error::ARCInvalidInstance)
842 } else if !seal.d.is_empty() && !seal.s.is_empty() && !seal.b.is_empty() {
843 Ok(seal)
844 } else {
845 @@ -148,9 +149,8 @@ impl<'x> Seal<'x> {
846
847 impl Results {
848 #[allow(clippy::while_let_on_iterator)]
849- pub fn parse(header: &'_ [u8]) -> super::Result<Self> {
850+ pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
851 let mut results = Results { i: 0 };
852- let header_len = header.len();
853 let mut header = header.iter();
854
855 while let Some(key) = header.key() {
856 @@ -166,7 +166,7 @@ impl Results {
857 if (1..=50).contains(&results.i) {
858 Ok(results)
859 } else {
860- Err(Error::InvalidInstance)
861+ Err(Error::ARCInvalidInstance)
862 }
863 }
864 }
865 diff --git a/src/arc/verify.rs b/src/arc/verify.rs
866deleted file mode 100644
867index e69de29..0000000
868--- a/src/arc/verify.rs
869+++ /dev/null
870 diff --git a/src/common/message.rs b/src/common/message.rs
871index 63111e8..0764e75 100644
872--- a/src/common/message.rs
873+++ b/src/common/message.rs
874 @@ -1,36 +1,41 @@
875- use std::borrow::Cow;
876+ use std::time::SystemTime;
877
878 use mail_parser::{parsers::MessageStream, HeaderValue};
879+ use sha1::Sha1;
880+ use sha2::Sha256;
881
882 use crate::{
883- arc::{self, ChainValidation, Seal, Set},
884- dkim::{self, Canonicalization, HashAlgorithm},
885+ arc::{self, ChainValidation, Set},
886+ dkim::{self, Algorithm, HashAlgorithm},
887+ Error,
888 };
889
890- use super::headers::{AuthenticatedHeader, Header, HeaderParser};
891-
892- pub struct AuthenticatedMessage<'x> {
893- pub(crate) headers: Vec<(&'x [u8], &'x [u8])>,
894- pub(crate) from: Vec<Cow<'x, str>>,
895- pub(crate) body: &'x [u8],
896- pub(crate) body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
897- pub(crate) failed: Vec<Header<'x, arc::Error>>,
898- pub(crate) dkim_headers: Vec<Header<'x, dkim::Signature<'x>>>,
899- pub(crate) arc_sets: Vec<Set<'x>>,
900- pub(crate) cv: ChainValidation,
901- }
902+ use super::{
903+ headers::{AuthenticatedHeader, Header, HeaderParser},
904+ AuthPhase, AuthResult, AuthenticatedMessage,
905+ };
906
907 impl<'x> AuthenticatedMessage<'x> {
908+ #[inline(always)]
909 pub fn new(raw_message: &'x [u8]) -> Option<Self> {
910+ Self::new_(
911+ raw_message,
912+ SystemTime::now()
913+ .duration_since(SystemTime::UNIX_EPOCH)
914+ .map(|d| d.as_secs())
915+ .unwrap_or(0),
916+ )
917+ }
918+
919+ pub(crate) fn new_(raw_message: &'x [u8], now: u64) -> Option<Self> {
920 let mut message = AuthenticatedMessage {
921 headers: Vec::new(),
922 from: Vec::new(),
923- body: raw_message,
924- body_hashes: Vec::new(),
925- failed: Vec::new(),
926 dkim_headers: Vec::new(),
927 arc_sets: Vec::new(),
928- cv: ChainValidation::None,
929+ arc_result: AuthResult::None,
930+ dkim_result: AuthResult::None,
931+ phase: AuthPhase::Done,
932 };
933
934 let mut ams_headers = Vec::new();
935 @@ -38,16 +43,27 @@ impl<'x> AuthenticatedMessage<'x> {
936 let mut aar_headers = Vec::new();
937
938 let mut headers = HeaderParser::new(raw_message);
939+ let mut dkim_headers = Vec::new();
940
941 for (header, value) in &mut headers {
942 let name = match header {
943 AuthenticatedHeader::Ds(name) => {
944 match dkim::Signature::parse(value) {
945- Ok(s) => {
946- message.dkim_headers.push(Header::new(name, value, s));
947+ Ok(signature) => {
948+ if signature.x == 0 || (signature.x > signature.t && signature.x > now)
949+ {
950+ dkim_headers.push(Header::new(name, value, signature));
951+ } else {
952+ message.dkim_result = AuthResult::PermFail(Header::new(
953+ name,
954+ value,
955+ crate::Error::SignatureExpired,
956+ ));
957+ }
958 }
959 Err(err) => {
960- message.failed.push(Header::new(name, value, err.into()));
961+ message.dkim_result =
962+ AuthResult::PermFail(Header::new(name, value, err));
963 }
964 }
965
966 @@ -59,7 +75,8 @@ impl<'x> AuthenticatedMessage<'x> {
967 aar_headers.push(Header::new(name, value, r));
968 }
969 Err(err) => {
970- message.failed.push(Header::new(name, value, err));
971+ message.arc_result =
972+ AuthResult::PermFail(Header::new(name, value, err));
973 }
974 }
975
976 @@ -71,7 +88,8 @@ impl<'x> AuthenticatedMessage<'x> {
977 ams_headers.push(Header::new(name, value, s));
978 }
979 Err(err) => {
980- message.failed.push(Header::new(name, value, err));
981+ message.arc_result =
982+ AuthResult::PermFail(Header::new(name, value, err));
983 }
984 }
985
986 @@ -83,7 +101,8 @@ impl<'x> AuthenticatedMessage<'x> {
987 as_headers.push(Header::new(name, value, s));
988 }
989 Err(err) => {
990- message.failed.push(Header::new(name, value, err));
991+ message.arc_result =
992+ AuthResult::PermFail(Header::new(name, value, err));
993 }
994 }
995 name
996 @@ -123,6 +142,17 @@ impl<'x> AuthenticatedMessage<'x> {
997 message.headers.push((name, value));
998 }
999
1000+ if message.headers.is_empty() {
1001+ return None;
1002+ }
1003+
1004+ // Obtain message body
1005+ let body = headers
1006+ .body_offset()
1007+ .and_then(|pos| raw_message.get(pos..))
1008+ .unwrap_or_default();
1009+ let mut body_hashes = Vec::new();
1010+
1011 // Group ARC headers in sets
1012 let arc_headers = ams_headers.len();
1013 if (1..=50).contains(&arc_headers)
1014 @@ -132,7 +162,6 @@ impl<'x> AuthenticatedMessage<'x> {
1015 as_headers.sort_unstable_by(|a, b| a.header.i.cmp(&b.header.i));
1016 ams_headers.sort_unstable_by(|a, b| a.header.i.cmp(&b.header.i));
1017 aar_headers.sort_unstable_by(|a, b| a.header.i.cmp(&b.header.i));
1018- let mut success = true;
1019
1020 for (pos, ((seal, signature), results)) in as_headers
1021 .into_iter()
1022 @@ -140,64 +169,133 @@ impl<'x> AuthenticatedMessage<'x> {
1023 .zip(aar_headers)
1024 .enumerate()
1025 {
1026- if success {
1027- success = (seal.header.i as usize == (pos + 1))
1028- && (signature.header.i as usize == (pos + 1))
1029- && (results.header.i as usize == (pos + 1))
1030- && ((pos == 0 && seal.header.cv == ChainValidation::None)
1031- || (pos > 0 && seal.header.cv == ChainValidation::Pass));
1032- }
1033- message.arc_sets.push(Set {
1034- signature,
1035- seal,
1036- results,
1037- });
1038- }
1039+ if (seal.header.i as usize == (pos + 1))
1040+ && (signature.header.i as usize == (pos + 1))
1041+ && (results.header.i as usize == (pos + 1))
1042+ && ((pos == 0 && seal.header.cv == ChainValidation::None)
1043+ || (pos > 0 && seal.header.cv == ChainValidation::Pass))
1044+ {
1045+ // Validate last signature in the chain
1046+ if pos == arc_headers - 1 {
1047+ // Validate expiration
1048+ let signature_ = &signature.header;
1049+ if signature_.x > 0 && (signature_.x < signature_.t || signature_.x < now) {
1050+ message.arc_result = AuthResult::PermFail(Header::new(
1051+ signature.name,
1052+ signature.value,
1053+ Error::SignatureExpired,
1054+ ));
1055+ break;
1056+ }
1057
1058- if !success {
1059- for set in message.arc_sets.drain(..) {
1060- for (name, value) in [
1061- (set.signature.name, set.signature.value),
1062- (set.seal.name, set.seal.value),
1063- (set.results.name, set.results.value),
1064- ] {
1065- message
1066- .failed
1067- .push(Header::new(name, value, arc::Error::BrokenArcChain));
1068+ // Validate body hash
1069+ let bh = match signature_.a {
1070+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1071+ signature_.cb.hash_body::<Sha256>(body, signature_.l)
1072+ }
1073+ Algorithm::RsaSha1 => {
1074+ signature_.cb.hash_body::<Sha1>(body, signature_.l)
1075+ }
1076+ }
1077+ .unwrap_or_default();
1078+
1079+ let success = bh == signature_.bh;
1080+ body_hashes.push((
1081+ signature_.cb,
1082+ HashAlgorithm::from(signature_.a),
1083+ signature_.l,
1084+ bh,
1085+ ));
1086+
1087+ if !success {
1088+ message.arc_result = AuthResult::PermFail(Header::new(
1089+ signature.name,
1090+ signature.value,
1091+ Error::FailedBodyHashMatch,
1092+ ));
1093+ break;
1094+ }
1095 }
1096+
1097+ message.arc_sets.push(Set {
1098+ signature,
1099+ seal,
1100+ results,
1101+ });
1102+ } else {
1103+ message.arc_result = AuthResult::PermFail(Header::new(
1104+ signature.name,
1105+ signature.value,
1106+ Error::ARCBrokenChain,
1107+ ));
1108+ break;
1109 }
1110- message.cv = ChainValidation::Fail;
1111 }
1112- } else if arc_headers > 0 {
1113+ } else if arc_headers > 0 && message.arc_result == AuthResult::None {
1114 // Missing ARC headers, fail all.
1115- message.failed.extend(
1116- ams_headers
1117- .into_iter()
1118- .map(|h| Header::new(h.name, h.value, arc::Error::BrokenArcChain))
1119- .chain(
1120- as_headers
1121- .into_iter()
1122- .map(|h| Header::new(h.name, h.value, arc::Error::BrokenArcChain)),
1123- )
1124- .chain(
1125- aar_headers
1126- .into_iter()
1127- .map(|h| Header::new(h.name, h.value, arc::Error::BrokenArcChain)),
1128- ),
1129- );
1130- message.cv = ChainValidation::Fail;
1131+ let header = ams_headers
1132+ .into_iter()
1133+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain))
1134+ .chain(
1135+ as_headers
1136+ .into_iter()
1137+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
1138+ )
1139+ .chain(
1140+ aar_headers
1141+ .into_iter()
1142+ .map(|h| Header::new(h.name, h.value, Error::ARCBrokenChain)),
1143+ )
1144+ .next()
1145+ .unwrap();
1146+ message.arc_result = AuthResult::PermFail(header);
1147 }
1148
1149- message.body = headers
1150- .body_offset()
1151- .and_then(|pos| raw_message.get(pos..))
1152- .unwrap_or_default();
1153+ // Validate body hash of DKIM signatures
1154+ if !dkim_headers.is_empty() {
1155+ message.dkim_headers = Vec::with_capacity(dkim_headers.len());
1156+ for header in dkim_headers {
1157+ let signature = &header.header;
1158+ let ha = HashAlgorithm::from(signature.a);
1159
1160- if !message.headers.is_empty() {
1161- message.into()
1162- } else {
1163- None
1164+ let bh = if let Some((_, _, _, bh)) = body_hashes
1165+ .iter()
1166+ .find(|(c, h, l, _)| c == &signature.cb && h == &ha && l == &signature.l)
1167+ {
1168+ bh
1169+ } else {
1170+ let bh = match signature.a {
1171+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1172+ signature.cb.hash_body::<Sha256>(body, signature.l)
1173+ }
1174+ Algorithm::RsaSha1 => signature.cb.hash_body::<Sha1>(body, signature.l),
1175+ }
1176+ .unwrap_or_default();
1177+
1178+ body_hashes.push((signature.cb, ha, signature.l, bh));
1179+ &body_hashes.last().unwrap().3
1180+ };
1181+
1182+ if bh == &signature.bh {
1183+ message.dkim_headers.push(header);
1184+ } else {
1185+ message.dkim_result = AuthResult::PermFail(Header::new(
1186+ header.name,
1187+ header.value,
1188+ crate::Error::FailedBodyHashMatch,
1189+ ));
1190+ }
1191+ }
1192+ }
1193+
1194+ if !message.dkim_headers.is_empty() {
1195+ message.dkim_headers.reverse();
1196+ message.phase = AuthPhase::Dkim;
1197+ } else if !message.arc_sets.is_empty() && message.arc_result == AuthResult::None {
1198+ message.phase = AuthPhase::Ams;
1199 }
1200+
1201+ message.into()
1202 }
1203 }
1204
1205 diff --git a/src/common/mod.rs b/src/common/mod.rs
1206index e0bff3e..930c63f 100644
1207--- a/src/common/mod.rs
1208+++ b/src/common/mod.rs
1209 @@ -1,3 +1,40 @@
1210+ use std::borrow::Cow;
1211+
1212+ use crate::{
1213+ arc::Set,
1214+ dkim::{self},
1215+ };
1216+
1217+ use self::headers::Header;
1218+
1219 pub mod headers;
1220 pub mod message;
1221 pub mod parse;
1222+ pub mod verify;
1223+
1224+ #[derive(Debug, Clone)]
1225+ pub struct AuthenticatedMessage<'x> {
1226+ pub(crate) headers: Vec<(&'x [u8], &'x [u8])>,
1227+ pub(crate) from: Vec<Cow<'x, str>>,
1228+ pub(crate) dkim_headers: Vec<Header<'x, dkim::Signature<'x>>>,
1229+ pub(crate) arc_sets: Vec<Set<'x>>,
1230+ pub(crate) arc_result: AuthResult<'x, ()>,
1231+ pub(crate) dkim_result: AuthResult<'x, dkim::Signature<'x>>,
1232+ pub(crate) phase: AuthPhase,
1233+ }
1234+
1235+ #[derive(Debug, PartialEq, Eq, Clone)]
1236+ pub enum AuthPhase {
1237+ Dkim,
1238+ Ams,
1239+ As(usize),
1240+ Done,
1241+ }
1242+
1243+ #[derive(Debug, PartialEq, Eq, Clone)]
1244+ pub enum AuthResult<'x, T> {
1245+ None,
1246+ PermFail(Header<'x, crate::Error>),
1247+ TempFail(Header<'x, crate::Error>),
1248+ Pass(T),
1249+ }
1250 diff --git a/src/common/verify.rs b/src/common/verify.rs
1251new file mode 100644
1252index 0000000..16fa632
1253--- /dev/null
1254+++ b/src/common/verify.rs
1255 @@ -0,0 +1,355 @@
1256+ use std::borrow::Cow;
1257+
1258+ use rsa::PaddingScheme;
1259+ use sha1::Sha1;
1260+ use sha2::Sha256;
1261+
1262+ use crate::{
1263+ dkim::{
1264+ self, parse::TryIntoRecord, verify::Verifier, Algorithm, Canonicalization, Flag, PublicKey,
1265+ Record,
1266+ },
1267+ Error,
1268+ };
1269+
1270+ use super::{headers::Header, AuthPhase, AuthResult, AuthenticatedMessage};
1271+
1272+ impl<'x> AuthenticatedMessage<'x> {
1273+ pub fn verify(&mut self, maybe_record: impl TryIntoRecord<'x>) {
1274+ let maybe_record = maybe_record.try_into_record();
1275+
1276+ match self.phase {
1277+ AuthPhase::Dkim => {
1278+ let header = self.dkim_headers.pop().unwrap();
1279+ let record = match maybe_record {
1280+ Ok(record) => record,
1281+ Err(err) => {
1282+ self.set_dkim_error(header, err);
1283+ return;
1284+ }
1285+ };
1286+ let signature = &header.header;
1287+
1288+ // Enforce t=s flag
1289+ if !record.validate_auid(&signature.i, &signature.d) {
1290+ self.set_dkim_error(header, Error::FailedAUIDMatch);
1291+ return;
1292+ }
1293+
1294+ // Hash headers
1295+ let dkim_hdr_value = header.value.strip_signature();
1296+ let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
1297+ let hh = match signature.a {
1298+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1299+ signature.ch.hash_headers::<Sha256>(headers)
1300+ }
1301+ Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
1302+ }
1303+ .unwrap_or_default();
1304+
1305+ // Verify signature
1306+ match signature.verify(record.as_ref(), &hh) {
1307+ Ok(_) => {
1308+ self.dkim_result = AuthResult::Pass(header.header);
1309+ self.phase = if !self.arc_sets.is_empty() {
1310+ AuthPhase::Ams
1311+ } else {
1312+ AuthPhase::Done
1313+ };
1314+ }
1315+ Err(err) => {
1316+ self.set_dkim_error(header, err);
1317+ }
1318+ }
1319+ }
1320+ AuthPhase::Ams => {
1321+ let header = &self.arc_sets.last().unwrap().signature;
1322+ let record = match maybe_record {
1323+ Ok(record) => record,
1324+ Err(err) => {
1325+ self.set_arc_error(header.name, header.value, err);
1326+ return;
1327+ }
1328+ };
1329+ let signature = &header.header;
1330+
1331+ // Hash headers
1332+ let dkim_hdr_value = header.value.strip_signature();
1333+ let headers = self.signed_headers(&signature.h, header.name, &dkim_hdr_value);
1334+ let hh = match signature.a {
1335+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1336+ signature.ch.hash_headers::<Sha256>(headers)
1337+ }
1338+ Algorithm::RsaSha1 => signature.ch.hash_headers::<Sha1>(headers),
1339+ }
1340+ .unwrap_or_default();
1341+
1342+ // Verify signature
1343+ match signature.verify(record.as_ref(), &hh) {
1344+ Ok(_) => {
1345+ self.phase = AuthPhase::As(self.arc_sets.len() - 1);
1346+ }
1347+ Err(err) => {
1348+ self.set_arc_error(header.name, header.value, err);
1349+ }
1350+ }
1351+ }
1352+ AuthPhase::As(pos) => {
1353+ let header = &self.arc_sets[pos].seal;
1354+ let record = match maybe_record {
1355+ Ok(record) => record,
1356+ Err(err) => {
1357+ self.set_arc_error(header.name, header.value, err);
1358+ return;
1359+ }
1360+ };
1361+ let seal = &header.header;
1362+
1363+ // Build seal headers
1364+ let cur_set = &self.arc_sets[pos];
1365+ let seal_signature = cur_set.seal.value.strip_signature();
1366+ let headers = self
1367+ .arc_sets
1368+ .iter()
1369+ .take(pos)
1370+ .flat_map(|set| {
1371+ [
1372+ (set.results.name, set.results.value),
1373+ (set.signature.name, set.signature.value),
1374+ (set.seal.name, set.seal.value),
1375+ ]
1376+ })
1377+ .chain([
1378+ (cur_set.results.name, cur_set.results.value),
1379+ (cur_set.signature.name, cur_set.signature.value),
1380+ (cur_set.seal.name, &seal_signature),
1381+ ]);
1382+
1383+ /*let mut headers = Vec::with_capacity((pos + 1) * 3);
1384+ for set in self.arc_sets.iter().take(pos + 1) {
1385+ headers.push((set.results.name, Cow::from(set.results.value)));
1386+ headers.push((set.signature.name, Cow::from(set.signature.value)));
1387+ headers.push((set.seal.name, Cow::from(set.seal.value.strip_signature())));
1388+ }
1389+ let headers_iter = headers.iter().map(|(h, v)| (*h, v.as_ref()));*/
1390+
1391+ let hh = match seal.a {
1392+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
1393+ Canonicalization::Relaxed.hash_headers::<Sha256>(headers)
1394+ }
1395+ Algorithm::RsaSha1 => Canonicalization::Relaxed.hash_headers::<Sha1>(headers),
1396+ }
1397+ .unwrap_or_default();
1398+
1399+ // Verify ARC seal
1400+ match seal.verify(record.as_ref(), &hh) {
1401+ Ok(_) => {
1402+ if pos > 0 {
1403+ self.phase = AuthPhase::As(pos - 1);
1404+ } else {
1405+ self.arc_result = AuthResult::Pass(());
1406+ self.phase = AuthPhase::Done;
1407+ }
1408+ }
1409+ Err(err) => {
1410+ self.set_arc_error(header.name, header.value, err);
1411+ }
1412+ }
1413+ }
1414+ AuthPhase::Done => (),
1415+ }
1416+ }
1417+
1418+ fn set_dkim_error(&mut self, header: Header<'x, dkim::Signature>, err: Error) {
1419+ let header = Header::new(header.name, header.value, err);
1420+ self.dkim_result = if header.header != Error::DNSFailure {
1421+ AuthResult::PermFail(header)
1422+ } else {
1423+ AuthResult::TempFail(header)
1424+ };
1425+ if self.dkim_headers.is_empty() {
1426+ self.phase = if !self.arc_sets.is_empty() {
1427+ AuthPhase::Ams
1428+ } else {
1429+ AuthPhase::Done
1430+ };
1431+ }
1432+ }
1433+
1434+ fn set_arc_error(&mut self, name: &'x [u8], value: &'x [u8], err: Error) {
1435+ self.arc_result = if err != Error::DNSFailure {
1436+ AuthResult::PermFail(Header::new(name, value, err))
1437+ } else {
1438+ AuthResult::TempFail(Header::new(name, value, err))
1439+ };
1440+ self.phase = AuthPhase::Done;
1441+ }
1442+
1443+ pub fn next_entry(&self) -> Option<String> {
1444+ let (s, d) = match self.phase {
1445+ AuthPhase::Dkim => {
1446+ let s = &self.dkim_headers.last().unwrap().header;
1447+ (s.s.as_ref(), s.d.as_ref())
1448+ }
1449+ AuthPhase::Ams => {
1450+ let s = &self.arc_sets.last().unwrap().signature.header;
1451+ (s.s.as_ref(), s.d.as_ref())
1452+ }
1453+ AuthPhase::As(pos) => {
1454+ let s = &self.arc_sets[pos].seal.header;
1455+ (s.s.as_ref(), s.d.as_ref())
1456+ }
1457+ AuthPhase::Done => return None,
1458+ };
1459+
1460+ format!(
1461+ "{}._domainkey.{}",
1462+ std::str::from_utf8(s).unwrap_or_default(),
1463+ std::str::from_utf8(d).unwrap_or_default()
1464+ )
1465+ .into()
1466+ }
1467+ }
1468+
1469+ pub(crate) trait VerifySignature {
1470+ fn b(&self) -> &[u8];
1471+ fn a(&self) -> Algorithm;
1472+ fn verify(&self, record: &Record, hh: &[u8]) -> crate::Result<()> {
1473+ match (&self.a(), &record.p) {
1474+ (Algorithm::RsaSha256, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
1475+ public_key,
1476+ PaddingScheme::new_pkcs1v15_sign::<Sha256>(),
1477+ hh,
1478+ self.b(),
1479+ )
1480+ .map_err(|_| Error::FailedVerification),
1481+
1482+ (Algorithm::RsaSha1, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
1483+ public_key,
1484+ PaddingScheme::new_pkcs1v15_sign::<Sha1>(),
1485+ hh,
1486+ self.b(),
1487+ )
1488+ .map_err(|_| Error::FailedVerification),
1489+
1490+ (Algorithm::Ed25519Sha256, PublicKey::Ed25519(public_key)) => public_key
1491+ .verify_strict(
1492+ hh,
1493+ &ed25519_dalek::Signature::from_bytes(self.b())
1494+ .map_err(|err| Error::CryptoError(err.to_string()))?,
1495+ )
1496+ .map_err(|_| Error::FailedVerification),
1497+
1498+ (_, PublicKey::Revoked) => Err(Error::RevokedPublicKey),
1499+
1500+ (_, _) => Err(Error::IncompatibleAlgorithms),
1501+ }
1502+ }
1503+ }
1504+
1505+ impl Record {
1506+ #[allow(clippy::while_let_on_iterator)]
1507+ pub fn validate_auid(&self, i: &[u8], d: &[u8]) -> bool {
1508+ // Enforce t=s flag
1509+ if !i.is_empty() && self.has_flag(Flag::MatchDomain) {
1510+ let mut auid = i.as_ref().iter();
1511+ let mut domain = d.as_ref().iter();
1512+ while let Some(&ch) = auid.next() {
1513+ if ch == b'@' {
1514+ break;
1515+ }
1516+ }
1517+ while let Some(ch) = auid.next() {
1518+ if let Some(dch) = domain.next() {
1519+ if !ch.eq_ignore_ascii_case(dch) {
1520+ return false;
1521+ }
1522+ } else {
1523+ break;
1524+ }
1525+ }
1526+ if domain.next().is_some() {
1527+ return false;
1528+ }
1529+ }
1530+
1531+ true
1532+ }
1533+ }
1534+
1535+ #[cfg(test)]
1536+ mod test {
1537+ use std::{collections::HashMap, fs, path::PathBuf};
1538+
1539+ use crate::common::{AuthResult, AuthenticatedMessage};
1540+
1541+ #[test]
1542+ fn dkim_verify() {
1543+ let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1544+ test_dir.push("resources");
1545+ test_dir.push("dkim");
1546+
1547+ for file_name in fs::read_dir(&test_dir).unwrap() {
1548+ let file_name = file_name.unwrap().path();
1549+ /*if !file_name.to_str().unwrap().contains("002") {
1550+ continue;
1551+ }*/
1552+ println!("file {}", file_name.to_str().unwrap());
1553+
1554+ let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
1555+ let (dns_records, message) = test.split_once("\n\n").unwrap();
1556+ let dns_records = dns_records
1557+ .split('\n')
1558+ .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
1559+ .collect::<HashMap<_, _>>();
1560+ let message = message.replace('\n', "\r\n");
1561+
1562+ let mut verifier = AuthenticatedMessage::new_(message.as_bytes(), 1667843664).unwrap();
1563+ while let Some(domain) = verifier.next_entry() {
1564+ verifier.verify(*dns_records.get(domain.as_str()).unwrap());
1565+ }
1566+ assert!(
1567+ matches!(verifier.dkim_result, AuthResult::Pass(_)),
1568+ "Failed: {:?}",
1569+ verifier.dkim_result
1570+ );
1571+ }
1572+ }
1573+
1574+ #[test]
1575+ fn arc_verify() {
1576+ let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1577+ test_dir.push("resources");
1578+ test_dir.push("arc");
1579+
1580+ for file_name in fs::read_dir(&test_dir).unwrap() {
1581+ let file_name = file_name.unwrap().path();
1582+ if !file_name.to_str().unwrap().contains("002") {
1583+ continue;
1584+ }
1585+ println!("file {}", file_name.to_str().unwrap());
1586+
1587+ let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
1588+ let (dns_records, message) = test.split_once("\n\n").unwrap();
1589+ let dns_records = dns_records
1590+ .split('\n')
1591+ .filter_map(|r| r.split_once(' ').map(|(a, b)| (a, b.as_bytes())))
1592+ .collect::<HashMap<_, _>>();
1593+ let message = message.replace('\n', "\r\n");
1594+
1595+ let mut verifier = AuthenticatedMessage::new_(message.as_bytes(), 1667843664).unwrap();
1596+ while let Some(domain) = verifier.next_entry() {
1597+ verifier.verify(*dns_records.get(domain.as_str()).unwrap());
1598+ }
1599+
1600+ println!("DKIM: {:?}", verifier.dkim_result);
1601+ println!("ARC: {:?}", verifier.arc_result);
1602+
1603+ /*assert!(
1604+ matches!(verifier.dkim_result, AuthResult::Pass(_)),
1605+ "Failed: {:?}",
1606+ verifier.dkim_result
1607+ );*/
1608+ }
1609+ }
1610+ }
1611 diff --git a/src/dkim/canonicalize.rs b/src/dkim/canonicalize.rs
1612index 62d2681..79152fa 100644
1613--- a/src/dkim/canonicalize.rs
1614+++ b/src/dkim/canonicalize.rs
1615 @@ -11,19 +11,21 @@
1616
1617 use std::io::Write;
1618
1619+ use sha1::Digest;
1620+
1621 use crate::common::headers::HeaderIterator;
1622
1623 use super::{Canonicalization, DKIMSigner};
1624
1625 impl Canonicalization {
1626- pub fn canonicalize_body(&self, message: &[u8], mut hasher: impl Write) -> std::io::Result<()> {
1627+ pub fn canonicalize_body(&self, body: &[u8], mut hasher: impl Write) -> std::io::Result<()> {
1628 let mut crlf_seq = 0;
1629
1630 match self {
1631 Canonicalization::Relaxed => {
1632 let mut last_ch = 0;
1633
1634- for &ch in message {
1635+ for &ch in body {
1636 match ch {
1637 b' ' | b'\t' => {
1638 while crlf_seq > 0 {
1639 @@ -53,7 +55,7 @@ impl Canonicalization {
1640 }
1641 }
1642 Canonicalization::Simple => {
1643- for &ch in message {
1644+ for &ch in body {
1645 match ch {
1646 b'\n' => {
1647 crlf_seq += 1;
1648 @@ -117,6 +119,34 @@ impl Canonicalization {
1649 Ok(())
1650 }
1651
1652+ pub fn hash_headers<'x, T>(
1653+ &self,
1654+ headers: impl Iterator<Item = (&'x [u8], &'x [u8])>,
1655+ ) -> std::io::Result<Vec<u8>>
1656+ where
1657+ T: Digest + std::io::Write,
1658+ {
1659+ let mut hasher = T::new();
1660+ self.canonicalize_headers(headers, &mut hasher)?;
1661+ Ok(hasher.finalize().to_vec())
1662+ }
1663+
1664+ pub fn hash_body<T>(&self, body: &[u8], l: u64) -> std::io::Result<Vec<u8>>
1665+ where
1666+ T: Digest + std::io::Write,
1667+ {
1668+ let mut hasher = T::new();
1669+ self.canonicalize_body(
1670+ if l == 0 || body.is_empty() {
1671+ body
1672+ } else {
1673+ &body[..std::cmp::min(l as usize, body.len())]
1674+ },
1675+ &mut hasher,
1676+ )?;
1677+ Ok(hasher.finalize().to_vec())
1678+ }
1679+
1680 pub fn serialize_name(&self, mut writer: impl Write) -> std::io::Result<()> {
1681 writer.write_all(match self {
1682 Canonicalization::Relaxed => b"relaxed",
1683 @@ -132,7 +162,7 @@ impl<'x> DKIMSigner<'x> {
1684 message: &[u8],
1685 header_hasher: impl Write,
1686 body_hasher: impl Write,
1687- ) -> super::Result<(usize, Vec<Vec<u8>>)> {
1688+ ) -> crate::Result<(usize, Vec<Vec<u8>>)> {
1689 let mut headers_it = HeaderIterator::new(message);
1690 let mut headers = Vec::with_capacity(self.sign_headers.len());
1691 let mut found_headers = vec![false; self.sign_headers.len()];
1692 diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs
1693index c04d28f..9e14ffc 100644
1694--- a/src/dkim/mod.rs
1695+++ b/src/dkim/mod.rs
1696 @@ -9,10 +9,12 @@
1697 * except according to those terms.
1698 */
1699
1700- use std::{borrow::Cow, fmt::Display};
1701+ use std::borrow::Cow;
1702
1703 use rsa::{RsaPrivateKey, RsaPublicKey};
1704
1705+ use crate::common::verify::VerifySignature;
1706+
1707 pub mod canonicalize;
1708 pub mod parse;
1709 pub mod sign;
1710 @@ -37,40 +39,6 @@ pub enum Algorithm {
1711 RsaSha256,
1712 Ed25519Sha256,
1713 }
1714-
1715- #[derive(Debug)]
1716- pub enum Error {
1717- ParseError,
1718- MissingParameters,
1719- NoHeadersFound,
1720- RSA(rsa::errors::Error),
1721- PKCS(rsa::pkcs1::Error),
1722- Ed25519Signature(ed25519_dalek::SignatureError),
1723- Ed25519(ed25519_dalek::ed25519::Error),
1724-
1725- /// I/O error
1726- Io(std::io::Error),
1727-
1728- /// Base64 decode/encode error
1729- Base64,
1730-
1731- UnsupportedVersion,
1732- UnsupportedAlgorithm,
1733- UnsupportedCanonicalization,
1734-
1735- UnsupportedRecordVersion,
1736- UnsupportedKeyType,
1737-
1738- FailedBodyHashMatch,
1739- RevokedPublicKey,
1740- IncompatibleAlgorithms,
1741- FailedVerification,
1742- SignatureExpired,
1743- FailedAUIDMatch,
1744- }
1745-
1746- pub type Result<T> = std::result::Result<T, Error>;
1747-
1748 #[derive(Debug)]
1749 pub struct DKIMSigner<'x> {
1750 private_key: PrivateKey,
1751 @@ -87,27 +55,27 @@ pub struct DKIMSigner<'x> {
1752
1753 #[derive(Debug, PartialEq, Eq, Clone)]
1754 pub struct Signature<'x> {
1755- v: u32,
1756- a: Algorithm,
1757- d: Cow<'x, [u8]>,
1758- s: Cow<'x, [u8]>,
1759- b: Vec<u8>,
1760- bh: Vec<u8>,
1761- h: Vec<Vec<u8>>,
1762- z: Vec<Vec<u8>>,
1763- i: Cow<'x, [u8]>,
1764- l: u64,
1765- x: u64,
1766- t: u64,
1767- ch: Canonicalization,
1768- cb: Canonicalization,
1769+ pub(crate) v: u32,
1770+ pub(crate) a: Algorithm,
1771+ pub(crate) d: Cow<'x, [u8]>,
1772+ pub(crate) s: Cow<'x, [u8]>,
1773+ pub(crate) b: Vec<u8>,
1774+ pub(crate) bh: Vec<u8>,
1775+ pub(crate) h: Vec<Vec<u8>>,
1776+ pub(crate) z: Vec<Vec<u8>>,
1777+ pub(crate) i: Cow<'x, [u8]>,
1778+ pub(crate) l: u64,
1779+ pub(crate) x: u64,
1780+ pub(crate) t: u64,
1781+ pub(crate) ch: Canonicalization,
1782+ pub(crate) cb: Canonicalization,
1783 }
1784
1785 #[derive(Debug, PartialEq, Eq, Clone)]
1786 pub struct Record {
1787- v: Version,
1788- p: PublicKey,
1789- f: u64,
1790+ pub(crate) v: Version,
1791+ pub(crate) p: PublicKey,
1792+ pub(crate) f: u64,
1793 }
1794
1795 pub(crate) const R_HASH_SHA1: u64 = 0x01;
1796 @@ -168,46 +136,21 @@ pub(crate) enum PublicKey {
1797 Revoked,
1798 }
1799
1800- impl Display for Error {
1801- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1802- match self {
1803- Error::ParseError => write!(f, "Parse error"),
1804- Error::MissingParameters => write!(f, "Missing parameters"),
1805- Error::NoHeadersFound => write!(f, "No headers found"),
1806- Error::RSA(err) => write!(f, "RSA error: {}", err),
1807- Error::PKCS(err) => write!(f, "PKCS error: {}", err),
1808- Error::Io(e) => write!(f, "I/O error: {}", e),
1809- Error::Base64 => write!(f, "Base64 encode or decode error."),
1810- Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature."),
1811- Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature."),
1812- Error::UnsupportedCanonicalization => {
1813- write!(f, "Unsupported canonicalization method in DKIM Signature.")
1814- }
1815- Error::UnsupportedRecordVersion => {
1816- write!(f, "Unsupported version in DKIM DNS record.")
1817- }
1818- Error::UnsupportedKeyType => {
1819- write!(f, "Unsupported key type in DKIM DNS record.")
1820- }
1821- Error::Ed25519Signature(err) => write!(f, "Ed25519 signature error: {}", err),
1822- Error::Ed25519(err) => write!(f, "Ed25519 error: {}", err),
1823- Error::FailedBodyHashMatch => {
1824- write!(f, "Calculated body hash does not match signature hash.")
1825- }
1826- Error::RevokedPublicKey => write!(f, "Public key for this signature has been revoked."),
1827- Error::IncompatibleAlgorithms => write!(
1828- f,
1829- "Incompatible algorithms used in signature and DKIM DNS record."
1830- ),
1831- Error::FailedVerification => write!(f, "Signature verification failed."),
1832- Error::SignatureExpired => write!(f, "Signature expired."),
1833- Error::FailedAUIDMatch => write!(f, "AUID does not match domain name."),
1834+ impl From<Algorithm> for HashAlgorithm {
1835+ fn from(a: Algorithm) -> Self {
1836+ match a {
1837+ Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => HashAlgorithm::Sha256,
1838+ Algorithm::RsaSha1 => HashAlgorithm::Sha1,
1839 }
1840 }
1841 }
1842
1843- impl From<std::io::Error> for Error {
1844- fn from(err: std::io::Error) -> Self {
1845- Error::Io(err)
1846+ impl<'x> VerifySignature for Signature<'x> {
1847+ fn b(&self) -> &[u8] {
1848+ &self.b
1849+ }
1850+
1851+ fn a(&self) -> Algorithm {
1852+ self.a
1853 }
1854 }
1855 diff --git a/src/dkim/parse.rs b/src/dkim/parse.rs
1856index 3bb9355..f864bcd 100644
1857--- a/src/dkim/parse.rs
1858+++ b/src/dkim/parse.rs
1859 @@ -1,18 +1,18 @@
1860- use std::slice::Iter;
1861+ use std::{borrow::Cow, slice::Iter};
1862
1863 use mail_parser::decoders::base64::base64_decode_stream;
1864 use rsa::RsaPublicKey;
1865
1866- use crate::common::parse::*;
1867+ use crate::{common::parse::*, Error};
1868
1869 use super::{
1870- Algorithm, Canonicalization, Error, Flag, HashAlgorithm, PublicKey, Record, Service, Signature,
1871+ Algorithm, Canonicalization, Flag, HashAlgorithm, PublicKey, Record, Service, Signature,
1872 Version,
1873 };
1874
1875 impl<'x> Signature<'x> {
1876 #[allow(clippy::while_let_on_iterator)]
1877- pub fn parse(header: &'_ [u8]) -> super::Result<Self> {
1878+ pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
1879 let mut signature = Signature {
1880 v: 0,
1881 a: Algorithm::RsaSha256,
1882 @@ -85,15 +85,15 @@ pub(crate) trait SignatureParser: Sized {
1883 fn canonicalization(
1884 &mut self,
1885 default: Canonicalization,
1886- ) -> super::Result<(Canonicalization, Canonicalization)>;
1887- fn algorithm(&mut self) -> super::Result<Algorithm>;
1888+ ) -> crate::Result<(Canonicalization, Canonicalization)>;
1889+ fn algorithm(&mut self) -> crate::Result<Algorithm>;
1890 }
1891
1892 impl SignatureParser for Iter<'_, u8> {
1893 fn canonicalization(
1894 &mut self,
1895 default: Canonicalization,
1896- ) -> super::Result<(Canonicalization, Canonicalization)> {
1897+ ) -> crate::Result<(Canonicalization, Canonicalization)> {
1898 let mut cb = default;
1899 let mut ch = default;
1900
1901 @@ -143,7 +143,7 @@ impl SignatureParser for Iter<'_, u8> {
1902 Ok((ch, cb))
1903 }
1904
1905- fn algorithm(&mut self) -> super::Result<Algorithm> {
1906+ fn algorithm(&mut self) -> crate::Result<Algorithm> {
1907 match self.next_skip_whitespaces().unwrap_or(0) {
1908 b'r' | b'R' => {
1909 if self.match_bytes(b"sa-sha") {
1910 @@ -195,7 +195,7 @@ enum KeyType {
1911
1912 impl Record {
1913 #[allow(clippy::while_let_on_iterator)]
1914- pub fn parse(header: &[u8]) -> super::Result<Self> {
1915+ pub fn parse(header: &[u8]) -> crate::Result<Self> {
1916 let header_len = header.len();
1917 let mut header = header.iter();
1918 let mut record = Record {
1919 @@ -255,11 +255,11 @@ impl Record {
1920 KeyType::Rsa | KeyType::None => PublicKey::Rsa(
1921 <RsaPublicKey as rsa::pkcs8::DecodePublicKey>::from_public_key_der(&public_key)
1922 .or_else(|_| rsa::pkcs1::DecodeRsaPublicKey::from_pkcs1_der(&public_key))
1923- .map_err(Error::PKCS)?,
1924+ .map_err(|err| Error::CryptoError(err.to_string()))?,
1925 ),
1926 KeyType::Ed25519 => PublicKey::Ed25519(
1927 ed25519_dalek::PublicKey::from_bytes(&public_key)
1928- .map_err(Error::Ed25519Signature)?,
1929+ .map_err(|err| Error::CryptoError(err.to_string()))?,
1930 ),
1931 }
1932 }
1933 @@ -272,6 +272,55 @@ impl Record {
1934 }
1935 }
1936
1937+ pub trait TryIntoRecord<'x>: Sized {
1938+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>>;
1939+ }
1940+
1941+ impl<'x> TryIntoRecord<'x> for Record {
1942+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1943+ Ok(Cow::Owned(self))
1944+ }
1945+ }
1946+
1947+ impl<'x> TryIntoRecord<'x> for &'x Record {
1948+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1949+ Ok(Cow::Borrowed(self))
1950+ }
1951+ }
1952+
1953+ impl<'x> TryIntoRecord<'x> for String {
1954+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1955+ Record::parse(self.as_bytes()).map(Cow::Owned)
1956+ }
1957+ }
1958+
1959+ impl<'x> TryIntoRecord<'x> for &str {
1960+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1961+ Record::parse(self.as_bytes()).map(Cow::Owned)
1962+ }
1963+ }
1964+
1965+ impl<'x> TryIntoRecord<'x> for &[u8] {
1966+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1967+ Record::parse(self).map(Cow::Owned)
1968+ }
1969+ }
1970+
1971+ impl<'x> TryIntoRecord<'x> for Vec<u8> {
1972+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1973+ Record::parse(&self).map(Cow::Owned)
1974+ }
1975+ }
1976+
1977+ impl<'x, T: TryIntoRecord<'x> + Sized> TryIntoRecord<'x> for Option<T> {
1978+ fn try_into_record(self) -> crate::Result<Cow<'x, Record>> {
1979+ match self {
1980+ Some(v) => v.try_into_record(),
1981+ None => Err(Error::DNSFailure),
1982+ }
1983+ }
1984+ }
1985+
1986 impl ItemParser for HashAlgorithm {
1987 fn parse(bytes: &[u8]) -> Option<Self> {
1988 if bytes.eq_ignore_ascii_case(b"sha256") {
1989 diff --git a/src/dkim/sign.rs b/src/dkim/sign.rs
1990index 5da2dcc..06a337b 100644
1991--- a/src/dkim/sign.rs
1992+++ b/src/dkim/sign.rs
1993 @@ -22,7 +22,9 @@ use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::AssociatedOid, PaddingScheme, RsaPr
1994 use sha1::Sha1;
1995 use sha2::{Digest, Sha256};
1996
1997- use super::{Algorithm, Canonicalization, DKIMSigner, Error, PrivateKey, Signature};
1998+ use crate::Error;
1999+
2000+ use super::{Algorithm, Canonicalization, DKIMSigner, PrivateKey, Signature};
2001
2002 impl<'x> DKIMSigner<'x> {
2003 /// Creates a new DKIM signer from an RsaPrivateKey.
2004 @@ -42,16 +44,20 @@ impl<'x> DKIMSigner<'x> {
2005 }
2006
2007 /// Creates a new RSA private key from a PKCS1 PEM string.
2008- pub fn rsa_pem(mut self, private_key_pem: &str) -> super::Result<Self> {
2009- self.private_key =
2010- PrivateKey::Rsa(RsaPrivateKey::from_pkcs1_pem(private_key_pem).map_err(Error::PKCS)?);
2011+ pub fn rsa_pem(mut self, private_key_pem: &str) -> crate::Result<Self> {
2012+ self.private_key = PrivateKey::Rsa(
2013+ RsaPrivateKey::from_pkcs1_pem(private_key_pem)
2014+ .map_err(|err| Error::CryptoError(err.to_string()))?,
2015+ );
2016 Ok(self)
2017 }
2018
2019 /// Creates a new RSA private key from a PKCS1 binary slice.
2020- pub fn rsa(mut self, private_key_bytes: &[u8]) -> super::Result<Self> {
2021- self.private_key =
2022- PrivateKey::Rsa(RsaPrivateKey::from_pkcs1_der(private_key_bytes).map_err(Error::PKCS)?);
2023+ pub fn rsa(mut self, private_key_bytes: &[u8]) -> crate::Result<Self> {
2024+ self.private_key = PrivateKey::Rsa(
2025+ RsaPrivateKey::from_pkcs1_der(private_key_bytes)
2026+ .map_err(|err| Error::CryptoError(err.to_string()))?,
2027+ );
2028 Ok(self)
2029 }
2030
2031 @@ -60,12 +66,12 @@ impl<'x> DKIMSigner<'x> {
2032 mut self,
2033 public_key_bytes: &[u8],
2034 private_key_bytes: &[u8],
2035- ) -> super::Result<Self> {
2036+ ) -> crate::Result<Self> {
2037 self.private_key = PrivateKey::Ed25519(ed25519_dalek::Keypair {
2038 public: ed25519_dalek::PublicKey::from_bytes(public_key_bytes)
2039- .map_err(Error::Ed25519)?,
2040+ .map_err(|err| Error::CryptoError(err.to_string()))?,
2041 secret: ed25519_dalek::SecretKey::from_bytes(private_key_bytes)
2042- .map_err(Error::Ed25519Signature)?,
2043+ .map_err(|err| Error::CryptoError(err.to_string()))?,
2044 });
2045 self.a = Algorithm::Ed25519Sha256;
2046 Ok(self)
2047 @@ -139,7 +145,7 @@ impl<'x> DKIMSigner<'x> {
2048
2049 /// Signs a message.
2050 #[inline(always)]
2051- pub fn sign(&self, message: &[u8]) -> super::Result<Signature> {
2052+ pub fn sign(&self, message: &[u8]) -> crate::Result<Signature> {
2053 if !self.d.is_empty() && !self.s.is_empty() {
2054 let now = SystemTime::now()
2055 .duration_since(SystemTime::UNIX_EPOCH)
2056 @@ -156,7 +162,7 @@ impl<'x> DKIMSigner<'x> {
2057 }
2058 }
2059
2060- fn sign_<T>(&self, message: &[u8], now: u64) -> super::Result<Signature>
2061+ fn sign_<T>(&self, message: &[u8], now: u64) -> crate::Result<Signature>
2062 where
2063 T: Digest + AssociatedOid + std::io::Write,
2064 {
2065 @@ -199,7 +205,7 @@ impl<'x> DKIMSigner<'x> {
2066 PaddingScheme::new_pkcs1v15_sign::<T>(),
2067 &header_hasher.finalize(),
2068 )
2069- .map_err(Error::RSA)?,
2070+ .map_err(|err| Error::CryptoError(err.to_string()))?,
2071 PrivateKey::Ed25519(key_pair) => {
2072 key_pair.sign(&header_hasher.finalize()).to_bytes().to_vec()
2073 }
2074 @@ -347,8 +353,8 @@ mod test {
2075 use sha2::Sha256;
2076
2077 use crate::{
2078- common::headers::HeaderIterator,
2079- dkim::{verify::DKIMVerifier, Canonicalization, Record, Signature},
2080+ common::{AuthResult, AuthenticatedMessage},
2081+ dkim::{Canonicalization, Signature},
2082 };
2083
2084 const RSA_PRIVATE_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY-----
2085 @@ -556,25 +562,15 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
2086 message.extend_from_slice(message_.as_bytes());
2087 //println!("[{}]", String::from_utf8_lossy(&message));
2088
2089- let mut headers_it = HeaderIterator::new(&message);
2090- let headers = (&mut headers_it).collect::<Vec<_>>();
2091- let body = headers_it
2092- .body_offset()
2093- .and_then(|pos| message.get(pos..))
2094- .unwrap_or_default();
2095- let mut verifier = DKIMVerifier::new(&headers, body);
2096- let mut num_signatures = 0;
2097-
2098- while let Some(signature) = verifier.next_signature() {
2099- let signature = signature.unwrap();
2100- let record = Record::parse(public_key.as_bytes()).unwrap();
2101- match (verifier.verify(&signature, &record), &expect) {
2102- (Ok(_), Ok(_)) | (Err(_), Err(_)) => (),
2103- (result, expect) => panic!("Expected {:?} but got {:?}.", expect, result),
2104- }
2105- num_signatures += 1;
2106+ let mut verifier = AuthenticatedMessage::new(&message).unwrap();
2107+ while verifier.next_entry().is_some() {
2108+ verifier.verify(public_key);
2109 }
2110
2111- assert_ne!(num_signatures, 0);
2112+ match (verifier.dkim_result, &expect) {
2113+ (AuthResult::Pass(_), Ok(_)) => (),
2114+ (AuthResult::PermFail(hdr), Err(err)) if &hdr.header == err => (),
2115+ (result, expect) => panic!("Expected {:?} but got {:?}.", expect, result),
2116+ }
2117 }
2118 }
2119 diff --git a/src/dkim/verify.rs b/src/dkim/verify.rs
2120index ef3f9bd..19fb50f 100644
2121--- a/src/dkim/verify.rs
2122+++ b/src/dkim/verify.rs
2123 @@ -1,129 +1,16 @@
2124- use std::{iter::Enumerate, slice::Iter, time::SystemTime};
2125-
2126- use rsa::PaddingScheme;
2127- use sha1::{Digest, Sha1};
2128- use sha2::Sha256;
2129-
2130- use super::{Algorithm, Canonicalization, Flag, HashAlgorithm, PublicKey, Record, Signature};
2131-
2132- pub struct DKIMVerifier<'x> {
2133- headers: &'x [(&'x [u8], &'x [u8])],
2134- headers_iter: Enumerate<Iter<'x, (&'x [u8], &'x [u8])>>,
2135- headers_pos: usize,
2136- body: &'x [u8],
2137- body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
2138- }
2139-
2140- #[derive(Debug)]
2141- pub struct Error<'x> {
2142- pub(crate) error: super::Error,
2143- pub(crate) header: &'x [u8],
2144- }
2145-
2146- impl<'x> DKIMVerifier<'x> {
2147- pub fn new(headers: &'x [(&'x [u8], &'x [u8])], body: &'x [u8]) -> Self {
2148- DKIMVerifier {
2149- headers,
2150- headers_iter: headers.iter().enumerate(),
2151- headers_pos: 0,
2152- body,
2153- body_hashes: Vec::new(),
2154- }
2155- }
2156-
2157- #[allow(clippy::while_let_on_iterator)]
2158- pub fn verify(&mut self, signature: &Signature, record: &Record) -> Result<(), Error> {
2159- let raw_signature = self.headers[self.headers_pos];
2160-
2161- // Make sure the signature has not expired
2162- if signature.x > 0 && signature.t > 0 {
2163- let now = SystemTime::now()
2164- .duration_since(SystemTime::UNIX_EPOCH)
2165- .map(|d| d.as_secs())
2166- .unwrap_or(0);
2167- if signature.x < signature.t || signature.x < now {
2168- return Err(Error::new(super::Error::SignatureExpired, raw_signature.1));
2169- }
2170- }
2171-
2172- // Enforce t=s flag
2173- if !signature.i.is_empty() && record.has_flag(Flag::MatchDomain) {
2174- let mut auid = signature.i.as_ref().iter();
2175- let mut domain = signature.d.as_ref().iter();
2176- while let Some(&ch) = auid.next() {
2177- if ch == b'@' {
2178- break;
2179- }
2180- }
2181- while let Some(ch) = auid.next() {
2182- if let Some(dch) = domain.next() {
2183- if !ch.eq_ignore_ascii_case(dch) {
2184- return Err(Error::new(super::Error::FailedAUIDMatch, raw_signature.1));
2185- }
2186- } else {
2187- break;
2188- }
2189- }
2190- if domain.next().is_some() {
2191- return Err(Error::new(super::Error::FailedAUIDMatch, raw_signature.1));
2192- }
2193- }
2194-
2195- // Canonicalize the message body and calculate its hash
2196- let bh = if let Some((_, _, _, bh)) = self.body_hashes.iter().find(|(c, h, l, _)| {
2197- c == &signature.cb
2198- && (matches!(
2199- (signature.a, h),
2200- (
2201- Algorithm::RsaSha256 | Algorithm::Ed25519Sha256,
2202- HashAlgorithm::Sha256
2203- ) | (Algorithm::RsaSha1, HashAlgorithm::Sha1)
2204- ) && l == &signature.l)
2205- }) {
2206- bh
2207- } else {
2208- let body = if signature.l == 0 || self.body.is_empty() {
2209- self.body
2210- } else {
2211- &self.body[..std::cmp::min(signature.l as usize, self.body.len())]
2212- };
2213- let (bh, h) = match signature.a {
2214- Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
2215- let mut hasher = Sha256::new();
2216- signature
2217- .cb
2218- .canonicalize_body(body, &mut hasher)
2219- .map_err(|err| Error::new(err.into(), raw_signature.1))?;
2220- (hasher.finalize().to_vec(), HashAlgorithm::Sha256)
2221- }
2222- Algorithm::RsaSha1 => {
2223- let mut hasher = Sha1::new();
2224- signature
2225- .cb
2226- .canonicalize_body(body, &mut hasher)
2227- .map_err(|err| Error::new(err.into(), raw_signature.1))?;
2228- (hasher.finalize().to_vec(), HashAlgorithm::Sha1)
2229- }
2230- };
2231- self.body_hashes.push((signature.cb, h, signature.l, bh));
2232- &self.body_hashes.last().unwrap().3
2233- };
2234-
2235- // Check that the body hash matches
2236- if bh != &signature.bh {
2237- return Err(Error::new(
2238- super::Error::FailedBodyHashMatch,
2239- raw_signature.1,
2240- ));
2241- }
2242-
2243- // Create header iterator
2244+ use crate::common::AuthenticatedMessage;
2245+
2246+ impl<'x> AuthenticatedMessage<'x> {
2247+ pub fn signed_headers<'z: 'x>(
2248+ &'z self,
2249+ headers: &'x [Vec<u8>],
2250+ dkim_hdr_name: &'x [u8],
2251+ dkim_hdr_value: &'x [u8],
2252+ ) -> impl Iterator<Item = (&'x [u8], &'x [u8])> {
2253 let mut last_header_pos: Vec<(&[u8], usize)> = Vec::new();
2254- let unsigned_dkim = strip_signature(raw_signature.1);
2255- let headers = signature
2256- .h
2257+ headers
2258 .iter()
2259- .filter_map(|h| {
2260+ .filter_map(move |h| {
2261 let header_pos = if let Some((_, header_pos)) = last_header_pos
2262 .iter_mut()
2263 .find(|(lh, _)| lh.eq_ignore_ascii_case(h))
2264 @@ -148,175 +35,58 @@ impl<'x> DKIMVerifier<'x> {
2265 None
2266 }
2267 })
2268- .chain([(raw_signature.0, unsigned_dkim.as_ref())]);
2269-
2270- // Canonicalize and hash headers
2271- let hh = match signature.a {
2272- Algorithm::RsaSha256 | Algorithm::Ed25519Sha256 => {
2273- let mut hasher = Sha256::new();
2274- signature
2275- .ch
2276- .canonicalize_headers(headers, &mut hasher)
2277- .map_err(|err| Error::new(err.into(), raw_signature.1))?;
2278- hasher.finalize().to_vec()
2279- }
2280- Algorithm::RsaSha1 => {
2281- let mut hasher = Sha1::new();
2282- signature
2283- .ch
2284- .canonicalize_headers(headers, &mut hasher)
2285- .map_err(|err| Error::new(err.into(), raw_signature.1))?;
2286- hasher.finalize().to_vec()
2287- }
2288- };
2289-
2290- // Verify signature
2291- match (&signature.a, &record.p) {
2292- (Algorithm::RsaSha256, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
2293- public_key,
2294- PaddingScheme::new_pkcs1v15_sign::<Sha256>(),
2295- &hh,
2296- &signature.b,
2297- )
2298- .map_err(|_| Error::new(super::Error::FailedVerification, raw_signature.1)),
2299-
2300- (Algorithm::RsaSha1, PublicKey::Rsa(public_key)) => rsa::PublicKey::verify(
2301- public_key,
2302- PaddingScheme::new_pkcs1v15_sign::<Sha1>(),
2303- &hh,
2304- &signature.b,
2305- )
2306- .map_err(|_| Error::new(super::Error::FailedVerification, raw_signature.1)),
2307-
2308- (Algorithm::Ed25519Sha256, PublicKey::Ed25519(public_key)) => public_key
2309- .verify_strict(
2310- &hh,
2311- &ed25519_dalek::Signature::from_bytes(&signature.b).map_err(|err| {
2312- Error::new(super::Error::Ed25519Signature(err), raw_signature.1)
2313- })?,
2314- )
2315- .map_err(|_| Error::new(super::Error::FailedVerification, raw_signature.1)),
2316-
2317- (_, PublicKey::Revoked) => {
2318- Err(Error::new(super::Error::RevokedPublicKey, raw_signature.1))
2319- }
2320-
2321- (_, _) => Err(Error::new(
2322- super::Error::IncompatibleAlgorithms,
2323- raw_signature.1,
2324- )),
2325- }
2326+ .chain([(dkim_hdr_name, dkim_hdr_value)])
2327 }
2328+ }
2329
2330- pub fn next_signature<'z>(&mut self) -> Option<Result<Signature<'z>, Error<'x>>> {
2331- for (pos, (name, value)) in &mut self.headers_iter {
2332- if name.eq_ignore_ascii_case(b"dkim-signature") {
2333- self.headers_pos = pos;
2334- return Signature::parse(value)
2335- .map_err(|error| Error {
2336- error,
2337- header: value,
2338- })
2339- .into();
2340- }
2341- }
2342-
2343- None
2344- }
2345+ pub(crate) trait Verifier: Sized {
2346+ fn strip_signature(&self) -> Vec<u8>;
2347 }
2348
2349- fn strip_signature(bytes: &[u8]) -> Vec<u8> {
2350- let mut unsigned_dkim = Vec::with_capacity(bytes.len());
2351- let mut iter = bytes.iter().enumerate();
2352- let mut last_ch = b';';
2353- while let Some((pos, &ch)) = iter.next() {
2354- match ch {
2355- b'=' if last_ch == b'b' => {
2356- unsigned_dkim.push(ch);
2357- #[allow(clippy::while_let_on_iterator)]
2358- while let Some((_, &ch)) = iter.next() {
2359- if ch == b';' {
2360- unsigned_dkim.push(b';');
2361- break;
2362+ impl Verifier for &[u8] {
2363+ fn strip_signature(&self) -> Vec<u8> {
2364+ let mut unsigned_dkim = Vec::with_capacity(self.len());
2365+ let mut iter = self.iter().enumerate();
2366+ let mut last_ch = b';';
2367+ while let Some((pos, &ch)) = iter.next() {
2368+ match ch {
2369+ b'=' if last_ch == b'b' => {
2370+ unsigned_dkim.push(ch);
2371+ #[allow(clippy::while_let_on_iterator)]
2372+ while let Some((_, &ch)) = iter.next() {
2373+ if ch == b';' {
2374+ unsigned_dkim.push(b';');
2375+ break;
2376+ }
2377 }
2378- }
2379- last_ch = 0;
2380- }
2381- b'b' | b'B' if last_ch == b';' => {
2382- last_ch = b'b';
2383- unsigned_dkim.push(ch);
2384- }
2385- b';' => {
2386- last_ch = b';';
2387- unsigned_dkim.push(ch);
2388- }
2389- b'\r' if pos == bytes.len() - 2 => (),
2390- b'\n' if pos == bytes.len() - 1 => (),
2391- _ => {
2392- unsigned_dkim.push(ch);
2393- if !ch.is_ascii_whitespace() {
2394 last_ch = 0;
2395 }
2396+ b'b' | b'B' if last_ch == b';' => {
2397+ last_ch = b'b';
2398+ unsigned_dkim.push(ch);
2399+ }
2400+ b';' => {
2401+ last_ch = b';';
2402+ unsigned_dkim.push(ch);
2403+ }
2404+ b'\r' if pos == self.len() - 2 => (),
2405+ b'\n' if pos == self.len() - 1 => (),
2406+ _ => {
2407+ unsigned_dkim.push(ch);
2408+ if !ch.is_ascii_whitespace() {
2409+ last_ch = 0;
2410+ }
2411+ }
2412 }
2413 }
2414- }
2415- unsigned_dkim
2416- }
2417-
2418- impl<'x> Error<'x> {
2419- pub fn new(error: super::Error, header: &'x [u8]) -> Self {
2420- Error { error, header }
2421+ unsigned_dkim
2422 }
2423 }
2424
2425 #[cfg(test)]
2426 mod test {
2427- use std::{collections::HashMap, fs, path::PathBuf};
2428-
2429- use crate::{common::headers::HeaderIterator, dkim::Record};
2430-
2431- use super::{strip_signature, DKIMVerifier};
2432-
2433- #[test]
2434- fn dkim_verify() {
2435- let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2436- test_dir.push("resources");
2437- test_dir.push("dkim");
2438-
2439- for file_name in fs::read_dir(&test_dir).unwrap() {
2440- let file_name = file_name.unwrap().path();
2441- /*if !file_name.to_str().unwrap().contains("002") {
2442- continue;
2443- }*/
2444- println!("file {}", file_name.to_str().unwrap());
2445-
2446- let test = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
2447- let (dns_records, message) = test.split_once("\n\n").unwrap();
2448- let dns_records = dns_records
2449- .split('\n')
2450- .filter_map(|r| r.split_once(' ').map(|(a, b)| (a.as_bytes(), b.as_bytes())))
2451- .collect::<HashMap<_, _>>();
2452- let message = message.replace('\n', "\r\n");
2453-
2454- let mut headers_it = HeaderIterator::new(message.as_bytes());
2455- let headers = (&mut headers_it).collect::<Vec<_>>();
2456- let body = headers_it
2457- .body_offset()
2458- .and_then(|pos| message.as_bytes().get(pos..))
2459- .unwrap_or_default();
2460- let mut verifier = DKIMVerifier::new(&headers, body);
2461- let mut num_signatures = 0;
2462
2463- while let Some(signature) = verifier.next_signature() {
2464- let signature = signature.unwrap();
2465- let record = Record::parse(dns_records.get(signature.s.as_ref()).unwrap()).unwrap();
2466- verifier.verify(&signature, &record).unwrap();
2467- num_signatures += 1;
2468- }
2469-
2470- assert_ne!(num_signatures, 0);
2471- }
2472- }
2473+ use crate::dkim::verify::Verifier;
2474
2475 #[test]
2476 fn dkim_strip_signature() {
2477 @@ -327,7 +97,7 @@ mod test {
2478 ("B\r\n=abc;v=1\r\n", "B\r\n=;v=1"),
2479 ] {
2480 assert_eq!(
2481- String::from_utf8(strip_signature(value.as_bytes())).unwrap(),
2482+ String::from_utf8(value.as_bytes().strip_signature()).unwrap(),
2483 stripped_value
2484 );
2485 }
2486 diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs
2487index 299dc6f..cf0d610 100644
2488--- a/src/dmarc/mod.rs
2489+++ b/src/dmarc/mod.rs
2490 @@ -56,6 +56,7 @@ impl From<Format> for u64 {
2491 }
2492
2493 impl URI {
2494+ #[cfg(test)]
2495 pub fn new(uri: impl Into<String>, max_size: usize) -> Self {
2496 URI {
2497 uri: uri.into().into_bytes(),
2498 @@ -63,12 +64,3 @@ impl URI {
2499 }
2500 }
2501 }
2502-
2503- #[derive(Debug)]
2504- pub enum Error {
2505- InvalidVersion,
2506- InvalidRecord,
2507- ParseFailed,
2508- }
2509-
2510- pub type Result<T> = std::result::Result<T, Error>;
2511 diff --git a/src/dmarc/parse.rs b/src/dmarc/parse.rs
2512index 53a94c2..0bb0d23 100644
2513--- a/src/dmarc/parse.rs
2514+++ b/src/dmarc/parse.rs
2515 @@ -2,12 +2,15 @@ use std::slice::Iter;
2516
2517 use mail_parser::decoders::quoted_printable::quoted_printable_decode_char;
2518
2519- use crate::common::parse::{ItemParser, TagParser, V};
2520+ use crate::{
2521+ common::parse::{ItemParser, TagParser, V},
2522+ Error,
2523+ };
2524
2525- use super::{Alignment, Error, Format, Policy, Report, DMARC, URI};
2526+ use super::{Alignment, Format, Policy, Report, DMARC, URI};
2527
2528 impl DMARC {
2529- pub fn parse(bytes: &[u8]) -> super::Result<Self> {
2530+ pub fn parse(bytes: &[u8]) -> crate::Result<Self> {
2531 let mut record = bytes.iter();
2532 if record.key().unwrap_or(0) != V {
2533 return Err(Error::InvalidRecord);
2534 @@ -47,14 +50,13 @@ impl DMARC {
2535 dmarc.p = record.policy()?;
2536 }
2537 PCT => {
2538- dmarc.pct =
2539- std::cmp::min(100, record.number().ok_or(Error::ParseFailed)?) as u8;
2540+ dmarc.pct = std::cmp::min(100, record.number().ok_or(Error::ParseError)?) as u8;
2541 }
2542 RF => {
2543 dmarc.rf = record.flags::<Format>() as u8;
2544 }
2545 RI => {
2546- dmarc.ri = record.number().ok_or(Error::ParseFailed)? as u32;
2547+ dmarc.ri = record.number().ok_or(Error::ParseError)? as u32;
2548 }
2549 RUA => {
2550 dmarc.rua = record.uris()?;
2551 @@ -83,57 +85,57 @@ impl DMARC {
2552 }
2553
2554 pub(crate) trait DMARCParser: Sized {
2555- fn alignment(&mut self) -> super::Result<Alignment>;
2556- fn report(&mut self) -> super::Result<Report>;
2557- fn policy(&mut self) -> super::Result<Policy>;
2558- fn uris(&mut self) -> super::Result<Vec<URI>>;
2559+ fn alignment(&mut self) -> crate::Result<Alignment>;
2560+ fn report(&mut self) -> crate::Result<Report>;
2561+ fn policy(&mut self) -> crate::Result<Policy>;
2562+ fn uris(&mut self) -> crate::Result<Vec<URI>>;
2563 }
2564
2565 impl DMARCParser for Iter<'_, u8> {
2566- fn alignment(&mut self) -> super::Result<Alignment> {
2567+ fn alignment(&mut self) -> crate::Result<Alignment> {
2568 let a = match self.next_skip_whitespaces().unwrap_or(0) {
2569 b'r' | b'R' => Alignment::Relaxed,
2570 b's' | b'S' => Alignment::Strict,
2571- _ => return Err(Error::ParseFailed),
2572+ _ => return Err(Error::ParseError),
2573 };
2574 if self.seek_tag_end() {
2575 Ok(a)
2576 } else {
2577- Err(Error::ParseFailed)
2578+ Err(Error::ParseError)
2579 }
2580 }
2581
2582- fn report(&mut self) -> super::Result<Report> {
2583+ fn report(&mut self) -> crate::Result<Report> {
2584 let r = match self.next_skip_whitespaces().unwrap_or(0) {
2585 b'0' => Report::All,
2586 b'1' => Report::Any,
2587 b'd' | b'D' => Report::Dkim,
2588 b's' | b'S' => Report::Spf,
2589- _ => return Err(Error::ParseFailed),
2590+ _ => return Err(Error::ParseError),
2591 };
2592 if self.seek_tag_end() {
2593 Ok(r)
2594 } else {
2595- Err(Error::ParseFailed)
2596+ Err(Error::ParseError)
2597 }
2598 }
2599
2600- fn policy(&mut self) -> super::Result<Policy> {
2601+ fn policy(&mut self) -> crate::Result<Policy> {
2602 let p = match self.next_skip_whitespaces().unwrap_or(0) {
2603 b'n' | b'N' if self.match_bytes(b"one") => Policy::None,
2604 b'q' | b'Q' if self.match_bytes(b"uarantine") => Policy::Quarantine,
2605 b'r' | b'R' if self.match_bytes(b"eject") => Policy::Reject,
2606- _ => return Err(Error::ParseFailed),
2607+ _ => return Err(Error::ParseError),
2608 };
2609 if self.seek_tag_end() {
2610 Ok(p)
2611 } else {
2612- Err(Error::ParseFailed)
2613+ Err(Error::ParseError)
2614 }
2615 }
2616
2617 #[allow(clippy::while_let_on_iterator)]
2618- fn uris(&mut self) -> super::Result<Vec<URI>> {
2619+ fn uris(&mut self) -> crate::Result<Vec<URI>> {
2620 let mut uris = Vec::new();
2621 let mut uri = Vec::with_capacity(16);
2622 let mut size: usize = 0;
2623 @@ -156,7 +158,7 @@ impl DMARCParser for Iter<'_, u8> {
2624 } else if ch == b';' {
2625 break 'outer;
2626 } else if !ch.is_ascii_whitespace() {
2627- return Err(Error::ParseFailed);
2628+ return Err(Error::ParseError);
2629 }
2630 }
2631 }
2632 @@ -203,7 +205,7 @@ impl DMARCParser for Iter<'_, u8> {
2633 }
2634 _ => {
2635 if !ch.is_ascii_whitespace() {
2636- return Err(Error::ParseFailed);
2637+ return Err(Error::ParseError);
2638 }
2639 }
2640 }
2641 diff --git a/src/lib.rs b/src/lib.rs
2642index d7bd8da..61f73a9 100644
2643--- a/src/lib.rs
2644+++ b/src/lib.rs
2645 @@ -9,12 +9,113 @@
2646 * except according to those terms.
2647 */
2648
2649+ use std::fmt::Display;
2650+
2651 pub mod arc;
2652 pub mod common;
2653 pub mod dkim;
2654 pub mod dmarc;
2655 pub mod spf;
2656
2657+ #[derive(Debug, Clone, PartialEq, Eq)]
2658+ pub enum Error {
2659+ ParseError,
2660+ MissingParameters,
2661+ NoHeadersFound,
2662+ CryptoError(String),
2663+ Io(String),
2664+ Base64,
2665+ UnsupportedVersion,
2666+ UnsupportedAlgorithm,
2667+ UnsupportedCanonicalization,
2668+ UnsupportedRecordVersion,
2669+ UnsupportedKeyType,
2670+ FailedBodyHashMatch,
2671+ RevokedPublicKey,
2672+ IncompatibleAlgorithms,
2673+ FailedVerification,
2674+ SignatureExpired,
2675+ FailedAUIDMatch,
2676+ DNSFailure,
2677+
2678+ ARCInvalidInstance,
2679+ ARCInvalidCV,
2680+ ARCHasHeaderTag,
2681+ ARCBrokenChain,
2682+
2683+ InvalidVersion,
2684+ InvalidRecord,
2685+
2686+ InvalidIp4,
2687+ InvalidIp6,
2688+ InvalidMacro,
2689+ }
2690+
2691+ pub type Result<T> = std::result::Result<T, Error>;
2692+
2693+ impl Display for Error {
2694+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2695+ match self {
2696+ Error::ParseError => write!(f, "Parse error"),
2697+ Error::MissingParameters => write!(f, "Missing parameters"),
2698+ Error::NoHeadersFound => write!(f, "No headers found"),
2699+ Error::CryptoError(err) => write!(f, "Cryptography layer error: {}", err),
2700+ Error::Io(e) => write!(f, "I/O error: {}", e),
2701+ Error::Base64 => write!(f, "Base64 encode or decode error."),
2702+ Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature."),
2703+ Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature."),
2704+ Error::UnsupportedCanonicalization => {
2705+ write!(f, "Unsupported canonicalization method in DKIM Signature.")
2706+ }
2707+ Error::UnsupportedRecordVersion => {
2708+ write!(f, "Unsupported version in DKIM DNS record.")
2709+ }
2710+ Error::UnsupportedKeyType => {
2711+ write!(f, "Unsupported key type in DKIM DNS record.")
2712+ }
2713+ Error::FailedBodyHashMatch => {
2714+ write!(f, "Calculated body hash does not match signature hash.")
2715+ }
2716+ Error::RevokedPublicKey => write!(f, "Public key for this signature has been revoked."),
2717+ Error::IncompatibleAlgorithms => write!(
2718+ f,
2719+ "Incompatible algorithms used in signature and DKIM DNS record."
2720+ ),
2721+ Error::FailedVerification => write!(f, "Signature verification failed."),
2722+ Error::SignatureExpired => write!(f, "Signature expired."),
2723+ Error::FailedAUIDMatch => write!(f, "AUID does not match domain name."),
2724+ Error::ARCInvalidInstance => write!(f, "Invalid 'i=' value found in ARC header."),
2725+ Error::ARCInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header."),
2726+ Error::ARCHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal."),
2727+ Error::ARCBrokenChain => write!(f, "Broken or missing ARC chain."),
2728+ Error::InvalidVersion => write!(f, "Invalid version."),
2729+ Error::InvalidRecord => write!(f, "Invalid record."),
2730+ Error::InvalidIp4 => write!(f, "Invalid IPv4."),
2731+ Error::InvalidIp6 => write!(f, "Invalid IPv6."),
2732+ Error::InvalidMacro => write!(f, "Invalid SPF macro."),
2733+ Error::DNSFailure => write!(f, "DNS failure."),
2734+ }
2735+ }
2736+ }
2737+
2738+ impl From<std::io::Error> for Error {
2739+ fn from(err: std::io::Error) -> Self {
2740+ Error::Io(err.to_string())
2741+ }
2742+ }
2743+
2744+ impl From<rsa::errors::Error> for Error {
2745+ fn from(err: rsa::errors::Error) -> Self {
2746+ Error::CryptoError(err.to_string())
2747+ }
2748+ }
2749+
2750+ impl From<ed25519_dalek::ed25519::Error> for Error {
2751+ fn from(err: ed25519_dalek::ed25519::Error) -> Self {
2752+ Error::CryptoError(err.to_string())
2753+ }
2754+ }
2755+
2756 pub fn add(left: usize, right: usize) -> usize {
2757 left + right
2758 }
2759 diff --git a/src/spf/mod.rs b/src/spf/mod.rs
2760index 4658aa9..3e56ad6 100644
2761--- a/src/spf/mod.rs
2762+++ b/src/spf/mod.rs
2763 @@ -138,18 +138,6 @@ pub(crate) enum Version {
2764 Spf1,
2765 }
2766
2767- #[derive(Debug)]
2768- pub enum Error {
2769- InvalidVersion,
2770- InvalidRecord,
2771- InvalidIp4,
2772- InvalidIp6,
2773- InvalidMacro,
2774- ParseFailed,
2775- }
2776-
2777- pub type Result<T> = std::result::Result<T, Error>;
2778-
2779 impl Directive {
2780 pub fn new(qualifier: Qualifier, mechanism: Mechanism) -> Self {
2781 Directive {
2782 diff --git a/src/spf/parse.rs b/src/spf/parse.rs
2783index fe89a80..144e433 100644
2784--- a/src/spf/parse.rs
2785+++ b/src/spf/parse.rs
2786 @@ -3,12 +3,15 @@ use std::{
2787 slice::Iter,
2788 };
2789
2790- use crate::common::parse::{TagParser, V};
2791+ use crate::{
2792+ common::parse::{TagParser, V},
2793+ Error,
2794+ };
2795
2796- use super::{Directive, Error, Macro, Mechanism, Modifier, Qualifier, Variable, SPF};
2797+ use super::{Directive, Macro, Mechanism, Modifier, Qualifier, Variable, SPF};
2798
2799 impl SPF {
2800- pub fn parse(bytes: &[u8]) -> super::Result<SPF> {
2801+ pub fn parse(bytes: &[u8]) -> crate::Result<SPF> {
2802 let mut record = bytes.iter();
2803 if !matches!(record.key(), Some(k) if k == V) {
2804 return Err(Error::InvalidRecord);
2805 @@ -41,7 +44,7 @@ impl SPF {
2806 ip4_cidr_length = l1;
2807 ip6_cidr_length = l2;
2808 } else if stop_char != b' ' {
2809- return Err(Error::ParseFailed);
2810+ return Err(Error::ParseError);
2811 }
2812 }
2813 b'/' => {
2814 @@ -49,7 +52,7 @@ impl SPF {
2815 ip4_cidr_length = l1;
2816 ip6_cidr_length = l2;
2817 }
2818- _ => return Err(Error::ParseFailed),
2819+ _ => return Err(Error::ParseError),
2820 }
2821
2822 spf.directives.push(Directive::new(
2823 @@ -74,12 +77,12 @@ impl SPF {
2824 spf.directives
2825 .push(Directive::new(qualifier, Mechanism::All))
2826 } else {
2827- return Err(Error::ParseFailed);
2828+ return Err(Error::ParseError);
2829 }
2830 }
2831 INCLUDE | EXISTS => {
2832 if stop_char != b':' {
2833- return Err(Error::ParseFailed);
2834+ return Err(Error::ParseError);
2835 }
2836 let (macro_string, stop_char) = record.macro_string(false)?;
2837 if stop_char == b' ' {
2838 @@ -92,19 +95,19 @@ impl SPF {
2839 },
2840 ));
2841 } else {
2842- return Err(Error::ParseFailed);
2843+ return Err(Error::ParseError);
2844 }
2845 }
2846 IP4 => {
2847 if stop_char != b':' {
2848- return Err(Error::ParseFailed);
2849+ return Err(Error::ParseError);
2850 }
2851 let mut cidr_length = 32;
2852 let (addr, stop_char) = record.ip4()?;
2853 if stop_char == b'/' {
2854 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
2855 } else if stop_char != b' ' {
2856- return Err(Error::ParseFailed);
2857+ return Err(Error::ParseError);
2858 }
2859 spf.directives.push(Directive::new(
2860 qualifier,
2861 @@ -113,14 +116,14 @@ impl SPF {
2862 }
2863 IP6 => {
2864 if stop_char != b':' {
2865- return Err(Error::ParseFailed);
2866+ return Err(Error::ParseError);
2867 }
2868 let mut cidr_length = 128;
2869 let (addr, stop_char) = record.ip6()?;
2870 if stop_char == b'/' {
2871 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
2872 } else if stop_char != b' ' {
2873- return Err(Error::ParseFailed);
2874+ return Err(Error::ParseError);
2875 }
2876 spf.directives.push(Directive::new(
2877 qualifier,
2878 @@ -139,16 +142,16 @@ impl SPF {
2879 spf.directives
2880 .push(Directive::new(qualifier, Mechanism::Ptr { macro_string }));
2881 } else {
2882- return Err(Error::ParseFailed);
2883+ return Err(Error::ParseError);
2884 }
2885 }
2886 EXP | REDIRECT => {
2887 if stop_char != b'=' {
2888- return Err(Error::ParseFailed);
2889+ return Err(Error::ParseError);
2890 }
2891 let (macro_string, stop_char) = record.macro_string(false)?;
2892 if stop_char != b' ' {
2893- return Err(Error::ParseFailed);
2894+ return Err(Error::ParseError);
2895 }
2896 spf.modifiers.push(if term == REDIRECT {
2897 Modifier::Redirect(macro_string)
2898 @@ -159,7 +162,7 @@ impl SPF {
2899 _ => {
2900 let (_, stop_char) = record.macro_string(false)?;
2901 if stop_char != b' ' {
2902- return Err(Error::ParseFailed);
2903+ return Err(Error::ParseError);
2904 }
2905 }
2906 }
2907 @@ -200,11 +203,11 @@ const REDIRECT: u64 = (b't' as u64) << 56
2908
2909 pub(crate) trait SPFParser: Sized {
2910 fn next_term(&mut self) -> Option<(u64, Qualifier, u8)>;
2911- fn macro_string(&mut self, is_exp: bool) -> super::Result<(Macro, u8)>;
2912- fn ip4(&mut self) -> super::Result<(Ipv4Addr, u8)>;
2913- fn ip6(&mut self) -> super::Result<(Ipv6Addr, u8)>;
2914- fn cidr_length(&mut self) -> super::Result<u8>;
2915- fn dual_cidr_length(&mut self) -> super::Result<(u8, u8)>;
2916+ fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
2917+ fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)>;
2918+ fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)>;
2919+ fn cidr_length(&mut self) -> crate::Result<u8>;
2920+ fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)>;
2921 }
2922
2923 impl SPFParser for Iter<'_, u8> {
2924 @@ -262,7 +265,7 @@ impl SPFParser for Iter<'_, u8> {
2925 }
2926
2927 #[allow(clippy::while_let_on_iterator)]
2928- fn macro_string(&mut self, is_exp: bool) -> super::Result<(Macro, u8)> {
2929+ fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
2930 let mut stop_char = b' ';
2931 let mut last_is_pct = false;
2932 let mut literal = Vec::with_capacity(16);
2933 @@ -363,12 +366,12 @@ impl SPFParser for Iter<'_, u8> {
2934
2935 match macro_string.len() {
2936 1 => Ok((macro_string.pop().unwrap(), stop_char)),
2937- 0 => Err(Error::ParseFailed),
2938+ 0 => Err(Error::ParseError),
2939 _ => Ok((Macro::List(macro_string), stop_char)),
2940 }
2941 }
2942
2943- fn ip4(&mut self) -> super::Result<(Ipv4Addr, u8)> {
2944+ fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)> {
2945 let mut stop_char = b' ';
2946 let mut pos = 0;
2947 let mut ip = [0u8; 4];
2948 @@ -395,7 +398,7 @@ impl SPFParser for Iter<'_, u8> {
2949 }
2950 }
2951
2952- fn ip6(&mut self) -> super::Result<(Ipv6Addr, u8)> {
2953+ fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)> {
2954 let mut stop_char = b' ';
2955 let mut ip = [0u16; 8];
2956 let mut ip_pos = 0;
2957 @@ -498,7 +501,7 @@ impl SPFParser for Iter<'_, u8> {
2958 }
2959 }
2960
2961- fn cidr_length(&mut self) -> super::Result<u8> {
2962+ fn cidr_length(&mut self) -> crate::Result<u8> {
2963 let mut cidr_length: u8 = 0;
2964 for &ch in self {
2965 match ch {
2966 @@ -509,7 +512,7 @@ impl SPFParser for Iter<'_, u8> {
2967 if ch.is_ascii_whitespace() {
2968 break;
2969 } else {
2970- return Err(Error::ParseFailed);
2971+ return Err(Error::ParseError);
2972 }
2973 }
2974 }
2975 @@ -518,7 +521,7 @@ impl SPFParser for Iter<'_, u8> {
2976 Ok(cidr_length)
2977 }
2978
2979- fn dual_cidr_length(&mut self) -> super::Result<(u8, u8)> {
2980+ fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)> {
2981 let mut ip4_length: u8 = u8::MAX;
2982 let mut ip6_length: u8 = u8::MAX;
2983 let mut in_ip6 = false;
2984 @@ -544,14 +547,14 @@ impl SPFParser for Iter<'_, u8> {
2985 if !in_ip6 {
2986 in_ip6 = true;
2987 } else if ip6_length != u8::MAX {
2988- return Err(Error::ParseFailed);
2989+ return Err(Error::ParseError);
2990 }
2991 }
2992 _ => {
2993 if ch.is_ascii_whitespace() {
2994 break;
2995 } else {
2996- return Err(Error::ParseFailed);
2997+ return Err(Error::ParseError);
2998 }
2999 }
3000 }