diff --git a/examples/opusfile_example.c b/examples/opusfile_example.c index 0394d3bde44e45ccbaed58a8aa40e5d724263ecc..b81e5efeb2f6e879a612231ff8d693905ff3ebdd 100644 --- a/examples/opusfile_example.c +++ b/examples/opusfile_example.c @@ -128,7 +128,7 @@ int main(int _argc,const char **_argv){ } else{ /*Try to treat the argument as a URL.*/ - of=op_open_url(_argv[1],&ret,OP_SSL_SKIP_CERTIFICATE_CHECK(1),NULL); + of=op_open_url(_argv[1],&ret,OP_SSL_SKIP_CERTIFICATE_CHECK(0),NULL); #if 0 if(of==NULL){ OpusFileCallbacks cb={NULL,NULL,NULL,NULL}; diff --git a/src/http.c b/src/http.c index d65928fe2cad5745bcc3f2bacd1e2852a336aaf2..0d549f80e6447eea3ed114bf5c9d849ae102c7c2 100644 --- a/src/http.c +++ b/src/http.c @@ -17,11 +17,15 @@ /*RFCs referenced in this file: RFC 761: DOD Standard Transmission Control Protocol + RFC 1535: A Security Problem and Proposed Correction With Widely Deployed DNS + Software RFC 1738: Uniform Resource Locators (URL) RFC 1945: Hypertext Transfer Protocol -- HTTP/1.0 RFC 2068: Hypertext Transfer Protocol -- HTTP/1.1 RFC 2145: Use and Interpretation of HTTP Version Numbers RFC 2246: The TLS Protocol Version 1.0 + RFC 2459: Internet X.509 Public Key Infrastructure Certificate and + Certificate Revocation List (CRL) Profile RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 RFC 2617: HTTP Authentication: Basic and Digest Access Authentication RFC 2817: Upgrading to TLS Within HTTP/1.1 @@ -30,9 +34,13 @@ Domain Names in Applications (IDNA) RFC 3986: Uniform Resource Identifier (URI): Generic Syntax RFC 3987: Internationalized Resource Identifiers (IRIs) + RFC 4343: Domain Name System (DNS) Case Insensitivity Clarification RFC 5894: Internationalized Domain Names for Applications (IDNA): Background, Explanation, and Rationale - RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions*/ + RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions + RFC 6125: Representation and Verification of Domain-Based Application Service + Identity within Internet Public Key Infrastructure Using X.509 (PKIX) + Certificates in the Context of Transport Layer Security (TLS)*/ typedef struct OpusParsedURL OpusParsedURL; typedef struct OpusStringBuf OpusStringBuf; @@ -44,6 +52,8 @@ static char *op_string_range_dup(const char *_start,const char *_end){ char *ret; OP_ASSERT(_start<=_end); len=_end-_start; + /*This is to help avoid overflow elsewhere, later.*/ + if(OP_UNLIKELY(len>=INT_MAX))return NULL; ret=(char *)_ogg_malloc(sizeof(*ret)*(len+1)); if(OP_LIKELY(ret!=NULL)){ memcpy(ret,_start,sizeof(*ret)*(len)); @@ -207,6 +217,7 @@ static const char *op_parse_file_url(const char *_src){ # include <poll.h> # include <unistd.h> # include <openssl/ssl.h> +# include <openssl/x509v3.h> /*The maximum number of simultaneous connections. RFC 2616 says this SHOULD NOT be more than 2, but everyone on the modern web @@ -690,6 +701,8 @@ struct OpusHTTPStream{ int seekable; /*Whether or not the server supports HTTP/1.1 with persistent connections.*/ int pipeline; + /*Whether or not we should skip certificate checks.*/ + int skip_certificate_check; /*The offset of the tail of the request. Only the offset in the Range: header appears after this, allowing us to quickly edit the request to ask for a new range.*/ @@ -1408,11 +1421,243 @@ int op_http_conn_establish_tunnel(OpusHTTPStream *_stream, return 0; } +/*Match a host name against a host with a possible wildcard pattern according + to the rules of RFC 6125 Section 6.4.3. + Return: 0 if the pattern doesn't match, and a non-zero value if it does.*/ +static int op_http_hostname_match(const char *_host,size_t _host_len, + ASN1_STRING *_pattern){ + const char *pattern; + size_t host_label_len; + size_t pattern_len; + size_t pattern_label_len; + size_t pattern_prefix_len; + pattern=(const char *)ASN1_STRING_data(_pattern); + pattern_len=strlen(pattern); + /*Check the pattern for embedded NULs.*/ + if(OP_UNLIKELY(pattern_len!=(size_t)ASN1_STRING_length(_pattern)))return 0; + pattern_label_len=strcspn(pattern,"."); + OP_ASSERT(pattern_label_len<=pattern_len); + pattern_prefix_len=strcspn(pattern,"*"); + if(pattern_prefix_len>=pattern_label_len){ + /*"The client SHOULD NOT attempt to match a presented identifier in which + the wildcard character comprises a label other than the left-most label + (e.g., do not match bar.*.example.net)."*/ + if(pattern_prefix_len<pattern_len)return 0; + /*If the pattern does not contain a wildcard in the first element, do an + exact match. + Don't use the system strcasecmp here, as that uses the locale and + RFC 4343 makes clear that DNS's case-insensitivity only applies to + the ASCII range.*/ + return _host_len==pattern_len&&op_strncasecmp(_host,pattern,_host_len)==0; + } + /*"However, the client SHOULD NOT attempt to match a presented identifier where + the wildcard character is embedded within an A-label or U-label of an + internationalized domain name.*/ + if(op_strncasecmp(pattern,"xn--",4)==0)return 0; + host_label_len=strcspn(_host,"."); + /*Make sure the host has at least two dots, to prevent the wildcard match + from being ridiculously wide. + We should have already checked to ensure it had at least one.*/ + if(OP_UNLIKELY(_host[host_label_len]!='.') + ||strchr(_host+host_label_len+1,'.')==NULL){ + return 0; + } + OP_ASSERT(host_label_len<_host_len); + /*"If the wildcard character is the only character of the left-most label in + the presented identifier, the client SHOULD NOT compare against anything + but the left-most label of the reference identifier (e.g., *.example.com + would match foo.example.com but not bar.foo.example.com)." + This is really confusingly worded, as we check this by actually comparing + the rest of the pattern for an exact match. + We also use the fact that the wildcard must match at least one character, + so the left-most label of the hostname must be at least as large as the + left-most label of the pattern.*/ + if(host_label_len<pattern_label_len + ||pattern_len-pattern_label_len!=_host_len-host_label_len + ||op_strncasecmp(_host+host_label_len,pattern+pattern_label_len, + _host_len-host_label_len)!=0){ + return 0; + } + OP_ASSERT(pattern[pattern_prefix_len]=='*'); + /*"The client MAY match a presented identifier in which the wildcard + character is not the only character of the label (e.g., baz*.example.net + and *baz.example.net and b*z.example.net would be taken to match + baz1.example.net and foobaz.example.net and buzz.example.net, + respectively)."*/ + return op_strncasecmp(_host,pattern,pattern_prefix_len)==0&& + op_strncasecmp(_host+host_label_len-(pattern_label_len-pattern_prefix_len-1), + pattern+pattern_prefix_len+1,pattern_label_len-pattern_prefix_len-1)==0; +} + +/*Convert a host to a numeric address, if possible. + Return: A struct addrinfo containing the address, if it was numeric, and NULL + otherise.*/ +static struct addrinfo *op_inet_pton(const char *_host){ + struct addrinfo *addrs; + struct addrinfo hints; + memset(&hints,0,sizeof(hints)); + hints.ai_socktype=SOCK_STREAM; + hints.ai_flags=AI_NUMERICHOST; + if(OP_LIKELY(!getaddrinfo(_host,NULL,&hints,&addrs)))return addrs; + return NULL; +} + +/*Verify the server's hostname matches the certificate they presented using + the procedure from Section 6 of RFC 6125. + Return: 0 if the certificate doesn't match, and a non-zero value if it does.*/ +static int op_http_verify_hostname(OpusHTTPStream *_stream, + SSL *_ssl_conn){ + X509 *peer_cert; + STACK_OF(GENERAL_NAME) *san_names; + char *host; + size_t host_len; + int ret; + host=_stream->url.host; + host_len=strlen(host); + /*We can only verify fully-qualified domain names. + To quote RFC 6125: "The extracted data MUST include only information that + can be securely parsed out of the inputs (e.g., parsing the fully + qualified DNS domain name out of the "host" component (or its equivalent) + of a URI or deriving the application service type from the scheme of a + URI) ..." + We don't have a way to check (without relying on DNS records, which might + be subverted), if this address is fully-qualified. + This is particularly problematic when using a CONNECT tunnel, as it is the + server that does DNS lookup, not us. + However, we are certain that if the hostname has no '.', it is definitely + not a fully-qualified domain name (with the exception of crazy TLDs that + actually resolve, like "uz", but I am willing to ignore those). + RFC 1535 says "...in any event where a '.' exists in a specified name it + should be assumed to be a fully qualified domain name (FQDN) and SHOULD be + tried as a rooted name first." + That doesn't give us any security guarantees, of course (a subverted DNS + could fail the original query and our resolver might still retry with a + local domain appended).*/ + if(strchr(host,'.')==NULL)return 0; + peer_cert=SSL_get_peer_certificate(_ssl_conn); + /*We set VERIFY_PEER, so we shouldn't get here without a certificate.*/ + if(OP_UNLIKELY(peer_cert==NULL))return 0; + ret=0; + OP_ASSERT(host_len<INT_MAX); + /*RFC 2818 says (after correcting for Eratta 1077): "If a subjectAltName + extension of type dNSName is present, that MUST be used as the identity. + Otherwise, the (most specific) Common Name field in the Subject field of + the certificate MUST be used. + Although the use of the Common Name is existing practice, it is deprecated + and Certification Authorities are encouraged to use the dNSName + instead." + "Matching is performed using the matching rules specified by RFC 2459. + If more than one identity of a given type is present in the certificate + (e.g., more than one dNSName name), a match in any one of the set is + considered acceptable. + Names may contain the wildcard character * which is condered to match any + single domain name component or component fragment. + E.g., *.a.com matches foo.a.com but not bar.foo.a.com. + f*.com matches foo.com but not bar.com." + "In some cases, the URI is specified as an IP address rather than a + hostname. + In this case, the iPAddress subjectAltName must be present in the + certificate and must exactly match the IP in the URI."*/ + san_names=X509_get_ext_d2i(peer_cert,NID_subject_alt_name,NULL,NULL); + if(san_names!=NULL){ + struct addrinfo *addr; + unsigned char *ip; + int ip_len; + int nsan_names; + int sni; + /*Check to see if the host was specified as a simple IP address.*/ + addr=op_inet_pton(host); + ip=NULL; + ip_len=0; + if(addr!=NULL){ + switch(addr->ai_family){ + case AF_INET:{ + struct sockaddr_in *s; + s=(struct sockaddr_in *)addr->ai_addr; + OP_ASSERT(addr->ai_addrlen>=sizeof(*s)); + ip=(unsigned char *)&s->sin_addr; + ip_len=sizeof(s->sin_addr); + }break; + case AF_INET6:{ + struct sockaddr_in6 *s; + s=(struct sockaddr_in6 *)addr->ai_addr; + OP_ASSERT(addr->ai_addrlen>=sizeof(*s)); + ip=(unsigned char *)&s->sin6_addr; + ip_len=sizeof(s->sin6_addr); + }break; + } + } + /*RFC 2459 says there MUST be at least one, but we don't depend on it.*/ + nsan_names=sk_GENERAL_NAME_num(san_names); + for(sni=0;sni<nsan_names;sni++){ + const GENERAL_NAME *name; + name=sk_GENERAL_NAME_value(san_names,sni); + if(ip==NULL){ + if(name->type==GEN_DNS + &&op_http_hostname_match(host,host_len,name->d.dNSName)){ + ret=1; + break; + } + } + else if(name->type==GEN_IPADD){ + unsigned char *cert_ip; + /*If we do have an IP address, compare it directly. + RFC 6125: "When the reference identity is an IP address, the identity + MUST be converted to the 'network byte order' octet string + representation. + For IP Version 4, as specified in RFC 791, the octet string will + contain exactly four octets. + For IP Version 6, as specified in RFC 2460, the octet string will + contain exactly sixteen octets. + This octet string is then compared against subjectAltName values of + type iPAddress. + A match occurs if the reference identity octet string and the value + octet strings are identical."*/ + cert_ip=ASN1_STRING_data(name->d.iPAddress); + if(ip_len==ASN1_STRING_length(name->d.iPAddress) + &&memcmp(ip,cert_ip,ip_len)==0){ + ret=1; + break; + } + } + } + sk_GENERAL_NAME_pop_free(san_names,GENERAL_NAME_free); + if(addr!=NULL)freeaddrinfo(addr); + } + else{ + int last_cn_loc; + int cn_loc; + /*If there is no subjectAltName, match against commonName. + RFC 6125 says that at least one significant CA is known to issue certs + with multiple CNs, although it SHOULD NOT. + It also says: "The server's identity may also be verified by comparing + the reference identity to the Common Name (CN) value in the last + Relative Distinguished Name (RDN) of the subject field of the server's + certificate (where "last" refers to the DER-encoded order...)." + So find the last one and check it.*/ + cn_loc=-1; + do{ + last_cn_loc=cn_loc; + cn_loc=X509_NAME_get_index_by_NID(X509_get_subject_name(peer_cert), + NID_commonName,last_cn_loc); + } + while(cn_loc>=0); + ret=last_cn_loc>=0 + &&op_http_hostname_match(host,host_len, + X509_NAME_ENTRY_get_data( + X509_NAME_get_entry(X509_get_subject_name(peer_cert),last_cn_loc))); + } + X509_free(peer_cert); + return ret; +} + /*Perform the TLS handshake on a new connection.*/ int op_http_conn_start_tls(OpusHTTPStream *_stream,OpusHTTPConn *_conn, int _fd,SSL *_ssl_conn){ - BIO *ssl_bio; - int ret; + SSL_SESSION *ssl_session; + BIO *ssl_bio; + int skip_certificate_check; + int ret; ssl_bio=BIO_new_socket(_fd,BIO_NOCLOSE); if(OP_LIKELY(ssl_bio==NULL))return OP_FALSE; # if !defined(OPENSSL_NO_TLSEXT) @@ -1437,11 +1682,22 @@ int op_http_conn_start_tls(OpusHTTPStream *_stream,OpusHTTPConn *_conn, } ret=op_do_ssl_step(_ssl_conn,_fd,SSL_connect); if(OP_UNLIKELY(ret<=0))return OP_FALSE; - if(_stream->ssl_session==NULL){ - /*Save a session for later resumption.*/ + ssl_session=_stream->ssl_session; + skip_certificate_check=_stream->skip_certificate_check; + if(ssl_session==NULL||!skip_certificate_check){ ret=op_do_ssl_step(_ssl_conn,_fd,SSL_do_handshake); if(OP_UNLIKELY(ret<=0))return OP_FALSE; - _stream->ssl_session=SSL_get1_session(_ssl_conn); + /*OpenSSL does not do hostname verification, despite the fact that we just + passed it the hostname above in the call to SSL_set_tlsext_host_name(), + because they are morons. + Do it for them.*/ + if(!skip_certificate_check&&!op_http_verify_hostname(_stream,_ssl_conn)){ + return OP_FALSE; + } + if(ssl_session==NULL){ + /*Save the session for later resumption.*/ + _stream->ssl_session=SSL_get1_session(_ssl_conn); + } } _conn->ssl_conn=_ssl_conn; _conn->fd=_fd; @@ -1794,9 +2050,15 @@ static int op_http_stream_open(OpusHTTPStream *_stream,const char *_url, ssl_ctx=SSL_CTX_new(SSLv23_client_method()); if(ssl_ctx==NULL)return OP_EFAULT; if(!_skip_certificate_check){ + /*We don't do anything if this fails, since it just means we won't load + any certificates (and thus all checks will fail). + However, as that is probably the result of a system + mis-configuration, assert here to make it easier to identify.*/ + OP_ALWAYS_TRUE(SSL_CTX_set_default_verify_paths(ssl_ctx)); SSL_CTX_set_verify(ssl_ctx,SSL_VERIFY_PEER,NULL); } _stream->ssl_ctx=ssl_ctx; + _stream->skip_certificate_check=_skip_certificate_check; if(_proxy_host!=NULL){ /*We need to establish a CONNECT tunnel to handle https proxying. Build the request we'll send to do so.*/