diff --git a/gennghttpxfun.py b/gennghttpxfun.py index 93ad43f8..4d4fb7e4 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -153,6 +153,8 @@ OPTIONS = [ "dns-lookup-timeout", "dns-max-try", "frontend-keep-alive-timeout", + "psk-secrets", + "client-psk-secrets", ] LOGVARS = [ diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index e725bd9e..c6a0940b 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -181,6 +181,9 @@ int main(int argc, char *argv[]) { !CU_add_test(pSuite, "util_random_alpha_digit", shrpx::test_util_random_alpha_digit) || !CU_add_test(pSuite, "util_format_hex", shrpx::test_util_format_hex) || + !CU_add_test(pSuite, "util_is_hex_string", + shrpx::test_util_is_hex_string) || + !CU_add_test(pSuite, "util_decode_hex", shrpx::test_util_decode_hex) || !CU_add_test(pSuite, "gzip_inflate", test_nghttp2_gzip_inflate) || !CU_add_test(pSuite, "buffer_write", nghttp2::test_buffer_write) || !CU_add_test(pSuite, "pool_recycle", nghttp2::test_pool_recycle) || diff --git a/src/shrpx.cc b/src/shrpx.cc index a3b9bab7..c31ae514 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -2103,6 +2103,22 @@ SSL/TLS: argument , or certificate option in configuration file. For additional certificates, use --subcert option. This option requires OpenSSL >= 1.0.2. + --psk-secrets= + Read list of PSK identity and secrets from . This + is used for frontend connection. The each line of input + file is formatted as :, where + is PSK identity, and is secret + in hex. An empty line, and line which starts with '#' + are skipped. The default enabled cipher list might not + contain any PSK cipher suite. In that case, desired PSK + cipher suites must be enabled using --ciphers option. + --client-psk-secrets= + Read PSK identity and secrets from . This is used + for backend connection. The each line of input file is + formatted as :, where + is PSK identity, and is secret in hex. An + empty line, and line which starts with '#' are skipped. + The first identity and secret pair encountered is used. HTTP/2 and SPDY: -c, --frontend-http2-max-concurrent-streams= @@ -3078,6 +3094,8 @@ int main(int argc, char **argv) { {SHRPX_OPT_DNS_MAX_TRY.c_str(), required_argument, &flag, 145}, {SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT.c_str(), required_argument, &flag, 146}, + {SHRPX_OPT_PSK_SECRETS.c_str(), required_argument, &flag, 147}, + {SHRPX_OPT_CLIENT_PSK_SECRETS.c_str(), required_argument, &flag, 148}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -3768,6 +3786,14 @@ int main(int argc, char **argv) { cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT, StringRef{optarg}); break; + case 147: + // --psk-secrets + cmdcfgs.emplace_back(SHRPX_OPT_PSK_SECRETS, StringRef{optarg}); + break; + case 148: + // --client-psk-secrets + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_PSK_SECRETS, StringRef{optarg}); + break; default: break; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index eb786e84..21f8f1a4 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -1199,6 +1199,130 @@ int read_tls_sct_from_dir(std::vector &dst, const StringRef &opt, } } // namespace +namespace { +// Reads PSK secrets from path, and parses each line. The result is +// directly stored into config->tls.psk_secrets. This function +// returns 0 if it succeeds, or -1. +int parse_psk_secrets(Config *config, const StringRef &path) { + auto &tlsconf = config->tls; + + std::ifstream f(path.c_str(), std::ios::binary); + if (!f) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": could not open file " << path; + return -1; + } + + size_t lineno = 0; + std::string line; + while (std::getline(f, line)) { + ++lineno; + if (line.empty() || line[0] == '#') { + continue; + } + + auto sep_it = std::find(std::begin(line), std::end(line), ':'); + if (sep_it == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": could not fine separator at line " << lineno; + return -1; + } + + if (sep_it == std::begin(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": empty identity at line " + << lineno; + return -1; + } + + if (sep_it + 1 == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": empty secret at line " + << lineno; + return -1; + } + + if (!util::is_hex_string(StringRef{sep_it + 1, std::end(line)})) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": secret must be hex string at line " << lineno; + return -1; + } + + auto identity = + make_string_ref(config->balloc, StringRef{std::begin(line), sep_it}); + + auto secret = + util::decode_hex(config->balloc, StringRef{sep_it + 1, std::end(line)}); + + auto rv = tlsconf.psk_secrets.emplace(identity, secret); + if (!rv.second) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": identity has already been registered at line " << lineno; + return -1; + } + } + + return 0; +} +} // namespace + +namespace { +// Reads PSK secrets from path, and parses each line. The result is +// directly stored into config->tls.client_psk. This function returns +// 0 if it succeeds, or -1. +int parse_client_psk_secrets(Config *config, const StringRef &path) { + auto &tlsconf = config->tls; + + std::ifstream f(path.c_str(), std::ios::binary); + if (!f) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": could not open file " + << path; + return -1; + } + + size_t lineno = 0; + std::string line; + while (std::getline(f, line)) { + ++lineno; + if (line.empty() || line[0] == '#') { + continue; + } + + auto sep_it = std::find(std::begin(line), std::end(line), ':'); + if (sep_it == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS + << ": could not fine separator at line " << lineno; + return -1; + } + + if (sep_it == std::begin(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": empty identity at line " + << lineno; + return -1; + } + + if (sep_it + 1 == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": empty secret at line " + << lineno; + return -1; + } + + if (!util::is_hex_string(StringRef{sep_it + 1, std::end(line)})) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS + << ": secret must be hex string at line " << lineno; + return -1; + } + + tlsconf.client_psk.identity = + make_string_ref(config->balloc, StringRef{std::begin(line), sep_it}); + + tlsconf.client_psk.secret = + util::decode_hex(config->balloc, StringRef{sep_it + 1, std::end(line)}); + + return 0; + } + + return 0; +} +} // namespace + // generated by gennghttpxfun.py int option_lookup_token(const char *name, size_t namelen) { switch (namelen) { @@ -1366,6 +1490,9 @@ int option_lookup_token(const char *name, size_t namelen) { if (util::strieq_l("ecdh-curve", name, 10)) { return SHRPX_OPTID_ECDH_CURVES; } + if (util::strieq_l("psk-secret", name, 10)) { + return SHRPX_OPTID_PSK_SECRETS; + } break; case 't': if (util::strieq_l("write-burs", name, 10)) { @@ -1553,6 +1680,11 @@ int option_lookup_token(const char *name, size_t namelen) { return SHRPX_OPTID_ADD_REQUEST_HEADER; } break; + case 's': + if (util::strieq_l("client-psk-secret", name, 17)) { + return SHRPX_OPTID_CLIENT_PSK_SECRETS; + } + break; case 't': if (util::strieq_l("dns-lookup-timeou", name, 17)) { return SHRPX_OPTID_DNS_LOOKUP_TIMEOUT; @@ -3138,6 +3270,10 @@ int parse_config(Config *config, int optid, const StringRef &opt, case SHRPX_OPTID_FRONTEND_KEEP_ALIVE_TIMEOUT: return parse_duration(&config->conn.upstream.timeout.idle_read, opt, optarg); + case SHRPX_OPTID_PSK_SECRETS: + return parse_psk_secrets(config, optarg); + case SHRPX_OPTID_CLIENT_PSK_SECRETS: + return parse_client_psk_secrets(config, optarg); case SHRPX_OPTID_CONF: LOG(WARN) << "conf: ignored"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index 594c2342..c033b639 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -319,6 +319,9 @@ constexpr auto SHRPX_OPT_DNS_LOOKUP_TIMEOUT = constexpr auto SHRPX_OPT_DNS_MAX_TRY = StringRef::from_lit("dns-max-try"); constexpr auto SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT = StringRef::from_lit("frontend-keep-alive-timeout"); +constexpr auto SHRPX_OPT_PSK_SECRETS = StringRef::from_lit("psk-secrets"); +constexpr auto SHRPX_OPT_CLIENT_PSK_SECRETS = + StringRef::from_lit("client-psk-secrets"); constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8; @@ -549,6 +552,16 @@ struct TLSConfig { StringRef cert_file; } client; + // Client PSK configuration + struct { + // identity must be NULL terminated string. + StringRef identity; + StringRef secret; + } client_psk; + + // PSK secrets. The key is identity, and the associated value is + // its secret. + std::map psk_secrets; // The list of additional TLS certificate pair std::vector subcerts; std::vector alpn_prefs; @@ -914,6 +927,7 @@ enum { SHRPX_OPTID_CLIENT_CERT_FILE, SHRPX_OPTID_CLIENT_PRIVATE_KEY_FILE, SHRPX_OPTID_CLIENT_PROXY, + SHRPX_OPTID_CLIENT_PSK_SECRETS, SHRPX_OPTID_CONF, SHRPX_OPTID_DAEMON, SHRPX_OPTID_DH_PARAM_FILE, @@ -975,6 +989,7 @@ enum { SHRPX_OPTID_PID_FILE, SHRPX_OPTID_PRIVATE_KEY_FILE, SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE, + SHRPX_OPTID_PSK_SECRETS, SHRPX_OPTID_READ_BURST, SHRPX_OPTID_READ_RATE, SHRPX_OPTID_REQUEST_HEADER_FIELD_BUFFER, diff --git a/src/shrpx_ssl.cc b/src/shrpx_ssl.cc index 063880e3..9912ac5c 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -525,6 +525,63 @@ int sct_parse_cb(SSL *ssl, unsigned int ext_type, const unsigned char *in, } // namespace #endif // !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L +namespace { +unsigned int psk_server_cb(SSL *ssl, const char *identity, unsigned char *psk, + unsigned int max_psk_len) { + auto config = get_config(); + auto &tlsconf = config->tls; + + auto it = tlsconf.psk_secrets.find(StringRef{identity}); + if (it == std::end(tlsconf.psk_secrets)) { + return 0; + } + + auto &secret = (*it).second; + if (secret.size() > max_psk_len) { + LOG(ERROR) << "The size of PSK secret is " << secret.size() + << ", but the acceptable maximum size is" << max_psk_len; + return 0; + } + + std::copy(std::begin(secret), std::end(secret), psk); + + return static_cast(secret.size()); +} +} // namespace + +namespace { +unsigned int psk_client_cb(SSL *ssl, const char *hint, char *identity_out, + unsigned int max_identity_len, unsigned char *psk, + unsigned int max_psk_len) { + auto config = get_config(); + auto &tlsconf = config->tls; + + auto &identity = tlsconf.client_psk.identity; + auto &secret = tlsconf.client_psk.secret; + + if (identity.empty()) { + return 0; + } + + if (identity.size() + 1 > max_identity_len) { + LOG(ERROR) << "The size of PSK identity is " << identity.size() + << ", but the acceptable maximum size is " << max_identity_len; + return 0; + } + + if (secret.size() > max_psk_len) { + LOG(ERROR) << "The size of PSK secret is " << secret.size() + << ", but the acceptable maximum size is " << max_psk_len; + return 0; + } + + *std::copy(std::begin(identity), std::end(identity), identity_out) = '\0'; + std::copy(std::begin(secret), std::end(secret), psk); + + return (unsigned int)secret.size(); +} +} // namespace + struct TLSProtocol { StringRef name; long int mask; @@ -734,6 +791,8 @@ SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file, } #endif // !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L + SSL_CTX_set_psk_server_callback(ssl_ctx, psk_server_cb); + auto tls_ctx_data = new TLSContextData(); tls_ctx_data->cert_file = cert_file; tls_ctx_data->sct_data = sct_data; @@ -873,6 +932,8 @@ SSL_CTX *create_ssl_client_context( #endif // HAVE_NEVERBLEED } + SSL_CTX_set_psk_client_callback(ssl_ctx, psk_client_cb); + // NPN selection callback. This is required to set SSL_CTX because // OpenSSL does not offer SSL_set_next_proto_select_cb. SSL_CTX_set_next_proto_select_cb(ssl_ctx, next_proto_select_cb, nullptr); @@ -1155,8 +1216,10 @@ int verify_hostname(X509 *cert, const StringRef &hostname, int check_cert(SSL *ssl, const Address *addr, const StringRef &host) { auto cert = SSL_get_peer_certificate(ssl); if (!cert) { - LOG(ERROR) << "No certificate found"; - return -1; + // By the protocol definition, TLS server always sends certificate + // if it has. If certificate cannot be retrieved, authentication + // without certificate is used, such as PSK. + return 0; } auto cert_deleter = defer(X509_free, cert); auto verify_res = SSL_get_verify_result(ssl); diff --git a/src/util.cc b/src/util.cc index 62442cb4..cd0f5cc8 100644 --- a/src/util.cc +++ b/src/util.cc @@ -1465,6 +1465,30 @@ int sha256(uint8_t *res, const StringRef &s) { return 0; } +bool is_hex_string(const StringRef &s) { + if (s.size() % 2) { + return false; + } + + for (auto c : s) { + if (!is_hex_digit(c)) { + return false; + } + } + + return true; +} + +StringRef decode_hex(BlockAllocator &balloc, const StringRef &s) { + auto iov = make_byte_ref(balloc, s.size() + 1); + auto p = iov.base; + for (auto it = std::begin(s); it != std::end(s); it += 2) { + *p++ = (hex_to_uint(*it) << 4) | hex_to_uint(*(it + 1)); + } + *p = '\0'; + return StringRef{iov.base, p}; +} + } // namespace util } // namespace nghttp2 diff --git a/src/util.h b/src/util.h index 9bb63ef7..d2f4d9f1 100644 --- a/src/util.h +++ b/src/util.h @@ -82,6 +82,9 @@ inline bool is_hex_digit(const char c) { return is_digit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); } +// Returns true if |s| is hex string. +bool is_hex_string(const StringRef &s); + bool in_rfc3986_unreserved_chars(const char c); bool in_rfc3986_sub_delims(const char c); @@ -147,6 +150,11 @@ template std::string format_hex(const std::array &s) { StringRef format_hex(BlockAllocator &balloc, const StringRef &s); +// decode_hex decodes hex string |s|, returns the decoded byte string. +// This function assumes |s| is hex string, that is is_hex_string(s) +// == true. +StringRef decode_hex(BlockAllocator &balloc, const StringRef &s); + // Returns given time |t| from epoch in HTTP Date format (e.g., Mon, // 10 Oct 2016 10:25:58 GMT). std::string http_date(time_t t); diff --git a/src/util_test.cc b/src/util_test.cc index e7da8f03..aed3e37f 100644 --- a/src/util_test.cc +++ b/src/util_test.cc @@ -593,4 +593,20 @@ void test_util_format_hex(void) { CU_ASSERT("" == util::format_hex(balloc, StringRef::from_lit(""))); } +void test_util_is_hex_string(void) { + CU_ASSERT(util::is_hex_string(StringRef{})); + CU_ASSERT(util::is_hex_string(StringRef::from_lit("0123456789abcdef"))); + CU_ASSERT(util::is_hex_string(StringRef::from_lit("0123456789ABCDEF"))); + CU_ASSERT(!util::is_hex_string(StringRef::from_lit("000"))); + CU_ASSERT(!util::is_hex_string(StringRef::from_lit("XX"))); +} + +void test_util_decode_hex(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("\x0f\xf0" == + util::decode_hex(balloc, StringRef::from_lit("0ff0"))); + CU_ASSERT("" == util::decode_hex(balloc, StringRef{})); +} + } // namespace shrpx diff --git a/src/util_test.h b/src/util_test.h index c498cbad..bed49f72 100644 --- a/src/util_test.h +++ b/src/util_test.h @@ -64,6 +64,8 @@ void test_util_make_hostport(void); void test_util_strifind(void); void test_util_random_alpha_digit(void); void test_util_format_hex(void); +void test_util_is_hex_string(void); +void test_util_decode_hex(void); } // namespace shrpx