libncftp v. macOS Native curl with Secure Transport APIs and Session Reuse

I am working on adding RFC4217 Secure FTP with TLS by extending Mike Gleason's classic libncftp client library. I refactored the code to include an FTP channel abstraction with FTP channel abstraction types for TCP, TLS, and TCP with Opportunistic TLS types. The first implementation of those included BSD sockets that libncftp has always supported with the clear TCP channel type.

I first embarked on extending the sockets implementation by adding TCP, TLS, and TCP with Opportunistic TLS channel abstraction types against the new, modern Network.framework C-based APIs, including using the “tricky” framer technique to employ a TCP with Opportunistic TLS FTP channel abstraction type to support explicit FTPS as specified by RFC4217 where you have to connect first in the clear with TCP, request AUTH TLS, and then start TLS after receiving positive confirmation. That all worked great.

Unfortunately, at the end of that effort, I discovered that many modern FTPS server implementations (vsftpd, pure-ftpd, proftpd) mandate TLS session reuse / resumption across the control and data channels, specifying the identical session ID and cipher suites across the control and data channels. Since Network.framework lacked a necessary and equivalent to the Secure Transport SSLSetPeerID, I retrenched and rewrote the necessary TLS and TCP with Opportunistic TLS FTP channel abstraction types using the now-deprecated Secure Transport APIs atop the Network.framework-based TCP clear FTP channel type abstraction I had just written.

Using the canonical test server I had been using throughout development, test.rebex.net, this Secure Transport solution seemed to work perfectly, working in clear, secure-control-only, and secure-control+data explicit FTPS operation.

