1 ///
2 module imap.ssl;
3 import imap.sil : SILdoc;
4 import std.stdio;
5 import std.string;
6 import deimos.openssl.ssl;
7 import deimos.openssl.err;
8 import deimos.openssl.x509;
9 import deimos.openssl.x509_vfy; // needed to use openssl master
10 import deimos.openssl.pem;
11 import deimos.openssl.evp;
12 
13 import imap.defines;
14 import imap.socket;
15 import imap.session;
16 
17 ///
18 enum STDIN_FILENO = 0;
19 
20 version (Posix) { import core.sys.posix.unistd : isatty; } else {
21     // FIXME
22     bool isatty(int fileno) {
23         return false;
24     }
25 }
26 
27 
28 ///
29 X509* getPeerCertificate(ref SSL context) {
30     import std.exception : enforce;
31     X509* cert = SSL_get_peer_certificate(&context);
32     enforce(cert, "unable to get peer certificate");
33     return cert;
34 }
35 
36 @SILdoc("Get SSL/TLS certificate check it, maybe ask user about it and act accordingly.")
37 Status getCert(Session session) {
38     import std.exception : enforce;
39     X509* pcert = getPeerCertificate(*session.sslConnection);
40     enforce(pcert !is null);
41     auto cert = pcert;
42 
43     scope (exit)
44         X509_free(pcert);
45 
46     long verify = SSL_get_verify_result(session.sslConnection);
47     enforce(((verify == X509_V_OK)
48         || (verify == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
49         || (verify == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY)),
50             format!"certificate verification failed; %d\n"(verify));
51 
52     if (verify != X509_V_OK) {
53         auto md = cert.getDigest(EVP_md5());
54         enforce(checkCert(cert, md) == Status.success, "certificate mismatch occurred");
55         enforce(isatty(STDIN_FILENO) != 0, "certificate error: cannot accept certificate in non-interactivemode");
56         printCert(cert, md);
57         storeCert(cert);
58     }
59     return Status.success;
60 }
61 
62 ///
63 enum Status {
64     success,
65     failure,
66 }
67 
68 @SILdoc("Check if the SSL/TLS certificate exists in the certificates file.")
69 Status checkCert(X509* pcert, string pmd) {
70     import std.file : exists;
71     Status status = Status.failure;
72     X509* certp;
73 
74     string certf = getFilePath("certificates");
75     if (!certf.exists())
76         return Status.failure;
77 
78     auto file = File(certf, "r");
79     string pcertSubject = pcert.getSubject();
80     string pIssuerName = pcert.getIssuerName();
81     string pSerial = pcert.getSerial();
82 
83     while ((certp = readX509(file, certp)) !is null) {
84         auto cert = certp;
85         if (cert.getSubject != pcertSubject || (cert.getIssuerName() != pIssuerName)
86             || (cert.getSerial() != pSerial))
87             continue;
88 
89         auto digest = getDigest(cert, EVP_md5());
90         if (digest.length != pmd.length)
91             continue;
92 
93         if (digest != pmd) {
94             status = Status.failure;
95             break;
96         }
97         status = Status.success;
98         break;
99     }
100 
101     X509_free(certp);
102 
103     return status;
104 }
105 
106 X509* readX509(File file, ref X509* cert) {
107     return PEM_read_X509(file.getFP(), &cert, null, null);
108 }
109 
110 
111 string getDigest(X509* cert, const(EVP_MD) * type) {
112     import std.exception : enforce;
113     import std.format : format;
114     ubyte[EVP_MAX_MD_SIZE] md;
115     uint len;
116     auto result = X509_digest(cert, type, md.ptr, &len);
117     enforce(result == 1, "failed to get digest for certificate");
118     enforce(len > 0, format!"X509_digest returned digest of length: %s"(len));
119     return cast(string) (md[0 .. len].idup);
120 }
121 
122 
123 ///
124 string getIssuerName(X509* cert) {
125     import core.memory : pureFree;
126     import std.string : fromStringz;
127     char* s = X509_NAME_oneline(X509_get_issuer_name(cert), null, 0);
128     scope (exit) pureFree(s);
129     return s.fromStringz.idup;
130 }
131 
132 ///
133 string getSubject(X509* cert) {
134     import core.memory : pureFree;
135     import std.string : fromStringz;
136     char* s = X509_NAME_oneline(X509_get_subject_name(cert), null, 0);
137     scope (exit) pureFree(s);
138     return s.fromStringz.idup;
139 }
140 
141 ///
142 string asHex(string s) {
143     import std.algorithm : map;
144     import std.format : format;
145     import std.array : array;
146     import std.string : join;
147     return s.map!(c => format!"%02X"(c)).array.join.idup;
148 }
149 
150 
151 @SILdoc("Print information about the SSL/TLS certificate.")
152 void printCert(X509* cert, string fingerprint) {
153     writefln("Server certificate subject: %s", cert.getSubject);
154     writefln("Server certificate issuer: %s", getIssuerName(cert));
155     writefln("Server certificate serial: %s", cert.getSerial);
156     writefln("Server key fingerprint: %s", fingerprint.asHex);
157 }
158 
159 
160 @SILdoc("Extract certificate serial number as a string.")
161 string getSerial(X509* cert) {
162     import std.string : fromStringz;
163     import std.format : format;
164     string buf;
165 
166     ASN1_INTEGER* serial = X509_get_serialNumber(cert);
167     if (serial.length <= cast(int) long.sizeof) {
168         long num = ASN1_INTEGER_get(serial);
169         if (serial.type == V_ASN1_NEG_INTEGER) {
170             buf ~= format!"-%X"(-num);
171         } else {
172             buf ~= format!"%X"(num);
173         }
174     } else {
175         if (serial.type == V_ASN1_NEG_INTEGER) {
176             buf ~= "-";
177         }
178         foreach (i; 0 .. serial.length) {
179             buf ~= format!"%02X"(serial.data[i]);
180         }
181     }
182     return buf;
183 }
184 
185 
186 @SILdoc("Store the SSL/TLS certificate after asking the user to accept/reject it.")
187 void storeCert(X509* cert) {
188     import std.string : toLower;
189     import std.stdio : stdin, writef, File;
190     import core.memory : pureFree;
191     import std.range : front;
192     import std.conv : to;
193     import std.exception : enforce;
194 
195     char c;
196     do {
197         writef("(R)eject, accept (t)emporarily or accept (p)ermanently? ");
198         do {} while (stdin.eof);
199         c = stdin.rawRead(new char[1]).toLower.front.to!char;
200     } while (c != 'r' && c != 't' && c != 'p');
201 
202     enforce(c != 'r', "certificate rejected");
203     if (c == 't')
204         return;
205     auto certf = getFilePath("certificates");
206     auto file  = File(certf, "a");
207     char* s = X509_NAME_oneline(X509_get_subject_name(cert), null, 0);
208     file.writefln("Subject: %s", s);
209     pureFree(s);
210     s = X509_NAME_oneline(X509_get_issuer_name(cert), null, 0);
211     file.writefln("Issuer: %s", s);
212     pureFree(s);
213     auto serialNo = getSerial(cert);
214     file.writefln("Serial: %s", serialNo);
215 
216     PEM_write_X509(file.getFP(), cert);
217 
218     file.writefln("");
219 }
220 
221 ///
222 string getFilePath(string subDir) {
223     import std.path : expandTilde, dirSeparator;
224     return expandTilde("~" ~ dirSeparator ~ subDir);
225 }
226 
227 void loadVerifyLocations(SSL_CTX* ctx, string caFile, string caPath) {
228     import std.exception : enforce;
229     auto ret = SSL_CTX_load_verify_locations(ctx, caFile.toStringz, caPath.toStringz);
230     enforce(ret == 0, "SSL unable to load verify locations");
231 }
232 
233 void setDefaultVerifyPaths(SSL_CTX* ctx) {
234     import std.exception : enforce;
235     auto ret = SSL_CTX_set_default_verify_paths(ctx);
236     enforce(ret == 0, "SSL unable to set default verify paths");
237 }