]> gitweb.factorcode.org Git - factor.git/blob - basis/io/sockets/secure/openssl/openssl.factor
io.files: exists? -> file-exists? and rename primitive.
[factor.git] / basis / io / sockets / secure / openssl / openssl.factor
1 ! Copyright (C) 2007, 2008, Slava Pestov, Elie CHAFTARI.
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: accessors alien alien.c-types alien.data alien.enums
4 alien.strings assocs byte-arrays classes.struct combinators
5 combinators.short-circuit destructors io io.backend io.binary
6 io.buffers io.encodings.latin1 io.encodings.string
7 io.encodings.utf8 io.files io.pathnames io.ports io.sockets
8 io.sockets.secure io.timeouts kernel libc math math.functions
9 math.order math.parser namespaces openssl openssl.libcrypto
10 openssl.libssl random sequences sets splitting unicode ;
11 SLOT: alpn-supported-protocols
12 IN: io.sockets.secure.openssl
13
14 GENERIC: ssl-method ( symbol -- method )
15 M: TLSv1 ssl-method drop TLSv1_method ;
16 M: TLSv1.1 ssl-method drop TLSv1_1_method ;
17 M: TLSv1.2 ssl-method drop TLSv1_2_method ;
18
19 MEMO: make-cipher-list ( -- string )
20     {
21         "ECDHE-ECDSA-AES256-GCM-SHA384"
22         "ECDHE-ECDSA-AES256-SHA384"
23         "ECDHE-ECDSA-AES128-GCM-SHA256"
24         "ECDHE-ECDSA-AES128-SHA256"
25         "ECDHE-RSA-AES256-GCM-SHA384"
26         "ECDHE-RSA-AES256-SHA384"
27         "ECDHE-RSA-AES128-GCM-SHA256"
28         "ECDHE-RSA-AES128-SHA256"
29         "ECDHE-ECDSA-AES256-CCM8"
30         "ECDHE-ECDSA-AES256-CCM"
31         "ECDHE-ECDSA-AES128-CCM8"
32         "ECDHE-ECDSA-AES128-CCM"
33         "ECDHE-ECDSA-CAMELLIA256-SHA384"
34         "ECDHE-RSA-CAMELLIA256-SHA384"
35         "ECDHE-ECDSA-CAMELLIA128-SHA256"
36         "ECDHE-RSA-CAMELLIA128-SHA256"
37         "ECDHE-RSA-CHACHA20-POLY1305"
38         "ECDHE-ECDSA-CHACHA20-POLY1305"
39         "ECDHE-PSK-CHACHA20-POLY1305"
40         "AES256-SHA"
41         "AES128-SHA256"
42         "AES128-SHA"
43         "CAMELLIA256-SHA"
44         "CAMELLIA128-SHA"
45         "IDEA-CBC-SHA"
46         "DES-CBC3-SHA"
47     } ":" join ;
48
49 TUPLE: openssl-context < secure-context aliens sessions ;
50
51 <PRIVATE
52
53 : bn-bytes-needed ( num -- bytes-required )
54     log2 1 + 8 / ceiling ;
55
56 PRIVATE>
57
58 : number>bn ( num -- bn )
59     dup bn-bytes-needed >be
60     dup length
61     f BN_bin2bn ; inline
62
63 : disable-old-tls ( ctx -- )
64     handle>>
65     SSL_OP_NO_TLSv1 SSL_OP_NO_TLSv1_1 bitor
66     SSL_CTX_set_options ssl-error ;
67
68 : set-session-cache ( ctx -- )
69     handle>>
70     [ SSL_SESS_CACHE_BOTH SSL_CTX_set_session_cache_mode ssl-error ]
71     [ 32 random-bits >hex dup length SSL_CTX_set_session_id_context ssl-error ]
72     bi ;
73
74 ERROR: file-expected path ;
75
76 : ensure-exists ( path -- path )
77     dup file-exists? [ file-expected ] unless ; inline
78
79 : ssl-file-path ( path -- path' )
80     absolute-path ensure-exists ;
81
82 : load-certificate-chain ( ctx -- )
83     dup config>> key-file>> [
84         [ handle>> ] [ config>> key-file>> ssl-file-path ] bi
85         SSL_CTX_use_certificate_chain_file
86         ssl-error
87     ] [ drop ] if ;
88
89 : password-callback ( -- alien )
90     int { void* int bool void* } cdecl
91     [| buf size rwflag password! |
92         password [ B{ 0 } password! ] unless
93
94         password strlen :> len
95         buf password len 1 + size min memcpy
96         len
97     ] alien-callback ;
98
99 : default-pasword ( ctx -- alien )
100     [ config>> password>> latin1 malloc-string ] [ aliens>> ] bi
101     [ push ] [ drop ] 2bi ;
102
103 : set-default-password ( ctx -- )
104     dup config>> password>> [
105         [ handle>> password-callback SSL_CTX_set_default_passwd_cb ]
106         [
107             [ handle>> ] [ default-pasword ] bi
108             SSL_CTX_set_default_passwd_cb_userdata
109         ] bi
110     ] [ drop ] if ;
111
112 : use-private-key-file ( ctx -- )
113     dup config>> key-file>> [
114         [ handle>> ]
115         [ config>> key-file>> ssl-file-path ] bi
116         SSL_FILETYPE_PEM SSL_CTX_use_PrivateKey_file
117         ssl-error
118     ] [ drop ] if ;
119
120 : load-verify-locations ( ctx -- )
121     dup config>> [ ca-file>> ] [ ca-path>> ] bi or [
122         [ handle>> ]
123         [
124             config>>
125             [ ca-file>> dup [ ssl-file-path ] when ]
126             [ ca-path>> dup [ ssl-file-path ] when ] bi
127         ] bi
128         SSL_CTX_load_verify_locations
129     ] [ handle>> SSL_CTX_set_default_verify_paths ] if ssl-error ;
130
131 : set-verify-depth ( ctx -- )
132     dup config>> verify-depth>> [
133         [ handle>> ] [ config>> verify-depth>> ] bi
134         SSL_CTX_set_verify_depth
135     ] [ drop ] if ;
136
137 TUPLE: bio < disposable handle ;
138
139 : <bio> ( handle -- bio ) bio new-disposable swap >>handle ;
140
141 M: bio dispose* handle>> BIO_free ssl-error ;
142
143 : <file-bio> ( path -- bio )
144     normalize-path "r" BIO_new_file dup ssl-error <bio> ;
145
146 : load-dh-params ( ctx -- )
147     dup config>> dh-file>> [
148         [ handle>> ] [ config>> dh-file>> ] bi <file-bio> &dispose
149         handle>> f f f PEM_read_bio_DHparams dup ssl-error
150         SSL_CTX_set_tmp_dh ssl-error
151     ] [ drop ] if ;
152
153 ! Attempt to set ecdh. If it fails, ignore...?
154 : set-ecdh-params ( ctx -- )
155     handle>> SSL_CTRL_SET_ECDH_AUTO 1 f SSL_CTX_ctrl drop ;
156
157 : <openssl-context> ( config ctx -- context )
158     openssl-context new-disposable
159         swap >>handle
160         swap >>config
161         V{ } clone >>aliens
162         H{ } clone >>sessions ;
163
164 M: openssl <secure-context>
165     maybe-init-ssl
166     [
167         dup method>> ssl-method SSL_CTX_new
168         dup ssl-error <openssl-context> |dispose
169         {
170             [ set-session-cache ]
171             [ load-certificate-chain ]
172             [ set-default-password ]
173             [ use-private-key-file ]
174             [ load-verify-locations ]
175             [ set-verify-depth ]
176             [ load-dh-params ]
177             [ set-ecdh-params ]
178             [ ]
179         } cleave
180     ] with-destructors ;
181
182 M: openssl-context dispose*
183     [
184         [ aliens>> [ &free drop ] each ]
185         [ sessions>> values [ SSL_SESSION_free ] each ]
186         [ handle>> SSL_CTX_free ]
187         tri
188     ] with-destructors ;
189
190 TUPLE: ssl-handle < disposable file handle connected terminated ;
191
192 SYMBOL: default-secure-context
193
194 : current-secure-context ( -- ctx )
195     secure-context get [
196         default-secure-context [
197             <secure-config> <secure-context>
198         ] initialize-alien
199     ] unless* ;
200
201 : get-session ( addrspec -- session/f )
202     current-secure-context sessions>> at ;
203
204 : save-session ( session addrspec -- )
205     current-secure-context sessions>> set-at ;
206
207 : set-secure-cipher-list-only ( ssl -- ssl )
208     dup handle>> make-cipher-list SSL_set_cipher_list ssl-error ;
209
210 : <ssl-handle> ( fd -- ssl )
211     [
212         ssl-handle new-disposable |dispose
213         current-secure-context handle>> SSL_new
214         dup ssl-error >>handle
215         swap >>file
216         set-secure-cipher-list-only
217     ] with-destructors ;
218
219 <PRIVATE
220
221 : alpn_select_cb_func ( -- alien )
222     [|  ssl out outlen in inlen arg |
223         ! if alpn-protocols is empty return err noack
224
225         ! current-secure-context relies on secure-context
226         ! variable being set. if this is not set in a callback,
227         ! we need some other way of accessing it (probably
228         ! passing it as arg to SSL_CTX_set_alpn_select_cb, but
229         ! need to make sure that stays defined as long as the
230         ! callback can be called)
231         current-secure-context config>> alpn-supported-protocols>>
232         [ SSL_TLSEXT_ERR_NOACK ]
233         [ [ out outlen ] dip
234           ! convert alpn-protocols from list of strings to
235           ! c-string in wire format and length.
236           ! see https://www.openssl.org/docs/manmaster/man3/SSL_set_alpn_protos.html
237           [ utf8 encode dup length prefix ] map
238           concat dup length
239           in inlen SSL_select_next_proto
240           ! the function returns OPENSSL_NPN_NO_OVERLAP when no
241           ! match is found, otherwise OPENSSL_NPN_NEGOTIATED
242           OPENSSL_NPN_NEGOTIATED =
243           [ ! DOUBLECHECK: The value in out is already copied
244             ! from the original, so we can just leave it and
245             ! return... otherwise this detail needs to be ironed
246             ! out, probably by finding the entry in in that out
247             ! is identical to. (out needs to point directly into
248             ! in, or a buffer that will outlive the tls
249             ! handshake.)
250             SSL_TLSEXT_ERR_OK ]
251           [ SSL_TLSEXT_ERR_ALERT_FATAL ] if
252         ] if-empty
253     ] SSL_CTX_alpn_select_cb_func ;
254
255 : get_alpn_selected_wrapper ( ssl* -- alpn_string/f )
256     { c-string int } [ SSL_get0_alpn_selected ] with-out-parameters
257     drop ! how do we unbox the c-string?
258     ! also, the string is not null-terminated, is that problematic?
259     ;
260
261 PRIVATE>
262
263 :: <ssl-socket> ( winsock hostname -- ssl )
264     winsock socket-handle BIO_NOCLOSE BIO_new_socket dup ssl-error :> bio
265     winsock <ssl-handle> :> handle
266     handle handle>> :> native-handle
267     current-secure-context config>> alpn-supported-protocols>>
268     [ drop native-handle ctx>> alpn_select_cb_func f SSL_CTX_set_alpn_select_cb ]
269     unless-empty
270     hostname [
271         utf8 string>alien
272         native-handle swap SSL_set_tlsext_host_name ssl-error
273     ] when*
274     native-handle bio bio SSL_set_bio
275     handle ;
276
277 : ssl-error-syscall ( ssl-handle -- event/f )
278     f >>connected
279     t >>terminated drop
280     ERR_get_error {
281         { -1 [
282             errno { [ ECONNRESET = ] [ EPIPE = ] } 1||
283             [ premature-close ] [ throw-errno ] if f
284         ] }
285         ! https://stackoverflow.com/questions/13686398/ssl-read-failing-with-ssl-error-syscall-error
286         ! 0 means EOF
287         { 0 [ f ] }
288     } case ;
289
290 : check-ssl-error ( ssl-handle ret -- event/f )
291     [ drop ] [ [ handle>> ] dip SSL_get_error ] 2bi
292     {
293         { SSL_ERROR_NONE [ drop f ] }
294         { SSL_ERROR_WANT_READ [ drop +input+ ] }
295         { SSL_ERROR_WANT_WRITE [ drop +output+ ] }
296         { SSL_ERROR_SYSCALL [ ssl-error-syscall ] }
297         { SSL_ERROR_SSL [ drop throw-ssl-error ] }
298         ! https://stackoverflow.com/questions/50223224/ssl-read-returns-ssl-error-zero-return-but-err-get-error-is-0
299         ! we got disconnected
300         { SSL_ERROR_ZERO_RETURN [ t >>terminated f >>connected drop f ] }
301         { SSL_ERROR_WANT_ACCEPT [ drop +input+ ] }
302     } case ;
303
304 ! Accept
305 : do-ssl-accept-once ( ssl-handle -- event/f )
306     dup handle>> SSL_accept check-ssl-error ;
307
308 : do-ssl-accept ( ssl-handle -- )
309     dup do-ssl-accept-once
310     [ [ dup file>> ] dip wait-for-fd do-ssl-accept ] [ drop ] if* ;
311
312 : maybe-handshake ( ssl-handle -- )
313     dup connected>> [ drop ] [
314         dup terminated>> [
315             drop
316         ] [
317             [ [ do-ssl-accept ] with-timeout ]
318             [ t swap connected<< ] bi
319         ] if
320     ] if ;
321
322 ! Input ports
323 : do-ssl-read ( buffer ssl-handle -- event/f )
324     2dup handle>> swap [ buffer-end ] [ buffer-capacity ] bi SSL_read
325     [ check-ssl-error ] keep swap [ 2nip ] [ swap buffer+ f ] if* ;
326
327 M: ssl-handle refill
328     dup maybe-handshake [ buffer>> ] dip do-ssl-read ;
329
330 ! Output ports
331 : do-ssl-write ( buffer ssl-handle -- event/f )
332     2dup handle>> swap [ buffer@ ] [ buffer-length ] bi SSL_write
333     [ check-ssl-error ] keep swap [ 2nip ] [ swap buffer-consume f ] if* ;
334
335 M: ssl-handle drain
336     dup maybe-handshake [ buffer>> ] dip do-ssl-write ;
337
338 ! Connect
339 : do-ssl-connect-once ( ssl-handle -- event/f )
340     dup handle>> SSL_connect check-ssl-error ;
341
342 : do-ssl-connect ( ssl-handle -- )
343     dup do-ssl-connect-once
344     [ dupd wait-for-fd do-ssl-connect ] [ drop ] if* ;
345
346 : resume-session ( ssl-handle ssl-session -- )
347     [ [ handle>> ] dip SSL_set_session ssl-error ]
348     [ drop do-ssl-connect ]
349     2bi ;
350
351 : begin-session ( ssl-handle addrspec -- )
352     [ drop do-ssl-connect ]
353     [ [ handle>> SSL_get1_session ] dip save-session ]
354     2bi ;
355
356 : secure-connection ( client-out addrspec -- )
357     [ handle>> ] dip
358     [
359         '[
360             _ dup get-session
361             [ resume-session ] [ begin-session ] ?if
362         ] with-timeout
363     ] [ drop t >>connected drop ] 2bi ;
364
365 M: ssl-handle timeout
366     drop secure-socket-timeout get ;
367
368 M: ssl-handle cancel-operation
369     file>> cancel-operation ;
370
371 M: ssl-handle dispose*
372     [
373         ! Free file>> after SSL_free
374         [ file>> &dispose drop ]
375         [ handle>> SSL_free ] bi
376     ] with-destructors ;
377
378 : check-verify-result ( ssl-handle -- )
379     SSL_get_verify_result X509_V_ERROR number>enum dup X509_V_ERR_OK =
380     [ drop ] [ certificate-verify-error ] if ;
381
382 : x509name>string ( x509name -- string )
383     NID_commonName 256 <byte-array>
384     [ 256 X509_NAME_get_text_by_NID ] keep
385     swap -1 = [ drop f ] [ latin1 alien>string ] if ;
386
387 : subject-name ( certificate -- host )
388     X509_get_subject_name x509name>string ;
389
390 : issuer-name ( certificate -- issuer )
391     X509_get_issuer_name x509name>string ;
392
393 : sk-value ( stack v -- obj )
394     ssl-new-api? get-global [ OPENSSL_sk_value ] [ sk_value ] if ;
395
396 : sk-num ( stack -- num )
397     ssl-new-api? get-global [ OPENSSL_sk_num ] [ sk_num ] if ;
398
399 : name-stack>sequence ( name-stack -- seq )
400     dup sk-num <iota> [
401         sk-value GENERAL_NAME_st memory>struct
402     ] with map ;
403
404 : alternative-dns-names ( certificate -- dns-names )
405     NID_subject_alt_name f f X509_get_ext_d2i
406     [ name-stack>sequence ] [ f ] if*
407     [ type>> GEN_DNS = ] filter
408     [ d>> dNSName>> data>> utf8 alien>string ] map ;
409
410 ! *.foo.com matches: foo.com, www.foo.com, a.foo.com
411 ! *.bar.foo.com matches: bar.foo.com, www.bar.foo.com, b.bar.foo.com
412 : subject-names-match? ( name pattern -- ? )
413     [ >lower ] bi@
414     "*." ?head [
415         {
416             [ tail? ]
417             [ [ [ CHAR: . = ] count ] bi@ - 1 <= ]
418         } 2&&
419     ] [
420         =
421     ] if ;
422
423 : check-subject-name ( host ssl-handle -- )
424     SSL_get_peer_certificate [
425         [ alternative-dns-names ]
426         [ subject-name ] bi suffix members
427         2dup [ subject-names-match? ] with any?
428         [ 2drop ] [ subject-name-verify-error ] if
429     ] [ certificate-missing-error ] if* ;
430
431 M: openssl check-certificate
432     current-secure-context config>> verify>> [
433         handle>>
434         [ nip check-verify-result ]
435         [ check-subject-name ]
436         2bi
437     ] [ 2drop ] if ;
438
439 : check-buffer ( port -- port )
440     dup buffer>> buffer-empty? [ upgrade-buffers-full ] unless ;
441
442 : input/output-ports ( -- input output )
443     input-stream output-stream
444     [ get underlying-port check-buffer ] bi@
445     2dup [ handle>> ] bi@ eq? [ upgrade-on-non-socket ] unless ;
446
447 : make-input/output-secure ( input output -- )
448     dup handle>> non-ssl-socket? [ upgrade-on-non-socket ] unless
449     [ f <ssl-socket> ] change-handle
450     handle>> >>handle drop ;
451
452 : (send-secure-handshake) ( output -- )
453     remote-address get [ upgrade-on-non-socket ] unless*
454     secure-connection ;
455
456 M: openssl send-secure-handshake
457     input/output-ports
458     [ make-input/output-secure ] keep
459     [ (send-secure-handshake) ] keep
460     remote-address get dup inet? [
461         host>> swap handle>> check-certificate
462     ] [ 2drop ] if ;
463
464 M: openssl accept-secure-handshake
465     input/output-ports
466     make-input/output-secure ;
467
468 openssl secure-socket-backend set-global