diff --git a/gennghttpxfun.py b/gennghttpxfun.py index 93ad43f8..ad5057e7 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -153,6 +153,7 @@ OPTIONS = [ "dns-lookup-timeout", "dns-max-try", "frontend-keep-alive-timeout", + "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..922fa600 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -2103,6 +2103,15 @@ 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. HTTP/2 and SPDY: -c, --frontend-http2-max-concurrent-streams= @@ -3078,6 +3087,7 @@ 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}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -3768,6 +3778,10 @@ 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; default: break; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index eb786e84..b59facd7 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -1199,6 +1199,70 @@ 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 + // generated by gennghttpxfun.py int option_lookup_token(const char *name, size_t namelen) { switch (namelen) { @@ -1366,6 +1430,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)) { @@ -3138,6 +3205,8 @@ 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_CONF: LOG(WARN) << "conf: ignored"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index 594c2342..a9b8ca11 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -319,6 +319,7 @@ 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 size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8; @@ -549,6 +550,9 @@ struct TLSConfig { StringRef cert_file; } client; + // 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; @@ -975,6 +979,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..cdd9c852 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -525,6 +525,30 @@ 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 + struct TLSProtocol { StringRef name; long int mask; @@ -734,6 +758,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; 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