diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index 90ceb93a9428159ad30c7c1a0ad05a5a843fed6f..86117cdb6527945425c06c65fd7b41588352e633 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -35,6 +35,7 @@ #include "asterisk/pbx.h" #include "asterisk/global_datastores.h" #include "asterisk/app.h" +#include "asterisk/test.h" #include "asterisk/res_stir_shaken.h" #include "res_stir_shaken/stir_shaken.h" @@ -1195,6 +1196,348 @@ static struct ast_custom_function stir_shaken_function = { .read = stir_shaken_read, }; +#ifdef TEST_FRAMEWORK + +static void test_stir_shaken_add_fake_astdb_entry(const char *public_key_url, const char *file_path) +{ + struct timeval expires = ast_tvnow(); + char time_buf[32]; + char hash[41]; + + ast_sha1_hash(hash, public_key_url); + add_public_key_to_astdb(public_key_url, file_path); + snprintf(time_buf, sizeof(time_buf), "%30lu", expires.tv_sec + 300); + + ast_db_put(hash, "expiration", time_buf); +} + +/*! + * \brief Create a private or public key certificate + * + * \param file_path The path of the file to create + * \param private Set to 0 if public, 1 if private + * + * \retval -1 on failure + * \retval 0 on success + */ +static int test_stir_shaken_write_temp_key(char *file_path, int private) +{ + FILE *file; + int fd; + char *data; + char *type = private ? "private" : "public"; + char *private_data = + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIFkNGlrmRky2j7wmjGBGoPFBsyEQELmEYN02BiiG508noAoGCCqGSM49\n" + "AwEHoUQDQgAECwCaeAYwVG/FAnEnkwaucz6o047iSWq3cJBBUc0n2ZlUDr5VywAz\n" + "MZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n" + "-----END EC PRIVATE KEY-----"; + char *public_data = + "-----BEGIN PUBLIC KEY-----\n" + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECwCaeAYwVG/FAnEnkwaucz6o047i\n" + "SWq3cJBBUc0n2ZlUDr5VywAzMZ86EthIqF3CGZjhLHn0xRITXYwfqTtWBw==\n" + "-----END PUBLIC KEY-----"; + + fd = mkstemp(file_path); + if (fd < 0) { + ast_log(LOG_ERROR, "Failed to create temp %s file: %s\n", type, strerror(errno)); + return -1; + } + + file = fdopen(fd, "w"); + if (!file) { + ast_log(LOG_ERROR, "Failed to create temp %s key file: %s\n", type, strerror(errno)); + return -1; + } + + data = private ? private_data : public_data; + if (fputs(data, file) == EOF) { + ast_log(LOG_ERROR, "Failed to write temp %s key file\n", type); + fclose(file); + return -1; + } + + fclose(file); + + return 0; +} + +AST_TEST_DEFINE(test_stir_shaken_sign) +{ + char *caller_id_number = "1234567"; + char file_path[] = "/tmp/stir_shaken_private.XXXXXX"; + RAII_VAR(char *, rm_on_exit, file_path, unlink); + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + RAII_VAR(struct ast_stir_shaken_payload *, payload, NULL, ast_stir_shaken_payload_free); + + switch (cmd) { + case TEST_INIT: + info->name = "stir_shaken_sign"; + info->category = "/res/res_stir_shaken/"; + info->summary = "STIR/SHAKEN sign unit test"; + info->description = + "Tests signing a JWT with a private key."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* We only need a private key to sign */ + test_stir_shaken_write_temp_key(file_path, 1); + test_stir_shaken_create_cert(caller_id_number, file_path); + + /* Test missing header section */ + json = ast_json_pack("{s: {s: {s: s}}}", "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'header')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing payload section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123"); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'payload')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing alg section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "ppt", + STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, "x5u", "http://testing123", "payload", + "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'alg')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test invalid alg value */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + "invalid algorithm", "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123", "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (wrong 'alg')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing ppt section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "typ", STIR_SHAKEN_TYPE, "x5u", "http://testing123", + "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'ppt')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test invalid ppt value */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", "invalid ppt", "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123", "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (wrong 'ppt')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing typ section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "x5u", "http://testing123", + "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'typ')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test invalid typ value */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", "invalid typ", + "x5u", "http://testing123", "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (wrong 'typ')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing orig section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: s}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123", "payload", "filler", "filler"); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'orig')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test missing tn section */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: s}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123", "payload", "orig", "filler"); + payload = ast_stir_shaken_sign(json); + if (payload) { + ast_test_status_update(test, "Signed an invalid JWT (missing 'tn')\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test valid JWT */ + ast_json_free(json); + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", "http://testing123", "payload", "orig", "tn", caller_id_number); + payload = ast_stir_shaken_sign(json); + if (!payload) { + ast_test_status_update(test, "Failed to sign a valid JWT\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + test_stir_shaken_cleanup_cert(caller_id_number); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(test_stir_shaken_verify) +{ + char *caller_id_number = "1234567"; + char *public_key_url = "http://testing123"; + char *header = "{\"header\": \"placeholder\"}"; + char public_path[] = "/tmp/stir_shaken_public.XXXXXX"; + char private_path[] = "/tmp/stir_shaken_public.XXXXXX"; + RAII_VAR(char *, rm_on_exit_public, public_path, unlink); + RAII_VAR(char *, rm_on_exit_private, private_path, unlink); + RAII_VAR(char *, json_str, NULL, ast_json_free); + RAII_VAR(struct ast_json *, json, NULL, ast_json_free); + RAII_VAR(struct ast_stir_shaken_payload *, signed_payload, NULL, ast_stir_shaken_payload_free); + RAII_VAR(struct ast_stir_shaken_payload *, returned_payload, NULL, ast_stir_shaken_payload_free); + + switch (cmd) { + case TEST_INIT: + info->name = "stir_shaken_verify"; + info->category = "/res/res_stir_shaken/"; + info->summary = "STIR/SHAKEN verify unit test"; + info->description = + "Tests verifying a signature with a public key"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + /* We need the private key to sign, but we also need the corresponding + * public key to verify */ + test_stir_shaken_write_temp_key(public_path, 0); + test_stir_shaken_write_temp_key(private_path, 1); + test_stir_shaken_create_cert(caller_id_number, private_path); + + /* Get the signature */ + json = ast_json_pack("{s: {s: s, s: s, s: s, s: s}, s: {s: {s: s}}}", "header", "alg", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, "ppt", STIR_SHAKEN_PPT, "typ", STIR_SHAKEN_TYPE, + "x5u", public_key_url, "payload", "orig", "tn", caller_id_number); + signed_payload = ast_stir_shaken_sign(json); + if (!signed_payload) { + ast_test_status_update(test, "Failed to sign a valid JWT\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Get the message to use for verification */ + json_str = ast_json_dump_string(json); + if (!json_str) { + ast_test_status_update(test, "Failed to create string from JSON\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test empty header parameter */ + returned_payload = ast_stir_shaken_verify("", json_str, (const char *)signed_payload->signature, + STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); + if (returned_payload) { + ast_test_status_update(test, "Verified a signature with missing 'header'\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test empty payload parameter */ + returned_payload = ast_stir_shaken_verify(header, "", (const char *)signed_payload->signature, + STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); + if (returned_payload) { + ast_test_status_update(test, "Verified a signature with missing 'payload'\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test empty signature parameter */ + returned_payload = ast_stir_shaken_verify(header, json_str, "", + STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); + if (returned_payload) { + ast_test_status_update(test, "Verified a signature with missing 'signature'\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test empty algorithm parameter */ + returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + "", public_key_url); + if (returned_payload) { + ast_test_status_update(test, "Verified a signature with missing 'algorithm'\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Test empty public key URL */ + returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + STIR_SHAKEN_ENCRYPTION_ALGORITHM, ""); + if (returned_payload) { + ast_test_status_update(test, "Verified a signature with missing 'public key URL'\n"); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + /* Trick the function into thinking we've already downloaded the key */ + test_stir_shaken_add_fake_astdb_entry(public_key_url, public_path); + + /* Verify a valid signature */ + returned_payload = ast_stir_shaken_verify(header, json_str, (const char *)signed_payload->signature, + STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_key_url); + if (!returned_payload) { + ast_test_status_update(test, "Failed to verify a valid signature\n"); + remove_public_key_from_astdb(public_key_url); + test_stir_shaken_cleanup_cert(caller_id_number); + return AST_TEST_FAIL; + } + + remove_public_key_from_astdb(public_key_url); + + test_stir_shaken_cleanup_cert(caller_id_number); + + return AST_TEST_PASS; +} + +#endif /* TEST_FRAMEWORK */ + static int reload_module(void) { if (stir_shaken_sorcery) { @@ -1217,6 +1560,9 @@ static int unload_module(void) res |= ast_custom_function_unregister(&stir_shaken_function); + AST_TEST_UNREGISTER(test_stir_shaken_sign); + AST_TEST_UNREGISTER(test_stir_shaken_verify); + return res; } @@ -1248,6 +1594,9 @@ static int load_module(void) res |= ast_custom_function_register(&stir_shaken_function); + AST_TEST_REGISTER(test_stir_shaken_sign); + AST_TEST_REGISTER(test_stir_shaken_verify); + return res; } diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c index e889a362810e51525b8d0ddd10129be90c9009e9..73b5ce107cf9578e23effe3f292af9bf79b8d0e8 100644 --- a/res/res_stir_shaken/certificate.c +++ b/res/res_stir_shaken/certificate.c @@ -244,6 +244,80 @@ static int public_key_url_to_str(const void *obj, const intptr_t *args, char **b return 0; } +#ifdef TEST_FRAMEWORK + +/* Name for test certificaate */ +#define TEST_CONFIG_NAME "test_stir_shaken_certificate" +/* The public key URL to use for the test certificate */ +#define TEST_CONFIG_URL "http://testing123" + +int test_stir_shaken_cleanup_cert(const char *caller_id_number) +{ + struct stir_shaken_certificate *cert; + struct ast_sorcery *sorcery; + int res = 0; + + sorcery = ast_stir_shaken_sorcery(); + + cert = stir_shaken_certificate_get_by_caller_id_number(caller_id_number); + if (!cert) { + return 0; + } + + res = ast_sorcery_delete(sorcery, cert); + ao2_cleanup(cert); + if (res) { + ast_log(LOG_ERROR, "Failed to delete sorcery object with caller ID " + "'%s'\n", caller_id_number); + return -1; + } + + res = ast_sorcery_remove_wizard_mapping(sorcery, CONFIG_TYPE, "memory"); + + return res; +} + +int test_stir_shaken_create_cert(const char *caller_id_number, const char *file_path) +{ + struct stir_shaken_certificate *cert; + struct ast_sorcery *sorcery; + EVP_PKEY *private_key; + int res = 0; + + sorcery = ast_stir_shaken_sorcery(); + + res = ast_sorcery_insert_wizard_mapping(sorcery, CONFIG_TYPE, "memory", "testing", 0, 0); + if (res) { + ast_log(LOG_ERROR, "Failed to insert STIR/SHAKEN test certificate mapping\n"); + return -1; + } + + cert = ast_sorcery_alloc(sorcery, CONFIG_TYPE, TEST_CONFIG_NAME); + if (!cert) { + ast_log(LOG_ERROR, "Failed to allocate test certificate\n"); + return -1; + } + + ast_string_field_set(cert, path, file_path); + ast_string_field_set(cert, public_key_url, TEST_CONFIG_URL); + ast_string_field_set(cert, caller_id_number, caller_id_number); + + private_key = stir_shaken_read_key(cert->path, 1); + if (!private_key) { + ast_log(LOG_ERROR, "Failed to read test key from %s\n", cert->path); + test_stir_shaken_cleanup_cert(caller_id_number); + return -1; + } + + cert->private_key = private_key; + + ast_sorcery_create(sorcery, cert); + + return res; +} + +#endif /* TEST_FRAMEWORK */ + int stir_shaken_certificate_unload(void) { ast_cli_unregister_multiple(stir_shaken_certificate_cli, diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h index fda3bf1a019ba1658a4ec333d193f452a31c72fd..ff303180bff560329b90734bab87490103aabb18 100644 --- a/res/res_stir_shaken/certificate.h +++ b/res/res_stir_shaken/certificate.h @@ -24,6 +24,14 @@ struct ast_sorcery; struct stir_shaken_certificate; +/*! + * \brief Get a STIR/SHAKEN certificate by caller ID number + * + * \param callier_id_number The caller ID number + * + * \retval NULL if not found + * \retval The certificate on success + */ struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number); /*! @@ -46,6 +54,33 @@ const char *stir_shaken_certificate_get_public_key_url(struct stir_shaken_certif */ EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert); +#ifdef TEST_FRAMEWORK + +/*! + * \brief Clean up the certificate and mappings set up in test_stir_shaken_init + * + * \param caller_id_number The caller ID of the certificate to clean up + * + * \retval non-zero on failure + * \retval 0 on success + */ +int test_stir_shaken_cleanup_cert(const char *caller_id_number); + +/*! + * \brief Initialize a test certificate through wizard mappings + * + * \note test_stir_shaken_cleanup should be called when done with this certificate + * + * \param caller_id_number The caller ID of the certificate to create + * \param file_path The path to the private key for this certificate + * + * \retval non-zero on failure + * \retval 0 on success + */ +int test_stir_shaken_create_cert(const char *caller_id_number, const char *file_path); + +#endif /* TEST_FRAMEWORK */ + /*! * \brief Load time initialization for the stir/shaken 'certificate' configuration * diff --git a/res/res_stir_shaken/stir_shaken.c b/res/res_stir_shaken/stir_shaken.c index 10caca98513bfa23861b1285451e48d1383e789b..220104a324e167d7944d76302ef3bc727dce8b4a 100644 --- a/res/res_stir_shaken/stir_shaken.c +++ b/res/res_stir_shaken/stir_shaken.c @@ -90,7 +90,7 @@ EVP_PKEY *stir_shaken_read_key(const char *path, int priv) fp = fopen(path, "r"); if (!fp) { - ast_log(LOG_ERROR, "Failed to read private key file '%s'\n", path); + ast_log(LOG_ERROR, "Failed to read %s key file '%s'\n", priv ? "private" : "public", path); return NULL; }