1 | /*************************************************************************** |
2 | * _ _ ____ _ |
3 | * Project ___| | | | _ \| | |
4 | * / __| | | | |_) | | |
5 | * | (__| |_| | _ <| |___ |
6 | * \___|\___/|_| \_\_____| |
7 | * |
8 | * Copyright (C) Marc Hoersken, <info@marc-hoersken.de> |
9 | * Copyright (C) Mark Salisbury, <mark.salisbury@hp.com> |
10 | * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. |
11 | * |
12 | * This software is licensed as described in the file COPYING, which |
13 | * you should have received as part of this distribution. The terms |
14 | * are also available at https://curl.se/docs/copyright.html. |
15 | * |
16 | * You may opt to use, copy, modify, merge, publish, distribute and/or sell |
17 | * copies of the Software, and permit persons to whom the Software is |
18 | * furnished to do so, under the terms of the COPYING file. |
19 | * |
20 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
21 | * KIND, either express or implied. |
22 | * |
23 | * SPDX-License-Identifier: curl |
24 | * |
25 | ***************************************************************************/ |
26 | |
27 | /* |
28 | * Source file for Schannel-specific certificate verification. This code should |
29 | * only be invoked by code in schannel.c. |
30 | */ |
31 | |
32 | #include "curl_setup.h" |
33 | |
34 | #ifdef USE_SCHANNEL |
35 | #ifndef USE_WINDOWS_SSPI |
36 | # error "Can't compile SCHANNEL support without SSPI." |
37 | #endif |
38 | |
39 | #include "schannel.h" |
40 | #include "schannel_int.h" |
41 | |
42 | #include "vtls.h" |
43 | #include "vtls_int.h" |
44 | #include "sendf.h" |
45 | #include "strerror.h" |
46 | #include "curl_multibyte.h" |
47 | #include "curl_printf.h" |
48 | #include "hostcheck.h" |
49 | #include "version_win32.h" |
50 | |
51 | /* The last #include file should be: */ |
52 | #include "curl_memory.h" |
53 | #include "memdebug.h" |
54 | |
55 | #define BACKEND ((struct schannel_ssl_backend_data *)connssl->backend) |
56 | |
57 | |
58 | #ifdef HAS_MANUAL_VERIFY_API |
59 | |
60 | #define MAX_CAFILE_SIZE 1048576 /* 1 MiB */ |
61 | #define BEGIN_CERT "-----BEGIN CERTIFICATE-----" |
62 | #define END_CERT "\n-----END CERTIFICATE-----" |
63 | |
64 | struct cert_chain_engine_config_win7 { |
65 | DWORD cbSize; |
66 | HCERTSTORE hRestrictedRoot; |
67 | HCERTSTORE hRestrictedTrust; |
68 | HCERTSTORE hRestrictedOther; |
69 | DWORD cAdditionalStore; |
70 | HCERTSTORE *rghAdditionalStore; |
71 | DWORD dwFlags; |
72 | DWORD dwUrlRetrievalTimeout; |
73 | DWORD MaximumCachedCertificates; |
74 | DWORD CycleDetectionModulus; |
75 | HCERTSTORE hExclusiveRoot; |
76 | HCERTSTORE hExclusiveTrustedPeople; |
77 | }; |
78 | |
79 | static int is_cr_or_lf(char c) |
80 | { |
81 | return c == '\r' || c == '\n'; |
82 | } |
83 | |
84 | /* Search the substring needle,needlelen into string haystack,haystacklen |
85 | * Strings don't need to be terminated by a '\0'. |
86 | * Similar of OSX/Linux memmem (not available on Visual Studio). |
87 | * Return position of beginning of first occurrence or NULL if not found |
88 | */ |
89 | static const char *c_memmem(const void *haystack, size_t haystacklen, |
90 | const void *needle, size_t needlelen) |
91 | { |
92 | const char *p; |
93 | char first; |
94 | const char *str_limit = (const char *)haystack + haystacklen; |
95 | if(!needlelen || needlelen > haystacklen) |
96 | return NULL; |
97 | first = *(const char *)needle; |
98 | for(p = (const char *)haystack; p <= (str_limit - needlelen); p++) |
99 | if(((*p) == first) && (memcmp(p, needle, needlelen) == 0)) |
100 | return p; |
101 | |
102 | return NULL; |
103 | } |
104 | |
105 | static CURLcode add_certs_data_to_store(HCERTSTORE trust_store, |
106 | const char *ca_buffer, |
107 | size_t ca_buffer_size, |
108 | const char *ca_file_text, |
109 | struct Curl_easy *data) |
110 | { |
111 | const size_t begin_cert_len = strlen(BEGIN_CERT); |
112 | const size_t end_cert_len = strlen(END_CERT); |
113 | CURLcode result = CURLE_OK; |
114 | int num_certs = 0; |
115 | bool more_certs = 1; |
116 | const char *current_ca_file_ptr = ca_buffer; |
117 | const char *ca_buffer_limit = ca_buffer + ca_buffer_size; |
118 | |
119 | while(more_certs && (current_ca_file_ptr<ca_buffer_limit)) { |
120 | const char *begin_cert_ptr = c_memmem(current_ca_file_ptr, |
121 | ca_buffer_limit-current_ca_file_ptr, |
122 | BEGIN_CERT, |
123 | begin_cert_len); |
124 | if(!begin_cert_ptr || !is_cr_or_lf(begin_cert_ptr[begin_cert_len])) { |
125 | more_certs = 0; |
126 | } |
127 | else { |
128 | const char *end_cert_ptr = c_memmem(begin_cert_ptr, |
129 | ca_buffer_limit-begin_cert_ptr, |
130 | END_CERT, |
131 | end_cert_len); |
132 | if(!end_cert_ptr) { |
133 | failf(data, |
134 | "schannel: CA file '%s' is not correctly formatted" , |
135 | ca_file_text); |
136 | result = CURLE_SSL_CACERT_BADFILE; |
137 | more_certs = 0; |
138 | } |
139 | else { |
140 | CERT_BLOB cert_blob; |
141 | CERT_CONTEXT *cert_context = NULL; |
142 | BOOL add_cert_result = FALSE; |
143 | DWORD actual_content_type = 0; |
144 | DWORD cert_size = (DWORD) |
145 | ((end_cert_ptr + end_cert_len) - begin_cert_ptr); |
146 | |
147 | cert_blob.pbData = (BYTE *)begin_cert_ptr; |
148 | cert_blob.cbData = cert_size; |
149 | if(!CryptQueryObject(CERT_QUERY_OBJECT_BLOB, |
150 | &cert_blob, |
151 | CERT_QUERY_CONTENT_FLAG_CERT, |
152 | CERT_QUERY_FORMAT_FLAG_ALL, |
153 | 0, |
154 | NULL, |
155 | &actual_content_type, |
156 | NULL, |
157 | NULL, |
158 | NULL, |
159 | (const void **)&cert_context)) { |
160 | char buffer[STRERROR_LEN]; |
161 | failf(data, |
162 | "schannel: failed to extract certificate from CA file " |
163 | "'%s': %s" , |
164 | ca_file_text, |
165 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
166 | result = CURLE_SSL_CACERT_BADFILE; |
167 | more_certs = 0; |
168 | } |
169 | else { |
170 | current_ca_file_ptr = begin_cert_ptr + cert_size; |
171 | |
172 | /* Sanity check that the cert_context object is the right type */ |
173 | if(CERT_QUERY_CONTENT_CERT != actual_content_type) { |
174 | failf(data, |
175 | "schannel: unexpected content type '%d' when extracting " |
176 | "certificate from CA file '%s'" , |
177 | actual_content_type, ca_file_text); |
178 | result = CURLE_SSL_CACERT_BADFILE; |
179 | more_certs = 0; |
180 | } |
181 | else { |
182 | add_cert_result = |
183 | CertAddCertificateContextToStore(trust_store, |
184 | cert_context, |
185 | CERT_STORE_ADD_ALWAYS, |
186 | NULL); |
187 | CertFreeCertificateContext(cert_context); |
188 | if(!add_cert_result) { |
189 | char buffer[STRERROR_LEN]; |
190 | failf(data, |
191 | "schannel: failed to add certificate from CA file '%s' " |
192 | "to certificate store: %s" , |
193 | ca_file_text, |
194 | Curl_winapi_strerror(GetLastError(), buffer, |
195 | sizeof(buffer))); |
196 | result = CURLE_SSL_CACERT_BADFILE; |
197 | more_certs = 0; |
198 | } |
199 | else { |
200 | num_certs++; |
201 | } |
202 | } |
203 | } |
204 | } |
205 | } |
206 | } |
207 | |
208 | if(result == CURLE_OK) { |
209 | if(!num_certs) { |
210 | infof(data, |
211 | "schannel: did not add any certificates from CA file '%s'" , |
212 | ca_file_text); |
213 | } |
214 | else { |
215 | infof(data, |
216 | "schannel: added %d certificate(s) from CA file '%s'" , |
217 | num_certs, ca_file_text); |
218 | } |
219 | } |
220 | return result; |
221 | } |
222 | |
223 | static CURLcode add_certs_file_to_store(HCERTSTORE trust_store, |
224 | const char *ca_file, |
225 | struct Curl_easy *data) |
226 | { |
227 | CURLcode result; |
228 | HANDLE ca_file_handle = INVALID_HANDLE_VALUE; |
229 | LARGE_INTEGER file_size; |
230 | char *ca_file_buffer = NULL; |
231 | TCHAR *ca_file_tstr = NULL; |
232 | size_t ca_file_bufsize = 0; |
233 | DWORD total_bytes_read = 0; |
234 | |
235 | ca_file_tstr = curlx_convert_UTF8_to_tchar((char *)ca_file); |
236 | if(!ca_file_tstr) { |
237 | char buffer[STRERROR_LEN]; |
238 | failf(data, |
239 | "schannel: invalid path name for CA file '%s': %s" , |
240 | ca_file, |
241 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
242 | result = CURLE_SSL_CACERT_BADFILE; |
243 | goto cleanup; |
244 | } |
245 | |
246 | /* |
247 | * Read the CA file completely into memory before parsing it. This |
248 | * optimizes for the common case where the CA file will be relatively |
249 | * small ( < 1 MiB ). |
250 | */ |
251 | ca_file_handle = CreateFile(ca_file_tstr, |
252 | GENERIC_READ, |
253 | FILE_SHARE_READ, |
254 | NULL, |
255 | OPEN_EXISTING, |
256 | FILE_ATTRIBUTE_NORMAL, |
257 | NULL); |
258 | if(ca_file_handle == INVALID_HANDLE_VALUE) { |
259 | char buffer[STRERROR_LEN]; |
260 | failf(data, |
261 | "schannel: failed to open CA file '%s': %s" , |
262 | ca_file, |
263 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
264 | result = CURLE_SSL_CACERT_BADFILE; |
265 | goto cleanup; |
266 | } |
267 | |
268 | if(!GetFileSizeEx(ca_file_handle, &file_size)) { |
269 | char buffer[STRERROR_LEN]; |
270 | failf(data, |
271 | "schannel: failed to determine size of CA file '%s': %s" , |
272 | ca_file, |
273 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
274 | result = CURLE_SSL_CACERT_BADFILE; |
275 | goto cleanup; |
276 | } |
277 | |
278 | if(file_size.QuadPart > MAX_CAFILE_SIZE) { |
279 | failf(data, |
280 | "schannel: CA file exceeds max size of %u bytes" , |
281 | MAX_CAFILE_SIZE); |
282 | result = CURLE_SSL_CACERT_BADFILE; |
283 | goto cleanup; |
284 | } |
285 | |
286 | ca_file_bufsize = (size_t)file_size.QuadPart; |
287 | ca_file_buffer = (char *)malloc(ca_file_bufsize + 1); |
288 | if(!ca_file_buffer) { |
289 | result = CURLE_OUT_OF_MEMORY; |
290 | goto cleanup; |
291 | } |
292 | |
293 | while(total_bytes_read < ca_file_bufsize) { |
294 | DWORD bytes_to_read = (DWORD)(ca_file_bufsize - total_bytes_read); |
295 | DWORD bytes_read = 0; |
296 | |
297 | if(!ReadFile(ca_file_handle, ca_file_buffer + total_bytes_read, |
298 | bytes_to_read, &bytes_read, NULL)) { |
299 | char buffer[STRERROR_LEN]; |
300 | failf(data, |
301 | "schannel: failed to read from CA file '%s': %s" , |
302 | ca_file, |
303 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
304 | result = CURLE_SSL_CACERT_BADFILE; |
305 | goto cleanup; |
306 | } |
307 | if(bytes_read == 0) { |
308 | /* Premature EOF -- adjust the bufsize to the new value */ |
309 | ca_file_bufsize = total_bytes_read; |
310 | } |
311 | else { |
312 | total_bytes_read += bytes_read; |
313 | } |
314 | } |
315 | |
316 | /* Null terminate the buffer */ |
317 | ca_file_buffer[ca_file_bufsize] = '\0'; |
318 | |
319 | result = add_certs_data_to_store(trust_store, |
320 | ca_file_buffer, ca_file_bufsize, |
321 | ca_file, |
322 | data); |
323 | |
324 | cleanup: |
325 | if(ca_file_handle != INVALID_HANDLE_VALUE) { |
326 | CloseHandle(ca_file_handle); |
327 | } |
328 | Curl_safefree(ca_file_buffer); |
329 | curlx_unicodefree(ca_file_tstr); |
330 | |
331 | return result; |
332 | } |
333 | |
334 | #endif /* HAS_MANUAL_VERIFY_API */ |
335 | |
336 | /* |
337 | * Returns the number of characters necessary to populate all the host_names. |
338 | * If host_names is not NULL, populate it with all the host names. Each string |
339 | * in the host_names is null-terminated and the last string is double |
340 | * null-terminated. If no DNS names are found, a single null-terminated empty |
341 | * string is returned. |
342 | */ |
343 | static DWORD cert_get_name_string(struct Curl_easy *data, |
344 | CERT_CONTEXT *cert_context, |
345 | LPTSTR host_names, |
346 | DWORD length) |
347 | { |
348 | DWORD actual_length = 0; |
349 | BOOL compute_content = FALSE; |
350 | CERT_INFO *cert_info = NULL; |
351 | CERT_EXTENSION *extension = NULL; |
352 | CRYPT_DECODE_PARA decode_para = {0, 0, 0}; |
353 | CERT_ALT_NAME_INFO *alt_name_info = NULL; |
354 | DWORD alt_name_info_size = 0; |
355 | BOOL ret_val = FALSE; |
356 | LPTSTR current_pos = NULL; |
357 | DWORD i; |
358 | |
359 | #ifdef CERT_NAME_SEARCH_ALL_NAMES_FLAG |
360 | /* CERT_NAME_SEARCH_ALL_NAMES_FLAG is available from Windows 8 onwards. */ |
361 | if(curlx_verify_windows_version(6, 2, 0, PLATFORM_WINNT, |
362 | VERSION_GREATER_THAN_EQUAL)) { |
363 | /* CertGetNameString will provide the 8-bit character string without |
364 | * any decoding */ |
365 | DWORD name_flags = |
366 | CERT_NAME_DISABLE_IE4_UTF8_FLAG | CERT_NAME_SEARCH_ALL_NAMES_FLAG; |
367 | actual_length = CertGetNameString(cert_context, |
368 | CERT_NAME_DNS_TYPE, |
369 | name_flags, |
370 | NULL, |
371 | host_names, |
372 | length); |
373 | return actual_length; |
374 | } |
375 | #endif |
376 | |
377 | compute_content = host_names != NULL && length != 0; |
378 | |
379 | /* Initialize default return values. */ |
380 | actual_length = 1; |
381 | if(compute_content) { |
382 | *host_names = '\0'; |
383 | } |
384 | |
385 | if(!cert_context) { |
386 | failf(data, "schannel: Null certificate context." ); |
387 | return actual_length; |
388 | } |
389 | |
390 | cert_info = cert_context->pCertInfo; |
391 | if(!cert_info) { |
392 | failf(data, "schannel: Null certificate info." ); |
393 | return actual_length; |
394 | } |
395 | |
396 | extension = CertFindExtension(szOID_SUBJECT_ALT_NAME2, |
397 | cert_info->cExtension, |
398 | cert_info->rgExtension); |
399 | if(!extension) { |
400 | failf(data, "schannel: CertFindExtension() returned no extension." ); |
401 | return actual_length; |
402 | } |
403 | |
404 | decode_para.cbSize = sizeof(CRYPT_DECODE_PARA); |
405 | |
406 | ret_val = |
407 | CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, |
408 | szOID_SUBJECT_ALT_NAME2, |
409 | extension->Value.pbData, |
410 | extension->Value.cbData, |
411 | CRYPT_DECODE_ALLOC_FLAG | CRYPT_DECODE_NOCOPY_FLAG, |
412 | &decode_para, |
413 | &alt_name_info, |
414 | &alt_name_info_size); |
415 | if(!ret_val) { |
416 | failf(data, |
417 | "schannel: CryptDecodeObjectEx() returned no alternate name " |
418 | "information." ); |
419 | return actual_length; |
420 | } |
421 | |
422 | current_pos = host_names; |
423 | |
424 | /* Iterate over the alternate names and populate host_names. */ |
425 | for(i = 0; i < alt_name_info->cAltEntry; i++) { |
426 | const CERT_ALT_NAME_ENTRY *entry = &alt_name_info->rgAltEntry[i]; |
427 | wchar_t *dns_w = NULL; |
428 | size_t current_length = 0; |
429 | |
430 | if(entry->dwAltNameChoice != CERT_ALT_NAME_DNS_NAME) { |
431 | continue; |
432 | } |
433 | if(!entry->pwszDNSName) { |
434 | infof(data, "schannel: Empty DNS name." ); |
435 | continue; |
436 | } |
437 | current_length = wcslen(entry->pwszDNSName) + 1; |
438 | if(!compute_content) { |
439 | actual_length += (DWORD)current_length; |
440 | continue; |
441 | } |
442 | /* Sanity check to prevent buffer overrun. */ |
443 | if((actual_length + current_length) > length) { |
444 | failf(data, "schannel: Not enough memory to list all host names." ); |
445 | break; |
446 | } |
447 | dns_w = entry->pwszDNSName; |
448 | /* pwszDNSName is in ia5 string format and hence doesn't contain any |
449 | * non-ascii characters. */ |
450 | while(*dns_w != '\0') { |
451 | *current_pos++ = (char)(*dns_w++); |
452 | } |
453 | *current_pos++ = '\0'; |
454 | actual_length += (DWORD)current_length; |
455 | } |
456 | if(compute_content) { |
457 | /* Last string has double null-terminator. */ |
458 | *current_pos = '\0'; |
459 | } |
460 | return actual_length; |
461 | } |
462 | |
463 | /* Verify the server's hostname */ |
464 | CURLcode Curl_verify_host(struct Curl_cfilter *cf, |
465 | struct Curl_easy *data) |
466 | { |
467 | struct ssl_connect_data *connssl = cf->ctx; |
468 | SECURITY_STATUS sspi_status; |
469 | CURLcode result = CURLE_PEER_FAILED_VERIFICATION; |
470 | CERT_CONTEXT *pCertContextServer = NULL; |
471 | TCHAR *cert_hostname_buff = NULL; |
472 | size_t cert_hostname_buff_index = 0; |
473 | const char *conn_hostname = connssl->hostname; |
474 | size_t hostlen = strlen(conn_hostname); |
475 | DWORD len = 0; |
476 | DWORD actual_len = 0; |
477 | |
478 | sspi_status = |
479 | s_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle, |
480 | SECPKG_ATTR_REMOTE_CERT_CONTEXT, |
481 | &pCertContextServer); |
482 | |
483 | if((sspi_status != SEC_E_OK) || !pCertContextServer) { |
484 | char buffer[STRERROR_LEN]; |
485 | failf(data, "schannel: Failed to read remote certificate context: %s" , |
486 | Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer))); |
487 | result = CURLE_PEER_FAILED_VERIFICATION; |
488 | goto cleanup; |
489 | } |
490 | |
491 | /* Determine the size of the string needed for the cert hostname */ |
492 | len = cert_get_name_string(data, pCertContextServer, NULL, 0); |
493 | if(len == 0) { |
494 | failf(data, |
495 | "schannel: CertGetNameString() returned no " |
496 | "certificate name information" ); |
497 | result = CURLE_PEER_FAILED_VERIFICATION; |
498 | goto cleanup; |
499 | } |
500 | |
501 | /* CertGetNameString guarantees that the returned name will not contain |
502 | * embedded null bytes. This appears to be undocumented behavior. |
503 | */ |
504 | cert_hostname_buff = (LPTSTR)malloc(len * sizeof(TCHAR)); |
505 | if(!cert_hostname_buff) { |
506 | result = CURLE_OUT_OF_MEMORY; |
507 | goto cleanup; |
508 | } |
509 | actual_len = cert_get_name_string( |
510 | data, pCertContextServer, (LPTSTR)cert_hostname_buff, len); |
511 | |
512 | /* Sanity check */ |
513 | if(actual_len != len) { |
514 | failf(data, |
515 | "schannel: CertGetNameString() returned certificate " |
516 | "name information of unexpected size" ); |
517 | result = CURLE_PEER_FAILED_VERIFICATION; |
518 | goto cleanup; |
519 | } |
520 | |
521 | /* cert_hostname_buff contains all DNS names, where each name is |
522 | * null-terminated and the last DNS name is double null-terminated. Due to |
523 | * this encoding, use the length of the buffer to iterate over all names. |
524 | */ |
525 | result = CURLE_PEER_FAILED_VERIFICATION; |
526 | while(cert_hostname_buff_index < len && |
527 | cert_hostname_buff[cert_hostname_buff_index] != TEXT('\0') && |
528 | result == CURLE_PEER_FAILED_VERIFICATION) { |
529 | |
530 | char *cert_hostname; |
531 | |
532 | /* Comparing the cert name and the connection hostname encoded as UTF-8 |
533 | * is acceptable since both values are assumed to use ASCII |
534 | * (or some equivalent) encoding |
535 | */ |
536 | cert_hostname = curlx_convert_tchar_to_UTF8( |
537 | &cert_hostname_buff[cert_hostname_buff_index]); |
538 | if(!cert_hostname) { |
539 | result = CURLE_OUT_OF_MEMORY; |
540 | } |
541 | else { |
542 | if(Curl_cert_hostcheck(cert_hostname, strlen(cert_hostname), |
543 | conn_hostname, hostlen)) { |
544 | infof(data, |
545 | "schannel: connection hostname (%s) validated " |
546 | "against certificate name (%s)" , |
547 | conn_hostname, cert_hostname); |
548 | result = CURLE_OK; |
549 | } |
550 | else { |
551 | size_t cert_hostname_len; |
552 | |
553 | infof(data, |
554 | "schannel: connection hostname (%s) did not match " |
555 | "against certificate name (%s)" , |
556 | conn_hostname, cert_hostname); |
557 | |
558 | cert_hostname_len = |
559 | _tcslen(&cert_hostname_buff[cert_hostname_buff_index]); |
560 | |
561 | /* Move on to next cert name */ |
562 | cert_hostname_buff_index += cert_hostname_len + 1; |
563 | |
564 | result = CURLE_PEER_FAILED_VERIFICATION; |
565 | } |
566 | curlx_unicodefree(cert_hostname); |
567 | } |
568 | } |
569 | |
570 | if(result == CURLE_PEER_FAILED_VERIFICATION) { |
571 | failf(data, |
572 | "schannel: CertGetNameString() failed to match " |
573 | "connection hostname (%s) against server certificate names" , |
574 | conn_hostname); |
575 | } |
576 | else if(result != CURLE_OK) |
577 | failf(data, "schannel: server certificate name verification failed" ); |
578 | |
579 | cleanup: |
580 | Curl_safefree(cert_hostname_buff); |
581 | |
582 | if(pCertContextServer) |
583 | CertFreeCertificateContext(pCertContextServer); |
584 | |
585 | return result; |
586 | } |
587 | |
588 | |
589 | #ifdef HAS_MANUAL_VERIFY_API |
590 | /* Verify the server's certificate and hostname */ |
591 | CURLcode Curl_verify_certificate(struct Curl_cfilter *cf, |
592 | struct Curl_easy *data) |
593 | { |
594 | struct ssl_connect_data *connssl = cf->ctx; |
595 | struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); |
596 | struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); |
597 | SECURITY_STATUS sspi_status; |
598 | CURLcode result = CURLE_OK; |
599 | CERT_CONTEXT *pCertContextServer = NULL; |
600 | const CERT_CHAIN_CONTEXT *pChainContext = NULL; |
601 | HCERTCHAINENGINE cert_chain_engine = NULL; |
602 | HCERTSTORE trust_store = NULL; |
603 | |
604 | DEBUGASSERT(BACKEND); |
605 | |
606 | sspi_status = |
607 | s_pSecFn->QueryContextAttributes(&BACKEND->ctxt->ctxt_handle, |
608 | SECPKG_ATTR_REMOTE_CERT_CONTEXT, |
609 | &pCertContextServer); |
610 | |
611 | if((sspi_status != SEC_E_OK) || !pCertContextServer) { |
612 | char buffer[STRERROR_LEN]; |
613 | failf(data, "schannel: Failed to read remote certificate context: %s" , |
614 | Curl_sspi_strerror(sspi_status, buffer, sizeof(buffer))); |
615 | result = CURLE_PEER_FAILED_VERIFICATION; |
616 | } |
617 | |
618 | if(result == CURLE_OK && |
619 | (conn_config->CAfile || conn_config->ca_info_blob) && |
620 | BACKEND->use_manual_cred_validation) { |
621 | /* |
622 | * Create a chain engine that uses the certificates in the CA file as |
623 | * trusted certificates. This is only supported on Windows 7+. |
624 | */ |
625 | |
626 | if(curlx_verify_windows_version(6, 1, 0, PLATFORM_WINNT, |
627 | VERSION_LESS_THAN)) { |
628 | failf(data, "schannel: this version of Windows is too old to support " |
629 | "certificate verification via CA bundle file." ); |
630 | result = CURLE_SSL_CACERT_BADFILE; |
631 | } |
632 | else { |
633 | /* Open the certificate store */ |
634 | trust_store = CertOpenStore(CERT_STORE_PROV_MEMORY, |
635 | 0, |
636 | (HCRYPTPROV)NULL, |
637 | CERT_STORE_CREATE_NEW_FLAG, |
638 | NULL); |
639 | if(!trust_store) { |
640 | char buffer[STRERROR_LEN]; |
641 | failf(data, "schannel: failed to create certificate store: %s" , |
642 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
643 | result = CURLE_SSL_CACERT_BADFILE; |
644 | } |
645 | else { |
646 | const struct curl_blob *ca_info_blob = conn_config->ca_info_blob; |
647 | if(ca_info_blob) { |
648 | result = add_certs_data_to_store(trust_store, |
649 | (const char *)ca_info_blob->data, |
650 | ca_info_blob->len, |
651 | "(memory blob)" , |
652 | data); |
653 | } |
654 | else { |
655 | result = add_certs_file_to_store(trust_store, |
656 | conn_config->CAfile, |
657 | data); |
658 | } |
659 | } |
660 | } |
661 | |
662 | if(result == CURLE_OK) { |
663 | struct cert_chain_engine_config_win7 engine_config; |
664 | BOOL create_engine_result; |
665 | |
666 | memset(&engine_config, 0, sizeof(engine_config)); |
667 | engine_config.cbSize = sizeof(engine_config); |
668 | engine_config.hExclusiveRoot = trust_store; |
669 | |
670 | /* CertCreateCertificateChainEngine will check the expected size of the |
671 | * CERT_CHAIN_ENGINE_CONFIG structure and fail if the specified size |
672 | * does not match the expected size. When this occurs, it indicates that |
673 | * CAINFO is not supported on the version of Windows in use. |
674 | */ |
675 | create_engine_result = |
676 | CertCreateCertificateChainEngine( |
677 | (CERT_CHAIN_ENGINE_CONFIG *)&engine_config, &cert_chain_engine); |
678 | if(!create_engine_result) { |
679 | char buffer[STRERROR_LEN]; |
680 | failf(data, |
681 | "schannel: failed to create certificate chain engine: %s" , |
682 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
683 | result = CURLE_SSL_CACERT_BADFILE; |
684 | } |
685 | } |
686 | } |
687 | |
688 | if(result == CURLE_OK) { |
689 | CERT_CHAIN_PARA ChainPara; |
690 | |
691 | memset(&ChainPara, 0, sizeof(ChainPara)); |
692 | ChainPara.cbSize = sizeof(ChainPara); |
693 | |
694 | if(!CertGetCertificateChain(cert_chain_engine, |
695 | pCertContextServer, |
696 | NULL, |
697 | pCertContextServer->hCertStore, |
698 | &ChainPara, |
699 | (ssl_config->no_revoke ? 0 : |
700 | CERT_CHAIN_REVOCATION_CHECK_CHAIN), |
701 | NULL, |
702 | &pChainContext)) { |
703 | char buffer[STRERROR_LEN]; |
704 | failf(data, "schannel: CertGetCertificateChain failed: %s" , |
705 | Curl_winapi_strerror(GetLastError(), buffer, sizeof(buffer))); |
706 | pChainContext = NULL; |
707 | result = CURLE_PEER_FAILED_VERIFICATION; |
708 | } |
709 | |
710 | if(result == CURLE_OK) { |
711 | CERT_SIMPLE_CHAIN *pSimpleChain = pChainContext->rgpChain[0]; |
712 | DWORD dwTrustErrorMask = ~(DWORD)(CERT_TRUST_IS_NOT_TIME_NESTED); |
713 | dwTrustErrorMask &= pSimpleChain->TrustStatus.dwErrorStatus; |
714 | |
715 | if(data->set.ssl.revoke_best_effort) { |
716 | /* Ignore errors when root certificates are missing the revocation |
717 | * list URL, or when the list could not be downloaded because the |
718 | * server is currently unreachable. */ |
719 | dwTrustErrorMask &= ~(DWORD)(CERT_TRUST_REVOCATION_STATUS_UNKNOWN | |
720 | CERT_TRUST_IS_OFFLINE_REVOCATION); |
721 | } |
722 | |
723 | if(dwTrustErrorMask) { |
724 | if(dwTrustErrorMask & CERT_TRUST_IS_REVOKED) |
725 | failf(data, "schannel: CertGetCertificateChain trust error" |
726 | " CERT_TRUST_IS_REVOKED" ); |
727 | else if(dwTrustErrorMask & CERT_TRUST_IS_PARTIAL_CHAIN) |
728 | failf(data, "schannel: CertGetCertificateChain trust error" |
729 | " CERT_TRUST_IS_PARTIAL_CHAIN" ); |
730 | else if(dwTrustErrorMask & CERT_TRUST_IS_UNTRUSTED_ROOT) |
731 | failf(data, "schannel: CertGetCertificateChain trust error" |
732 | " CERT_TRUST_IS_UNTRUSTED_ROOT" ); |
733 | else if(dwTrustErrorMask & CERT_TRUST_IS_NOT_TIME_VALID) |
734 | failf(data, "schannel: CertGetCertificateChain trust error" |
735 | " CERT_TRUST_IS_NOT_TIME_VALID" ); |
736 | else if(dwTrustErrorMask & CERT_TRUST_REVOCATION_STATUS_UNKNOWN) |
737 | failf(data, "schannel: CertGetCertificateChain trust error" |
738 | " CERT_TRUST_REVOCATION_STATUS_UNKNOWN" ); |
739 | else |
740 | failf(data, "schannel: CertGetCertificateChain error mask: 0x%08x" , |
741 | dwTrustErrorMask); |
742 | result = CURLE_PEER_FAILED_VERIFICATION; |
743 | } |
744 | } |
745 | } |
746 | |
747 | if(result == CURLE_OK) { |
748 | if(conn_config->verifyhost) { |
749 | result = Curl_verify_host(cf, data); |
750 | } |
751 | } |
752 | |
753 | if(cert_chain_engine) { |
754 | CertFreeCertificateChainEngine(cert_chain_engine); |
755 | } |
756 | |
757 | if(trust_store) { |
758 | CertCloseStore(trust_store, 0); |
759 | } |
760 | |
761 | if(pChainContext) |
762 | CertFreeCertificateChain(pChainContext); |
763 | |
764 | if(pCertContextServer) |
765 | CertFreeCertificateContext(pCertContextServer); |
766 | |
767 | return result; |
768 | } |
769 | |
770 | #endif /* HAS_MANUAL_VERIFY_API */ |
771 | #endif /* USE_SCHANNEL */ |
772 | |