racket/collects/openssl/mzssl.rkt
Ryan Culpepper 07c6e89899 remove cancel box, no longer needed with ffi/unsafe/alloc
Also, found the source of mem corruption: the finalizer's cancel box
(from create-ssl) was shadowed by a new box put in mzssl struct.
2012-11-21 12:56:31 -05:00

1411 lines
54 KiB
Racket

;; Disabled when `enforce-retry?' is #f:
;; Warn clients: when a (non-blocking) write fails to write all the
;; data, the stream is actually committed to writing the given data
;; in the future. (This requirement comes from the SSL library.)
;; Another warning: data that is written and not buffered may still be
;; in flight between Racket and the underlying ports. A `flush-output'
;; won't return until sent data is actually in the underlying port.
;; (This is due to the fact that unbuffered data cannot be written
;; without blocking.)
;; One last warning: a write/read must block because a previous
;; read/write (the opposite direction) didn't finish, and so that
;; opposite must be completed, first.
(module mzssl racket/base
(require ffi/unsafe
ffi/unsafe/define
ffi/unsafe/atomic
ffi/unsafe/alloc
ffi/file
racket/port
racket/tcp
racket/string
"libcrypto.rkt"
"libssl.rkt")
(provide ssl-available?
ssl-load-fail-reason
ssl-make-client-context
ssl-secure-client-context
ssl-make-server-context
ssl-client-context?
ssl-server-context?
ssl-context?
ssl-load-certificate-chain!
ssl-load-private-key!
ssl-load-verify-root-certificates!
ssl-load-suggested-certificate-authorities!
ssl-set-ciphers!
ssl-seal-context!
ssl-default-root-certificate-locations
ssl-load-default-verify-root-certificates!
ssl-set-verify!
ssl-try-verify!
ssl-set-verify-hostname!
ssl-peer-verified?
ssl-peer-certificate-hostnames
ssl-peer-check-hostname
ssl-peer-subject-name
ssl-peer-issuer-name
ports->ssl-ports
ssl-listen
ssl-close
ssl-accept
ssl-accept/enable-break
ssl-connect
ssl-connect/enable-break
ssl-listener?
ssl-addresses
ssl-abandon-port
ssl-port?)
(define ssl-load-fail-reason
(or libssl-load-fail-reason
libcrypto-load-fail-reason))
(define 3m? (eq? '3m (system-type 'gc)))
(define libmz (ffi-lib #f))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL bindings and constants
(define-ffi-definer define-crypto libcrypto
#:default-make-fail make-not-available)
(define-ffi-definer define-ssl libssl
#:default-make-fail make-not-available)
(define-ffi-definer define-mzscheme libmz)
(define-cpointer-type _BIO_METHOD*)
(define-cpointer-type _BIO*)
(define-cpointer-type _SSL_METHOD*)
(define-cpointer-type _SSL_CTX*)
(define-cpointer-type _SSL*)
(define-cpointer-type _X509_NAME*)
(define-cpointer-type _X509_NAME_ENTRY*)
(define-cpointer-type _X509*)
(define-cpointer-type _ASN1_STRING*)
(define-cpointer-type _STACK*)
(define-cstruct _GENERAL_NAME ([type _int] [d _ASN1_STRING*]))
(define-ssl SSLv2_client_method (_fun -> _SSL_METHOD*))
(define-ssl SSLv2_server_method (_fun -> _SSL_METHOD*))
(define-ssl SSLv3_client_method (_fun -> _SSL_METHOD*))
(define-ssl SSLv3_server_method (_fun -> _SSL_METHOD*))
(define-ssl SSLv23_client_method (_fun -> _SSL_METHOD*))
(define-ssl SSLv23_server_method (_fun -> _SSL_METHOD*))
(define-ssl TLSv1_client_method (_fun -> _SSL_METHOD*))
(define-ssl TLSv1_server_method (_fun -> _SSL_METHOD*))
(define-crypto BIO_s_mem (_fun -> _BIO_METHOD*))
(define-crypto BIO_new (_fun _BIO_METHOD* -> _BIO*/null))
(define-crypto BIO_free (_fun _BIO* -> _void))
(define-crypto BIO_read (_fun _BIO* _bytes _int -> _int))
(define-crypto BIO_write (_fun _BIO* _bytes _int -> _int))
(define-crypto BIO_ctrl (_fun _BIO* _int _long _long -> _long))
(define (BIO_set_mem_eof_return b v)
(BIO_ctrl b BIO_C_SET_BUF_MEM_EOF_RETURN v 0))
(define-ssl SSL_CTX_free (_fun _SSL_CTX* -> _void)
#:wrap (deallocator))
(define-ssl SSL_CTX_new (_fun _SSL_METHOD* -> _SSL_CTX*)
#:wrap (allocator SSL_CTX_free))
(define-ssl SSL_CTX_ctrl (_fun _SSL_CTX* _int _long _pointer -> _long))
(define (SSL_CTX_set_mode ctx m)
(SSL_CTX_ctrl ctx SSL_CTRL_MODE m #f))
(define-ssl SSL_CTX_set_verify (_fun _SSL_CTX* _int _pointer -> _void))
(define-ssl SSL_CTX_use_certificate_chain_file (_fun _SSL_CTX* _bytes -> _int))
(define-ssl SSL_CTX_load_verify_locations (_fun _SSL_CTX* _bytes _bytes -> _int))
(define-ssl SSL_CTX_set_client_CA_list (_fun _SSL_CTX* _X509_NAME* -> _int))
(define-ssl SSL_CTX_set_session_id_context (_fun _SSL_CTX* _bytes _int -> _int))
(define-ssl SSL_CTX_use_RSAPrivateKey_file (_fun _SSL_CTX* _bytes _int -> _int))
(define-ssl SSL_CTX_use_PrivateKey_file (_fun _SSL_CTX* _bytes _int -> _int))
(define-ssl SSL_load_client_CA_file (_fun _bytes -> _X509_NAME*/null))
(define-ssl SSL_CTX_set_cipher_list (_fun _SSL_CTX* _string -> _int))
(define-ssl SSL_free (_fun _SSL* -> _void)
#:wrap (deallocator))
(define-ssl SSL_new (_fun _SSL_CTX* -> _SSL*)
#:wrap (allocator SSL_free))
(define-ssl SSL_set_bio (_fun _SSL* _BIO* _BIO* -> _void))
(define-ssl SSL_connect (_fun _SSL* -> _int))
(define-ssl SSL_accept (_fun _SSL* -> _int))
(define-ssl SSL_read (_fun _SSL* _bytes _int -> _int))
(define-ssl SSL_write (_fun _SSL* _bytes _int -> _int))
(define-ssl SSL_shutdown (_fun _SSL* -> _int))
(define-ssl SSL_get_verify_result (_fun _SSL* -> _long))
(define-ssl SSL_set_verify (_fun _SSL* _int _pointer -> _void))
(define-ssl SSL_set_session_id_context (_fun _SSL* _bytes _int -> _int))
(define-ssl SSL_renegotiate (_fun _SSL* -> _int))
(define-ssl SSL_renegotiate_pending (_fun _SSL* -> _int))
(define-ssl SSL_do_handshake (_fun _SSL* -> _int))
(define-crypto X509_free (_fun _X509* -> _void)
#:wrap (deallocator))
(define-ssl SSL_get_peer_certificate (_fun _SSL* -> _X509*/null)
#:wrap (allocator X509_free))
(define-crypto X509_get_subject_name (_fun _X509* -> _X509_NAME*))
(define-crypto X509_get_issuer_name (_fun _X509* -> _X509_NAME*))
(define-crypto X509_NAME_oneline (_fun _X509_NAME* _bytes _int -> _bytes))
(define-ssl SSL_get_error (_fun _SSL* _int -> _int))
(define-crypto ERR_get_error (_fun -> _long))
(define-crypto ERR_error_string_n (_fun _long _bytes _long -> _void))
(define-ssl SSL_library_init (_fun -> _void))
(define-ssl SSL_load_error_strings (_fun -> _void))
(define-crypto GENERAL_NAME_free _fpointer)
(define-crypto ASN1_STRING_length (_fun _ASN1_STRING* -> _int))
(define-crypto ASN1_STRING_data (_fun _ASN1_STRING* -> _pointer))
(define-crypto X509_NAME_get_index_by_NID (_fun _X509_NAME* _int _int -> _int))
(define-crypto X509_NAME_get_entry (_fun _X509_NAME* _int -> _X509_NAME_ENTRY*/null))
(define-crypto X509_NAME_ENTRY_get_data (_fun _X509_NAME_ENTRY* -> _ASN1_STRING*))
(define-crypto X509_get_ext_d2i (_fun _X509* _int _pointer _pointer -> _STACK*/null))
(define-crypto sk_num (_fun _STACK* -> _int))
(define-crypto sk_GENERAL_NAME_value (_fun _STACK* _int -> _GENERAL_NAME-pointer)
#:c-id sk_value)
(define-crypto sk_pop_free (_fun _STACK* _fpointer -> _void))
;; (define-crypto X509_get_default_cert_area (_fun -> _string))
(define-crypto X509_get_default_cert_dir (_fun -> _string))
(define-crypto X509_get_default_cert_file (_fun -> _string))
(define-crypto X509_get_default_cert_dir_env (_fun -> _string))
(define-crypto X509_get_default_cert_file_env (_fun -> _string))
(define (get-x509-default get-env get-path)
(case (system-type)
((windows)
;; On Windows, SSLeay produces paths like "/usr/local/ssl/certs", which
;; aren't useful. So just skip them.
#f)
(else
(and libcrypto
(let ([result (or (getenv (get-env)) (get-path))])
(with-handlers ([exn:fail? (lambda (e) #f)])
(string->path result)))))))
(define ssl-default-root-certificate-locations
(make-parameter
(filter values
;; FIXME: openssl treats dir as dir-list w/ platform-specific separator
;; (see /crypto/x509/by_dir.c)
(list (get-x509-default X509_get_default_cert_dir_env X509_get_default_cert_dir)
(get-x509-default X509_get_default_cert_file_env X509_get_default_cert_file)))
(lambda (v)
(define (bad)
(raise-argument-error 'ssl-default-root-certificate-locations
"(listof path-string?)"
v))
(unless (list? v) (bad))
(for ([entry (in-list v)]) (unless (or (eq? v #f) (path-string? v)) (bad)))
v)))
(define X509_V_OK 0)
(define SSL_ERROR_SSL 1)
(define SSL_ERROR_WANT_READ 2)
(define SSL_ERROR_WANT_WRITE 3)
(define SSL_ERROR_SYSCALL 5)
(define SSL_ERROR_ZERO_RETURN 6)
(define BIO_C_SET_BUF_MEM_EOF_RETURN 130)
(define SSL_FILETYPE_PEM 1)
(define SSL_FILETYPE_ASN1 2)
(define SSL_VERIFY_NONE #x00)
(define SSL_VERIFY_PEER #x01)
(define SSL_VERIFY_FAIL_IF_NO_PEER_CERT #x02)
(define SSL_MODE_ENABLE_PARTIAL_WRITE #x01)
(define SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER #x02)
(define SSL_CTRL_MODE 33)
(define NID_subject_alt_name 85)
(define NID_commonName 13)
(define GEN_DNS 2)
(define-mzscheme scheme_make_custodian (_fun _pointer -> _scheme))
;; Make this bigger than 4096 to accommodate at least
;; 4096 of unencrypted data
(define BUFFER-SIZE 8000)
;; The man pages for SSL_read and SSL_write say that they must be
;; retried with the same arguments when they return SSL_ERROR_WANT_READ
;; or SSL_ERROR_WANT_WRITE. This may not actually be true, especially
;; when SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is used, and "retry" may or
;; may not mean "retry without doing other things first". Set `enforce-retry?'
;; to #t to obey the manpage and retry without doing other things, which
;; has an implicitation for clients as noted at the top of this file.
(define enforce-retry? #f)
;; Needed for `renegotiate':
(define-cstruct _ssl_struct ([version _int]
[type _int]
[method _pointer]
[rbio _pointer]
[wbio _pointer]
[bbio _pointer]
[rwstate _int]
[in_handshake _int]
[handshake_func _fpointer]
[server _int]
[new_session _int]
[quiet_shutdown _int]
[shutdown _int]
[state _int]
;; ...
))
(define SSL_ST_ACCEPT #x2000)
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Error handling
(define-syntax with-failure
(syntax-rules ()
[(_ thunk body ...)
(with-handlers ([exn? (lambda (exn)
(thunk)
(raise exn))])
body ...)]))
(define (get-error-message id)
(let* ([buffer (make-bytes 512)])
(ERR_error_string_n id buffer (bytes-length buffer))
(regexp-match #rx#"^[^\0]*" buffer)))
(define (check-valid v who what)
(when (ptr-equal? v #f)
(let ([id (ERR_get_error)])
(error who "~a failed ~a" what (get-error-message id)))))
(define (error/network who fmt . args)
(raise (make-exn:fail:network
(format "~a: ~a" who (apply format fmt args))
(current-continuation-marks))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Atomic blocks
(define-syntax-rule (atomically body ...)
(call-as-atomic (lambda () body ...)))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Structs
(define-struct ssl-context (ctx [verify-hostname? #:mutable] [sealed? #:mutable]))
(define-struct (ssl-client-context ssl-context) ())
(define-struct (ssl-server-context ssl-context) ())
(define-struct ssl-listener (l mzctx)
#:property prop:evt (lambda (lst) (wrap-evt (ssl-listener-l lst)
(lambda (x) lst))))
;; internal:
(define-struct mzssl (ssl i o r-bio w-bio pipe-r pipe-w
buffer lock
w-closed? r-closed?
flushing? must-write must-read
refcount
close-original? shutdown-on-close?
error)
#:mutable)
(define (make-immobile-bytes n)
(if 3m?
;; Allocate the byte string via malloc:
(let ([p (malloc 'atomic-interior n)])
(make-sized-byte-string p n))
;; Normal byte string is immobile:
(make-bytes n)))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Errors
(define (do-save-errors thunk ssl)
;; Atomically run a function and get error results
;; so that this library is thread-safe (at the level of Racket threads)
(atomically
(define v (thunk))
(define e (if (positive? v)
0
(SSL_get_error ssl v)))
(define unknown "(unknown error)")
(define estr
(cond
[(= e SSL_ERROR_SSL)
(get-error-message (ERR_get_error))]
[(= e SSL_ERROR_SYSCALL)
(define v (ERR_get_error))
(if (zero? v)
unknown
(get-error-message v))]
[else unknown]))
(values v e estr)))
(define-syntax-rule (save-errors e ssl)
(do-save-errors (lambda () e) ssl))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Contexts, certificates, etc.
(define default-encrypt 'sslv2-or-v3)
(define (encrypt->method who also-expect e client?)
((case e
[(sslv2-or-v3) (if client?
SSLv23_client_method
SSLv23_server_method)]
[(sslv2) (if client?
SSLv2_client_method
SSLv2_server_method)]
[(sslv3) (if client?
SSLv3_client_method
SSLv3_server_method)]
[(tls) (if client?
TLSv1_client_method
TLSv1_server_method)]
[else
(raise-argument-error who
(format "(or/c ~a'sslv2-or-v3 'sslv2 'sslv3 'tls)" also-expect)
e)])))
(define (make-context who protocol-symbol also-expected client?)
(let ([meth (encrypt->method who also-expected protocol-symbol client?)])
(atomically ;; connect SSL_CTX_new to subsequent check-valid (ERR_get_error)
(let ([ctx (SSL_CTX_new meth)])
(check-valid ctx who "context creation")
(SSL_CTX_set_mode ctx (bitwise-ior SSL_MODE_ENABLE_PARTIAL_WRITE
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER))
((if client? make-ssl-client-context make-ssl-server-context) ctx #f #f)))))
(define (ssl-make-client-context [protocol-symbol default-encrypt])
(make-context 'ssl-make-client-context protocol-symbol "" #t))
(define (ssl-make-server-context [protocol-symbol default-encrypt])
(make-context 'ssl-make-server-context protocol-symbol "" #f))
(define (get-context who context-or-encrypt-method client?
#:need-unsealed? [need-unsealed? #f])
(if (ssl-context? context-or-encrypt-method)
(extract-ctx who need-unsealed? context-or-encrypt-method)
(let ([ctx (SSL_CTX_new (encrypt->method who "ssl-context? " context-or-encrypt-method client?))])
(SSL_CTX_set_mode ctx SSL_MODE_ENABLE_PARTIAL_WRITE)
ctx)))
(define (get-context/listener who ssl-context-or-listener [fail? #t]
#:need-unsealed? [need-unsealed? #f])
(cond
[(ssl-context? ssl-context-or-listener)
(extract-ctx who need-unsealed? ssl-context-or-listener)]
[(ssl-listener? ssl-context-or-listener)
(extract-ctx who need-unsealed? (ssl-listener-mzctx ssl-context-or-listener))]
[else
(if fail?
(raise-argument-error who
"(or/c ssl-context? ssl-listener?)"
ssl-context-or-listener)
#f)]))
(define (extract-ctx who need-unsealed? mzctx)
(when (and need-unsealed? (ssl-context-sealed? mzctx))
(error who "context is sealed; no further changes are allowed"))
(ssl-context-ctx mzctx))
(define (ssl-seal-context! mzctx)
(unless (ssl-context? mzctx)
(raise-argument-error 'ssl-seal-context!
"(or/c ssl-client-context? ssl-server-context?)"
mzctx))
(set-ssl-context-sealed?! mzctx #t))
(define (ssl-load-... who load-it ssl-context-or-listener pathname)
(let ([ctx (get-context/listener 'ssl-load-certificate-chain!
ssl-context-or-listener
#:need-unsealed? #t)])
(unless (path-string? pathname)
(raise-argument-error 'ssl-load-certificate-chain!
"path-string?"
pathname))
(let ([path
(path->complete-path (cleanse-path pathname)
(current-directory))])
(security-guard-check-file who path '(read))
(let ([path (path->bytes path)])
(atomically ;; for to connect ERR_get_error to `load-it'
(let ([n (load-it ctx path)])
(unless (= n 1)
(error who "load failed from: ~e ~a"
pathname
(get-error-message (ERR_get_error))))))))))
(define (ssl-load-certificate-chain! ssl-context-or-listener pathname)
(ssl-load-... 'ssl-load-certificate-chain!
SSL_CTX_use_certificate_chain_file
ssl-context-or-listener pathname))
(define (ssl-load-verify-root-certificates! ssl-context-or-listener pathname)
(ssl-load-... 'ssl-load-verify-root-certificates!
(lambda (a b)
(cond [(directory-exists? pathname)
(SSL_CTX_load_verify_locations a #f b)]
[(file-exists? pathname)
(SSL_CTX_load_verify_locations a b #f)]
[else
(error 'ssl-load-verify-root-certificates!
"file or directory does not exist")]))
ssl-context-or-listener pathname))
(define (ssl-load-default-verify-root-certificates! ctx)
(let ([cert-locs (ssl-default-root-certificate-locations)])
(for ([cert-loc (in-list cert-locs)])
(cond [(or (file-exists? cert-loc) (directory-exists? cert-loc))
(ssl-load-verify-root-certificates! ctx cert-loc)]
[else (void)]))))
(define (ssl-load-suggested-certificate-authorities! ssl-listener pathname)
(ssl-load-... 'ssl-load-suggested-certificate-authorities!
(lambda (ctx path)
(let ([stk (SSL_load_client_CA_file path)])
(if (ptr-equal? stk #f)
0
(begin
(SSL_CTX_set_client_CA_list ctx stk)
1))))
ssl-listener pathname))
(define (ssl-load-private-key! ssl-context-or-listener pathname
[rsa? #t] [asn1? #f])
(ssl-load-...
'ssl-load-private-key!
(lambda (ctx path)
((if rsa? SSL_CTX_use_RSAPrivateKey_file SSL_CTX_use_PrivateKey_file)
ctx path
(if asn1? SSL_FILETYPE_ASN1 SSL_FILETYPE_PEM)))
ssl-context-or-listener pathname))
(define (ssl-set-ciphers! context cipher-spec)
(unless (ssl-context? context)
(raise-argument-error 'ssl-set-ciphers!
"(or/c ssl-client-context? ssl-server-context?)"
context))
(unless (string? cipher-spec)
(raise-argument-error 'ssl-set-ciphers! "string?" cipher-spec))
(let* ([ctx (extract-ctx 'ssl-set-ciphers! #t context)]
[result (SSL_CTX_set_cipher_list ctx cipher-spec)])
(unless (= result 1)
(error 'ssl-set-ciphers! "setting cipher list failed"))
(void)))
(define (ssl-set-verify-hostname! ssl-context on?)
(unless (ssl-context? ssl-context)
(raise-argument-error 'ssl-set-verify-hostname!
"(or/c ssl-client-context? ssl-server-context?)"
ssl-context))
(void (extract-ctx 'ssl-set-verify-hostname! #t ssl-context))
(set-ssl-context-verify-hostname?! ssl-context (and on? #t)))
(define (ssl-try-verify! ssl-context-or-listener-or-port on?)
(do-ssl-set-verify! ssl-context-or-listener-or-port on?
'ssl-try-verify!
SSL_VERIFY_PEER))
(define (ssl-set-verify! ssl-context-or-listener-or-port on?)
(do-ssl-set-verify! ssl-context-or-listener-or-port on?
'ssl-set-verify!
(bitwise-ior SSL_VERIFY_PEER
SSL_VERIFY_FAIL_IF_NO_PEER_CERT)))
(define (do-ssl-set-verify! ssl-context-or-listener-or-port on? who mode)
(cond
[(get-context/listener who
ssl-context-or-listener-or-port
#f
#:need-unsealed? #t)
=> (lambda (ctx)
;; required by openssl. This is more for when calling i2d_SSL_SESSION/d2i_SSL_SESSION
;; for instance if we were saving sessions in a database etc... We aren't using that
;; so a generic session name should be fine.
(let ([bytes #"racket"])
(SSL_CTX_set_session_id_context ctx bytes (bytes-length bytes)))
(SSL_CTX_set_verify ctx
(if on?
mode
SSL_VERIFY_NONE)
#f))]
[else
(let-values ([(mzssl input?) (lookup who "(or/c ssl-context? ssl-listener? ssl-port?)"
ssl-context-or-listener-or-port)])
(SSL_set_verify (mzssl-ssl mzssl)
(if on?
mode
SSL_VERIFY_NONE)
#f)
(let ([bytes #"racket"])
(SSL_set_session_id_context (mzssl-ssl mzssl) bytes (bytes-length bytes)))
(when on? (renegotiate who mzssl)))]))
(define (renegotiate who mzssl)
(define (check-err thunk)
(let loop ()
(define-values (v err estr) (save-errors (thunk) (mzssl-ssl mzssl)))
(when (negative? v)
(cond
[(= err SSL_ERROR_WANT_READ)
(let ([n (pump-input-once mzssl #f)])
(if (eq? n 0)
(let ([out-blocked? (pump-output mzssl)])
(sync (mzssl-i mzssl)
(if out-blocked?
(mzssl-o mzssl)
never-evt))
(loop))
(loop)))]
[(= err SSL_ERROR_WANT_WRITE)
(if (pump-output-once mzssl #f #f)
(loop)
(begin
(sync (mzssl-o mzssl))
(loop)))]
[else
(error who "failed: ~a" estr)]))))
(check-err (lambda () (SSL_renegotiate (mzssl-ssl mzssl))))
(check-err (lambda () (SSL_do_handshake (mzssl-ssl mzssl))))
;; Really demanding a negotiation from the server side
;; requires a hacky little dance:
(when (positive? (ssl_struct-server
(cast (mzssl-ssl mzssl) _pointer _ssl_struct-pointer)))
(set-ssl_struct-state! (cast (mzssl-ssl mzssl) _pointer _ssl_struct-pointer)
SSL_ST_ACCEPT)
(check-err (lambda () (SSL_do_handshake (mzssl-ssl mzssl))))))
;; ----
(define (ssl-make-secure-client-context sym)
(let ([ctx (ssl-make-client-context sym)])
;; Load root certificates
(ssl-load-default-verify-root-certificates! ctx)
;; Require verification
(ssl-set-verify! ctx #t)
(ssl-set-verify-hostname! ctx #t)
;; No weak cipher suites; see discussion, patch at http://bugs.python.org/issue13636
(ssl-set-ciphers! ctx "DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2")
;; Seal context so further changes cannot weaken it
(ssl-seal-context! ctx)
ctx))
;; context-cache: (list (weak-box ssl-client-context) (listof path-string) nat) or #f
(define context-cache #f)
(define (ssl-secure-client-context)
(let ([locs (ssl-default-root-certificate-locations)])
(define (reset)
(let* ([now (current-seconds)]
[ctx (ssl-make-secure-client-context 'tls)])
(set! context-cache (list (make-weak-box ctx) locs now))
ctx))
(let* ([cached context-cache]
[c-wb (and cached (car cached))]
[c-ctx (and c-wb (weak-box-value c-wb))]
[c-locs (and cached (cadr cached))]
[c-time (and cached (caddr cached))])
(cond [c-ctx
;; May reuse only if locations haven't changed
;; FIXME: ideally, should also check that no file in locs has changed since
;; c-time, but don't want to hit the filesystem so often
(cond [(equal? locs c-locs) c-ctx]
[else (reset)])]
[else (reset)]))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL ports
(define (mzssl-release mzssl)
;; Lock must be held
(set-mzssl-refcount! mzssl (sub1 (mzssl-refcount mzssl)))
(when (zero? (mzssl-refcount mzssl))
(SSL_free (mzssl-ssl mzssl))
(when (mzssl-close-original? mzssl)
(close-input-port (mzssl-i mzssl))
(close-output-port (mzssl-o mzssl)))))
(define (pump-input-once mzssl need-progress?/out)
(let ([buffer (mzssl-buffer mzssl)]
[i (mzssl-i mzssl)]
[r-bio (mzssl-r-bio mzssl)])
(let ([n ((if (and need-progress?/out
(not (output-port? need-progress?/out)))
read-bytes-avail!
read-bytes-avail!*)
buffer i)])
(cond
[(eof-object? n)
(BIO_set_mem_eof_return r-bio 0)
eof]
[(zero? n)
(when need-progress?/out
(sync need-progress?/out i))
0]
[else
(let ([m (BIO_write r-bio buffer n)])
(unless (= m n)
((mzssl-error mzssl) 'pump-input-once "couldn't write all bytes to BIO!"))
m)]))))
(define (pump-output-once mzssl need-progress? output-blocked-result)
(let ([buffer (mzssl-buffer mzssl)]
[pipe-r (mzssl-pipe-r mzssl)]
[pipe-w (mzssl-pipe-w mzssl)]
[o (mzssl-o mzssl)]
[w-bio (mzssl-w-bio mzssl)])
(let ([n (peek-bytes-avail!* buffer 0 #f pipe-r)])
(if (zero? n)
(let ([n (BIO_read w-bio buffer (bytes-length buffer))])
(if (n . <= . 0)
(begin
(when need-progress?
((mzssl-error mzssl) 'pump-output-once "no output to pump!"))
#f)
(begin
(write-bytes buffer pipe-w 0 n)
(pump-output-once mzssl need-progress? output-blocked-result))))
(let ([n ((if need-progress? write-bytes-avail write-bytes-avail*) buffer o 0 n)])
(if (zero? n)
output-blocked-result
(begin
(port-commit-peeked n (port-progress-evt pipe-r) always-evt pipe-r)
#t)))))))
;; result is #t if there's more data to send out the
;; underlying output port, but the port is full
(define (pump-output mzssl)
(let ([v (pump-output-once mzssl #f 'blocked)])
(if (eq? v 'blocked)
#t
(and v
(pump-output mzssl)))))
(define (make-ssl-input-port mzssl)
;; If SSL_read produces NEED_READ or NEED_WRITE, then the next
;; call to SSL_read must use the same arguments.
;; Use xfer-buffer so we have a consistent buffer to use with
;; SSL_read across calls to the port's write function.
(let-values ([(xfer-buffer) (make-immobile-bytes BUFFER-SIZE)]
[(got-r got-w) (make-pipe)]
[(must-read-len) #f])
(make-input-port/read-to-peek
(format "SSL ~a" (object-name (mzssl-i mzssl)))
;; read proc:
(letrec ([do-read
(lambda (buffer)
(let ([len (or must-read-len (min (bytes-length xfer-buffer)
(bytes-length buffer)))])
(let-values ([(n err estr) (save-errors
(SSL_read (mzssl-ssl mzssl) xfer-buffer len)
(mzssl-ssl mzssl))])
(if (n . >= . 1)
(begin
(set! must-read-len #f)
(if must-read-len
;; If we were forced to try to read a certain amount,
;; then we may have read too much for the immediate
;; request.
(let ([orig-n (bytes-length buffer)])
(bytes-copy! buffer 0 xfer-buffer 0 (min n orig-n))
(when (n . > . orig-n)
(write-bytes buffer got-w orig-n n)))
(bytes-copy! buffer 0 xfer-buffer 0 n))
n)
(let ()
(cond
[(or (= err SSL_ERROR_ZERO_RETURN)
(and (= err SSL_ERROR_SYSCALL) (zero? n)))
;; We hit the end-of-file
(set! must-read-len #f)
eof]
[(= err SSL_ERROR_WANT_READ)
(when enforce-retry?
(set! must-read-len len))
(let ([n (pump-input-once mzssl #f)])
(if (eq? n 0)
(let ([out-blocked? (pump-output mzssl)])
(when enforce-retry?
(set-mzssl-must-read! mzssl (make-semaphore)))
(wrap-evt (choice-evt
(mzssl-i mzssl)
(if out-blocked?
(mzssl-o mzssl)
never-evt))
(lambda (x) 0)))
(do-read buffer)))]
[(= err SSL_ERROR_WANT_WRITE)
(when enforce-retry?
(set! must-read-len len))
(if (pump-output-once mzssl #f #f)
(do-read buffer)
(begin
(when enforce-retry?
(set-mzssl-must-read! mzssl (make-semaphore)))
(wrap-evt (mzssl-o mzssl) (lambda (x) 0))))]
[else
(set! must-read-len #f)
((mzssl-error mzssl) 'read-bytes
"SSL read failed ~a"
estr)]))))))]
[top-read
(lambda (buffer)
(cond
[(mzssl-flushing? mzssl)
;; Flush in progress; try again later:
0]
[(mzssl-must-write mzssl)
=> (lambda (sema)
(wrap-evt (semaphore-peek-evt sema) (lambda (x) 0)))]
[(mzssl-r-closed? mzssl)
0]
[else
(let ([sema (mzssl-must-read mzssl)])
(when sema
(set-mzssl-must-read! mzssl #f)
(semaphore-post sema)))
;; First, try pipe for previously read data:
(let ([n (read-bytes-avail!* buffer got-r)])
(if (zero? n)
;; Nothing already read, so use SSL_read:
(do-read buffer)
;; Got previously read data:
n))]))]
[lock-unavailable
(lambda () (wrap-evt (semaphore-peek-evt (mzssl-lock mzssl))
(lambda (x) 0)))])
(lambda (buffer)
(call-with-semaphore
(mzssl-lock mzssl)
top-read
lock-unavailable
buffer)))
;; fast peek:
#f
;; close proc:
(lambda ()
(call-with-semaphore
(mzssl-lock mzssl)
(lambda ()
(unless (mzssl-r-closed? mzssl)
(set-mzssl-r-closed?! mzssl #t)
(mzssl-release mzssl))))))))
(define (flush-ssl mzssl enable-break?)
;; Make sure that this SSL connection has said everything that it
;; wants to say --- that is, move data from the SLL output to the
;; underlying output port. Depending on the transport, the other end
;; may be stuck trying to tell us something before it will listen,
;; so we also have to read in any available information.
(let loop ()
(let ([v (pump-input-once mzssl #f)])
(if (and (number? v) (positive? v))
;; Received some input, so start over
(loop)
;; Try sending output
(let ([v (pump-output-once mzssl #f 'blocked)])
;; If we sent something, continue tring in case there's more.
;; Further, if we blocked on the underlying output, then
;; wait until either input or output is ready:
(when v
(when (eq? v 'blocked)
((if enable-break? sync/enable-break sync) (mzssl-o mzssl) (mzssl-i mzssl)))
(loop)))))))
(define (kernel-thread thunk)
;; Since we provide #f to scheme_make_custodian,
;; the custodian is managed directly by the root:
(parameterize ([current-custodian (scheme_make_custodian #f)])
(thread thunk)))
(define (make-ssl-output-port mzssl)
;; If SSL_write produces NEED_READ or NEED_WRITE, then the next
;; call to SSL_write must use the same arguments.
;; Use xfer-buffer so we have a consistent buffer to use with
;; SSL_write across calls to the port's write function.
(let ([xfer-buffer (make-immobile-bytes BUFFER-SIZE)]
[buffer-mode (or (file-stream-buffer-mode (mzssl-o mzssl)) 'bloack)]
[flush-ch (make-channel)]
[must-write-len #f])
;; This thread mkoves data from the SLL stream to the underlying
;; output port, because this port's write prodcue claims that the
;; data is flushed if it gets into the SSL stream. In other words,
;; this flushing thread is analogous to the OS's job of pushing
;; data from a socket through the actual network device. It therefore
;; runs with the highest possible custodian:
(kernel-thread (lambda ()
(let loop ()
(sync flush-ch)
(semaphore-wait (mzssl-lock mzssl))
(flush-ssl mzssl #f)
(semaphore-post (mzssl-flushing? mzssl))
(set-mzssl-flushing?! mzssl #f)
(semaphore-post (mzssl-lock mzssl))
(loop))))
;; Create the output port:
(make-output-port
(format "SSL ~a" (object-name (mzssl-o mzssl)))
(mzssl-o mzssl)
;; write proc:
(letrec ([do-write
(lambda (len non-block? enable-break?)
(let ([out-blocked? (pump-output mzssl)])
(if (zero? len)
;; Flush request; all data is in the SSL
;; stream, but make sure it's gone
;; through the ports:
(begin
(flush-ssl mzssl enable-break?)
0)
;; Write request; even if blocking is ok, we treat
;; it as non-blocking and let Racket handle blocking
(let-values ([(n err estr) (save-errors (SSL_write (mzssl-ssl mzssl) xfer-buffer len)
(mzssl-ssl mzssl))])
(if (n . > . 0)
(begin
(set! must-write-len #f)
;; Start flush as necessary:
(cond
[non-block?
;; We can't block, so start the background thread
;; to flush from SSL to the underlying ports:
(set-mzssl-flushing?! mzssl (make-semaphore))
(channel-put flush-ch #t)]
[else
;; We're allowed to block, and things seem to
;; work better if we, try to flush all the way
;; through (even though we're allowed to buffer):
(flush-ssl mzssl enable-break?)])
n)
(let ()
(cond
[(= err SSL_ERROR_WANT_READ)
(when enforce-retry?
(set! must-write-len len))
(let ([n (pump-input-once mzssl #f)])
(if (eq? n 0)
(let ([out-blocked? (pump-output mzssl)])
(when enforce-retry?
(set-mzssl-must-write! mzssl (make-semaphore)))
(wrap-evt (choice-evt
(mzssl-i mzssl)
(if out-blocked?
(mzssl-o mzssl)
never-evt))
(lambda (x) #f)))
(do-write len non-block? enable-break?)))]
[(= err SSL_ERROR_WANT_WRITE)
(when enforce-retry?
(set! must-write-len len))
(if (pump-output-once mzssl #f #f)
(do-write len non-block? enable-break?)
(let ([n (pump-input-once mzssl #f)])
(if (positive? n)
(do-write len non-block? enable-break?)
(begin
(when enforce-retry?
(set-mzssl-must-write! mzssl (make-semaphore)))
(wrap-evt (mzssl-o mzssl) (lambda (x) #f))))))]
[else
(set! must-write-len #f)
((mzssl-error mzssl) 'write-bytes
"SSL write failed ~a"
estr)])))))))]
[top-write
(lambda (buffer s e non-block? enable-break?)
(cond
[(mzssl-flushing? mzssl)
;; Need to wait until flush done
(if (= s e)
;; Let the background flush finish:
(list (semaphore-peek-evt (mzssl-flushing? mzssl)))
;; Try again later:
(wrap-evt always-evt (lambda (v) #f)))]
[(mzssl-w-closed? mzssl)
#f]
[(mzssl-must-read mzssl)
;; Read pending, so wait until it's done:
=> (lambda (sema)
(wrap-evt (semaphore-peek-evt sema) (lambda (x) #f)))]
[else
;; Normal write (since no flush is active or read pending):
(let ([sema (mzssl-must-write mzssl)])
(when sema
(set-mzssl-must-write! mzssl #f)
(semaphore-post sema)))
(let ([len (min (- e s) (bytes-length xfer-buffer))])
(if must-write-len
;; Previous SSL_write result obligates certain output:
(begin
(unless (and (len . >= . must-write-len)
(bytes=? (subbytes xfer-buffer 0 must-write-len)
(subbytes buffer s (+ s must-write-len))))
((mzssl-error mzssl) 'write-bytes
"SSL output request: ~e different from previous unsatisfied request: ~e"
(subbytes buffer s e)
(subbytes xfer-buffer 0 must-write-len)))
(do-write must-write-len non-block? enable-break?))
;; No previous write obligation:
(begin
(bytes-copy! xfer-buffer 0 buffer s (+ s len))
(do-write len non-block? enable-break?))))]))]
[lock-unavailable
(lambda () (wrap-evt (semaphore-peek-evt (mzssl-lock mzssl))
(lambda (x) #f)))])
(lambda (buffer s e non-block? enable-break?)
(let ([v (call-with-semaphore
(mzssl-lock mzssl)
top-write
lock-unavailable
buffer s e non-block? enable-break?)])
(if (pair? v)
(begin
;; Wait on background flush to implement requested flush
(sync (car v))
0)
v))))
;; close proc:
(letrec ([do-close
(lambda ()
(cond
[(mzssl-flushing? mzssl)
(semaphore-peek-evt (mzssl-flushing? mzssl))]
[(mzssl-w-closed? mzssl)
#f]
[else
;; issue shutdown (i.e., EOF on read end)
(when (mzssl-shutdown-on-close? mzssl)
(let loop ([cnt 0])
(let ()
(flush-ssl mzssl #f)
(let-values ([(n err estr) (save-errors (SSL_shutdown (mzssl-ssl mzssl))
(mzssl-ssl mzssl))])
(if (= n 1)
(flush-ssl mzssl #f)
(cond
[(= err SSL_ERROR_WANT_READ)
(let ([out-blocked? (pump-output mzssl)])
(pump-input-once mzssl (if out-blocked? (mzssl-o mzssl) #t)))
(loop cnt)]
[(= err SSL_ERROR_WANT_WRITE)
(pump-output-once mzssl #t #f)
(loop cnt)]
[else
(if (= n 0)
;; When 0 is returned, a shutdown depends on
;; input from the peer. If we've already tried twice,
;; wait for some input and try again.
(begin
(when (cnt . >= . 2)
(pump-input-once mzssl #t))
(loop (add1 cnt)))
((mzssl-error mzssl) 'read-bytes
"SSL shutdown failed ~a"
estr))]))))))
(set-mzssl-w-closed?! mzssl #t)
(mzssl-release mzssl)
#f]))]
[close-loop
(lambda ()
(let ([v (call-with-semaphore
(mzssl-lock mzssl)
do-close)])
(if v
(begin
;; Wait for background flush to finish:
(sync v)
(close-loop))
v)))])
(lambda ()
(close-loop)))
;; Unimplemented port methods:
#f #f #f #f
void 1
;; Buffer mode proc:
(case-lambda
[() buffer-mode]
[(mode) (set! buffer-mode mode)]))))
(define (ports->ssl-ports i o
#:context [context #f]
#:encrypt [encrypt default-encrypt]
#:mode [mode 'connect]
#:close-original? [close-original? #f]
#:shutdown-on-close? [shutdown-on-close? #f]
#:error/ssl [error/ssl error]
#:hostname [hostname #f])
(wrap-ports 'port->ssl-ports i o (or context encrypt) mode
close-original? shutdown-on-close? error/ssl
hostname))
(define (create-ssl who context-or-encrypt-method connect/accept error/ssl)
(define connect?
(case connect/accept
[(connect) #t]
[(accept) #f]
[else (raise-argument-error who "(or/c 'connect 'accept)" connect/accept)]))
(unless (or (symbol? context-or-encrypt-method)
(if connect?
(ssl-client-context? context-or-encrypt-method)
(ssl-server-context? context-or-encrypt-method)))
(error who
"'~a mode requires a ~a context, given: ~e"
(if connect? 'connect 'accept)
(if connect? "client" "server")
context-or-encrypt-method))
(atomically ;; connect functions to subsequent check-valid (ie, ERR_get_error)
(let ([ctx (get-context who context-or-encrypt-method (eq? connect/accept 'connect))])
(check-valid ctx who "context creation")
(with-failure
(lambda () (when (and ctx (symbol? context-or-encrypt-method))
(SSL_CTX_free ctx)))
(let ([r-bio (BIO_new (BIO_s_mem))]
[w-bio (BIO_new (BIO_s_mem))]
[free-bio? #t])
(with-failure
(lambda () (when free-bio?
(BIO_free r-bio)
(BIO_free w-bio)))
(let ([ssl (SSL_new ctx)])
(check-valid ssl who "ssl setup")
;; ssl has a ref count on ctx, so release:
(when (symbol? context-or-encrypt-method)
(SSL_CTX_free ctx)
(set! ctx #f))
(with-failure
(lambda () (SSL_free ssl))
(SSL_set_bio ssl r-bio w-bio)
;; ssl has r-bio & w-bio (no ref count?), so drop it:
(set! free-bio? #f)
(values ssl r-bio w-bio connect?)))))))))
(define (wrap-ports who i o context-or-encrypt-method connect/accept
close? shutdown-on-close? error/ssl
hostname)
(unless (input-port? i)
(raise-argument-error who "input-port?" i))
(unless (output-port? o)
(raise-argument-error who "output-port?" o))
(unless (or (string? hostname) (eq? hostname #f))
(raise-argument-error who "(or/c string? #f)" hostname))
;; Create the SSL connection:
(let-values ([(ssl r-bio w-bio connect?)
(create-ssl who context-or-encrypt-method connect/accept error/ssl)]
[(verify-hostname?)
(cond [(ssl-context? context-or-encrypt-method)
(ssl-context-verify-hostname? context-or-encrypt-method)]
[else #f])])
;; connect/accept:
(let-values ([(buffer) (make-bytes BUFFER-SIZE)]
[(pipe-r pipe-w) (make-pipe)])
(let ([mzssl (make-mzssl ssl i o r-bio w-bio pipe-r pipe-w
buffer (make-semaphore 1)
#f #f
#f #f #f 2
close? shutdown-on-close?
error/ssl)])
(let loop ()
(let-values ([(status err estr) (save-errors (if connect?
(SSL_connect ssl)
(SSL_accept ssl))
ssl)])
(let ([out-blocked? (pump-output mzssl)])
(when (status . < . 1)
(let ()
(cond
[(= err SSL_ERROR_WANT_READ)
(let ([n (pump-input-once mzssl (if out-blocked? o #t))])
(when (eof-object? n)
(error/ssl who "~a failed (input terminated prematurely)"
(if connect? "connect" "accept"))))
(loop)]
[(= err SSL_ERROR_WANT_WRITE)
(pump-output-once mzssl #t #f)
(loop)]
[else
(error/ssl who "~a failed ~a"
(if connect? "connect" "accept")
estr)]))))))
(when verify-hostname?
(unless hostname
(error/ssl who "~a failed (hostname not provided for verification)"
(if connect? "connect" "accept")))
(unless (hostname-in-cert? hostname (SSL_get_peer_certificate ssl))
(error/ssl who "~a failed (certificate not valid for hostname)"
(if connect? "connect" "accept"))))
;; Connection complete; make ports
(values (register (make-ssl-input-port mzssl) mzssl #t)
(register (make-ssl-output-port mzssl) mzssl #f))))))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL port registry
(define ssl-ports (make-weak-hasheq))
(define (register port mzssl input?)
(hash-set! ssl-ports port (make-ephemeron port (cons mzssl input?)))
port)
(define (lookup who what port)
(let ([v (hash-ref ssl-ports port #f)])
(unless v
(raise-argument-error who what port))
(let ([p (ephemeron-value v)])
(values (car p) (cdr p)))))
(define (ssl-addresses p [port-numbers? #f])
(let-values ([(mzssl input?) (lookup 'ssl-addresses "(or/c ssl-port? ssl-listener?)" p)])
(tcp-addresses (if (eq? 'listener input?)
(ssl-listener-l mzssl)
(if input? (mzssl-i mzssl) (mzssl-o mzssl)))
port-numbers?)))
(define (ssl-abandon-port p)
(let-values ([(mzssl input?) (lookup 'ssl-abandon-port "(and/c ssl-port? output-port?)" p)])
(when input?
(raise-argument-error 'ssl-abandon-port "(and/c ssl-port? output-port?)" p))
(set-mzssl-shutdown-on-close?! mzssl #f)
;; Call close-output-port to flush, shutdown, and decrement mzssl refcount.
(close-output-port p)))
(define (ssl-peer-verified? p)
(let-values ([(mzssl input?) (lookup 'ssl-peer-verified? "ssl-port?" p)])
(and (eq? X509_V_OK (SSL_get_verify_result (mzssl-ssl mzssl)))
(SSL_get_peer_certificate (mzssl-ssl mzssl))
#t)))
(define (ssl-peer-subject-name p)
(let ([cert (ssl-port->cert 'ssl-peer-subject-name p)])
(if cert
(let ([bytes (make-bytes 1024 0)])
(X509_NAME_oneline (X509_get_subject_name cert) bytes (bytes-length bytes)))
#f)))
(define (ssl-peer-issuer-name p)
(let ([cert (ssl-port->cert 'ssl-peer-subject-name p)])
(if cert
(let ([bytes (make-bytes 1024 0)])
(X509_NAME_oneline (X509_get_issuer_name cert) bytes (bytes-length bytes)))
#f)))
;; ssl-peer-certificate-hostnames : ssl-port -> (listof string)
(define (ssl-peer-certificate-hostnames p)
(let ([cert (ssl-port->cert 'ssl-peer-certificate-hostnames p)])
(if cert (cert->names cert) null)))
;; ssl-peer-check-hostname : ssl-port string -> boolean
(define (ssl-peer-check-hostname p hostname)
(let ([cert (ssl-port->cert 'ssl-peer-check-hostname p)])
(hostname-in-cert? hostname cert)))
;; ssl-port->cert : symbol ssl-port -> Cert/#f
(define (ssl-port->cert who p)
(let-values ([(mzssl _input?) (lookup who "ssl-port?" p)])
(SSL_get_peer_certificate (mzssl-ssl mzssl))))
;; hostname-in-cert? : string Cert -> boolean
(define (hostname-in-cert? hostname cert)
(for/or ([cert-hostname (in-list (cert->names cert))])
(check-hostname hostname cert-hostname)))
(define (cert->names cert)
;; RFC 2818 (section 3.1) says use subjectAltName dNSName extensions
;; if present, else use final commonName entry.
(let ([names (cert->altnames cert)])
(cond [(pair? names) names]
[else (let ([name (cert->name cert)])
(if name (list name) null))])))
(define (cert->name cert)
;; Returns commonName DNS name if exists, #f otherwise.
(let* ([name (X509_get_subject_name cert)]
[last-cn-index
(let loop ([i -1])
(let ([next (X509_NAME_get_index_by_NID name NID_commonName i)])
(cond [(>= next 0) (loop next)]
[else i])))])
(cond [(< last-cn-index 0) #f]
[else
(let* ([entry (X509_NAME_get_entry name last-cn-index)]
[asn1str (X509_NAME_ENTRY_get_data entry)])
(asn1string->bytes asn1str))])))
(define (asn1string->bytes asn1str)
(let* ([len (ASN1_STRING_length asn1str)]
[data (ASN1_STRING_data asn1str)]
[buf (make-bytes len 0)])
(memcpy buf data len)
;; FIXME: detect UTF-8 strings?
(bytes->string/latin-1 buf)))
(define (cert->altnames cert)
;; Returns list of DNS names in subjectAltName extension
;; FIXME: also return IP addresses?
;; Reference: curl-7.28.0/lib/ssluse.c verifyhost()
;; from http://www.mail-archive.com/openssl-users@openssl.org/msg39142.html
(let* ([namestack (X509_get_ext_d2i cert NID_subject_alt_name #f #f)]
[names
(reverse
(for/fold ([acc null])
([i (in-range (if namestack (sk_num namestack) 0))])
(let ([gn (sk_GENERAL_NAME_value namestack i)])
(cond [(= (GENERAL_NAME-type gn) GEN_DNS)
(let* ([asn1str (GENERAL_NAME-d gn)])
(cons (asn1string->bytes asn1str) acc))]
[else acc]))))])
(when namestack (sk_pop_free namestack GENERAL_NAME_free))
names))
(define (check-hostname cx-name cert-name-pattern)
(let* ([cx-parts (string-split cx-name "." #:trim? #f)]
[cert-parts (string-split cert-name-pattern "." #:trim? #f)])
(and (equal? (length cx-parts)
(length cert-parts))
(andmap check-hostname-part cx-parts cert-parts))))
(define (check-hostname-part cx-part cert-part)
(cond [(equal? cert-part "*")
#t]
[(for/or ([c (in-string cert-part)]) (eqv? c #\*))
(regexp-match? (glob->regexp cert-part) cx-part)]
[else (string-ci=? cx-part cert-part)]))
(define (glob->regexp glob)
(let* ([lit-parts (string-split glob #rx"[*]" #:trim? #f)]
[lit-rxs (for/list ([part (in-list lit-parts)]) (regexp-quote part #f))])
(regexp (string-join lit-rxs ".*"))))
(define (ssl-port? v)
(and (hash-ref ssl-ports v #f) #t))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL listen
(define (ssl-listen port-k
[queue-k 5] [reuse? #f] [hostname-or-#f #f]
[protocol-symbol-or-context default-encrypt])
(let* ([ctx (if (ssl-server-context? protocol-symbol-or-context)
protocol-symbol-or-context
(make-context 'ssl-listen protocol-symbol-or-context
"server context, " #f))]
[l (tcp-listen port-k queue-k reuse? hostname-or-#f)]
[ssl-l (make-ssl-listener l ctx)])
(register ssl-l ssl-l 'listener)))
(define (ssl-close l)
(unless (ssl-listener? l)
(raise-argument-error 'ssl-close "ssl-listener?" l))
(tcp-close (ssl-listener-l l)))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL accept
(define (do-ssl-accept who tcp-accept ssl-listener)
(let-values ([(i o) (tcp-accept (ssl-listener-l ssl-listener))])
;; Obviously, there's a race condition between accepting the
;; connections and installing the exception handler below. However,
;; if breaks are enabled, then i and o could get lost between
;; the time that tcp-accept returns and `i' and `o' are bound,
;; anyway. So we can assume that breaks are enabled without loss
;; of (additional) resources.
(with-handlers ([void (lambda (exn)
(close-input-port i)
(close-output-port o)
(raise exn))])
(wrap-ports who i o (ssl-listener-mzctx ssl-listener) 'accept #t #f error/network #f))))
(define (ssl-accept ssl-listener)
(do-ssl-accept 'ssl-accept tcp-accept ssl-listener))
(define (ssl-accept/enable-break ssl-listener)
(do-ssl-accept 'ssl-accept/enable-break tcp-accept/enable-break ssl-listener))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SSL connect
(define (do-ssl-connect who tcp-connect hostname port-k client-context-or-protocol-symbol)
(let-values ([(i o) (tcp-connect hostname port-k)])
;; See do-ssl-accept for note on race condition here:
(with-handlers ([void (lambda (exn)
(close-input-port i)
(close-output-port o)
(raise exn))])
(wrap-ports who i o client-context-or-protocol-symbol 'connect #t #f error/network
hostname))))
(define (ssl-connect
hostname port-k
[client-context-or-protocol-symbol default-encrypt])
(do-ssl-connect 'ssl-connect
tcp-connect
hostname
port-k
client-context-or-protocol-symbol))
(define (ssl-connect/enable-break
hostname port-k
[client-context-or-protocol-symbol default-encrypt])
(do-ssl-connect 'ssl-connect/enable-break
tcp-connect/enable-break
hostname
port-k
client-context-or-protocol-symbol))
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
(define ssl-available? (and libssl #t))
(define scheme_register_process_global
(and ssl-available?
(get-ffi-obj 'scheme_register_process_global #f (_fun _string _pointer -> _pointer))))
(when ssl-available?
;; Make sure only one place tries to initialize OpenSSL,
;; and wait in case some other place is currently initializing
;; it.
(begin
(start-atomic)
(let* ([done (cast 1 _scheme _pointer)]
[v (scheme_register_process_global "OpenSSL-support-initializing" done)])
(if v
;; Some other place is initializing:
(begin
(end-atomic)
(let loop ()
(unless (scheme_register_process_global "OpenSSL-support-initialized" #f)
(sleep 0.01) ;; busy wait! --- this should be rare
(loop))))
;; This place must initialize:
(begin
(SSL_library_init)
(SSL_load_error_strings)
(scheme_register_process_global "OpenSSL-support-initialized" done)
(end-atomic)))))))