I then proceeded to expand testing to include a broad set of Microsoft FTP Service, pure-ftpd, vsftpd, proftpd, and other FTP servers identified on the Internet (a subset from this list: https://gist.github.com/mnjstwins/85ac8348d6faeb32b25908d447943300).

In doing that testing, beyond test.rebex.net, I was unable to identify a single (among hundreds), that successfully work with secure-control+data explicit FTPS operation even though nearly all of them work with secure-control-only explicit FTPS operation.

So, I started regressing my libncftp + Network.framework + Secure Transport implementation against curl 8.7.1 on macOS 14.7.2 “Sonoma":

% which curl; `which curl` --version
/usr/bin/curl
curl 8.7.1 (x86_64-apple-darwin23.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.61.0
Release-Date: 2024-03-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe UnixSockets

I find that curl (also apparently written against Secure Transport) works in almost all of the cases my libncftp does not. This is a representative example:

% ./samples/misc/ncftpgetbytes -d stderr --secure --explicit --secure-both ftps://ftp.sjtu.edu.cn:21/pub/README.NetInstall

which fails in the secure-control+data case with errSSLClosedAbort on the data channel TLS handshake, just after ClientHello, attempts whereas:

% curl -4 --verbose --ftp-pasv --ftp-ssl-reqd ftp://ftp.sjtu.edu.cn:21/pub/README.NetInstall

succeeds.

I took an in-depth look at the implementation of github.com/apple-oss-distributions/curl/ and git/github.com/apple-oss-distributions/Security/ to identify areas where my implementation was, perhaps, deficient relative to curl and its curl/lib/vtls/sectransp.c Secure Transport implementation. As far as I can tell, I am doing everything consistently with what the Apple OSS implementation of curl is doing. The analysis included:

  • SSLSetALPNProtocols
    • Not applicable for FTP; only used for HTTP/2 and HTTP/3.
  • SSLSetCertificate
    • Should only be relevant when a custom, non-Keychain-based certificate is used.
  • SSLSetEnabledCiphers
    • This could be an issue; however, the cipher suite used for the data channel should be the same as that used for the control channel. curl talks about disabling "weak" cipher suites that are known-insecure even though the default suites macOS enables are unlikely to enable them.
  • SSLSetProtocolVersionEnabled
    • We do not appear to be getting a protocol version negotiation error, so this seems unlikely, but possible.
  • SSLSetProtocolVersionMax
    • We do not appear to be getting a protocol version negotiation error, so this seems unlikely, but possible.
  • SSLSetProtocolVersionMin
    • We do not appear to be getting a protocol version negotiation error, so this seems unlikely, but possible.
  • SSLSetSessionOption( , kSSLSessionOptionFalseStart)
    • curl does seem to enable this for certain versions of macOS and disables it for others. Possible.
    • Running curl with the --false-start option does not seem to make a difference.
  • SSLSetSessionOption( , kSSLSessionOptionSendOneByteRecord)
    • Corresponds to "*****" which seems defaulted and is related to an SSL security flaw when using CBC-based block encryption ciphers, which is not applicable here.

Based on that, further experiments I attempted included:

  • Disable use of kSSLSessionOptionBreakOnServerAuth: No impact
  • Assert use of kSSLSessionOptionFalseStart: No impact
  • Assert use of kSSLSessionOptionSendOneByteRecord: No impact
  • Use SSLSetProtocolVersionMin and SSLSetProtocolVersionMax in various combinations: No impact
  • Use SSLSetProtocolVersionEnabled in various combinations: No impact
  • Forcibly set a single cipher suite (TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, known to work with a given server): No impact
  • Employ a SetDefaultCipherSuites function similar to what curl does (filtering out “weak” cipher suites): No impact
    • Notably, I can never coax a similar set of cipher suites that macOS curl does with that technique. In fact, it publishes ciphers that aren’t even in <Security/CipherSuite.h> nor referenced by github.com/apple-oss-distributions/curl/curl/lib/vtls/sectransp.c.
  • Assert use of kSSLSessionOptionAllowRenegotiation: No impact
  • Assert use of kSSLSessionOptionEnableSessionTickets: No impact

Looking at Wireshark, my ClientHello includes status_request, signed_certificate_timestamp, and extended_master_secret extensions whereas macOS curl's never do--same Secure Transport APIs. None of the above API experiments seem to influence the inclusion / exclusion of those three ClientHello additions.

Any suggestions are welcomed that might shine a light on what native curl has access to that allows it to work with ST for these FTP secure-control+data use cases.

After much debugging, I was able to resolve this.

RFC4217 is vague, at best, on this; however, while intuition might dictate using a kTypeTLS (in my implementation, one in which the TCP connection and TLS handshake are synchronous and done back-to-back) channel type for securing the data channel and clubbing the TCP connection with the TLS handshake and doing all of this before sending a data-initiating command (such as NLST or RETR), in practice this works with only a scant number of FTPS server implementations (such as the custom Microsoft .NET implementation used for test.rebex.net).

Instead, the kTypeTCPOpportunisticTLS (in my implementation, one in which the TCP connection and TLS handshake are asynchronous and done independently) channel type hits the "broader side of the barn" of FTPS server implementations with a:

  1. TCP connect
  2. Sending the data-initiating command
  3. Performing the TLS handshake to secure the channel

order of operations.

That sending the data-initiating command (such as NLST or RETR) is interposed between (1) and (3) was a bit surprising.

Now that I have made this change, this allows my implementation to work with either secure control-only or control+data with the following implementations:

  • The test.rebex.net custom Microsoft .NET implementation.
  • FileZilla
  • Microsoft FTP Service
  • proftpd
  • pure-ftpd
  • vsftpd

But, just to confirm, that means that can reproduce this with a server under your control, right? If so, that’s definitely a progress.

Confirmed. A correlating factor seems to be those servers written against OpenSSL, possibly running on Linux. See below.

OK. And cases 1 and 2 (curl with Secure Transport, and Fetch 5.8) work whereas case 3 (your libncftp code) fails, right?

Confirmed.

FTPS ServerSecure Control-onlySecure Control+Data
test.rebex.net Custom .NET Implementation
Microsoft FTP Service
proftpd
pureftpd
vsftpd

Until I added code to utilize SSLSetPeerID, Secure Control+Data was ❌ up and down that column.

Adding code to work with SSLSetPeerID allowed the test.rebex.net Custom .NET Implementation to work on stop responding with 425: Cannot secure data connection - TLS session resumption required. on attempting to establish the secure data channel session.

I dove back into Wireshark to cross-check the Session IDs sent among the control channel ClientHello / ServerHello and the data channel ClientHello / ServerHello.

  • control channel
    • ClientHello: Session ID Length: 0
    • ServerHello: Session ID Length: 32, Session ID: ad28106094597d2dbba4b319761e30ed9f896fdc01d467ce66764967cebf45bc
  • data channel
    • ClientHello: Session ID Length: 32, Session ID: ad28106094597d2dbba4b319761e30ed9f896fdc01d467ce66764967cebf45bc

Of course, the server doesn't like the data channel ClientHello, so we never see the ServerHello there; however, this does confirm that the session ID is reused from the control channel, as desired. This implies that there is something else about the ClientHello the server does not like.

Looking at the Wireshark decode a bit more closely between curl w/ Secure Transport and libncftp w/ Secure Transport, I noticed something that I missed the first time I looked.

The two ClientHello payloads are otherwise identical in size and content, except for, as expected, the random, timestamp, and session ID portions.

However, the item that escaped my glance was this from the packet decode:

  • curl w/ Secure Transport data ClientHello: TLSv1.2 Record Layer: Handshake Protocol: Client Hello
  • libncftp w/ Secure Transport data ClientHello: TLSv1 Record Layer: Handshake Protocol: Client Hello

Note that curl has TLSv1.2 whereas libncftp has only TLSv1. I assume this is simply a decoding anomaly since the hex dumps compare as expected. The brackets and pipes are my own additions to call out the expected timestamp, random bytes, and session ID differences.

curl

0000   f0 9f c2 10 27 1c e4 50 eb ba 2f d7 08 00 45 00   ....'..P../...E.
0010   01 11 00 00 40 00 40 06 00 00 c0 a8 01 80 ca 26   ....@.@........&
0020   61 e6 c7 63 88 53 fa c6 37 4e ea 9b 3f 32 80 18   a..c.S..7N..?2..
0030   08 0a ef 38 00 00 01 01 08 0a 38 20 83 ef e3 37   ...8......8 ...7
0040   2e 58 16 03 01 00 d8 01 00 00 d4 03 03[67 a5 bb   .X...........g..
0050   d6|81 ca 86 5c 73 35 b5 81 d3 e5 dd 36 76 91 58   ....\s5.....6v.X
0060   5d 34 b6 f7 ee be ad dc d5 cd b8 a6 4d]20[91 51   ]4..........M .Q
0070   72 52 d3 42 49 6d fd 12 39 e8 ad e2 c0 6f 48 22   rR.BIm..9....oH"
0080   81 57 91 d5 4f 03 92 c8 36 71 6a 7c 1b 64]00 3a   .W..O...6qj|.d.:
0090   00 ff c0 2c c0 2b c0 24 c0 23 c0 0a c0 09 c0 30   ...,.+.$.#.....0
00a0   c0 2f c0 28 c0 27 c0 14 c0 13 00 9f 00 9e 00 6b   ./.(.'.........k
00b0   00 67 00 39 00 33 00 9d 00 9c 00 3d 00 3c 00 35   .g.9.3.....=.<.5
00c0   00 2f 00 af 00 ae 00 8d 00 8c 01 00 00 51 00 00   ./...........Q..
00d0   00 14 00 12 00 00 0f 66 74 70 2e 73 6a 74 75 2e   .......ftp.sjtu.
00e0   65 64 75 2e 63 6e 00 0a 00 08 00 06 00 17 00 18   edu.cn..........
00f0   00 19 00 0b 00 02 01 00 00 0d 00 12 00 10 04 01   ................
0100   02 01 05 01 06 01 04 03 02 03 05 03 06 03 00 05   ................
0110   00 05 01 00 00 00 00 00 12 00 00 00 17 00 00      ...............

libncftp

0000   f0 9f c2 10 27 1c e4 50 eb ba 2f d7 08 00 45 00   ....'..P../...E.
0010   01 11 00 00 40 00 40 06 00 00 c0 a8 01 80 ca 26   ....@.@........&
0020   61 e6 c5 cc ac d7 d3 d9 07 db 23 63 aa 6c 80 18   a.........#c.l..
0030   08 0a ef 38 00 00 01 01 08 0a 41 60 7d d1 e3 26   ...8......A`}..&
0040   39 99 16 03 01 00 d8 01 00 00 d4 03 03[67 a5 b7   9............g..
0050   7f|8f 37 c3 08 81 8f 72 3a 2d 51 26 9b ce95 8a   ..7....r:-Q&....
0060   0b 06 9d 81 c3 6b 3c f3 3a 81 a6 fa 4c]20[f8 f4   .....k<.:...L ..
0070   23 72 b1 38 f1 ea 12 12 4e 09 bd e2 ab b2 8f 34   #r.8....N......4
0080   8b 97 74 5f 79 2a 37 b4 96 10 35 ca 7a b1]00 3a   ..t_y*7...5.z..:
0090   00 ff c0 2c c0 2b c0 24 c0 23 c0 0a c0 09 c0 30   ...,.+.$.#.....0
00a0   c0 2f c0 28 c0 27 c0 14 c0 13 00 9f 00 9e 00 6b   ./.(.'.........k
00b0   00 67 00 39 00 33 00 9d 00 9c 00 3d 00 3c 00 35   .g.9.3.....=.<.5
00c0   00 2f 00 af 00 ae 00 8d 00 8c 01 00 00 51 00 00   ./...........Q..
00d0   00 14 00 12 00 00 0f 66 74 70 2e 73 6a 74 75 2e   .......ftp.sjtu.
00e0   65 64 75 2e 63 6e 00 0a 00 08 00 06 00 17 00 18   edu.cn..........
00f0   00 19 00 0b 00 02 01 00 00 0d 00 12 00 10 04 01   ................
0100   02 01 05 01 06 01 04 03 02 03 05 03 06 03 00 05   ................
0110   00 05 01 00 00 00 00 00 12 00 00 00 17 00 00      ...............

After much debugging, I was able to resolve this.

RFC4217 is vague, at best, on this; however, while intuition might dictate using a kTypeTLS (in my implementation, one in which the TCP connection and TLS handshake are synchronous and done back-to-back) channel type for securing the data channel and clubbing the TCP connection with the TLS handshake and doing all of this before sending a data-initiating command (such as NLST or RETR), in practice this works with only a scant number of FTPS server implementations (such as the custom Microsoft .NET implementation used for test.rebex.net).

Instead, the kTypeTCPOpportunisticTLS (in my implementation, one in which the TCP connection and TLS handshake are asynchronous and done independently) channel type hits the "broader side of the barn" of FTPS server implementations with a:

  1. TCP connect
  2. Sending the data-initiating command
  3. Performing the TLS handshake to secure the channel

order of operations.

That sending the data-initiating command (such as NLST or RETR) is interposed between (1) and (3) was a bit surprising.

Now that I have made this change, this allows my implementation to work with either secure control-only or control+data with the following implementations:

  • The test.rebex.net custom Microsoft .NET implementation.
  • FileZilla
  • Microsoft FTP Service
  • proftpd
  • pure-ftpd
  • vsftpd

Interesting. Thanks for sharing your resolution.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

libncftp v. macOS Native curl with Secure Transport APIs and Session Reuse
 
 
Q