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