From 17e81f2cd1843f01838245eae7b5ed5edf83d6be Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 09:30:55 +0200 Subject: Adding upstream version 0.12.1+dfsg. Signed-off-by: Daniel Baumann --- crypto/picotls/picotls.c | 701 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 crypto/picotls/picotls.c (limited to 'crypto/picotls/picotls.c') diff --git a/crypto/picotls/picotls.c b/crypto/picotls/picotls.c new file mode 100644 index 0000000..b1bbb6c --- /dev/null +++ b/crypto/picotls/picotls.c @@ -0,0 +1,701 @@ +/* + * ngtcp2 + * + * Copyright (c) 2022 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include +#endif /* HAVE_CONFIG_H */ + +#include +#include + +#include +#include + +#include +#include + +#include "shared.h" + +ngtcp2_crypto_aead *ngtcp2_crypto_aead_aes_128_gcm(ngtcp2_crypto_aead *aead) { + return ngtcp2_crypto_aead_init(aead, (void *)&ptls_openssl_aes128gcm); +} + +ngtcp2_crypto_md *ngtcp2_crypto_md_sha256(ngtcp2_crypto_md *md) { + md->native_handle = (void *)&ptls_openssl_sha256; + return md; +} + +ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_initial(ngtcp2_crypto_ctx *ctx) { + ngtcp2_crypto_aead_init(&ctx->aead, (void *)&ptls_openssl_aes128gcm); + ctx->md.native_handle = (void *)&ptls_openssl_sha256; + ctx->hp.native_handle = (void *)&ptls_openssl_aes128ctr; + ctx->max_encryption = 0; + ctx->max_decryption_failure = 0; + return ctx; +} + +ngtcp2_crypto_aead *ngtcp2_crypto_aead_init(ngtcp2_crypto_aead *aead, + void *aead_native_handle) { + ptls_aead_algorithm_t *alg = aead_native_handle; + + aead->native_handle = aead_native_handle; + aead->max_overhead = alg->tag_size; + return aead; +} + +ngtcp2_crypto_aead *ngtcp2_crypto_aead_retry(ngtcp2_crypto_aead *aead) { + return ngtcp2_crypto_aead_init(aead, (void *)&ptls_openssl_aes128gcm); +} + +static const ptls_aead_algorithm_t *crypto_ptls_get_aead(ptls_t *ptls) { + ptls_cipher_suite_t *cs = ptls_get_cipher(ptls); + + return cs->aead; +} + +static uint64_t crypto_ptls_get_aead_max_encryption(ptls_t *ptls) { + ptls_cipher_suite_t *cs = ptls_get_cipher(ptls); + + if (cs->aead == &ptls_openssl_aes128gcm || + cs->aead == &ptls_openssl_aes256gcm) { + return NGTCP2_CRYPTO_MAX_ENCRYPTION_AES_GCM; + } + + if (cs->aead == &ptls_openssl_chacha20poly1305) { + return NGTCP2_CRYPTO_MAX_ENCRYPTION_CHACHA20_POLY1305; + } + + return 0; +} + +static uint64_t crypto_ptls_get_aead_max_decryption_failure(ptls_t *ptls) { + ptls_cipher_suite_t *cs = ptls_get_cipher(ptls); + + if (cs->aead == &ptls_openssl_aes128gcm || + cs->aead == &ptls_openssl_aes256gcm) { + return NGTCP2_CRYPTO_MAX_DECRYPTION_FAILURE_AES_GCM; + } + + if (cs->aead == &ptls_openssl_chacha20poly1305) { + return NGTCP2_CRYPTO_MAX_DECRYPTION_FAILURE_CHACHA20_POLY1305; + } + + return 0; +} + +static const ptls_cipher_algorithm_t *crypto_ptls_get_hp(ptls_t *ptls) { + ptls_cipher_suite_t *cs = ptls_get_cipher(ptls); + + if (cs->aead == &ptls_openssl_aes128gcm) { + return &ptls_openssl_aes128ctr; + } + + if (cs->aead == &ptls_openssl_aes256gcm) { + return &ptls_openssl_aes256ctr; + } + + if (cs->aead == &ptls_openssl_chacha20poly1305) { + return &ptls_openssl_chacha20; + } + + return NULL; +} + +static const ptls_hash_algorithm_t *crypto_ptls_get_md(ptls_t *ptls) { + ptls_cipher_suite_t *cs = ptls_get_cipher(ptls); + + return cs->hash; +} + +ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_tls(ngtcp2_crypto_ctx *ctx, + void *tls_native_handle) { + ngtcp2_crypto_picotls_ctx *cptls = tls_native_handle; + ngtcp2_crypto_aead_init(&ctx->aead, + (void *)crypto_ptls_get_aead(cptls->ptls)); + ctx->md.native_handle = (void *)crypto_ptls_get_md(cptls->ptls); + ctx->hp.native_handle = (void *)crypto_ptls_get_hp(cptls->ptls); + ctx->max_encryption = crypto_ptls_get_aead_max_encryption(cptls->ptls); + ctx->max_decryption_failure = + crypto_ptls_get_aead_max_decryption_failure(cptls->ptls); + return ctx; +} + +ngtcp2_crypto_ctx *ngtcp2_crypto_ctx_tls_early(ngtcp2_crypto_ctx *ctx, + void *tls_native_handle) { + return ngtcp2_crypto_ctx_tls(ctx, tls_native_handle); +} + +static size_t crypto_md_hashlen(const ptls_hash_algorithm_t *md) { + return md->digest_size; +} + +size_t ngtcp2_crypto_md_hashlen(const ngtcp2_crypto_md *md) { + return crypto_md_hashlen(md->native_handle); +} + +static size_t crypto_aead_keylen(const ptls_aead_algorithm_t *aead) { + return aead->key_size; +} + +size_t ngtcp2_crypto_aead_keylen(const ngtcp2_crypto_aead *aead) { + return crypto_aead_keylen(aead->native_handle); +} + +static size_t crypto_aead_noncelen(const ptls_aead_algorithm_t *aead) { + return aead->iv_size; +} + +size_t ngtcp2_crypto_aead_noncelen(const ngtcp2_crypto_aead *aead) { + return crypto_aead_noncelen(aead->native_handle); +} + +int ngtcp2_crypto_aead_ctx_encrypt_init(ngtcp2_crypto_aead_ctx *aead_ctx, + const ngtcp2_crypto_aead *aead, + const uint8_t *key, size_t noncelen) { + const ptls_aead_algorithm_t *cipher = aead->native_handle; + size_t keylen = crypto_aead_keylen(cipher); + ptls_aead_context_t *actx; + static const uint8_t iv[PTLS_MAX_IV_SIZE] = {0}; + + (void)noncelen; + (void)keylen; + + actx = ptls_aead_new_direct(cipher, /* is_enc = */ 1, key, iv); + if (actx == NULL) { + return -1; + } + + aead_ctx->native_handle = actx; + + return 0; +} + +int ngtcp2_crypto_aead_ctx_decrypt_init(ngtcp2_crypto_aead_ctx *aead_ctx, + const ngtcp2_crypto_aead *aead, + const uint8_t *key, size_t noncelen) { + const ptls_aead_algorithm_t *cipher = aead->native_handle; + size_t keylen = crypto_aead_keylen(cipher); + ptls_aead_context_t *actx; + const uint8_t iv[PTLS_MAX_IV_SIZE] = {0}; + + (void)noncelen; + (void)keylen; + + actx = ptls_aead_new_direct(cipher, /* is_enc = */ 0, key, iv); + if (actx == NULL) { + return -1; + } + + aead_ctx->native_handle = actx; + + return 0; +} + +void ngtcp2_crypto_aead_ctx_free(ngtcp2_crypto_aead_ctx *aead_ctx) { + if (aead_ctx->native_handle) { + ptls_aead_free(aead_ctx->native_handle); + } +} + +int ngtcp2_crypto_cipher_ctx_encrypt_init(ngtcp2_crypto_cipher_ctx *cipher_ctx, + const ngtcp2_crypto_cipher *cipher, + const uint8_t *key) { + ptls_cipher_context_t *actx; + + actx = ptls_cipher_new(cipher->native_handle, /* is_enc = */ 1, key); + if (actx == NULL) { + return -1; + } + + cipher_ctx->native_handle = actx; + + return 0; +} + +void ngtcp2_crypto_cipher_ctx_free(ngtcp2_crypto_cipher_ctx *cipher_ctx) { + if (cipher_ctx->native_handle) { + ptls_cipher_free(cipher_ctx->native_handle); + } +} + +int ngtcp2_crypto_hkdf_extract(uint8_t *dest, const ngtcp2_crypto_md *md, + const uint8_t *secret, size_t secretlen, + const uint8_t *salt, size_t saltlen) { + ptls_iovec_t saltv, ikm; + + saltv = ptls_iovec_init(salt, saltlen); + ikm = ptls_iovec_init(secret, secretlen); + + if (ptls_hkdf_extract(md->native_handle, dest, saltv, ikm) != 0) { + return -1; + } + + return 0; +} + +int ngtcp2_crypto_hkdf_expand(uint8_t *dest, size_t destlen, + const ngtcp2_crypto_md *md, const uint8_t *secret, + size_t secretlen, const uint8_t *info, + size_t infolen) { + ptls_iovec_t prk, infov; + + prk = ptls_iovec_init(secret, secretlen); + infov = ptls_iovec_init(info, infolen); + + if (ptls_hkdf_expand(md->native_handle, dest, destlen, prk, infov) != 0) { + return -1; + } + + return 0; +} + +int ngtcp2_crypto_hkdf(uint8_t *dest, size_t destlen, + const ngtcp2_crypto_md *md, const uint8_t *secret, + size_t secretlen, const uint8_t *salt, size_t saltlen, + const uint8_t *info, size_t infolen) { + ptls_iovec_t saltv, ikm, prk, infov; + uint8_t prkbuf[PTLS_MAX_DIGEST_SIZE]; + ptls_hash_algorithm_t *algo = md->native_handle; + + saltv = ptls_iovec_init(salt, saltlen); + ikm = ptls_iovec_init(secret, secretlen); + + if (ptls_hkdf_extract(algo, prkbuf, saltv, ikm) != 0) { + return -1; + } + + prk = ptls_iovec_init(prkbuf, algo->digest_size); + infov = ptls_iovec_init(info, infolen); + + if (ptls_hkdf_expand(algo, dest, destlen, prk, infov) != 0) { + return -1; + } + + return 0; +} + +int ngtcp2_crypto_encrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, + const ngtcp2_crypto_aead_ctx *aead_ctx, + const uint8_t *plaintext, size_t plaintextlen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *aad, size_t aadlen) { + ptls_aead_context_t *actx = aead_ctx->native_handle; + + (void)aead; + + ptls_aead_xor_iv(actx, nonce, noncelen); + + ptls_aead_encrypt(actx, dest, plaintext, plaintextlen, 0, aad, aadlen); + + /* zero-out static iv once again */ + ptls_aead_xor_iv(actx, nonce, noncelen); + + return 0; +} + +int ngtcp2_crypto_decrypt(uint8_t *dest, const ngtcp2_crypto_aead *aead, + const ngtcp2_crypto_aead_ctx *aead_ctx, + const uint8_t *ciphertext, size_t ciphertextlen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *aad, size_t aadlen) { + ptls_aead_context_t *actx = aead_ctx->native_handle; + + (void)aead; + + ptls_aead_xor_iv(actx, nonce, noncelen); + + if (ptls_aead_decrypt(actx, dest, ciphertext, ciphertextlen, 0, aad, + aadlen) == SIZE_MAX) { + return -1; + } + + /* zero-out static iv once again */ + ptls_aead_xor_iv(actx, nonce, noncelen); + + return 0; +} + +int ngtcp2_crypto_hp_mask(uint8_t *dest, const ngtcp2_crypto_cipher *hp, + const ngtcp2_crypto_cipher_ctx *hp_ctx, + const uint8_t *sample) { + ptls_cipher_context_t *actx = hp_ctx->native_handle; + static const uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + + (void)hp; + + ptls_cipher_init(actx, sample); + ptls_cipher_encrypt(actx, dest, PLAINTEXT, sizeof(PLAINTEXT) - 1); + + return 0; +} + +int ngtcp2_crypto_read_write_crypto_data(ngtcp2_conn *conn, + ngtcp2_crypto_level crypto_level, + const uint8_t *data, size_t datalen) { + ngtcp2_crypto_picotls_ctx *cptls = ngtcp2_conn_get_tls_native_handle(conn); + ptls_buffer_t sendbuf; + size_t epoch_offsets[5] = {0}; + size_t epoch = ngtcp2_crypto_picotls_from_ngtcp2_crypto_level(crypto_level); + size_t epoch_datalen; + size_t i; + int rv; + + ptls_buffer_init(&sendbuf, (void *)"", 0); + + assert(epoch == ptls_get_read_epoch(cptls->ptls)); + + rv = ptls_handle_message(cptls->ptls, &sendbuf, epoch_offsets, epoch, data, + datalen, &cptls->handshake_properties); + if (rv != 0 && rv != PTLS_ERROR_IN_PROGRESS) { + if (PTLS_ERROR_GET_CLASS(rv) == PTLS_ERROR_CLASS_SELF_ALERT) { + ngtcp2_conn_set_tls_alert(conn, (uint8_t)PTLS_ERROR_TO_ALERT(rv)); + } + + rv = -1; + goto fin; + } + + if (!ngtcp2_conn_is_server(conn) && + cptls->handshake_properties.client.early_data_acceptance == + PTLS_EARLY_DATA_REJECTED) { + rv = ngtcp2_conn_early_data_rejected(conn); + if (rv != 0) { + rv = -1; + goto fin; + } + } + + for (i = 0; i < 4; ++i) { + epoch_datalen = epoch_offsets[i + 1] - epoch_offsets[i]; + if (epoch_datalen == 0) { + continue; + } + + assert(i != 1); + + if (ngtcp2_conn_submit_crypto_data( + conn, ngtcp2_crypto_picotls_from_epoch(i), + sendbuf.base + epoch_offsets[i], epoch_datalen) != 0) { + rv = -1; + goto fin; + } + } + + if (rv == 0) { + ngtcp2_conn_handshake_completed(conn); + } + + rv = 0; + +fin: + ptls_buffer_dispose(&sendbuf); + + return rv; +} + +int ngtcp2_crypto_set_remote_transport_params(ngtcp2_conn *conn, void *tls) { + (void)conn; + (void)tls; + + /* The remote transport parameters will be set via picotls + collected_extensions callback */ + + return 0; +} + +int ngtcp2_crypto_set_local_transport_params(void *tls, const uint8_t *buf, + size_t len) { + (void)tls; + (void)buf; + (void)len; + + /* The local transport parameters will be set in an external + call. */ + + return 0; +} + +ngtcp2_crypto_level ngtcp2_crypto_picotls_from_epoch(size_t epoch) { + switch (epoch) { + case 0: + return NGTCP2_CRYPTO_LEVEL_INITIAL; + case 1: + return NGTCP2_CRYPTO_LEVEL_EARLY; + case 2: + return NGTCP2_CRYPTO_LEVEL_HANDSHAKE; + case 3: + return NGTCP2_CRYPTO_LEVEL_APPLICATION; + default: + assert(0); + abort(); + } +} + +size_t ngtcp2_crypto_picotls_from_ngtcp2_crypto_level( + ngtcp2_crypto_level crypto_level) { + switch (crypto_level) { + case NGTCP2_CRYPTO_LEVEL_INITIAL: + return 0; + case NGTCP2_CRYPTO_LEVEL_EARLY: + return 1; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + return 2; + case NGTCP2_CRYPTO_LEVEL_APPLICATION: + return 3; + default: + assert(0); + abort(); + } +} + +int ngtcp2_crypto_get_path_challenge_data_cb(ngtcp2_conn *conn, uint8_t *data, + void *user_data) { + (void)conn; + (void)user_data; + + ptls_openssl_random_bytes(data, NGTCP2_PATH_CHALLENGE_DATALEN); + + return 0; +} + +int ngtcp2_crypto_random(uint8_t *data, size_t datalen) { + ptls_openssl_random_bytes(data, datalen); + + return 0; +} + +void ngtcp2_crypto_picotls_ctx_init(ngtcp2_crypto_picotls_ctx *cptls) { + cptls->ptls = NULL; + memset(&cptls->handshake_properties, 0, sizeof(cptls->handshake_properties)); +} + +static int set_additional_extensions(ptls_handshake_properties_t *hsprops, + ngtcp2_conn *conn) { + const size_t buflen = 256; + uint8_t *buf; + ngtcp2_ssize nwrite; + ptls_raw_extension_t *exts = hsprops->additional_extensions; + + assert(exts); + + buf = malloc(buflen); + if (buf == NULL) { + return -1; + } + + nwrite = ngtcp2_conn_encode_local_transport_params(conn, buf, buflen); + if (nwrite < 0) { + goto fail; + } + + exts[0].type = NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1; + exts[0].data.base = buf; + exts[0].data.len = (size_t)nwrite; + + return 0; + +fail: + free(buf); + + return -1; +} + +int ngtcp2_crypto_picotls_collect_extension( + ptls_t *ptls, struct st_ptls_handshake_properties_t *properties, + uint16_t type) { + (void)ptls; + (void)properties; + + return type == NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1; +} + +int ngtcp2_crypto_picotls_collected_extensions( + ptls_t *ptls, struct st_ptls_handshake_properties_t *properties, + ptls_raw_extension_t *extensions) { + ngtcp2_crypto_conn_ref *conn_ref; + ngtcp2_conn *conn; + int rv; + + (void)properties; + + for (; extensions->type != UINT16_MAX; ++extensions) { + if (extensions->type != NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS_V1) { + continue; + } + + conn_ref = *ptls_get_data_ptr(ptls); + conn = conn_ref->get_conn(conn_ref); + + rv = ngtcp2_conn_decode_remote_transport_params(conn, extensions->data.base, + extensions->data.len); + if (rv != 0) { + ngtcp2_conn_set_tls_error(conn, rv); + return -1; + } + + return 0; + } + + return 0; +} + +static int update_traffic_key_server_cb(ptls_update_traffic_key_t *self, + ptls_t *ptls, int is_enc, size_t epoch, + const void *secret) { + ngtcp2_crypto_conn_ref *conn_ref = *ptls_get_data_ptr(ptls); + ngtcp2_conn *conn = conn_ref->get_conn(conn_ref); + ngtcp2_crypto_level level = ngtcp2_crypto_picotls_from_epoch(epoch); + ptls_cipher_suite_t *cipher = ptls_get_cipher(ptls); + size_t secretlen = cipher->hash->digest_size; + ngtcp2_crypto_picotls_ctx *cptls; + + (void)self; + + if (is_enc) { + if (ngtcp2_crypto_derive_and_install_tx_key(conn, NULL, NULL, NULL, level, + secret, secretlen) != 0) { + return -1; + } + + if (level == NGTCP2_CRYPTO_LEVEL_HANDSHAKE) { + /* libngtcp2 allows an application to change QUIC transport + * parameters before installing Handshake tx key. We need to + * wait for the key to get the correct local transport + * parameters from ngtcp2_conn. + */ + cptls = ngtcp2_conn_get_tls_native_handle(conn); + + if (set_additional_extensions(&cptls->handshake_properties, conn) != 0) { + return -1; + } + } + + return 0; + } + + if (ngtcp2_crypto_derive_and_install_rx_key(conn, NULL, NULL, NULL, level, + secret, secretlen) != 0) { + return -1; + } + + return 0; +} + +static ptls_update_traffic_key_t update_traffic_key_server = { + update_traffic_key_server_cb, +}; + +static int update_traffic_key_cb(ptls_update_traffic_key_t *self, ptls_t *ptls, + int is_enc, size_t epoch, const void *secret) { + ngtcp2_crypto_conn_ref *conn_ref = *ptls_get_data_ptr(ptls); + ngtcp2_conn *conn = conn_ref->get_conn(conn_ref); + ngtcp2_crypto_level level = ngtcp2_crypto_picotls_from_epoch(epoch); + ptls_cipher_suite_t *cipher = ptls_get_cipher(ptls); + size_t secretlen = cipher->hash->digest_size; + + (void)self; + + if (is_enc) { + if (ngtcp2_crypto_derive_and_install_tx_key(conn, NULL, NULL, NULL, level, + secret, secretlen) != 0) { + return -1; + } + + return 0; + } + + if (ngtcp2_crypto_derive_and_install_rx_key(conn, NULL, NULL, NULL, level, + secret, secretlen) != 0) { + return -1; + } + + return 0; +} + +static ptls_update_traffic_key_t update_traffic_key = {update_traffic_key_cb}; + +int ngtcp2_crypto_picotls_configure_server_context(ptls_context_t *ctx) { + ctx->max_early_data_size = UINT32_MAX; + ctx->omit_end_of_early_data = 1; + ctx->update_traffic_key = &update_traffic_key_server; + + return 0; +} + +int ngtcp2_crypto_picotls_configure_client_context(ptls_context_t *ctx) { + ctx->omit_end_of_early_data = 1; + ctx->update_traffic_key = &update_traffic_key; + + return 0; +} + +int ngtcp2_crypto_picotls_configure_server_session( + ngtcp2_crypto_picotls_ctx *cptls) { + ptls_handshake_properties_t *hsprops = &cptls->handshake_properties; + + hsprops->collect_extension = ngtcp2_crypto_picotls_collect_extension; + hsprops->collected_extensions = ngtcp2_crypto_picotls_collected_extensions; + + return 0; +} + +int ngtcp2_crypto_picotls_configure_client_session( + ngtcp2_crypto_picotls_ctx *cptls, ngtcp2_conn *conn) { + ptls_handshake_properties_t *hsprops = &cptls->handshake_properties; + + hsprops->client.max_early_data_size = calloc(1, sizeof(uint32_t)); + if (hsprops->client.max_early_data_size == NULL) { + return -1; + } + + if (set_additional_extensions(hsprops, conn) != 0) { + free(hsprops->client.max_early_data_size); + hsprops->client.max_early_data_size = NULL; + return -1; + } + + hsprops->collect_extension = ngtcp2_crypto_picotls_collect_extension; + hsprops->collected_extensions = ngtcp2_crypto_picotls_collected_extensions; + + return 0; +} + +void ngtcp2_crypto_picotls_deconfigure_session( + ngtcp2_crypto_picotls_ctx *cptls) { + ptls_handshake_properties_t *hsprops; + ptls_raw_extension_t *exts; + + if (cptls == NULL) { + return; + } + + hsprops = &cptls->handshake_properties; + + free(hsprops->client.max_early_data_size); + + exts = hsprops->additional_extensions; + if (exts) { + free(hsprops->additional_extensions[0].data.base); + } +} -- cgit v1.2.3