diff --git a/configs/samples/extconfig.conf.sample b/configs/samples/extconfig.conf.sample index df154381d2..984a64ef69 100644 --- a/configs/samples/extconfig.conf.sample +++ b/configs/samples/extconfig.conf.sample @@ -84,6 +84,7 @@ ;ps_outbound_publishes => odbc,asterisk ;ps_inbound_publications = odbc,asterisk ;ps_asterisk_publications = odbc,asterisk +;stir_tn => odbc,asterisk ;voicemail => odbc,asterisk ;extensions => odbc,asterisk ;meetme => mysql,general diff --git a/configs/samples/sorcery.conf.sample b/configs/samples/sorcery.conf.sample index 1e7d72a723..393015c36b 100644 --- a/configs/samples/sorcery.conf.sample +++ b/configs/samples/sorcery.conf.sample @@ -76,3 +76,6 @@ test=memory ;[res_pjsip_publish_asterisk] ;asterisk-publication=realtime,ps_asterisk_publications + +;[res_stir_shaken] +;tn=realtime,stir_tn diff --git a/configs/samples/stir_shaken.conf.sample b/configs/samples/stir_shaken.conf.sample index bc4220e8fc..5a15d0f4d0 100644 --- a/configs/samples/stir_shaken.conf.sample +++ b/configs/samples/stir_shaken.conf.sample @@ -1,103 +1,459 @@ -; -; This file is used by the res_stir_shaken module to configure parameters -; used for STIR/SHAKEN. -; -; There are 2 sides to STIR/SHAKEN: attestation and verification. -; -; Attestation is done on outgoing calls and makes use out of the certificate -; objects. The cert located at path will be used to sign, and the cert -; located at public_cert_url will be placed in the Identity header to let the -; remote side know where to download the public cert from. These 2 certs must -; match; that is, the cert located at public_cert_url must be the public cert -; derived from the private cert located at path. -; -; Verification is done on incoming calls and doesn't rely on cert objects -; defined in this file. -; -; The general section applies to all STIR/SHAKEN operations. However, -; cache_max_size, curl_timeout, and signature_timeout only apply to the -; verification side. -; -; It's important to note that downloaded certificates are stored in -; /keys/stir_shaken, which is usually -; /etc/asterisk/keys/stir_shaken, but may be changed depending on where your -; config directory is. -; -; Visit the wiki page: -; https://docs.asterisk.org/Deployment/STIR-SHAKEN/ -; -; [general] -; -; File path to the certificate authority certificate -;ca_file=/etc/asterisk/stir/ca.crt -; -; File path to a chain of trust -;ca_path=/etc/asterisk/stir/ca -; -; Maximum size to use for caching public keys -;cache_max_size=1000 -; -; Maximum time (in seconds) to wait to CURL certificates -;curl_timeout=2 -; -; Amount of time (in seconds) a signature is valid for -;signature_timeout=15 -; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -; -; A certificate store is used to examine, and load all certificates found in a -; given directory. When using this type the public key URL is generated based -; upon the filename, and variable substitution. -;[certificates] -; -; type must be "store" -;type=store -; -; Path to a directory containing certificates -;path=/etc/asterisk/stir -; -; URL to the public certificate(s). Must contain variable '${CERTIFICATE}' used for -; substitution. '${CERTIFICATE}' will be replaced by the names of the files located -; at path. -; This will be put in the Identity header when signing. -;public_cert_url=http://mycompany.com/${CERTIFICATE}.pem -; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -; -; Individual certificates are declared by using the certificate type. -;[alice] -; -; type must be "certificate" -;type=certificate -; -; File path to a certificate. This can be RSA or ECDSA, but eventually only ECDSA will be supported. -;path=/etc/asterisk/stir/alice.pem -; -; URL to the public certificate. Must be of type X509 and be derived from the -; certificate located at path. -; This will be put in the identity header when signing. -;public_cert_url=http://mycompany.com/alice.pem -; -; The caller ID number to match on -;caller_id_number=1234567 -; -; Must have an attestation of A, B, or C -;attestation=C -; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -; -; Profiles can be defined here which can be referenced by channel drivers. -;[my_profile] -; -; type must be "profile" -;type=profile -; -; Set stir_shaken to 'attest', 'verify', or 'on', which is the default -;stir_shaken=on -; -; You can specify an ACL that will be used strictly for the Identity header when downloading public certificates -;acllist=myacllist -; -; You can also do permit / deny lines if you want (also supports IPv6) +;-- + +There are 4 object types used by the STIR/SHAKEN process... + +The "attestation" object sets the parameters for creating an Identity +header which attests to the ownership of the caller id on outgoing +INVITE requests. + +One or more "tn" objects that are used to create the outgoing Identity +header. Each object's "id" is a specific caller-id telephone number +and the object contains the URL to the certificate that was used to +attest to the ownership of the caller-id, the level (A,B,C) of the +attestation you're making, and the private key the asterisk +attestation service will use to sign the Identity header. When +an outgoing INVITE request is placed, the attestation service will +look up the caller-id in the tn object list and if it's found, use +the information in the object to create the Identity header. + +The "verification" object sets the parameters for verification +of the Identity header and caller id on incoming INVITE requests. + +One or more "profile" objects that can be associated to channel +driver endpoints (currently only chan_pjsip). Profiles can set +whether verification, attestation, both or neither should be +performed on requests coming in to this endpoint or requests +going out from this endpoint. Additionally they can override +most of the attestation and verification options to make them +specific to an endpoint. When Asterisk loads the configs, it +creates "effective profiles" or "eprofiles" on the fly that are +the amalgamation of the attestation, verification and profile. +You can see them in the CLI with "stir_shaken show eprofiles". + +NOTE: The "tn" object can be configured to source its data from a +realtime database by configuring sorcery.conf and extconfig.conf. +Both of those files have examples for "stir_tn". There is also an +Alembic script in the "config" section of contrib/ast-db-manage that +will create the table. Since there can be only one "verification" +or "attestation" object, and will probably be only a few "profile" +objects, those objects aren't realtime enabled. + +--; + +;-- +======================================================================= + Attestation Object Description +======================================================================= +The "attestation" object sets the parameters for creating an Identity +header which attests to the ownership of the caller id on outgoing +INVITE requests. + +All parameters except 'global_disable" may be overridden in a "profile" +or "tn" object. + +Only one "attestation" object may exist. + +Parameters: + +-- global_disable ----------------------------------------------------- +If set, globally disables the attestation service. No Identity headers +will be added to any outgoing INVITE requests. + +Default: no + +-- private_key_file --------------------------------------------------- +The path to a file containing the private key you received from the +issuing authority. The file must NOT be group or world readable or +writable so make sure the user the asterisk process is running as is +the owner. + +Default: none + +-- public_cert_url ---------------------------------------------------- +The URL to the certificate you received from the issueing authority. +They may give you a URL to use or you may have to host the certificate +yourself and provide your own URL here. + +Default: none + +WARNING: Make absolutely sure the file that's made public doesn't +accidentally include the privite key as well as the certificate. +If you set "check_tn_cert_public_url" in the "attestation" section +above, the tn will not be loaded and a "DANGER" message will be output +on the asterisk console if the file does contain a private key. + +-- check_tn_cert_public_url ------------------------------------------- +Identity headers in outgoing requests must contain a URL that points +to the certificate used to sign the header. Setting this parameter +tells Asterisk to actually try to retrieve the certificates indicated +by "public_cert_url" parameters and fail loading that tn if the cert +can't be retrieved or if its 'Not Valid Before" -> 'Not Valid After" +date range doesn't include today. This is a network intensive process +so use with caution. + +Default: no + +-- attest_level ------------------------------------------------------- +The level of the attestation you're making. +One of "A", "B", "C" + +Default: none + +-- send_mky ----------------------------------------------------------- +If set and an outgoing call uses DTLS, an "mky" Media Key grant will +be added to the Identity header. Although RFC8224/8225 require this, +not many implementations support it so a remote verification service +may fail to verify the signature. + +Default: no + +----------------------------------------------------------------------- +Example "attestation" object: +--; + +;[attestation] +;global_disable = no +;private_key_path = /var/lib/asterisk/keys/stir_shaken/tns/multi-tns-key.pem +;public_cert_url = https://example.com/tncerts/multi-tns-cert.pem +;attest_level = C + +;-- +======================================================================= + TN Object Description +======================================================================= +Each "tn" object contains the parameters needed to create the Identity +header used to attest to the ownership of the caller-id on outgoing +requests. When an outgoing INVITE request is placed, the attestation +service will look up the caller-id in this list and if it's found, use +the information in the object to create the Identity header. +The private key and certificate needed to sign the Identity header are +usually provided to you by the telephone number issuing authority along +with their certificate authority certificate. You should give the CA +certificate to any recipients who expect to receive calls from you +although this has probably already been done by the issuing authority. + +The "id" of this object MUST be a canonicalized telephone number which +starts with a country code. The only valid characters are the numbers +0-9, '#' and '*'. + +Parameters: + +-- type (required) ---------------------------------------------------- +Must be set to "tn" + +Default: none + +-- private_key_file --------------------------------------------------- +The path to a file containing the private key you received from the +issuing authority. The file must NOT be group or world readable or +writable so make sure the user the asterisk process is running as is +the owner. + +Default: private_key_file from the profile or attestation objects. + +-- public_cert_url ---------------------------------------------------- +The URL to the certificate you received from the issueing authority. +They may give you a URL to use or you may have to host the certificate +yourself and provide your own URL here. + +Default: public_cert_url from the profile or attestation objects. + +WARNING: Make absolutely sure the file that's made public doesn't +accidentally include the privite key as well as the certificate. +If you set "check_tn_cert_public_url" in the "attestation" section +above, the tn will not be loaded and a "DANGER" message will be output +on the asterisk console if the file does contain a private key. + +-- attest_level ------------------------------------------------------- +The level of the attestation you're making. +One of "A", "B", "C" + +Default: attest_level from the profile or attestation objects. + +----------------------------------------------------------------------- +Example "tn" object: +--; + +;[18005551515] +;type = tn +;private_key_path = /var/lib/asterisk/keys/stir_shaken/tns/18005551515-key.pem +;public_cert_url = https://example.com/tncerts/18005551515-cert.pem +;attest_level = C + +;-- +======================================================================= + Verification Object Description +======================================================================= +The "verification" object sets the parameters for verification +of the Identity header on incoming INVITE requests. + +All parameters except 'global_disable" may be overridden in a "profile" +object. + +Only one "verification" object may exist. + +Parameters: + +-- global_disable ----------------------------------------------------- +If set, globally disables the verification service. + +Default: no + +-- load_system_certs--------------------------------------------------- +If set, loads the system Certificate Authority certificates +(usually located in /etc/pki/CA) into the trust store used to +validate the certificates in incoming requests. This is not +normally required as service providers will usually provide their +CA certififcate to you separately. + +Default: no + +-- ca_file ----------------------------------------------------------- +Path to a single file containing a CA certificate or certificate chain +to be used to validate the certificates in incoming requests. + +Default: none + +-- ca_path ----------------------------------------------------------- +Path to a directory containing one or more CA certificates to be used +to validate the certificates in incoming requests. The files in that +directory must contain only one certificate each and the directory +must be hashed using the OpenSSL 'c_rehash' utility. + +Default: none + +NOTE: Both ca_file and ca_path can be specified but at least one +MUST be. + +-- crl_file ----------------------------------------------------------- +Path to a single file containing a CA certificate revocation list +to be used to validate the certificates in incoming requests. + +Default: none + +-- crl_path ----------------------------------------------------------- +Path to a directory containing one or more CA certificate revocation +lists to be used to validate the certificates in incoming requests. +The files in that directory must contain only one certificate each and +the directory must be hashed using the OpenSSL 'c_rehash' utility. + +Default: none + +NOTE: Neither crl_file nor crl_path are required. + +-- cert_cache_dir ----------------------------------------------------- +Incoming Identity headers will have a URL pointing to the certificate +used to sign the header. To prevent us from having to retrieve the +certificate for every request, we maintain a cache of them in the +'cert_cache_dir' specified. The directory will be checked for +existence and writability at startup. + +Default: /keys/stir_shaken/cache + +-- curl_timeout ------------------------------------------------------- +The number of seconds we'll wait for a response when trying to retrieve +the certificate specified in the incoming Identity header's "x5u" +parameter. + +Default: 2 + +-- max_cache_entry_age ------------------------------------------------ +Maximum age in seconds a certificate in the cache can reach before +re-retrieving it. + +Default: 86400 (24 hours per ATIS-1000074) + +NOTE: If, when retrieving the URL specified by the "x5u" parameter, +we receive a recognized caching directive in the HTTP response AND that +directive indicates caching for MORE than the value set here, we'll use +that time for the max_cache_entry_age. + +-- max_cache_size ----------------------------------------------------- +Maximum number of entries the cache can hold. +Not presently implemented. + +-- max_iat_age -------------------------------------------------------- +The "iat" parameter in the Identity header indicates the time the +sender actually created their attestation. If that is older than the +current time by the number of seconds set here, the request will be +considered "failed". + +Default: 15 + +-- max_date_header_age ------------------------------------------------ +The sender MUST also send a SIP Date header in their request. If we +receive one that is older than the current time by the number of seconds +set here, the request will be considered "failed". + +Default: 15 + +-- failure_action ----------------------------------------------------- +Indicates what will happen to requests that have failed verification. +Must be one of: +- continue - + Continue processing the request. You can use the STIR_SHAKEN + dialplan function to determine whether the request passed or failed + verification and take the action you deem appropriate. + +- reject_request - + Reject the request immediately using the SIP response codes + defined by RFC8224. + +- continue_return_reason - + Continue processing the request but, per RFC8224, send a SIP Reason + header back to the originator in the next provisional response + indicating the issue according to RFC8224. You can use the + STIR_SHAKEN dialplan function to determine whether the request + passed or failed verification and take the action you deem + appropriate. + +Default: continue + +NOTE: If you select "continue" or "continue_return_reason", and, +based on the results from the STIR_SHAKEN function, you determine you +want to terminate the call, you can use the PJSIPHangup() dialplan +application to reject the call using a STIR/SHAKEN-specific SIP +response code. + +-- use_rfc9410_responses ---------------------------------------------- +If set, when sending Reason headers back to originators, the protocol +header parameter will be set to "STIR" rather than "SIP". This is a +new protocol defined in RFC9410 and may not be supported by all +participants. + +Default: no + +-- relax_x5u_port_scheme_restrictions --------------------------------- +If set, the port and scheme restrictions imposed by ATIS-1000074 +section 5.3.1 that require the scheme to be "https" and the port to +be 443 or 8443 are relaxed. This will allow schemes like "http" +and ports other than the two mentioned to appear in x5u URLs received +in Identity headers. + +Default: no + +CAUTION: Setting this parameter could have serious security +implications and should only be use for testing. + +-- relax_x5u_path_restrictions ---------------------------------------- +If set, the path restrictions imposed by ATIS-1000074 section 5.3.1 +that require the x5u URL to be rejected if it contains a query string, +path parameters, fragment identifier or user/password are relaxed. + +Default: no + +CAUTION: Setting this parameter could have serious security +implications and should only be use for testing. + +-- x5u_permit/x5u_deny ------------------------------------------------ +When set, the IP address of the host in a received Identity header x5u +URL is checked against the acl created by this list of permit/deny +parameters. If the check fails, the x5u URL will be considered invalid +and verification will fail. This can prevent an attacker from sending +you a request pretending to be a known originator with a mailcious +certificate URL. (Server-side request forgery (SSRF)). +See acl.conf.sample to see examples of how to specify the permit/deny +parameters. + +Default: Deny all "Special-Purpose" IP addresses described in RFC 6890. +This includes the loopback addresses 127.0.0.0/8, private use networks such +as 10.0.0/8, 172.16.0.0/12 and 192.168.0.0/16, and the link local network +169.254.0.0/16 among others. + +CAUTION: Setting this parameter could have serious security +implications and should only be use for testing. + +-- x5u_acl ------------------------------------------------------------ +Rather than providing individual permit/deny parameters, you can set +the acllist parameter to an acl list predefined in acl.conf. + +Default: none + +CAUTION: Setting this parameter could have serious security +implications and should only be use for testing. + +----------------------------------------------------------------------- +Example "verification" object: +--; + +;[verification] +;global_disable = yes +;load_system_certs = no +;ca_path = /var/lib/asterisk/keys/stir_shaken/verification_ca +;cert_cache_dir = /var/lib/asterisk/keys/stir_shaken/verification_cache +;failure_action = reject_request +;curl_timeout=5 +;max_iat_age=60 +;max_date_header_age=60 +;max_cache_entry_age = 300 +; For internal testing +;x5u_deny=0.0.0.0/0.0.0.0 +;x5u_permit=127.0.0.0/8 +;x5u_permit=192.168.100.0/24 +;relax_x5u_port_scheme_restrictions = yes +;relax_x5u_path_restrictions = yes + +;-- +======================================================================= + Profile Object Description +======================================================================= +A "profile" object can be associated to channel driver endpoint +(currently only chan_pjsip) and can set verification and attestation +parameters specific to endpoints using this profile. If you have +multiple upstream providers, this is the place to set parameters +specific to them. + +The "id" of this object is arbitrary and you'd specify it in the +"stir_shaken_profile" parameter of the endpoint. + +Parameters: + +-- type (required) ---------------------------------------------------- +Must be set to "profile" + +Default: none + +-- endpoint_behhavior-------------------------------------------------- +Actions to be performed for endpoints referencing this profile. +Must be one of: +- off - + Don't do any STIR/SHAKEN processing. +- attest - + Attest on outgoing calls. +- verify + Verify incoming calls. +- on - + Attest outgoing calls and verify incoming calls. +Default: off + +All of the "verification" parameters defined above can be set on a profile +with the exception of 'global_disable'. + +All of the "attestation" parameters defined above can be set on a profile +with the exception of 'global_disable'. + +When Asterisk loads the configs, it creates "effective profiles" or +"eprofiles" on the fly that are the amalgamation of the attestation, +verification and profile. You can see them in the CLI with +"stir_shaken show eprofiles". + +----------------------------------------------------------------------- +Example "profile" object: +--; + +;[myprofile] +;type = profile +;endpoint_behavior = verify +;failure_action = continue_return_reason +;x5u_acl = myacllist + +;In pjsip.conf... +;[myendpoint] +;type = endpoint +;stir_shaken_profile = myprofile + +;In acl.conf... +;[myacllist] ;permit=0.0.0.0/0.0.0.0 -;deny=127.0.0.1 +;deny=10.24.20.171 + diff --git a/contrib/ast-db-manage/config/versions/bd335bae5d33_create_stir_shaken_tn_table.py b/contrib/ast-db-manage/config/versions/bd335bae5d33_create_stir_shaken_tn_table.py new file mode 100644 index 0000000000..8789bba74b --- /dev/null +++ b/contrib/ast-db-manage/config/versions/bd335bae5d33_create_stir_shaken_tn_table.py @@ -0,0 +1,35 @@ +"""Create STIR/SHAKEN TN table + +Revision ID: bd335bae5d33 +Revises: 24c12d8e9014 +Create Date: 2024-01-09 12:17:47.353533 + +""" + +# revision identifiers, used by Alembic. +revision = 'bd335bae5d33' +down_revision = '24c12d8e9014' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import ENUM + +AST_BOOL_NAME = 'ast_bool_values' +AST_BOOL_VALUES = [ '0', '1', + 'off', 'on', + 'false', 'true', + 'no', 'yes' ] + +def upgrade(): + ast_bool_values = ENUM(*AST_BOOL_VALUES, name=AST_BOOL_NAME, create_type=False) + op.create_table( + 'stir_tn', + sa.Column('id', sa.String(80), nullable=False, primary_key=True), + sa.Column('private_key_file', sa.String(1024), nullable=True), + sa.Column('public_cert_url', sa.String(1024), nullable=True), + sa.Column('attest_level', sa.String(1), nullable=True), + sa.Column('send_mky', ast_bool_values) + ) + +def downgrade(): + op.drop_table('stir_tn') diff --git a/include/asterisk/astdb.h b/include/asterisk/astdb.h index 216b8d3d9d..6b87dcc113 100644 --- a/include/asterisk/astdb.h +++ b/include/asterisk/astdb.h @@ -50,6 +50,16 @@ int ast_db_get(const char *family, const char *key, char *value, int valuelen); */ int ast_db_get_allocated(const char *family, const char *key, char **out); +/*! + * \brief Check if family/key exitsts + * + * \param family + * \param key + * \retval 1 if family/key exists + * \retval 0 if family/key does not exist or an error occurred + */ +int ast_db_exists(const char *family, const char *key); + /*! \brief Store value addressed by family/key */ int ast_db_put(const char *family, const char *key, const char *value); diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index 949f66fdc5..b320cff525 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -71,22 +71,6 @@ #define PJSTR_PRINTF_SPEC "%.*s" #define PJSTR_PRINTF_VAR(_v) ((int)(_v).slen), ((_v).ptr) -/* Response codes from RFC8224 */ -#define AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE 403 -#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER 428 -#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT 428 -#define AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO 436 -#define AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL 437 -#define AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER 438 - -/* Response strings from RFC8224 */ -#define AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE "Stale Date" -#define AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER "Use Identity Header" -#define AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT "Use Supported PASSporT Format" -#define AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO "Bad Identity Info" -#define AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL "Unsupported Credential" -#define AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER "Invalid Identity Header" - #define AST_SIP_AUTH_MAX_REALM_LENGTH 255 /* From the auth/realm realtime column size */ /* ":12345" */ @@ -666,17 +650,6 @@ enum ast_sip_session_redirect { AST_SIP_REDIRECT_URI_PJSIP, }; -enum ast_sip_stir_shaken_behavior { - /*! Don't do any STIR/SHAKEN operations */ - AST_SIP_STIR_SHAKEN_OFF = 0, - /*! Only do STIR/SHAKEN attestation */ - AST_SIP_STIR_SHAKEN_ATTEST = 1, - /*! Only do STIR/SHAKEN verification */ - AST_SIP_STIR_SHAKEN_VERIFY = 2, - /*! Do STIR/SHAKEN attestation and verification */ - AST_SIP_STIR_SHAKEN_ON = 3, -}; - /*! * \brief Incoming/Outgoing call offer/answer joint codec preference. * diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h index c3b93c3751..d287e96865 100644 --- a/include/asterisk/res_pjsip_session.h +++ b/include/asterisk/res_pjsip_session.h @@ -963,4 +963,29 @@ struct ast_sip_session_media *ast_sip_session_media_get_transport(struct ast_sip */ const char *ast_sip_session_get_name(const struct ast_sip_session *session); +/*! + * \brief Determines if the Connected Line info can be presented for this session + * + * \param session The session + * \param id The Connected Line info to evaluate + * + * \retval 1 The Connected Line info can be presented + * \retval 0 The Connected Line info cannot be presented + */ +int ast_sip_can_present_connected_id(const struct ast_sip_session *session, const struct ast_party_id *id); + +/*! + * \brief Adds a Reason header in the next reponse to an incoming INVITE + * + * \param session The session + * \param protocol Usually "SIP" but may be "STIR" for stir-shaken + * \param code SIP response code + * \param text Reason string + * + * \retval 0 the header is accepted + * \retval -1 the header is rejected + */ +int ast_sip_session_add_reason_header(struct ast_sip_session *session, + const char *protocol, int code, const char *text); + #endif /* _RES_PJSIP_SESSION_H */ diff --git a/include/asterisk/res_stir_shaken.h b/include/asterisk/res_stir_shaken.h index 540f988c44..81e330c41d 100644 --- a/include/asterisk/res_stir_shaken.h +++ b/include/asterisk/res_stir_shaken.h @@ -18,176 +18,241 @@ #ifndef _RES_STIR_SHAKEN_H #define _RES_STIR_SHAKEN_H -#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256" -#define STIR_SHAKEN_PPT "shaken" -#define STIR_SHAKEN_TYPE "passport" +#include "asterisk/sorcery.h" -enum ast_stir_shaken_verification_result { - AST_STIR_SHAKEN_VERIFY_NOT_PRESENT, /*! No STIR/SHAKEN information was available */ - AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED, /*! Signature verification failed */ - AST_STIR_SHAKEN_VERIFY_MISMATCH, /*! Contents of the signaling and the STIR/SHAKEN payload did not match */ - AST_STIR_SHAKEN_VERIFY_PASSED, /*! Signature verified and contents match signaling */ +enum ast_stir_shaken_vs_response_code { + AST_STIR_SHAKEN_VS_SUCCESS = 0, + AST_STIR_SHAKEN_VS_DISABLED, + AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS, + AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR, + AST_STIR_SHAKEN_VS_NO_DATE_HDR, + AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE, + AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED, + AST_STIR_SHAKEN_VS_NO_JWT_HDR, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, + AST_STIR_SHAKEN_VS_CERT_CACHE_MISS, + AST_STIR_SHAKEN_VS_CERT_CACHE_INVALID, + AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED, + AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE, + AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, + AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED, + AST_STIR_SHAKEN_VS_CERT_DATE_INVALID, + AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT, + AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT, + AST_STIR_SHAKEN_VS_NO_RAW_KEY, + AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION, + AST_STIR_SHAKEN_VS_NO_IAT, + AST_STIR_SHAKEN_VS_IAT_EXPIRED, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS, + AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST, + AST_STIR_SHAKEN_VS_NO_ORIGID, + AST_STIR_SHAKEN_VS_NO_ORIG_TN, + AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH, + AST_STIR_SHAKEN_VS_NO_DEST_TN, + AST_STIR_SHAKEN_VS_INVALID_HEADER, + AST_STIR_SHAKEN_VS_INVALID_GRANT, + AST_STIR_SHAKEN_VS_RESPONSE_CODE_MAX }; -/*! Different from ast_stir_shaken_verification_result. Used to determine why ast_stir_shaken_verify returned NULL */ -enum ast_stir_shaken_verify_failure_reason { - AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC, /*! Memory allocation failure */ - AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT, /*! Failed to get the credentials to verify */ - AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION, /*! Failed validating the signature */ +enum ast_stir_shaken_as_response_code { + AST_STIR_SHAKEN_AS_SUCCESS = 0, + AST_STIR_SHAKEN_AS_DISABLED, + AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + AST_STIR_SHAKEN_AS_MISSING_PARAMETERS, + AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + AST_STIR_SHAKEN_AS_NO_TN_FOR_CALLERID, + AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL, + AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL, + AST_STIR_SHAKEN_AS_NO_ATTEST_LEVEL, + AST_STIR_SHAKEN_AS_IDENTITY_HDR_EXISTS, + AST_STIR_SHAKEN_AS_NO_TO_HDR, + AST_STIR_SHAKEN_AS_TO_HDR_BAD_URI, + AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE, + AST_STIR_SHAKEN_AS_RESPONSE_CODE_MAX }; -struct ast_stir_shaken_payload; +enum stir_shaken_failure_action_enum { + /*! Unknown value */ + stir_shaken_failure_action_UNKNOWN = -1, + /*! Continue and let dialplan decide action */ + stir_shaken_failure_action_CONTINUE = 0, + /*! Reject request with respone codes defined in RFC8224 */ + stir_shaken_failure_action_REJECT_REQUEST, + /*! Continue but return a Reason header in next provisional response */ + stir_shaken_failure_action_CONTINUE_RETURN_REASON, + /*! Not set in config */ + stir_shaken_failure_action_NOT_SET, +}; -struct ast_acl_list; - -struct ast_json; +struct ast_stir_shaken_as_ctx; /*! - * \brief Retrieve the value for 'signature' from an ast_stir_shaken_payload + * \brief Create Attestation Service Context * - * \param payload The payload - * - * \retval The signature + * \param caller_id The caller_id for the outgoing call + * \param dest_tn Canonicalized destination tn + * \param chan The outgoing channel + * \param profile_name The profile name on the endpoint + * May be NULL. + * \param tag Identifying string to output in log and trace messages. + * \param ctxout Receives a pointer to the newly created context + * The caller must release with ao2_ref or ao2_cleanup. + + * \retval AST_STIR_SHAKEN_AS_SUCCESS if successful. + * \retval AST_STIR_SHAKEN_AS_DISABLED if attestation is disabled + * by the endpoint itself, the profile or globally. + * \retval Other AST_STIR_SHAKEN_AS errors. */ -unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload); +enum ast_stir_shaken_as_response_code + ast_stir_shaken_as_ctx_create(const char *caller_id, + const char *dest_tn, struct ast_channel *chan, + const char *profile_name, + const char *tag, struct ast_stir_shaken_as_ctx **ctxout); /*! - * \brief Retrieve the value for 'public_cert_url' from an ast_stir_shaken_payload + * \brief Indicates if the AS context needs DTLS fingerprints * - * \param payload The payload + * \param ctx AS Context * - * \retval The public key URL + * \retval 0 Not needed + * \retval 1 Needed */ -char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload); +int ast_stir_shaken_as_ctx_wants_fingerprints(struct ast_stir_shaken_as_ctx *ctx); /*! - * \brief Retrieve the value for 'signature_timeout' from 'general' config object + * \brief Add DTLS fingerprints to AS context * - * \retval The signature timeout + * \param ctx AS context + * \param alg Fingerprint algorithm ("sha-1" or "sha-256") + * \param fingerprint Fingerprint + * + * \retval AST_STIR_SHAKEN_AS_SUCCESS if successful + * \retval Other AST_STIR_SHAKEN_AS errors. */ -unsigned int ast_stir_shaken_get_signature_timeout(void); +enum ast_stir_shaken_as_response_code ast_stir_shaken_as_ctx_add_fingerprint( + struct ast_stir_shaken_as_ctx *ctx, const char *alg, const char *fingerprint); /*! - * \brief Retrieve a stir_shaken_profile by id + * \brief Attest and return Identity header value * - * \note The profile will need to be unref'd when not needed anymore + * \param ctx AS Context + * \param header Pointer to buffer to receive the header value + * Must be freed with ast_free when done * - * \param id The id of the stir_shaken_profile to get - * - * \retval stir_shaken_profile on success - * \retval NULL on failure + * \retval AST_STIR_SHAKEN_AS_SUCCESS if successful + * \retval Other AST_STIR_SHAKEN_AS errors. */ -struct stir_shaken_profile *ast_stir_shaken_get_profile(const char *id); +enum ast_stir_shaken_as_response_code ast_stir_shaken_attest( + struct ast_stir_shaken_as_ctx *ctx, char **header); + + +struct ast_stir_shaken_vs_ctx; /*! - * \brief Check if a stir_shaken_profile supports attestation + * \brief Create Verification Service context * - * \param profile The stir_shaken_profile to test + * \param caller_id Incoming caller id + * \param chan Incoming channel + * \param profile_name The profile name on the endpoint + * May be NULL. + * \param endpoint_behavior Behavior associated to the specific + * endpoint + * \param tag Identifying string to output in log and trace messages. + * \param ctxout Receives a pointer to the newly created context + * The caller must release with ao2_ref or ao2_cleanup. * - * \retval 0 if not supported - * \retval 1 if supported + * \retval AST_STIR_SHAKEN_VS_SUCCESS if successful. + * \retval AST_STIR_SHAKEN_VS_DISABLED if verification is disabled + * by the endpoint itself, the profile or globally. + * \retval Other AST_STIR_SHAKEN_VS errors. */ -unsigned int ast_stir_shaken_profile_supports_attestation(const struct stir_shaken_profile *profile); +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_create(const char *caller_id, + struct ast_channel *chan, const char *profile_name, + const char *tag, struct ast_stir_shaken_vs_ctx **ctxout); /*! - * \brief Check if a stir_shaken_profile supports verification + * \brief Sets response code on VS context * - * \param profile The stir_shaken_profile to test - * - * \retval 0 if not supported - * \retval 1 if supported + * \param ctx VS context + * \param vs_rc ast_stir_shaken_vs_response_code to set */ -unsigned int ast_stir_shaken_profile_supports_verification(const struct stir_shaken_profile *profile); +void ast_stir_shaken_vs_ctx_set_response_code( + struct ast_stir_shaken_vs_ctx *ctx, + enum ast_stir_shaken_vs_response_code vs_rc); + +/*! + * \brief Add the received Identity header value to the VS context + * + * \param ctx VS context + * \param identity_hdr Identity header value + * + * \retval AST_STIR_SHAKEN_VS_SUCCESS if successful + * \retval Other AST_STIR_SHAKEN_VS errors. + */ +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_add_identity_hdr(struct ast_stir_shaken_vs_ctx * ctx, + const char *identity_hdr); + +/*! + * \brief Add the received Date header value to the VS context + * + * \param ctx VS context + * \param date_hdr Date header value + * + * \retval AST_STIR_SHAKEN_VS_SUCCESS if successful + * \retval Other AST_STIR_SHAKEN_VS errors. + */ +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_add_date_hdr(struct ast_stir_shaken_vs_ctx * ctx, + const char *date_hdr); + +/*! + * \brief Get failure_action from context + * + * \param ctx VS context + * + * \retval ast_stir_shaken_failure_action + */ +enum stir_shaken_failure_action_enum + ast_stir_shaken_vs_get_failure_action( + struct ast_stir_shaken_vs_ctx *ctx); + +/*! + * \brief Get use_rfc9410_responses from context + * + * \param ctx VS context + * + * \retval 1 if true + * \retval 0 if false + */ +int ast_stir_shaken_vs_get_use_rfc9410_responses( + struct ast_stir_shaken_vs_ctx *ctx); /*! * \brief Add a STIR/SHAKEN verification result to a channel * - * \param chan The channel - * \param identity The identity - * \param attestation The attestation - * \param result The verification result + * \param ctx VS context * * \retval -1 on failure * \retval 0 on success */ -int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation, - enum ast_stir_shaken_verification_result result); +int ast_stir_shaken_add_result_to_channel( + struct ast_stir_shaken_vs_ctx *ctx); /*! - * \brief Verify a JSON STIR/SHAKEN payload + * \brief Perform incoming call verification * - * \param header The payload header - * \param payload The payload section - * \param signature The payload signature - * \param algorithm The signature algorithm - * \param public_cert_url The public key URL + * \param ctx VS context * - * \retval ast_stir_shaken_payload on success - * \retval NULL on failure + * \retval AST_STIR_SHAKEN_AS_SUCCESS if successful + * \retval Other AST_STIR_SHAKEN_AS errors. */ -struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url); - -/*! - * \brief Same as ast_stir_shaken_verify, but will populate a struct with additional information on failure - * - * \note failure_code will be written to in this function - * - * \param header The payload header - * \param payload The payload section - * \param signature The payload signature - * \param algorithm The signature algorithm - * \param public_cert_url The public key URL - * \param failure_code Additional failure information - * - * \retval ast_stir_shaken_payload on success - * \retval NULL on failure - */ -struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url, int *failure_code); - -/*! - * \brief Same as ast_stir_shaken_verify2, but passes in a stir_shaken_profile with additional configuration - * - * \note failure_code will be written to in this function - * - * \param header The payload header - * \param payload The payload section - * \param signature The payload signature - * \param algorithm The signature algorithm - * \param public_cert_url The public key URL - * \param failure Additional failure information - * \param profile The stir_shaken_profile - * - * \retval ast_stir_shaken_payload on success - * \retval NULL on failure - */ -struct ast_stir_shaken_payload *ast_stir_shaken_verify_with_profile(const char *header, const char *payload, - const char *signature, const char *algorithm, const char *public_cert_url, int *failure, - const struct stir_shaken_profile *profile); - -/*! - * \brief Retrieve the stir/shaken sorcery context - * - * \retval The stir/shaken sorcery context - */ -struct ast_sorcery *ast_stir_shaken_sorcery(void); - -/*! - * \brief Free a STIR/SHAKEN payload - */ -void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload); - -/*! - * \brief Sign a JSON STIR/SHAKEN payload - * - * \note This function will automatically add the "attest", "iat", and "origid" fields. - * - * \param json The JWT to sign - * - * \retval ast_stir_shaken_payload on success - * \retval NULL on failure - */ -struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json); +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_verify(struct ast_stir_shaken_vs_ctx * ctx); #endif /* _RES_STIR_SHAKEN_H */ diff --git a/main/db.c b/main/db.c index a807fdd3bc..ed4b89752c 100644 --- a/main/db.c +++ b/main/db.c @@ -131,6 +131,7 @@ static void db_sync(void); DEFINE_SQL_STATEMENT(put_stmt, "INSERT OR REPLACE INTO astdb (key, value) VALUES (?, ?)") DEFINE_SQL_STATEMENT(get_stmt, "SELECT value FROM astdb WHERE key=?") +DEFINE_SQL_STATEMENT(exists_stmt, "SELECT CAST(COUNT(1) AS INTEGER) AS 'exists' FROM astdb WHERE key=?") DEFINE_SQL_STATEMENT(del_stmt, "DELETE FROM astdb WHERE key=?") DEFINE_SQL_STATEMENT(deltree_stmt, "DELETE FROM astdb WHERE key || '/' LIKE ? || '/' || '%'") DEFINE_SQL_STATEMENT(deltree_all_stmt, "DELETE FROM astdb") @@ -188,6 +189,7 @@ static int clean_stmt(sqlite3_stmt **stmt, const char *sql) static void clean_statements(void) { clean_stmt(&get_stmt, get_stmt_sql); + clean_stmt(&exists_stmt, exists_stmt_sql); clean_stmt(&del_stmt, del_stmt_sql); clean_stmt(&deltree_stmt, deltree_stmt_sql); clean_stmt(&deltree_all_stmt, deltree_all_stmt_sql); @@ -204,6 +206,7 @@ static int init_statements(void) /* Don't initialize create_astdb_statement here as the astdb table needs to exist * brefore these statements can be initialized */ return init_stmt(&get_stmt, get_stmt_sql, sizeof(get_stmt_sql)) + || init_stmt(&exists_stmt, exists_stmt_sql, sizeof(exists_stmt_sql)) || init_stmt(&del_stmt, del_stmt_sql, sizeof(del_stmt_sql)) || init_stmt(&deltree_stmt, deltree_stmt_sql, sizeof(deltree_stmt_sql)) || init_stmt(&deltree_all_stmt, deltree_all_stmt_sql, sizeof(deltree_all_stmt_sql)) @@ -438,6 +441,38 @@ int ast_db_get_allocated(const char *family, const char *key, char **out) return db_get_common(family, key, out, -1); } +int ast_db_exists(const char *family, const char *key) +{ + int result; + char fullkey[MAX_DB_FIELD]; + size_t fullkey_len; + int res = 0; + + fullkey_len = snprintf(fullkey, sizeof(fullkey), "/%s/%s", family, key); + if (fullkey_len >= sizeof(fullkey)) { + ast_log(LOG_WARNING, "Family and key length must be less than %zu bytes\n", sizeof(fullkey) - 3); + return -1; + } + + ast_mutex_lock(&dblock); + res = sqlite3_bind_text(exists_stmt, 1, fullkey, fullkey_len, SQLITE_STATIC); + if (res != SQLITE_OK) { + ast_log(LOG_WARNING, "Couldn't bind key to stmt: %d:%s\n", res, sqlite3_errmsg(astdb)); + res = 0; + } else if (sqlite3_step(exists_stmt) != SQLITE_ROW) { + res = 0; + } else if (!(result = sqlite3_column_int(exists_stmt, 0))) { + res = 0; + } else { + res = result; + } + sqlite3_reset(exists_stmt); + ast_mutex_unlock(&dblock); + + return res; +} + + int ast_db_del(const char *family, const char *key) { char fullkey[MAX_DB_FIELD]; diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index 698ff20f28..7dbe7ad16e 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -37,6 +37,7 @@ #include "asterisk/stream.h" #include "asterisk/stasis.h" #include "asterisk/security_events.h" +#include "asterisk/res_stir_shaken.h" /*! \brief Number of buckets for persistent endpoint information */ #define PERSISTENT_BUCKETS 53 @@ -800,37 +801,17 @@ static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable { struct ast_sip_endpoint *endpoint = obj; - if (!strcasecmp("off", var->value)) { - endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_OFF; - } else if (!strcasecmp("attest", var->value)) { - endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ATTEST; - } else if (!strcasecmp("verify", var->value)) { - endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_VERIFY; - } else if (!strcasecmp("on", var->value)) { - endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ON; - } else { - ast_log(LOG_WARNING, "'%s' is not a valid value for option " - "'stir_shaken' for endpoint %s\n", - var->value, ast_sorcery_object_get_id(endpoint)); - return -1; - } + ast_log(LOG_WARNING, "Endpoint %s: Option 'stir_shaken' is no longer supported. Use 'stir_shaken_profile' instead.\n", + ast_sorcery_object_get_id(endpoint)); + endpoint->stir_shaken = 0; return 0; } -static const char *stir_shaken_map[] = { - [AST_SIP_STIR_SHAKEN_OFF] = "off", - [AST_SIP_STIR_SHAKEN_ATTEST] = "attest", - [AST_SIP_STIR_SHAKEN_VERIFY] = "verify", - [AST_SIP_STIR_SHAKEN_ON] = "on", -}; - static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf) { - const struct ast_sip_endpoint *endpoint = obj; - if (ARRAY_IN_BOUNDS(endpoint->stir_shaken, stir_shaken_map)) { - *buf = ast_strdup(stir_shaken_map[endpoint->stir_shaken]); - } + *buf = ast_strdup("no"); + return 0; } @@ -2308,7 +2289,8 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "codec_prefs_outgoing_answer", "prefer: pending, operation: intersect, keep: all", codec_prefs_handler, outgoing_answer_codec_prefs_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "stir_shaken", "off", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", + "stir_shaken", 0, stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, stir_shaken_profile)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_unauthenticated_options", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, allow_unauthenticated_options)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "geoloc_incoming_call_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, geoloc_incoming_call_profile)); diff --git a/res/res_pjsip_caller_id.c b/res/res_pjsip_caller_id.c index b11e5908bf..1f0d2cb6f5 100644 --- a/res/res_pjsip_caller_id.c +++ b/res/res_pjsip_caller_id.c @@ -521,9 +521,7 @@ static void add_rpid_header(const struct ast_sip_session *session, pjsip_tx_data */ static void add_id_headers(const struct ast_sip_session *session, pjsip_tx_data *tdata, const struct ast_party_id *id) { - if (!id->number.valid - || (!session->endpoint->id.trust_outbound - && (ast_party_id_presentation(id) & AST_PRES_RESTRICTION) != AST_PRES_ALLOWED)) { + if (!ast_sip_can_present_connected_id(session, id)) { return; } if (session->endpoint->id.send_pai) { diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index 0ff35736c1..496af5deca 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -52,6 +52,8 @@ #include "asterisk/stream.h" #include "asterisk/vector.h" +#include "res_pjsip_session/pjsip_session.h" + #define SDP_HANDLER_BUCKETS 11 #define MOD_DATA_ON_RESPONSE "on_response" @@ -126,6 +128,13 @@ const char *ast_sip_session_get_name(const struct ast_sip_session *session) } } +int ast_sip_can_present_connected_id(const struct ast_sip_session *session, const struct ast_party_id *id) +{ + return id->number.valid + && (session->endpoint->id.trust_outbound + || (ast_party_id_presentation(id) & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED); +} + static int sdp_handler_list_cmp(void *obj, void *arg, int flags) { struct sdp_handler_list *handler_list1 = obj; @@ -4122,11 +4131,6 @@ static void handle_new_invite_request(pjsip_rx_data *rdata) { RAII_VAR(struct ast_sip_endpoint *, endpoint, ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup); - static const pj_str_t identity_str = { "Identity", 8 }; - const pj_str_t use_identity_header_str = { - AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER) - }; pjsip_inv_session *inv_session = NULL; struct ast_sip_session *session; struct new_invite invite; @@ -4136,14 +4140,6 @@ static void handle_new_invite_request(pjsip_rx_data *rdata) ast_assert(endpoint != NULL); - if ((endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) && - !ast_sip_rdata_get_header_value(rdata, identity_str)) { - pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, - AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER, &use_identity_header_str, NULL, NULL); - ast_debug(3, "No Identity header when we require one\n"); - return; - } - inv_session = pre_session_setup(rdata, endpoint); if (!inv_session) { /* pre_session_setup() returns a response on failure */ @@ -6243,6 +6239,8 @@ static int load_module(void) ast_sip_register_service(&session_reinvite_module); ast_sip_register_service(&outbound_invite_auth_module); + pjsip_reason_header_load(); + ast_module_shutdown_ref(ast_module_info->self); #ifdef TEST_FRAMEWORK AST_TEST_REGISTER(test_resolve_refresh_media_states); @@ -6252,6 +6250,8 @@ static int load_module(void) static int unload_module(void) { + pjsip_reason_header_unload(); + #ifdef TEST_FRAMEWORK AST_TEST_UNREGISTER(test_resolve_refresh_media_states); #endif diff --git a/res/res_pjsip_session.exports.in b/res/res_pjsip_session.exports.in index 65c35addc8..b88e3e5b39 100644 --- a/res/res_pjsip_session.exports.in +++ b/res/res_pjsip_session.exports.in @@ -5,6 +5,7 @@ LINKER_SYMBOL_PREFIXast_sip_dialog_get_session; LINKER_SYMBOL_PREFIXast_sip_channel_pvt_alloc; LINKER_SYMBOL_PREFIXast_sip_create_joint_call_cap; + LINKER_SYMBOL_PREFIXast_sip_can_present_connected_id; local: *; }; diff --git a/res/res_stir_shaken/store.h b/res/res_pjsip_session/pjsip_session.h similarity index 50% rename from res/res_stir_shaken/store.h rename to res/res_pjsip_session/pjsip_session.h index c2874cda0c..14bd0c4b14 100644 --- a/res/res_stir_shaken/store.h +++ b/res/res_pjsip_session/pjsip_session.h @@ -1,9 +1,9 @@ /* * Asterisk -- An open source telephony toolkit. * - * Copyright (C) 2020, Sangoma Technologies Corporation + * Copyright (C) 2023, Sangoma Technologies Corporation * - * Kevin Harwell + * George Joseph * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact @@ -15,23 +15,20 @@ * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ -#ifndef _STIR_SHAKEN_STORE_H -#define _STIR_SHAKEN_STORE_H -struct ast_sorcery; +#ifndef PJSIP_SESSION_H_ +#define PJSIP_SESSION_H_ /*! - * \brief Load time initialization for the stir/shaken 'store' configuration - * - * \retval 0 on success, -1 on error + * \internal + * \brief Unregisters the session supplement */ -int stir_shaken_store_load(void); +void pjsip_reason_header_unload(void); /*! - * \brief Unload time cleanup for the stir/shaken 'store' configuration - * - * \retval 0 on success, -1 on error + * \internal + * \brief Registers the session supplement */ -int stir_shaken_store_unload(void); +void pjsip_reason_header_load(void); -#endif /* _STIR_SHAKEN_STORE_H */ +#endif /* PJSIP_SESSION_H_ */ diff --git a/res/res_pjsip_session/pjsip_session_reason_header.c b/res/res_pjsip_session/pjsip_session_reason_header.c new file mode 100644 index 0000000000..45a5506797 --- /dev/null +++ b/res/res_pjsip_session/pjsip_session_reason_header.c @@ -0,0 +1,168 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" +#include "asterisk/res_pjsip_session.h" +#include "asterisk/utils.h" +#include "pjsip_session.h" + +static const pj_str_t reason_hdr_str = { "Reason", 6}; + +struct return_reason_data { + char *protocol; + int response_code; + char *response_str; + int already_sent; +}; + +static void return_reason_destructor(void *obj) +{ + struct return_reason_data *rr = obj; + SCOPE_ENTER(3, "Destroying RR"); + ast_free(rr->protocol); + ast_free(rr->response_str); + ast_free(rr); + SCOPE_EXIT("Done"); +} + +#define RETURN_REASON_DATASTORE_NAME "pjsip_session_return_reason" +static struct ast_datastore_info return_reason_info = { + .type = RETURN_REASON_DATASTORE_NAME, + .destroy = return_reason_destructor, +}; + +static void reason_header_outgoing_response(struct ast_sip_session *session, + struct pjsip_tx_data *tdata) +{ + RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup); + pjsip_generic_string_hdr *reason_hdr; + pj_str_t reason_val; + RAII_VAR(char *, reason_str, NULL, ast_free); + struct return_reason_data *rr = NULL; + int rc = 0; + struct pjsip_status_line status = tdata->msg->line.status; + const char *tag = ast_sip_session_get_name(session); + SCOPE_ENTER(3, "%s: Response Code: %d\n", tag, + status.code); + + /* + * Include the Reason header if this is a provisional + * response other than a 100 OR it's a 200. + */ + if (!((PJSIP_IS_STATUS_IN_CLASS(status.code, 100) && status.code != 100) || status.code == 200)) { + SCOPE_EXIT_RTN("%s: RC %d not eligible for Reason header\n", tag, status.code); + } + + datastore = ast_sip_session_get_datastore(session, RETURN_REASON_DATASTORE_NAME); + if (!datastore) { + SCOPE_EXIT_RTN("%s: No datastore on session. Nothing to do\n", tag); + } + rr = datastore->data; + + rc = ast_asprintf(&reason_str, "%s; cause=%d; text=\"%s\"", + rr->protocol, rr->response_code, rr->response_str); + if (rc < 0) { + ast_sip_session_remove_datastore(session, RETURN_REASON_DATASTORE_NAME); + SCOPE_EXIT_RTN("%s: Failed to create reason string\n", tag); + } + reason_val = pj_str(reason_str); + + /* + * pjproject re-uses the tdata for a transaction so if we've + * already sent the Reason header, it'll get sent again unless + * we remove it. It's possible something else is sending a Reason + * header so we need to ensure we only remove our own. + */ + if (rr->already_sent) { + ast_trace(3, "%s: Reason already sent\n", tag); + reason_hdr = pjsip_msg_find_hdr_by_name(tdata->msg, &reason_hdr_str, NULL); + while (reason_hdr) { + ast_trace(3, "%s: Checking old reason: <" PJSTR_PRINTF_SPEC "> - <" PJSTR_PRINTF_SPEC "> \n", + tag, + PJSTR_PRINTF_VAR(reason_hdr->hvalue), PJSTR_PRINTF_VAR(reason_val)); + if (pj_strcmp(&reason_hdr->hvalue, &reason_val) == 0) { + ast_trace(3, "%s: MATCH. Cleaning up old reason\n", tag); + pj_list_erase(reason_hdr); + break; + } + reason_hdr = pjsip_msg_find_hdr_by_name(tdata->msg, &reason_hdr_str, reason_hdr->next); + } + ast_sip_session_remove_datastore(session, RETURN_REASON_DATASTORE_NAME); + SCOPE_EXIT_RTN("%s: Done\n", tag); + } + + reason_hdr = pjsip_generic_string_hdr_create(tdata->pool, &reason_hdr_str, &reason_val); + if (reason_hdr) { + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)reason_hdr); + rr->already_sent = 1; + ast_trace(1, "%s: Created reason header: Reason: %s\n", + tag, reason_str); + } else { + ast_trace(1, "%s: Failed to create reason header: Reason: %s\n", + tag, reason_str); + } + + SCOPE_EXIT_RTN("%s: Done\n", tag); +} + +int ast_sip_session_add_reason_header(struct ast_sip_session *session, + const char *protocol, int code, const char *text) +{ + struct return_reason_data *rr; + RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup); + const char *tag = ast_sip_session_get_name(session); + SCOPE_ENTER(4, "%s: Adding Reason header %s %d %s\n", + tag, S_OR(protocol,""), + code, S_OR(text, "")); + + if (ast_strlen_zero(protocol) || !text) { + SCOPE_EXIT_RTN_VALUE(-1, "%s: Missing protocol or text\n", tag); + } + rr = ast_calloc(1, sizeof(*rr)); + if (!rr) { + SCOPE_EXIT_RTN_VALUE(-1, "%s: Failed to allocate datastore\n", tag); + } + datastore = ast_sip_session_alloc_datastore( + &return_reason_info, return_reason_info.type); + rr->protocol = ast_strdup(protocol); + rr->response_code = code; + rr->response_str = ast_strdup(text); + datastore->data = rr; + if (ast_sip_session_add_datastore(session, datastore) != 0) { + SCOPE_EXIT_RTN_VALUE(-1, + "%s: Failed to add datastore to session\n", tag); + } + + SCOPE_EXIT_RTN_VALUE(0, "%s: Done\n", tag); +} + +static struct ast_sip_session_supplement reason_header_supplement = { + .method = "INVITE", + .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 1, /* Run AFTER channel creation */ + .outgoing_response = reason_header_outgoing_response, +}; + +void pjsip_reason_header_unload(void) +{ + ast_sip_session_unregister_supplement(&reason_header_supplement); +} + +void pjsip_reason_header_load(void) +{ + ast_sip_session_register_supplement(&reason_header_supplement); +} diff --git a/res/res_pjsip_stir_shaken.c b/res/res_pjsip_stir_shaken.c index 1089e60a7c..1e364e2519 100644 --- a/res/res_pjsip_stir_shaken.c +++ b/res/res_pjsip_stir_shaken.c @@ -26,149 +26,167 @@ #include "asterisk.h" +#define _TRACE_PREFIX_ "pjss",__LINE__, "" + +#include "asterisk/callerid.h" #include "asterisk/res_pjsip.h" #include "asterisk/res_pjsip_session.h" #include "asterisk/module.h" +#include "asterisk/rtp_engine.h" #include "asterisk/res_stir_shaken.h" -/*! The Date header will not be valid after this many milliseconds (60 seconds recommended) */ -#define STIR_SHAKEN_DATE_HEADER_TIMEOUT 60000 +static const pj_str_t identity_hdr_str = { "Identity", 8 }; +static const pj_str_t date_hdr_str = { "Date", 4 }; -/*! - * \brief Get the attestation from the payload - * - * \param json_str The JSON string representation of the payload - * - * \retval Empty string on failure - * \retval The attestation on success - */ -static char *get_attestation_from_payload(const char *json_str) +/* Response codes from RFC8224 */ +enum sip_response_code { + SIP_RESPONSE_CODE_OK = 200, + SIP_RESPONSE_CODE_STALE_DATE = 403, + SIP_RESPONSE_CODE_USE_IDENTITY_HEADER = 428, + SIP_RESPONSE_CODE_BAD_IDENTITY_INFO = 436, + SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL = 437, + SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER = 438, + SIP_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT = 428, + SIP_RESPONSE_CODE_INTERNAL_ERROR = 500, +}; + +#define SIP_RESPONSE_CODE_OK_STR "OK" +/* Response strings from RFC8224 */ +#define SIP_RESPONSE_CODE_STALE_DATE_STR "Stale Date" +#define SIP_RESPONSE_CODE_USE_IDENTITY_HEADER_STR "Use Identity Header" +#define SIP_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT_STR "Use Supported PASSporT Format" +#define SIP_RESPONSE_CODE_BAD_IDENTITY_INFO_STR "Bad Identity Info" +#define SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL_STR "Unsupported Credential" +#define SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER_STR "Invalid Identity Header" +#define SIP_RESPONSE_CODE_INTERNAL_ERROR_STR "Internal Error" + +#define response_to_str(_code) \ +case _code: \ + return _code ## _STR; + +static const char *sip_response_code_to_str(enum sip_response_code code) { - RAII_VAR(struct ast_json *, json, NULL, ast_json_free); - char *attestation; - - json = ast_json_load_string(json_str, NULL); - attestation = (char *)ast_json_string_get(ast_json_object_get(json, "attest")); - - if (!ast_strlen_zero(attestation)) { - return attestation; + switch (code) { + response_to_str(SIP_RESPONSE_CODE_OK) + response_to_str(SIP_RESPONSE_CODE_STALE_DATE) + response_to_str(SIP_RESPONSE_CODE_USE_IDENTITY_HEADER) + response_to_str(SIP_RESPONSE_CODE_BAD_IDENTITY_INFO) + response_to_str(SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL) + response_to_str(SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER) + default: + break; } - return ""; } -/*! - * \brief Compare the caller ID from the INVITE with the one in the payload - * - * \param caller_id - * \param json_str The JSON string representation of the payload - * - * \retval -1 on failure - * \retval 0 on success - */ -static int compare_caller_id(char *caller_id, const char *json_str) +#define translate_code(_vs_rc, _sip_rc) \ +case AST_STIR_SHAKEN_VS_ ## _vs_rc: \ + return SIP_RESPONSE_CODE_ ## _sip_rc; + +static enum sip_response_code vs_code_to_sip_code( + enum ast_stir_shaken_vs_response_code vs_rc) { - RAII_VAR(struct ast_json *, json, NULL, ast_json_free); - char *caller_id_other; - - json = ast_json_load_string(json_str, NULL); - caller_id_other = (char *)ast_json_string_get(ast_json_object_get( - ast_json_object_get(json, "orig"), "tn")); - - if (strcmp(caller_id, caller_id_other)) { - return -1; + /* + * We want to use a switch/case statement here because + * it'll spit out an error if VS codes are added to the + * enum but aren't present here. + */ + switch (vs_rc) { + translate_code(SUCCESS, OK) + translate_code(DISABLED, OK) + translate_code(INVALID_ARGUMENTS, INTERNAL_ERROR) + translate_code(INTERNAL_ERROR, INTERNAL_ERROR) + translate_code(NO_IDENTITY_HDR, USE_IDENTITY_HEADER) + translate_code(NO_DATE_HDR, STALE_DATE) + translate_code(DATE_HDR_PARSE_FAILURE, STALE_DATE) + translate_code(DATE_HDR_EXPIRED, STALE_DATE) + translate_code(NO_JWT_HDR, INVALID_IDENTITY_HEADER) + translate_code(INVALID_OR_NO_X5U, INVALID_IDENTITY_HEADER) + translate_code(CERT_CACHE_MISS, INVALID_IDENTITY_HEADER) + translate_code(CERT_CACHE_INVALID, INVALID_IDENTITY_HEADER) + translate_code(CERT_CACHE_EXPIRED, INVALID_IDENTITY_HEADER) + translate_code(CERT_RETRIEVAL_FAILURE, BAD_IDENTITY_INFO) + translate_code(CERT_CONTENTS_INVALID, UNSUPPORTED_CREDENTIAL) + translate_code(CERT_NOT_TRUSTED, UNSUPPORTED_CREDENTIAL) + translate_code(CERT_DATE_INVALID, UNSUPPORTED_CREDENTIAL) + translate_code(CERT_NO_TN_AUTH_EXT, UNSUPPORTED_CREDENTIAL) + translate_code(CERT_NO_SPC_IN_TN_AUTH_EXT, UNSUPPORTED_CREDENTIAL) + translate_code(NO_RAW_KEY, UNSUPPORTED_CREDENTIAL) + translate_code(SIGNATURE_VALIDATION, INVALID_IDENTITY_HEADER) + translate_code(NO_IAT, INVALID_IDENTITY_HEADER) + translate_code(IAT_EXPIRED, STALE_DATE) + translate_code(INVALID_OR_NO_PPT, INVALID_IDENTITY_HEADER) + translate_code(INVALID_OR_NO_ALG, INVALID_IDENTITY_HEADER) + translate_code(INVALID_OR_NO_TYP, INVALID_IDENTITY_HEADER) + translate_code(INVALID_OR_NO_ATTEST, INVALID_IDENTITY_HEADER) + translate_code(NO_ORIGID, INVALID_IDENTITY_HEADER) + translate_code(NO_ORIG_TN, INVALID_IDENTITY_HEADER) + translate_code(NO_DEST_TN, INVALID_IDENTITY_HEADER) + translate_code(INVALID_HEADER, INVALID_IDENTITY_HEADER) + translate_code(INVALID_GRANT, INVALID_IDENTITY_HEADER) + translate_code(INVALID_OR_NO_GRANTS, INVALID_IDENTITY_HEADER) + translate_code(CID_ORIG_TN_MISMATCH, INVALID_IDENTITY_HEADER) + translate_code(RESPONSE_CODE_MAX, INVALID_IDENTITY_HEADER) } - return 0; + return 500; } -/*! - * \brief Compare the current timestamp with the one in the payload. If the difference - * is greater than the signature timeout, it's not valid anymore - * - * \param json_str The JSON string representation of the payload - * - * \retval -1 on failure - * \retval 0 on success - */ -static int compare_timestamp(const char *json_str) +enum process_failure_rc { + PROCESS_FAILURE_CONTINUE = 0, + PROCESS_FAILURE_REJECT, + PROCESS_FAILURE_SYSTEM_FAILURE, +}; + +static void reject_incoming_call(struct ast_sip_session *session, + enum sip_response_code response_code) { - RAII_VAR(struct ast_json *, json, NULL, ast_json_free); - long int timestamp; - struct timeval now = ast_tvnow(); - -#ifdef TEST_FRAMEWORK - ast_debug(3, "Ignoring STIR/SHAKEN timestamp\n"); - return 0; -#endif - - json = ast_json_load_string(json_str, NULL); - timestamp = ast_json_integer_get(ast_json_object_get(json, "iat")); - - if (now.tv_sec - timestamp > ast_stir_shaken_get_signature_timeout()) { - return -1; - } - - return 0; -} - -static int check_date_header(pjsip_rx_data *rdata) -{ - static const pj_str_t date_hdr_str = { "Date", 4 }; - char *date_hdr_val; - struct ast_tm date_hdr_tm; - struct timeval date_hdr_timeval; - struct timeval current_timeval; - char *remainder; - char timezone[80] = { 0 }; - int64_t time_diff; - - date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str); - if (ast_strlen_zero(date_hdr_val)) { - ast_log(LOG_ERROR, "Failed to get Date header from incoming INVITE for STIR/SHAKEN\n"); - return -1; - } - - if (!(remainder = ast_strptime(date_hdr_val, "%a, %d %b %Y %T", &date_hdr_tm))) { - ast_log(LOG_ERROR, "Failed to parse Date header\n"); - return -1; - } - - sscanf(remainder, "%79s", timezone); - - if (ast_strlen_zero(timezone)) { - ast_log(LOG_ERROR, "A timezone is required for STIR/SHAKEN Date header, but we didn't get one\n"); - return -1; - } - - date_hdr_timeval = ast_mktime(&date_hdr_tm, timezone); - current_timeval = ast_tvnow(); - - time_diff = ast_tvdiff_ms(current_timeval, date_hdr_timeval); - if (time_diff < 0) { - /* An INVITE from the future! */ - ast_log(LOG_ERROR, "STIR/SHAKEN Date header has a future date\n"); - return -1; - } else if (time_diff > STIR_SHAKEN_DATE_HEADER_TIMEOUT) { - ast_log(LOG_ERROR, "STIR/SHAKEN Date header was outside of the allowable range (60 seconds)\n"); - return -1; - } - - return 0; -} - -/* Send a response back and end the session */ -static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_rx_data *rdata, int response_code, const pj_str_t response_str) -{ - pjsip_tx_data *tdata; - - if (pjsip_inv_end_session(session->inv_session, response_code, &response_str, &tdata) == PJ_SUCCESS) { - pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); - } + ast_sip_session_terminate(session, response_code); ast_hangup(session->channel); } +static enum process_failure_rc process_failure(struct ast_stir_shaken_vs_ctx *ctx, + const char *caller_id, struct ast_sip_session *session, + pjsip_rx_data *rdata, enum ast_stir_shaken_vs_response_code vs_rc) +{ + enum sip_response_code response_code = vs_code_to_sip_code(vs_rc); + pj_str_t response_str; + const char *response_string = + sip_response_code_to_str(response_code); + enum stir_shaken_failure_action_enum failure_action = + ast_stir_shaken_vs_get_failure_action(ctx); + const char *tag = ast_sip_session_get_name(session); + SCOPE_ENTER(1, "%s: FA: %d RC: %d\n", tag, + failure_action, response_code); + + pj_cstr(&response_str, response_string); + + if (failure_action == stir_shaken_failure_action_REJECT_REQUEST) { + reject_incoming_call(session, response_code); + SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_REJECT, + "%s: Rejecting request and terminating session\n", + tag); + } + + ast_stir_shaken_vs_ctx_set_response_code(ctx, vs_rc); + ast_stir_shaken_add_result_to_channel(ctx); + + if (failure_action == stir_shaken_failure_action_CONTINUE_RETURN_REASON) { + int rc = ast_sip_session_add_reason_header(session, + ast_stir_shaken_vs_get_use_rfc9410_responses(ctx) ? "STIR" : "SIP", + response_code, response_str.ptr); + if (rc != 0) { + SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_SYSTEM_FAILURE, + "%s: Failed to add Reason header\n", tag); + } + SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_CONTINUE, + "%s: Attaching reason code to session\n", tag); + } + SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_CONTINUE, + "%s: Continuing\n", tag); +} + /*! * \internal * \brief Session supplement callback on an incoming INVITE request @@ -181,222 +199,155 @@ static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_r */ static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata) { - static const pj_str_t identity_str = { "Identity", 8 }; - const pj_str_t bad_identity_info_str = { - AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO) - }; - const pj_str_t unsupported_credential_str = { - AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL) - }; - const pj_str_t stale_date_str = { - AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE) - }; - const pj_str_t use_supported_passport_format_str = { - AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT) - }; - const pj_str_t invalid_identity_hdr_str = { - AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER, - strlen(AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER) - }; - const pj_str_t server_internal_error_str = { "Server Internal Error", 21 }; - char *identity_hdr_val; - char *encoded_val; - struct ast_channel *chan = session->channel; - char *caller_id = session->id.number.str; + RAII_VAR(struct ast_stir_shaken_vs_ctx *, ctx, NULL, ao2_cleanup); RAII_VAR(char *, header, NULL, ast_free); RAII_VAR(char *, payload, NULL, ast_free); - char *signature; - char *algorithm; - char *public_cert_url; - char *attestation; - char *ppt; - int mismatch = 0; - struct ast_stir_shaken_payload *ss_payload; - int failure_code = 0; - RAII_VAR(struct stir_shaken_profile *, profile, NULL, ao2_cleanup); + char *identity_hdr_val; + char *date_hdr_val; + char *caller_id = session->id.number.str; + const char *session_name = ast_sip_session_get_name(session); + struct ast_channel *chan = session->channel; + enum ast_stir_shaken_vs_response_code vs_rc; + enum process_failure_rc p_rc; + SCOPE_ENTER(1, "%s: Enter\n", session_name); /* Check if this is a reinvite. If it is, we don't need to do anything */ if (rdata->msg_info.to->tag.slen) { - return 0; + SCOPE_EXIT_RTN_VALUE(0, "%s: Reinvite. No action needed\n", session_name); } - profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile); - /* Profile should be checked first as it takes priority over anything else. - * If there is a profile and it doesn't have verification enabled, do nothing. - * If there is no profile and the stir_shaken option is either not set or does - * not support verification, do nothing. + /* + * Shortcut: If there's no callerid or profile name, + * just bail now. */ - if ((profile && !ast_stir_shaken_profile_supports_verification(profile)) - || (!profile && (session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0)) { - return 0; + if (ast_strlen_zero(caller_id) + || ast_strlen_zero(session->endpoint->stir_shaken_profile)) { + SCOPE_EXIT_RTN_VALUE(0, "%s: No callerid or profile name. No action needed\n", session_name); } - identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_str); + vs_rc = ast_stir_shaken_vs_ctx_create(caller_id, chan, + session->endpoint->stir_shaken_profile, + session_name, &ctx); + if (vs_rc == AST_STIR_SHAKEN_VS_DISABLED) { + SCOPE_EXIT_RTN_VALUE(0, "%s: VS Disabled\n", session_name); + } else if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + reject_incoming_call(session, 500); + SCOPE_EXIT_RTN_VALUE(1, "%s: Unable to create context. Call terminated\n", + session_name); + } + + identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_hdr_str); if (ast_strlen_zero(identity_hdr_val)) { - ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_NOT_PRESENT); - return 0; - } - - encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); - header = ast_base64url_decode_string(encoded_val); - if (ast_strlen_zero(header)) { - ast_debug(3, "STIR/SHAKEN INVITE for %s is missing header\n", - ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); - return 1; - } - - encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val); - payload = ast_base64url_decode_string(encoded_val); - if (ast_strlen_zero(payload)) { - ast_debug(3, "STIR/SHAKEN INVITE for %s is missing payload\n", - ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); - return 1; - } - - /* It's fine to leave the signature encoded */ - signature = strtok_r(identity_hdr_val, ";", &identity_hdr_val); - if (ast_strlen_zero(signature)) { - ast_debug(3, "STIR/SHAKEN INVITE for %s is missing signature\n", - ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); - return 1; - } - - /* Trim "info=<" to get public cert URL */ - strtok_r(identity_hdr_val, "<", &identity_hdr_val); - public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val); - - /* Make sure the public URL is actually a URL */ - if (ast_strlen_zero(public_cert_url) || !ast_begins_with(public_cert_url, "http")) { - /* RFC8224 states that if we can't acquire the credentials needed - * by the verification service, we should send a 436 */ - ast_debug(3, "STIR/SHAKEN INVITE for %s did not have valid URL (%s)\n", - ast_sorcery_object_get_id(session->endpoint), public_cert_url); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str); - return 1; - } - - algorithm = strtok_r(identity_hdr_val, ";", &identity_hdr_val); - if (ast_strlen_zero(algorithm)) { - /* RFC8224 states that if the algorithm is not specified, use ES256 */ - algorithm = STIR_SHAKEN_ENCRYPTION_ALGORITHM; - } else { - strtok_r(algorithm, "=", &algorithm); - if (strcmp(algorithm, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { - /* RFC8224 states that if we don't support the algorithm, send a 437 */ - ast_debug(3, "STIR/SHAKEN INVITE for %s uses an unsupported algorithm (%s)\n", - ast_sorcery_object_get_id(session->endpoint), algorithm); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str); - return 1; + p_rc = process_failure(ctx, caller_id, session, rdata, + AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR); + if (p_rc == PROCESS_FAILURE_CONTINUE) { + SCOPE_EXIT_RTN_VALUE(0, "%s: No Identity header found. Call continuing\n", + session_name); } + SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: No Identity header found. Call terminated\n", + session_name); } - /* The only thing left should be ppt=shaken (which could have more values later), - * unless using the compact PASSport form */ - strtok_r(identity_hdr_val, "=", &identity_hdr_val); - ppt = ast_strip(identity_hdr_val); - if (!ast_strlen_zero(ppt) && strcmp(ppt, STIR_SHAKEN_PPT)) { - ast_log(LOG_ERROR, "STIR/SHAKEN INVITE for %s has unsupported ppt (%s)\n", - ast_sorcery_object_get_id(session->endpoint), ppt); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT, use_supported_passport_format_str); - return 1; + vs_rc = ast_stir_shaken_vs_ctx_add_identity_hdr(ctx, identity_hdr_val); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + reject_incoming_call(session, 500); + SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Unable to add Identity header. Call terminated.\n", + session_name); } - if (check_date_header(rdata)) { - ast_debug(3, "STIR/SHAKEN INVITE for %s has old Date header\n", - ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE, stale_date_str); - return 1; - } - - attestation = get_attestation_from_payload(payload); - - ss_payload = ast_stir_shaken_verify_with_profile(header, payload, signature, algorithm, public_cert_url, &failure_code, profile); - - if (!ss_payload) { - - if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT) { - /* RFC8224 states that if we can't get the credentials we need, send a 437 */ - ast_debug(3, "STIR/SHAKEN INVITE for %s failed to acquire cert during verification process\n", - ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str); - } else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC) { - ast_log(LOG_ERROR, "Failed to allocate memory during STIR/SHAKEN verification" - " for %s\n", ast_sorcery_object_get_id(session->endpoint)); - stir_shaken_inv_end_session(session, rdata, 500, server_internal_error_str); - } else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION) { - /* RFC8224 states that if we can't validate the signature, send a 438 */ - ast_debug(3, "STIR/SHAKEN INVITE for %s failed signature validation during verification process\n", - ast_sorcery_object_get_id(session->endpoint)); - ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED); - stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER, invalid_identity_hdr_str); + date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str); + if (ast_strlen_zero(date_hdr_val)) { + p_rc = process_failure(ctx, caller_id, session, rdata, + AST_STIR_SHAKEN_VS_NO_DATE_HDR); + if (p_rc == PROCESS_FAILURE_CONTINUE) { + SCOPE_EXIT_RTN_VALUE(0, "%s: No Date header found. Call continuing\n", + session_name); } - - return 1; - } - ast_stir_shaken_payload_free(ss_payload); - - mismatch |= compare_caller_id(caller_id, payload); - mismatch |= compare_timestamp(payload); - - if (mismatch) { - ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_MISMATCH); - return 0; + SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: No Date header found. Call terminated\n", + session_name); } - ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_PASSED); + ast_stir_shaken_vs_ctx_add_date_hdr(ctx, date_hdr_val); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + reject_incoming_call(session, 500); + SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Unable to add Date header. Call terminated.\n", + session_name); + } - return 0; + vs_rc = ast_stir_shaken_vs_verify(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + p_rc = process_failure(ctx, caller_id, session, rdata, vs_rc); + if (p_rc == PROCESS_FAILURE_CONTINUE) { + SCOPE_EXIT_RTN_VALUE(0, "%s: Verification failed. Call continuing\n", + session_name); + } + SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Verification failed. Call terminated\n", + session_name); + + } + + ast_stir_shaken_add_result_to_channel(ctx); + + SCOPE_EXIT_RTN_VALUE(0, "Passed\n"); } -static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata) +static void add_fingerprints_if_present(struct ast_sip_session *session, + struct ast_stir_shaken_as_ctx *ctx) +{ + struct ast_sip_session_media_state *ms = session->pending_media_state; + struct ast_sip_session_media *m = NULL; + struct ast_rtp_engine_dtls *d = NULL; + enum ast_rtp_dtls_hash h; + int i; + const char *tag = ast_sip_session_get_name(session); + size_t count = AST_VECTOR_SIZE(&ms->sessions); + SCOPE_ENTER(4, "%s: Check %zu media sessions for fingerprints\n", + tag, count); + + if (!ast_stir_shaken_as_ctx_wants_fingerprints(ctx)) { + SCOPE_EXIT_RTN("%s: Fingerprints not needed\n", tag); + } + + for (i = 0; i < count; i++) { + const char *f; + + m = AST_VECTOR_GET(&ms->sessions, i); + if (!m|| !m->rtp) { + ast_trace(1, "Session: %d: No session or rtp instance\n", i); + continue; + } + d = ast_rtp_instance_get_dtls(m->rtp); + h = d->get_fingerprint_hash(m->rtp); + f = d->get_fingerprint(m->rtp); + + ast_stir_shaken_as_ctx_add_fingerprint(ctx, + h == AST_RTP_DTLS_HASH_SHA256 ? "sha-256" : "sha-1", f); + } + SCOPE_EXIT_RTN("%s: Done\n", tag); +} + +static char *get_dest_tn(pjsip_tx_data *tdata, const char *tag) { - static const pj_str_t identity_str = { "Identity", 8 }; - pjsip_generic_string_hdr *identity_hdr; - pj_str_t identity_val; - pjsip_fromto_hdr *old_identity; pjsip_fromto_hdr *to; pjsip_sip_uri *uri; - char *signature; - char *public_cert_url; - struct ast_json *header; - struct ast_json *payload; - char *dumped_string; - RAII_VAR(char *, dest_tn, NULL, ast_free); - RAII_VAR(struct ast_json *, json, NULL, ast_json_free); - RAII_VAR(struct ast_stir_shaken_payload *, ss_payload, NULL, ast_stir_shaken_payload_free); - RAII_VAR(char *, encoded_header, NULL, ast_free); - RAII_VAR(char *, encoded_payload, NULL, ast_free); - RAII_VAR(char *, combined_str, NULL, ast_free); - size_t combined_size; - - old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_str, NULL); - if (old_identity) { - return 0; - } + char *dest_tn = NULL; + SCOPE_ENTER(4, "%s: Enter\n", tag); to = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_TO, NULL); if (!to) { - ast_log(LOG_ERROR, "Failed to find To header while adding STIR/SHAKEN Identity header\n"); - return -1; + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Failed to find To header\n", tag); } uri = pjsip_uri_get_uri(to->uri); if (!uri) { - ast_log(LOG_ERROR, "Failed to retrieve URI from To header while adding STIR/SHAKEN Identity header\n"); - return -1; + SCOPE_EXIT_RTN_VALUE(NULL, + "%s: Failed to retrieve URI from To header\n", tag); } dest_tn = ast_malloc(uri->user.slen + 1); if (!dest_tn) { - ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN dest->tn\n"); - return -1; + SCOPE_EXIT_RTN_VALUE(NULL, + "%s: Failed to allocate memory for dest_tn\n", tag); } /* Remove everything except 0-9, *, and # in telephone number according to RFC 8224 @@ -413,114 +364,105 @@ static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_d s++; } *new_tn = '\0'; - ast_debug(4, "Canonicalized telephone number %.*s -> %s\n", (int) uri->user.slen, uri->user.ptr, dest_tn); + ast_trace(2, "Canonicalized telephone number " PJSTR_PRINTF_SPEC " -> %s\n", + PJSTR_PRINTF_VAR(uri->user), dest_tn); } - /* x5u (public key URL), attestation, and origid will be added by ast_stir_shaken_sign */ - json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: [s]}, s: {s: s}}}", - "header", "alg", "ES256", "ppt", "shaken", "typ", "passport", - "payload", "dest", "tn", dest_tn, "orig", "tn", - session->id.number.str); - if (!json) { - ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN JSON\n"); - return -1; - } - - ss_payload = ast_stir_shaken_sign(json); - if (!ss_payload) { - ast_log(LOG_ERROR, "Failed to sign STIR/SHAKEN payload\n"); - return -1; - } - - header = ast_json_object_get(json, "header"); - dumped_string = ast_json_dump_string(header); - encoded_header = ast_base64url_encode_string(dumped_string); - ast_json_free(dumped_string); - if (!encoded_header) { - ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN header\n"); - return -1; - } - - payload = ast_json_object_get(json, "payload"); - /* Fields must appear in lexiographic order: https://www.rfc-editor.org/rfc/rfc8588.html#section-6 - * https://www.rfc-editor.org/rfc/rfc8225.html#section-9 */ - dumped_string = ast_json_dump_string_sorted(payload); - encoded_payload = ast_base64url_encode_string(dumped_string); - ast_json_free(dumped_string); - if (!encoded_payload) { - ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN payload\n"); - return -1; - } - - signature = (char *)ast_stir_shaken_payload_get_signature(ss_payload); - public_cert_url = ast_stir_shaken_payload_get_public_cert_url(ss_payload); - - /* The format for the identity header: - * header.payload.signature;info=alg=STIR_SHAKEN_ENCRYPTION_ALGORITHM;ppt=STIR_SHAKEN_PPT - */ - combined_size = strlen(encoded_header) + 1 + strlen(encoded_payload) + 1 - + strlen(signature) + strlen(";info=<>alg=;ppt=") + strlen(public_cert_url) - + strlen(STIR_SHAKEN_ENCRYPTION_ALGORITHM) + strlen(STIR_SHAKEN_PPT) + 1; - combined_str = ast_calloc(1, combined_size); - if (!combined_str) { - ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN identity string\n"); - return -1; - } - snprintf(combined_str, combined_size, "%s.%s.%s;info=<%s>alg=%s;ppt=%s", encoded_header, - encoded_payload, signature, public_cert_url, STIR_SHAKEN_ENCRYPTION_ALGORITHM, STIR_SHAKEN_PPT); - - identity_val = pj_str(combined_str); - identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_str, &identity_val); - if (!identity_hdr) { - ast_log(LOG_ERROR, "Failed to create STIR/SHAKEN Identity header\n"); - return -1; - } - - pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)identity_hdr); - - return 0; + SCOPE_EXIT_RTN_VALUE(dest_tn, "%s: Done\n", tag); } static void add_date_header(const struct ast_sip_session *session, pjsip_tx_data *tdata) { - static const pj_str_t date_str = { "Date", 4 }; pjsip_fromto_hdr *old_date; + const char *session_name = ast_sip_session_get_name(session); + SCOPE_ENTER(1, "%s: Enter\n", session_name); - old_date = pjsip_msg_find_hdr_by_name(tdata->msg, &date_str, NULL); + old_date = pjsip_msg_find_hdr_by_name(tdata->msg, &date_hdr_str, NULL); if (old_date) { - ast_debug(3, "Found old STIR/SHAKEN date header, no need to add one\n"); - return; + SCOPE_EXIT_RTN("Found existing Date header, no need to add one\n"); } ast_sip_add_date_header(tdata); + SCOPE_EXIT_RTN("Done\n"); } -static void stir_shaken_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata) +static void stir_shaken_outgoing_request(struct ast_sip_session *session, + pjsip_tx_data *tdata) { - RAII_VAR(struct stir_shaken_profile *, profile, NULL, ao2_cleanup); + struct ast_party_id effective_id; + struct ast_party_id connected_id; + pjsip_generic_string_hdr *old_identity; + pjsip_generic_string_hdr *identity_hdr; + pj_str_t identity_val; + char *dest_tn; + char *identity_str; + struct ast_stir_shaken_as_ctx *ctx = NULL; + enum ast_stir_shaken_as_response_code as_rc; + const char *session_name = ast_sip_session_get_name(session); + SCOPE_ENTER(1, "%s: Enter\n", session_name); - profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile); - /* Profile should be checked first as it takes priority over anything else. - * If there is a profile and it doesn't have attestation enabled, do nothing. - * If there is no profile and the stir_shaken option is either not set or does - * not support attestation, do nothing. - */ - if ((profile && !ast_stir_shaken_profile_supports_attestation(profile)) - || (!profile && (session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_ATTEST) == 0)) { - return; + old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_hdr_str, NULL); + if (old_identity) { + SCOPE_EXIT_RTN("Found an existing Identity header\n"); } - if (ast_strlen_zero(session->id.number.str) && session->id.number.valid) { - return; + dest_tn = get_dest_tn(tdata, session_name); + if (!dest_tn) { + SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Unable to find destination tn\n", + session_name); } - /* If adding the Identity header fails for some reason, there's no point - * adding the Date header. - */ - if ((add_identity_header(session, tdata)) != 0) { - return; + ast_party_id_init(&connected_id); + ast_channel_lock(session->channel); + effective_id = ast_channel_connected_effective_id(session->channel); + ast_party_id_copy(&connected_id, &effective_id); + ast_channel_unlock(session->channel); + + if (!ast_sip_can_present_connected_id(session, &connected_id)) { + ast_free(dest_tn); + ast_party_id_free(&connected_id); + SCOPE_EXIT_RTN("Unable to get caller id\n"); } + + as_rc = ast_stir_shaken_as_ctx_create(connected_id.number.str, + dest_tn, session->channel, + session->endpoint->stir_shaken_profile, + session_name, &ctx); + + ast_free(dest_tn); + ast_party_id_free(&connected_id); + + if (as_rc == AST_STIR_SHAKEN_AS_DISABLED) { + SCOPE_EXIT_RTN("%s: AS Disabled\n", session_name); + } else if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) { + SCOPE_EXIT_RTN("%s: Unable to create context\n", + session_name); + } + add_date_header(session, tdata); + add_fingerprints_if_present(session, ctx); + + as_rc = ast_stir_shaken_attest(ctx, &identity_str); + if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) { + ao2_cleanup(ctx); + SCOPE_EXIT_LOG(LOG_ERROR, + "%s: Failed to create attestation\n", session_name); + } + + ast_trace(1, "%s: Identity header: %s\n", session_name, identity_str); + identity_val = pj_str(identity_str); + identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_hdr_str, &identity_val); + ast_free(identity_str); + if (!identity_hdr) { + ao2_cleanup(ctx); + SCOPE_EXIT_LOG_RTN(LOG_ERROR, + "%s: Unable to create Identity header\n", session_name); + } + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)identity_hdr); + + ao2_cleanup(ctx); + SCOPE_EXIT_RTN("Done\n"); } static struct ast_sip_session_supplement stir_shaken_supplement = { diff --git a/res/res_stir_shaken.c b/res/res_stir_shaken.c index efb8be957d..4e268508bf 100644 --- a/res/res_stir_shaken.c +++ b/res/res_stir_shaken.c @@ -17,280 +17,40 @@ */ /*** MODULEINFO - crypto curl res_curl + libjwt core ***/ +#define _TRACE_PREFIX_ "rss",__LINE__, "" + #include "asterisk.h" -#include - -#include "asterisk/module.h" -#include "asterisk/sorcery.h" -#include "asterisk/time.h" -#include "asterisk/json.h" -#include "asterisk/astdb.h" -#include "asterisk/paths.h" -#include "asterisk/conversions.h" -#include "asterisk/pbx.h" -#include "asterisk/global_datastores.h" #include "asterisk/app.h" -#include "asterisk/test.h" -#include "asterisk/acl.h" +#include "asterisk/cli.h" +#include "asterisk/conversions.h" +#include "asterisk/module.h" +#include "asterisk/global_datastores.h" +#include "asterisk/pbx.h" -#include "asterisk/res_stir_shaken.h" #include "res_stir_shaken/stir_shaken.h" -#include "res_stir_shaken/general.h" -#include "res_stir_shaken/store.h" -#include "res_stir_shaken/certificate.h" -#include "res_stir_shaken/curl.h" -#include "res_stir_shaken/profile.h" -/*** DOCUMENTATION - - STIR/SHAKEN module for Asterisk - - - STIR/SHAKEN general options - - Must be of type 'general'. - - - File path to the certificate authority certificate - - - File path to a chain of trust - - - Maximum size to use for caching public keys - - - Maximum time to wait to CURL certificates - - - Amount of time a signature is valid for - - - - STIR/SHAKEN certificate store options - - Must be of type 'store'. - - - Path to a directory containing certificates - - - URL to the public certificate(s) - - Must be a valid http, or https, URL. The URL must also contain the ${CERTIFICATE} variable, which is used for public key name substitution. - For example: http://mycompany.com/${CERTIFICATE}.pub - - - - - STIR/SHAKEN certificate options - - Must be of type 'certificate'. - - - File path to a certificate - - - URL to the public certificate - - Must be a valid http, or https, URL. - - - - Attestation level - - - The caller ID number to match on. - - - - STIR/SHAKEN profile configuration options - - Must be of type 'profile'. - - - STIR/SHAKEN configuration settings - - Attest, verify, or do both STIR/SHAKEN operations. On incoming - INVITEs, the Identity header will be checked for validity. On - outgoing INVITEs, an Identity header will be added. - - - - An existing ACL from acl.conf to use - - - An IP or subnet to permit - - - An IP or subnet to deny - - - - - - - Gets the number of STIR/SHAKEN results or a specific STIR/SHAKEN value from a result on the channel. - - - - The index of the STIR/SHAKEN result to get. If only 'count' is passed in, gets the number of STIR/SHAKEN results instead. - - - The value to get from the STIR/SHAKEN result. Only used when an index is passed in (instead of 'count'). Allowable values: - - - - - - - - - This function will either return the number of STIR/SHAKEN identities, or return information on the specified identity. - To get the number of identities, just pass 'count' as the only parameter to the function. If you want to get information on a - specific STIR/SHAKEN identity, you can get the number of identities and then pass an index as the first parameter and one of - the values you would like to retrieve as the second parameter. - - - same => n,NoOp(Number of STIR/SHAKEN identities: ${STIR_SHAKEN(count)}) - same => n,NoOp(Identity ${STIR_SHAKEN(0, identity)} has attestation level ${STIR_SHAKEN(0, attestation)}) - - - - ***/ +static int tn_auth_list_nid; -static struct ast_sorcery *stir_shaken_sorcery; - -/* Used for AstDB entries */ -#define AST_DB_FAMILY "STIR_SHAKEN" - -/* The directory name to store keys in. Appended to ast_config_DATA_DIR */ -#define STIR_SHAKEN_DIR_NAME "stir_shaken" - -/* The maximum length for path storage */ -#define MAX_PATH_LEN 256 - -/* The default amount of time (in seconds) to use for certificate expiration - * if no cache data is available - */ -#define EXPIRATION_BUFFER 15 - -struct ast_stir_shaken_payload { - /*! The JWT header */ - struct ast_json *header; - /*! The JWT payload */ - struct ast_json *payload; - /*! Signature for the payload */ - unsigned char *signature; - /*! The algorithm used */ - char *algorithm; - /*! THe URL to the public certificate */ - char *public_cert_url; -}; - -struct ast_sorcery *ast_stir_shaken_sorcery(void) +int get_tn_auth_nid(void) { - return stir_shaken_sorcery; -} - -void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload) -{ - if (!payload) { - return; - } - - ast_json_unref(payload->header); - ast_json_unref(payload->payload); - ast_free(payload->algorithm); - ast_free(payload->public_cert_url); - ast_free(payload->signature); - - ast_free(payload); -} - -unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload) -{ - return payload ? payload->signature : NULL; -} - -char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload) -{ - return payload ? payload->public_cert_url : NULL; -} - -unsigned int ast_stir_shaken_get_signature_timeout(void) -{ - return ast_stir_shaken_signature_timeout(stir_shaken_general_get()); -} - -struct stir_shaken_profile *ast_stir_shaken_get_profile(const char *id) -{ - if (ast_strlen_zero(id)) { - return NULL; - } - - return ast_stir_shaken_get_profile_by_name(id); -} - -unsigned int ast_stir_shaken_profile_supports_attestation(const struct stir_shaken_profile *profile) -{ - if (!profile) { - return 0; - } - - return (profile->stir_shaken & STIR_SHAKEN_ATTEST); -} - -unsigned int ast_stir_shaken_profile_supports_verification(const struct stir_shaken_profile *profile) -{ - if (!profile) { - return 0; - } - - return (profile->stir_shaken & STIR_SHAKEN_VERIFY); -} - -/*! - * \brief Convert an ast_stir_shaken_verification_result to string representation - * - * \param result The result to convert - * - * \retval empty string if not a valid enum value - * \retval string representation of result otherwise - */ -static const char *stir_shaken_verification_result_to_string(enum ast_stir_shaken_verification_result result) -{ - switch (result) { - case AST_STIR_SHAKEN_VERIFY_NOT_PRESENT: - return "Verification not present"; - case AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED: - return "Signature failed"; - case AST_STIR_SHAKEN_VERIFY_MISMATCH: - return "Verification mismatch"; - case AST_STIR_SHAKEN_VERIFY_PASSED: - return "Verification passed"; - default: - break; - } - - return ""; + return tn_auth_list_nid; } /* The datastore struct holding verification information for the channel */ -struct stir_shaken_datastore { +struct stir_datastore { /* The identitifier for the STIR/SHAKEN verification */ char *identity; /* The attestation value */ char *attestation; /* The actual verification result */ - enum ast_stir_shaken_verification_result verify_result; + enum ast_stir_shaken_vs_response_code verify_result; }; /*! @@ -298,7 +58,7 @@ struct stir_shaken_datastore { * * \param datastore The datastore to free */ -static void stir_shaken_datastore_free(struct stir_shaken_datastore *datastore) +static void stir_datastore_free(struct stir_datastore *datastore) { if (!datastore) { return; @@ -314,948 +74,86 @@ static void stir_shaken_datastore_free(struct stir_shaken_datastore *datastore) * * \param data The stir_shaken_datastore */ -static void stir_shaken_datastore_destroy_cb(void *data) +static void stir_datastore_destroy_cb(void *data) { - struct stir_shaken_datastore *datastore = data; - stir_shaken_datastore_free(datastore); + struct stir_datastore *datastore = data; + stir_datastore_free(datastore); } /* The stir_shaken_datastore info used to add and compare stir_shaken_datastores on the channel */ static const struct ast_datastore_info stir_shaken_datastore_info = { .type = "STIR/SHAKEN VERIFICATION", - .destroy = stir_shaken_datastore_destroy_cb, + .destroy = stir_datastore_destroy_cb, }; -int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation, - enum ast_stir_shaken_verification_result result) +int ast_stir_shaken_add_result_to_channel( + struct ast_stir_shaken_vs_ctx *ctx) { - struct stir_shaken_datastore *ss_datastore; - struct ast_datastore *datastore; + struct stir_datastore *stir_datastore; + struct ast_datastore *chan_datastore; const char *chan_name; - if (!chan) { + if (!ctx->chan) { ast_log(LOG_ERROR, "Channel is required to add STIR/SHAKEN verification\n"); return -1; } - chan_name = ast_channel_name(chan); + chan_name = ast_channel_name(ctx->chan); - if (!identity) { + if (!ctx->identity_hdr) { ast_log(LOG_ERROR, "No identity to add STIR/SHAKEN verification to channel " "%s\n", chan_name); return -1; } - if (!attestation) { + if (!ctx->attestation) { ast_log(LOG_ERROR, "Attestation cannot be NULL to add STIR/SHAKEN verification to " "channel %s\n", chan_name); return -1; } - ss_datastore = ast_calloc(1, sizeof(*ss_datastore)); - if (!ss_datastore) { + stir_datastore = ast_calloc(1, sizeof(*stir_datastore)); + if (!stir_datastore) { ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore for " "channel %s\n", chan_name); return -1; } - ss_datastore->identity = ast_strdup(identity); - if (!ss_datastore->identity) { + stir_datastore->identity = ast_strdup(ctx->identity_hdr); + if (!stir_datastore->identity) { ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore " "identity for channel %s\n", chan_name); - stir_shaken_datastore_free(ss_datastore); + stir_datastore_free(stir_datastore); return -1; } - ss_datastore->attestation = ast_strdup(attestation); - if (!ss_datastore->attestation) { + stir_datastore->attestation = ast_strdup(ctx->attestation); + if (!stir_datastore->attestation) { ast_log(LOG_ERROR, "Failed to allocate space for STIR/SHAKEN datastore " "attestation for channel %s\n", chan_name); - stir_shaken_datastore_free(ss_datastore); + stir_datastore_free(stir_datastore); return -1; } - ss_datastore->verify_result = result; + stir_datastore->verify_result = ctx->failure_reason; - datastore = ast_datastore_alloc(&stir_shaken_datastore_info, NULL); - if (!datastore) { + chan_datastore = ast_datastore_alloc(&stir_shaken_datastore_info, NULL); + if (!chan_datastore) { ast_log(LOG_ERROR, "Failed to allocate space for datastore for channel " "%s\n", chan_name); - stir_shaken_datastore_free(ss_datastore); + stir_datastore_free(stir_datastore); return -1; } - datastore->data = ss_datastore; + chan_datastore->data = stir_datastore; - ast_channel_lock(chan); - ast_channel_datastore_add(chan, datastore); - ast_channel_unlock(chan); + ast_channel_lock(ctx->chan); + ast_channel_datastore_add(ctx->chan, chan_datastore); + ast_channel_unlock(ctx->chan); return 0; } -/*! - * \brief Sets the expiration for the public key based on the provided fields. - * If Cache-Control is present, use it. Otherwise, use Expires. - * - * \param public_cert_url The URL to the public certificate - * \param data The CURL callback data containing expiration data - */ -static void set_public_key_expiration(const char *public_cert_url, const struct curl_cb_data *data) -{ - char time_buf[32], secs[AST_TIME_T_LEN]; - char *value; - struct timeval actual_expires = ast_tvnow(); - char hash[41]; - - ast_sha1_hash(hash, public_cert_url); - - value = curl_cb_data_get_cache_control(data); - if (!ast_strlen_zero(value)) { - char *str_max_age; - - str_max_age = strstr(value, "s-maxage"); - if (!str_max_age) { - str_max_age = strstr(value, "max-age"); - } - - if (str_max_age) { - unsigned int max_age; - char *equal = strchr(str_max_age, '='); - if (equal && !ast_str_to_uint(equal + 1, &max_age)) { - actual_expires.tv_sec += max_age; - } - } - } else { - value = curl_cb_data_get_expires(data); - if (!ast_strlen_zero(value)) { - struct tm expires_time; - - strptime(value, "%a, %d %b %Y %T %z", &expires_time); - expires_time.tm_isdst = -1; - actual_expires.tv_sec = mktime(&expires_time); - } - } - - if (ast_strlen_zero(value)) { - actual_expires.tv_sec += EXPIRATION_BUFFER; - } - - ast_time_t_to_string(actual_expires.tv_sec, secs, sizeof(secs)); - - snprintf(time_buf, sizeof(time_buf), "%30s", secs); - - ast_db_put(hash, "expiration", time_buf); -} - -/*! - * \brief Check to see if the public key is expired - * - * \param public_cert_url The public cert URL - * - * \retval 1 if expired - * \retval 0 if not expired - */ -static int public_key_is_expired(const char *public_cert_url) -{ - struct timeval current_time = ast_tvnow(); - struct timeval expires = { .tv_sec = 0, .tv_usec = 0 }; - char expiration[32]; - char hash[41]; - - ast_sha1_hash(hash, public_cert_url); - ast_db_get(hash, "expiration", expiration, sizeof(expiration)); - - if (ast_strlen_zero(expiration)) { - return 1; - } - - if (ast_str_to_ulong(expiration, (unsigned long *)&expires.tv_sec)) { - return 1; - } - - return ast_tvcmp(current_time, expires) == -1 ? 0 : 1; -} - -/*! - * \brief Returns the path to the downloaded file for the provided URL - * - * \param public_cert_url The public cert URL - * - * \retval Empty string if not present in AstDB - * \retval The file path if present in AstDB - */ -static char *get_path_to_public_key(const char *public_cert_url) -{ - char hash[41]; - char file_path[MAX_PATH_LEN]; - - ast_sha1_hash(hash, public_cert_url); - - ast_db_get(hash, "path", file_path, sizeof(file_path)); - - if (ast_strlen_zero(file_path)) { - file_path[0] = '\0'; - } - - return ast_strdup(file_path); -} - -/*! - * \brief Add the public key details and file path to AstDB - * - * \param public_cert_url The public cert URL - * \param filepath The path to the file - */ -static void add_public_key_to_astdb(const char *public_cert_url, const char *filepath) -{ - char hash[41]; - - ast_sha1_hash(hash, public_cert_url); - - ast_db_put(AST_DB_FAMILY, public_cert_url, hash); - ast_db_put(hash, "path", filepath); -} - -/*! - * \brief Remove the public key details and associated information from AstDB - * - * \param public_cert_url The public cert URL - */ -static void remove_public_key_from_astdb(const char *public_cert_url) -{ - char hash[41]; - char filepath[MAX_PATH_LEN]; - - ast_sha1_hash(hash, public_cert_url); - - /* Remove this public key from storage */ - ast_db_get(hash, "path", filepath, sizeof(filepath)); - - /* Remove the actual file from the system */ - remove(filepath); - - ast_db_del(AST_DB_FAMILY, public_cert_url); - ast_db_deltree(hash, NULL); -} - -/*! - * \brief Verifies the signature using a public key - * - * \param msg The payload - * \param signature The signature to verify - * \param public_key The public key used for verification - * - * \retval -1 on failure - * \retval 0 on success - */ -static int stir_shaken_verify_signature(const char *msg, const char *signature, EVP_PKEY *public_key) -{ - EVP_MD_CTX *mdctx = NULL; - int ret = 0; - unsigned char *decoded_signature; - size_t signature_length, decoded_signature_length; - - mdctx = EVP_MD_CTX_create(); - if (!mdctx) { - ast_log(LOG_ERROR, "Failed to create Message Digest Context\n"); - return -1; - } - - ret = EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, public_key); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n"); - EVP_MD_CTX_destroy(mdctx); - return -1; - } - - ret = EVP_DigestVerifyUpdate(mdctx, (unsigned char *)msg, strlen(msg)); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed to update Message Digest Context\n"); - EVP_MD_CTX_destroy(mdctx); - return -1; - } - - /* We need to decode the signature from base64 URL to bytes. Make sure we have - * at least enough characters for this check */ - signature_length = strlen(signature); - decoded_signature_length = (signature_length * 3 / 4); - decoded_signature = ast_calloc(1, decoded_signature_length); - ast_base64url_decode(decoded_signature, signature, decoded_signature_length); - - ret = EVP_DigestVerifyFinal(mdctx, decoded_signature, decoded_signature_length); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed final phase of signature verification\n"); - EVP_MD_CTX_destroy(mdctx); - ast_free(decoded_signature); - return -1; - } - - EVP_MD_CTX_destroy(mdctx); - ast_free(decoded_signature); - - return 0; -} - -/*! - * \brief CURL the file located at public_cert_url to the specified path - * - * \note filename will need to be freed by the caller - * - * \param public_cert_url The public cert URL - * \param path The path to download the file to - * \param acl The ACL to use for cURL (if not NULL) - * - * \retval NULL on failure - * \retval full path filename on success - */ -static char *run_curl(const char *public_cert_url, const char *path, const struct ast_acl_list *acl) -{ - struct curl_cb_data *data; - char *filename; - - data = curl_cb_data_create(); - if (!data) { - ast_log(LOG_ERROR, "Failed to create CURL callback data\n"); - return NULL; - } - - filename = curl_public_key(public_cert_url, path, data, acl); - if (!filename) { - ast_log(LOG_ERROR, "Could not retrieve public key for '%s'\n", public_cert_url); - curl_cb_data_free(data); - return NULL; - } - - set_public_key_expiration(public_cert_url, data); - curl_cb_data_free(data); - - return filename; -} - -/*! - * \brief Downloads the public cert from public_cert_url. If curl is non-zero, that signals - * CURL has already been run, and we should bail here. The entry is added to AstDB as well. - * - * \note filename will need to be freed by the caller - * - * \param public_cert_url The public cert URL - * \param path The path to download the file to - * \param curl Flag signaling if we have run CURL or not - * \param acl The ACL to use for cURL (if not NULL) - * - * \retval NULL on failure - * \retval full path filename on success - */ -static char *curl_and_check_expiration(const char *public_cert_url, const char *path, int *curl, const struct ast_acl_list *acl) -{ - char *filename; - - if (curl) { - ast_log(LOG_ERROR, "Already downloaded public key '%s'\n", path); - return NULL; - } - - filename = run_curl(public_cert_url, path, acl); - if (!filename) { - return NULL; - } - - if (public_key_is_expired(public_cert_url)) { - ast_log(LOG_ERROR, "Newly downloaded public key '%s' is expired\n", path); - ast_free(filename); - return NULL; - } - - *curl = 1; - add_public_key_to_astdb(public_cert_url, filename); - - return filename; -} - -/*! - * \brief Verifies that the string parameters are not empty for STIR/SHAKEN verification - * - * \retval 0 on success - * \retval 1 on failure - */ -static int stir_shaken_verify_check_empty_strings(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url) -{ - if (ast_strlen_zero(header)) { - ast_log(LOG_ERROR, "'header' is required for STIR/SHAKEN verification\n"); - return 1; - } - - if (ast_strlen_zero(payload)) { - ast_log(LOG_ERROR, "'payload' is required for STIR/SHAKEN verification\n"); - return 1; - } - - if (ast_strlen_zero(signature)) { - ast_log(LOG_ERROR, "'signature' is required for STIR/SHAKEN verification\n"); - return 1; - } - - if (ast_strlen_zero(algorithm)) { - ast_log(LOG_ERROR, "'algorithm' is required for STIR/SHAKEN verification\n"); - return 1; - } - - if (ast_strlen_zero(public_cert_url)) { - ast_log(LOG_ERROR, "'public_cert_url' is required for STIR/SHAKEN verification\n"); - return 1; - } - - return 0; -} - -/*! - * \brief Get or set up the file path for the certificate - * - * \note This function will allocate memory for file_path and dir_path and populate them - * - * \retval 0 on success - * \retval 1 on failure - */ -static int stir_shaken_verify_setup_file_paths(const char *public_cert_url, char **file_path, char **dir_path, int *curl, - const struct ast_acl_list *acl) -{ - *file_path = get_path_to_public_key(public_cert_url); - if (ast_asprintf(dir_path, "%s/keys/%s", ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME) < 0) { - return 1; - } - - /* If we don't have an entry in AstDB, CURL from the provided URL */ - if (ast_strlen_zero(*file_path)) { - /* Remove this entry from the database, since we will be - * downloading a new file anyways. - */ - remove_public_key_from_astdb(public_cert_url); - - /* Go ahead and free file_path, in case anything was allocated above */ - ast_free(*file_path); - - /* Download to the default path */ - *file_path = run_curl(public_cert_url, *dir_path, acl); - if (!(*file_path)) { - return 1; - } - - /* Signal that we have already downloaded a new file, no reason to do it again */ - *curl = 1; - - /* We should have a successful download at this point, so - * add an entry to the database. - */ - add_public_key_to_astdb(public_cert_url, *file_path); - } - - return 0; -} - -/*! - * \brief See if the cert is expired. If it is, remove it and try downloading again if we haven't already. - * - * \retval 0 on success - * \retval 1 on failure - */ -static int stir_shaken_verify_validate_cert(const char *public_cert_url, char **file_path, char *dir_path, int *curl, - EVP_PKEY **public_key, const struct ast_acl_list *acl) -{ - if (public_key_is_expired(public_cert_url)) { - - ast_debug(3, "Public cert '%s' is expired\n", public_cert_url); - - remove_public_key_from_astdb(public_cert_url); - - /* If this fails, then there's nothing we can do */ - ast_free(*file_path); - *file_path = curl_and_check_expiration(public_cert_url, dir_path, curl, acl); - if (!(*file_path)) { - return 1; - } - } - - /* First attempt to read the key. If it fails, try downloading the file, - * unless we already did. Check for expiration again */ - *public_key = stir_shaken_read_key(*file_path, 0); - if (!(*public_key)) { - - ast_debug(3, "Failed first read of public key file '%s'\n", *file_path); - - remove_public_key_from_astdb(public_cert_url); - - ast_free(*file_path); - *file_path = curl_and_check_expiration(public_cert_url, dir_path, curl, acl); - if (!(*file_path)) { - return 1; - } - - *public_key = stir_shaken_read_key(*file_path, 0); - if (!(*public_key)) { - ast_log(LOG_ERROR, "Failed to read public key from '%s'\n", *file_path); - remove_public_key_from_astdb(public_cert_url); - return 1; - } - } - - return 0; -} - -struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url) -{ - int code = 0; - - return ast_stir_shaken_verify2(header, payload, signature, algorithm, public_cert_url, &code); -} - -struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url, int *failure_code) -{ - return ast_stir_shaken_verify_with_profile(header, payload, signature, algorithm, public_cert_url, failure_code, NULL); -} - -struct ast_stir_shaken_payload *ast_stir_shaken_verify_with_profile(const char *header, const char *payload, const char *signature, - const char *algorithm, const char *public_cert_url, int *failure_code, const struct stir_shaken_profile *profile) -{ - struct ast_stir_shaken_payload *ret_payload; - EVP_PKEY *public_key; - int curl = 0; - RAII_VAR(char *, file_path, NULL, ast_free); - RAII_VAR(char *, dir_path, NULL, ast_free); - RAII_VAR(char *, combined_str, NULL, ast_free); - size_t combined_size; - const struct ast_acl_list *acl; - - if (stir_shaken_verify_check_empty_strings(header, payload, signature, algorithm, public_cert_url)) { - return NULL; - } - - acl = profile ? (const struct ast_acl_list *)profile->acl : NULL; - - /* Check to see if we have already downloaded this public cert. The reason we - * store the file path is because: - * - * 1. If, for some reason, the default directory changes, we still know where - * to look for the files we already have. - * - * 2. In the future, if we want to add a way to store the certs in multiple - * {configurable) directories, we already have the storage mechanism in place. - * The only thing that would be left to do is pull from the configuration. - */ - if (stir_shaken_verify_setup_file_paths(public_cert_url, &file_path, &dir_path, &curl, acl)) { - return NULL; - } - - /* Check to see if the cert we downloaded (or already had) is expired */ - if (stir_shaken_verify_validate_cert(public_cert_url, &file_path, dir_path, &curl, &public_key, acl)) { - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT; - return NULL; - } - - /* Combine the header and payload to get the original signed message: header.payload */ - combined_size = strlen(header) + strlen(payload) + 2; - combined_str = ast_calloc(1, combined_size); - if (!combined_str) { - ast_log(LOG_ERROR, "Failed to allocate space for message to verify\n"); - EVP_PKEY_free(public_key); - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; - return NULL; - } - snprintf(combined_str, combined_size, "%s.%s", header, payload); - if (stir_shaken_verify_signature(combined_str, signature, public_key)) { - ast_log(LOG_ERROR, "Failed to verify signature\n"); - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION; - EVP_PKEY_free(public_key); - return NULL; - } - - /* We don't need the public key anymore */ - EVP_PKEY_free(public_key); - - ret_payload = ast_calloc(1, sizeof(*ret_payload)); - if (!ret_payload) { - ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n"); - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; - return NULL; - } - - ret_payload->header = ast_json_load_string(header, NULL); - if (!ret_payload->header) { - ast_log(LOG_ERROR, "Failed to create JSON from header\n"); - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; - ast_stir_shaken_payload_free(ret_payload); - return NULL; - } - - ret_payload->payload = ast_json_load_string(payload, NULL); - if (!ret_payload->payload) { - ast_log(LOG_ERROR, "Failed to create JSON from payload\n"); - *failure_code = AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC; - ast_stir_shaken_payload_free(ret_payload); - return NULL; - } - - ret_payload->signature = (unsigned char *)ast_strdup(signature); - ret_payload->algorithm = ast_strdup(algorithm); - ret_payload->public_cert_url = ast_strdup(public_cert_url); - - return ret_payload; -} - -/*! - * \brief Verifies the necessary contents are in the JSON and returns a - * ast_stir_shaken_payload with the extracted values. - * - * \param json The JSON to verify - * - * \return ast_stir_shaken_payload on success - * \return NULL on failure - */ -static struct ast_stir_shaken_payload *stir_shaken_verify_json(struct ast_json *json) -{ - struct ast_stir_shaken_payload *payload; - struct ast_json *obj; - const char *val; - - payload = ast_calloc(1, sizeof(*payload)); - if (!payload) { - ast_log(LOG_ERROR, "Failed to allocate STIR/SHAKEN payload\n"); - goto cleanup; - } - - /* Look through the header first */ - obj = ast_json_object_get(json, "header"); - if (!obj) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'header'\n"); - goto cleanup; - } - - payload->header = ast_json_deep_copy(obj); - if (!payload->header) { - ast_log(LOG_ERROR, "STIR_SHAKEN payload failed to copy 'header'\n"); - goto cleanup; - } - - /* Check the ppt value for "shaken" */ - val = ast_json_string_get(ast_json_object_get(obj, "ppt")); - if (ast_strlen_zero(val)) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'ppt'\n"); - goto cleanup; - } - if (strcmp(val, STIR_SHAKEN_PPT)) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'ppt' did not have " - "required value '%s' (was '%s')\n", STIR_SHAKEN_PPT, val); - goto cleanup; - } - - /* Check the typ value for "passport" */ - val = ast_json_string_get(ast_json_object_get(obj, "typ")); - if (ast_strlen_zero(val)) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have the required field 'typ'\n"); - goto cleanup; - } - if (strcmp(val, STIR_SHAKEN_TYPE)) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT field 'typ' did not have " - "required value '%s' (was '%s')\n", STIR_SHAKEN_TYPE, val); - goto cleanup; - } - - /* Check to see if there is a value for alg */ - val = ast_json_string_get(ast_json_object_get(obj, "alg")); - if (!ast_strlen_zero(val) && strcmp(val, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { - /* If alg is not present that's fine; if it is and is not ES256, cleanup */ - ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have supported type for field 'alg' (was %s)\n", val); - goto cleanup; - } - - payload->algorithm = ast_strdup(val); - if (!payload->algorithm) { - ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'algorithm'\n"); - goto cleanup; - } - - /* Now let's check the payload section */ - obj = ast_json_object_get(json, "payload"); - if (!obj) { - ast_log(LOG_ERROR, "STIR/SHAKEN payload JWT did not have required field 'payload'\n"); - goto cleanup; - } - - /* Check the orig tn value for not NULL */ - val = ast_json_string_get(ast_json_object_get(ast_json_object_get(obj, "orig"), "tn")); - if (ast_strlen_zero(val)) { - ast_log(LOG_ERROR, "STIR/SHAKEN JWT did not have required field 'orig->tn'\n"); - goto cleanup; - } - - /* Payload seems sane. Copy it and return on success */ - payload->payload = ast_json_deep_copy(obj); - if (!payload->payload) { - ast_log(LOG_ERROR, "STIR/SHAKEN payload failed to copy 'payload'\n"); - goto cleanup; - } - - return payload; - -cleanup: - ast_stir_shaken_payload_free(payload); - return NULL; -} - -/*! - * \brief Signs the payload and returns the signature. - * - * \param json_str The string representation of the JSON - * \param private_key The private key used to sign the payload - * - * \retval signature on success - * \retval NULL on failure - */ -static unsigned char *stir_shaken_sign(char *json_str, EVP_PKEY *private_key) -{ - EVP_MD_CTX *mdctx = NULL; - int ret = 0; - unsigned char *encoded_signature = NULL; - unsigned char *signature = NULL; - size_t encoded_length = 0; - size_t signature_length = 0; - - mdctx = EVP_MD_CTX_create(); - if (!mdctx) { - ast_log(LOG_ERROR, "Failed to create Message Digest Context\n"); - goto cleanup; - } - - ret = EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, private_key); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed to initialize Message Digest Context\n"); - goto cleanup; - } - - ret = EVP_DigestSignUpdate(mdctx, json_str, strlen(json_str)); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed to update Message Digest Context\n"); - goto cleanup; - } - - ret = EVP_DigestSignFinal(mdctx, NULL, &signature_length); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed initial phase of Message Digest Context signing\n"); - goto cleanup; - } - - signature = ast_calloc(1, sizeof(unsigned char) * signature_length); - if (!signature) { - ast_log(LOG_ERROR, "Failed to allocate space for signature\n"); - goto cleanup; - } - - ret = EVP_DigestSignFinal(mdctx, signature, &signature_length); - if (ret != 1) { - ast_log(LOG_ERROR, "Failed final phase of Message Digest Context signing\n"); - goto cleanup; - } - - /* There are 6 bits to 1 base64 URL digit, so in order to get the size of the base64 encoded - * signature, we need to multiply by the number of bits in a byte and divide by 6. Since - * there's rounding when doing base64 conversions, add 3 bytes, just in case, and account - * for padding. Add another byte for the NULL-terminator. - */ - encoded_length = ((signature_length * 4 / 3 + 3) & ~3) + 1; - encoded_signature = ast_calloc(1, encoded_length); - if (!encoded_signature) { - ast_log(LOG_ERROR, "Failed to allocate space for encoded signature\n"); - goto cleanup; - } - - ast_base64url_encode((char *)encoded_signature, signature, signature_length, encoded_length); - -cleanup: - if (mdctx) { - EVP_MD_CTX_destroy(mdctx); - } - ast_free(signature); - - return encoded_signature; -} - -/*! - * \brief Adds the 'x5u' (public key URL) field to the JWT. - * - * \param json The JWT - * \param x5u The public key URL - * - * \retval 0 on success - * \retval -1 on failure - */ -static int stir_shaken_add_x5u(struct ast_json *json, const char *x5u) -{ - struct ast_json *value; - - value = ast_json_string_create(x5u); - if (!value) { - return -1; - } - - return ast_json_object_set(ast_json_object_get(json, "header"), "x5u", value); -} - -/*! - * \brief Adds the 'attest' field to the JWT. - * - * \param json The JWT - * \param attest The value to set attest to - * - * \retval 0 on success - * \retval -1 on failure - */ -static int stir_shaken_add_attest(struct ast_json *json, const char *attest) -{ - struct ast_json *value; - - value = ast_json_string_create(attest); - if (!value) { - return -1; - } - - return ast_json_object_set(ast_json_object_get(json, "payload"), "attest", value); -} - -/*! - * \brief Adds the 'origid' field to the JWT. - * - * \param json The JWT - * - * \retval 0 on success - * \retval -1 on failure - */ -static int stir_shaken_add_origid(struct ast_json *json) -{ - struct ast_json *value; - char uuid_str[AST_UUID_STR_LEN]; - - ast_uuid_generate_str(uuid_str, sizeof(uuid_str)); - if (strlen(uuid_str) != (AST_UUID_STR_LEN - 1)) { - return -1; - } - - value = ast_json_string_create(uuid_str); - - return ast_json_object_set(ast_json_object_get(json, "payload"), "origid", value); -} - -/*! - * \brief Adds the 'iat' field to the JWT. - * - * \param json The JWT - * - * \retval 0 on success - * \retval -1 on failure - */ -static int stir_shaken_add_iat(struct ast_json *json) -{ - struct ast_json *value; - struct timeval tv; - int timestamp; - - tv = ast_tvnow(); - timestamp = tv.tv_sec + tv.tv_usec / 1000; - value = ast_json_integer_create(timestamp); - - return ast_json_object_set(ast_json_object_get(json, "payload"), "iat", value); -} - -struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json) -{ - struct ast_stir_shaken_payload *ss_payload; - unsigned char *signature; - const char *public_cert_url; - const char *caller_id_num; - const char *header; - const char *payload; - struct ast_json *tmp_json; - char *msg = NULL; - size_t msg_len; - struct stir_shaken_certificate *cert = NULL; - - ss_payload = stir_shaken_verify_json(json); - if (!ss_payload) { - return NULL; - } - - /* From the payload section of the JSON, get the orig section, and then get - * the value of tn. This will be the caller ID number */ - caller_id_num = ast_json_string_get(ast_json_object_get(ast_json_object_get( - ast_json_object_get(json, "payload"), "orig"), "tn")); - if (!caller_id_num) { - ast_log(LOG_ERROR, "Failed to get caller ID number from JWT\n"); - goto cleanup; - } - - cert = stir_shaken_certificate_get_by_caller_id_number(caller_id_num); - if (!cert) { - ast_log(LOG_ERROR, "Failed to retrieve certificate for caller ID " - "'%s'\n", caller_id_num); - goto cleanup; - } - - public_cert_url = stir_shaken_certificate_get_public_cert_url(cert); - if (stir_shaken_add_x5u(json, public_cert_url)) { - ast_log(LOG_ERROR, "Failed to add 'x5u' (public cert URL) to payload\n"); - goto cleanup; - } - ss_payload->public_cert_url = ast_strdup(public_cert_url); - - if (stir_shaken_add_attest(json, stir_shaken_certificate_get_attestation(cert))) { - ast_log(LOG_ERROR, "Failed to add 'attest' to payload\n"); - goto cleanup; - } - - if (stir_shaken_add_origid(json)) { - ast_log(LOG_ERROR, "Failed to add 'origid' to payload\n"); - goto cleanup; - } - - if (stir_shaken_add_iat(json)) { - ast_log(LOG_ERROR, "Failed to add 'iat' to payload\n"); - goto cleanup; - } - - /* Get the header and the payload. Combine them to get the message to sign */ - tmp_json = ast_json_object_get(json, "header"); - header = ast_json_dump_string(tmp_json); - tmp_json = ast_json_object_get(json, "payload"); - - payload = ast_json_dump_string_sorted(tmp_json); - msg_len = strlen(header) + strlen(payload) + 2; - msg = ast_calloc(1, msg_len); - if (!msg) { - ast_log(LOG_ERROR, "Failed to allocate space for message to sign\n"); - goto cleanup; - } - snprintf(msg, msg_len, "%s.%s", header, payload); - - signature = stir_shaken_sign(msg, stir_shaken_certificate_get_private_key(cert)); - if (!signature) { - goto cleanup; - } - - ss_payload->signature = signature; - ao2_cleanup(cert); - ast_free(msg); - - return ss_payload; - -cleanup: - ao2_cleanup(cert); - ast_stir_shaken_payload_free(ss_payload); - ast_free(msg); - return NULL; -} - /*! * \brief Retrieves STIR/SHAKEN verification information for the channel via dialplan. * Examples: @@ -1268,11 +166,11 @@ cleanup: * \retval -1 on failure * \retval 0 on success */ -static int stir_shaken_read(struct ast_channel *chan, const char *function, +static int func_read(struct ast_channel *chan, const char *function, char *data, char *buf, size_t len) { - struct stir_shaken_datastore *ss_datastore; - struct ast_datastore *datastore; + struct stir_datastore *stir_datastore; + struct ast_datastore *chan_datastore; char *parse; char *first; char *second; @@ -1306,7 +204,6 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function, /* Check if we are only looking for the number of STIR/SHAKEN verification results */ if (!strcasecmp(first, "count")) { - size_t count = 0; if (!ast_strlen_zero(second)) { @@ -1315,8 +212,8 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function, } ast_channel_lock(chan); - AST_LIST_TRAVERSE(ast_channel_datastores(chan), datastore, entry) { - if (datastore->info != &stir_shaken_datastore_info) { + AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) { + if (chan_datastore->info != &stir_shaken_datastore_info) { continue; } count++; @@ -1344,8 +241,8 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function, /* We don't store by uid for the datastore, so just search for the specified index */ ast_channel_lock(chan); - AST_LIST_TRAVERSE(ast_channel_datastores(chan), datastore, entry) { - if (datastore->info != &stir_shaken_datastore_info) { + AST_LIST_TRAVERSE(ast_channel_datastores(chan), chan_datastore, entry) { + if (chan_datastore->info != &stir_shaken_datastore_info) { continue; } @@ -1356,18 +253,18 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function, current_index++; } ast_channel_unlock(chan); - if (current_index != target_index || !datastore) { + if (current_index != target_index || !chan_datastore) { ast_log(LOG_WARNING, "No STIR/SHAKEN results for index '%s'\n", first); return -1; } - ss_datastore = datastore->data; + stir_datastore = chan_datastore->data; if (!strcasecmp(second, "identity")) { - ast_copy_string(buf, ss_datastore->identity, len); + ast_copy_string(buf, stir_datastore->identity, len); } else if (!strcasecmp(second, "attestation")) { - ast_copy_string(buf, ss_datastore->attestation, len); + ast_copy_string(buf, stir_datastore->attestation, len); } else if (!strcasecmp(second, "verify_result")) { - ast_copy_string(buf, stir_shaken_verification_result_to_string(ss_datastore->verify_result), len); + ast_copy_string(buf, vs_response_code_to_str(stir_datastore->verify_result), len); } else { ast_log(LOG_ERROR, "No such value '%s' for %s\n", second, function); return -1; @@ -1378,424 +275,93 @@ static int stir_shaken_read(struct ast_channel *chan, const char *function, static struct ast_custom_function stir_shaken_function = { .name = "STIR_SHAKEN", - .read = stir_shaken_read, + .read = func_read, }; -#ifdef TEST_FRAMEWORK - -static void test_stir_shaken_add_fake_astdb_entry(const char *public_cert_url, const char *file_path) -{ - struct timeval expires = ast_tvnow(); - char time_buf[32]; - char hash[41]; - - ast_sha1_hash(hash, public_cert_url); - add_public_key_to_astdb(public_cert_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" - "MHcCAQEEIC+xv2GKNTDd81vJM8rwGAGNqgklKKxz9Qejn+pcRPC1oAoGCCqGSM49\n" - "AwEHoUQDQgAEq12QXu8lH295ZMZ4udKy5VV8wVgE4qSOnkdofn3hEDsh6QTKTZg9\n" - "W6PncYAVnmOFRL4cTGRbmAIShN4naZk2Yg==\n" - "-----END EC PRIVATE KEY-----"; - char *public_data = - "-----BEGIN CERTIFICATE-----\n" - "MIIBzDCCAXGgAwIBAgIUXDt6EC0OixT1iRSSPV3jB/zQAlQwCgYIKoZIzj0EAwIw\n" - "RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\n" - "dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA0MTMwNjM3MjRaFw0yMzA3MTcw\n" - "NjM3MjRaMGoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTESMBAGA1UEBwwJU29t\n" - "ZXdoZXJlMRowGAYDVQQKDBFBY21lVGVsZWNvbSwgSW5jLjENMAsGA1UECwwEVk9J\n" - "UDEPMA0GA1UEAwwGU0hBS0VOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq12Q\n" - "Xu8lH295ZMZ4udKy5VV8wVgE4qSOnkdofn3hEDsh6QTKTZg9W6PncYAVnmOFRL4c\n" - "TGRbmAIShN4naZk2YqMaMBgwFgYIKwYBBQUHARoECjAIoAYWBDEwMDEwCgYIKoZI\n" - "zj0EAwIDSQAwRgIhAMa9Ky38DgVaIgVm9Mgws/qN3zxjMQXfxEExAbDwyq/WAiEA\n" - "zbC29mvtSulwbvQJ4fBdFU84cFC3Ctu1QrCeFOiZHc4=\n" - "-----END CERTIFICATE-----"; - - 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)); - close(fd); - 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_cert_url = "http://testing123"; - char *header; - char *payload; - struct ast_json *tmp_json; - 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(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_cert_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 header and payload for ast_stir_shaken_verify */ - tmp_json = ast_json_object_get(json, "header"); - header = ast_json_dump_string(tmp_json); - tmp_json = ast_json_object_get(json, "payload"); - payload = ast_json_dump_string_sorted(tmp_json); - - /* Test empty header parameter */ - returned_payload = ast_stir_shaken_verify("", payload, (const char *)signed_payload->signature, - STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_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_cert_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, payload, "", - STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_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, payload, (const char *)signed_payload->signature, - "", public_cert_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, payload, (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_cert_url, public_path); - - /* Verify a valid signature */ - returned_payload = ast_stir_shaken_verify(header, payload, (const char *)signed_payload->signature, - STIR_SHAKEN_ENCRYPTION_ALGORITHM, public_cert_url); - if (!returned_payload) { - ast_test_status_update(test, "Failed to verify a valid signature\n"); - remove_public_key_from_astdb(public_cert_url); - test_stir_shaken_cleanup_cert(caller_id_number); - return AST_TEST_FAIL; - } - - remove_public_key_from_astdb(public_cert_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) { - ast_sorcery_reload(stir_shaken_sorcery); - } - - return 0; + return common_config_reload(); } static int unload_module(void) { int res = 0; - stir_shaken_profile_unload(); - stir_shaken_certificate_unload(); - stir_shaken_store_unload(); - stir_shaken_general_unload(); - - ast_sorcery_unref(stir_shaken_sorcery); - stir_shaken_sorcery = NULL; + common_config_unload(); + crypto_unload(); res |= ast_custom_function_unregister(&stir_shaken_function); - AST_TEST_UNREGISTER(test_stir_shaken_sign); - AST_TEST_UNREGISTER(test_stir_shaken_verify); + return 0; +} - return res; +#define TN_AUTH_LIST_OID "1.3.6.1.5.5.7.1.26" +#define TN_AUTH_LIST_SHORT "TNAuthList" +#define TN_AUTH_LIST_LONG "TNAuthorizationList" + +static int check_for_old_config(void) +{ + const char *error_msg = "There appears to be a 'stir_shaken.conf' file" + " with old configuration options in it. Please see the new config" + " file format in the configs/samples/stir_shaken.conf.sample file" + " in the source tree at https://github.com/asterisk/asterisk/raw/master/configs/samples/stir_shaken.conf.sample" + " or visit https://docs.asterisk.org/Deployment/STIR-SHAKEN for more information."; + RAII_VAR(struct ast_config *, cfg, NULL, ast_config_destroy); + struct ast_flags config_flags = { 0 }; + char *cat = NULL; + + cfg = ast_config_load("stir_shaken.conf", config_flags); + if (cfg == NULL) { + /* + * They may be loading from realtime so the fact that there's + * no stir-shaken.conf file isn't an issue for this purpose. + */ + return AST_MODULE_LOAD_DECLINE; + } + while ((cat = ast_category_browse(cfg, cat))) { + const char *val; + if (strcasecmp(cat, "general") == 0) { + ast_log(LOG_ERROR, "%s\n", error_msg); + return AST_MODULE_LOAD_DECLINE; + } + val = ast_variable_retrieve(cfg, cat, "type"); + if (val && (strcasecmp(val, "store") == 0 || + strcasecmp(val, "certificate") == 0)) { + ast_log(LOG_ERROR, "%s\n", error_msg); + return AST_MODULE_LOAD_DECLINE; + } + } + + return AST_MODULE_LOAD_SUCCESS; } static int load_module(void) { int res = 0; - if (!(stir_shaken_sorcery = ast_sorcery_open())) { - ast_log(LOG_ERROR, "stir/shaken - failed to open sorcery\n"); + if (check_for_old_config()) { return AST_MODULE_LOAD_DECLINE; } - if (stir_shaken_general_load()) { + if (crypto_load()) { unload_module(); return AST_MODULE_LOAD_DECLINE; } - if (stir_shaken_store_load()) { + tn_auth_list_nid = crypto_register_x509_extension(TN_AUTH_LIST_OID, + TN_AUTH_LIST_SHORT, TN_AUTH_LIST_LONG); + if (tn_auth_list_nid < 0) { unload_module(); return AST_MODULE_LOAD_DECLINE; } - if (stir_shaken_certificate_load()) { + if (common_config_load()) { unload_module(); return AST_MODULE_LOAD_DECLINE; } - if (stir_shaken_profile_load()) { - unload_module(); - return AST_MODULE_LOAD_DECLINE; - } - - ast_sorcery_load(ast_stir_shaken_sorcery()); - 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/attestation.c b/res/res_stir_shaken/attestation.c new file mode 100644 index 0000000000..0583fdbaa0 --- /dev/null +++ b/res/res_stir_shaken/attestation.c @@ -0,0 +1,443 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include + +#define _TRACE_PREFIX_ "a",__LINE__, "" + +#include "asterisk.h" +#include "asterisk/module.h" +#include "asterisk/uuid.h" +#include "asterisk/json.h" +#include "asterisk/channel.h" + +#include "stir_shaken.h" + +static const char *as_rc_map[] = { + [AST_STIR_SHAKEN_AS_SUCCESS] = "success", + [AST_STIR_SHAKEN_AS_DISABLED] = "disabled", + [AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS] = "invalid_arguments", + [AST_STIR_SHAKEN_AS_MISSING_PARAMETERS] = "missing_parameters", + [AST_STIR_SHAKEN_AS_INTERNAL_ERROR] = "internal_error", + [AST_STIR_SHAKEN_AS_NO_TN_FOR_CALLERID] = "no_tn_for_callerid", + [AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL] = "no_private_key_avail", + [AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL] = "no_public_cert_url_avail", + [AST_STIR_SHAKEN_AS_NO_ATTEST_LEVEL] = "no_attest_level", + [AST_STIR_SHAKEN_AS_IDENTITY_HDR_EXISTS] = "identity_header_exists", + [AST_STIR_SHAKEN_AS_NO_TO_HDR] = "no_to_hdr", + [AST_STIR_SHAKEN_AS_TO_HDR_BAD_URI] = "to_hdr_bad_uri", + [AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE] "sign_encode_failure", +}; + +const char *as_response_code_to_str( + enum ast_stir_shaken_as_response_code as_rc) +{ + return ARRAY_IN_BOUNDS(as_rc, as_rc_map) ? + as_rc_map[as_rc] : NULL; +} + +static void ctx_destructor(void *obj) +{ + struct ast_stir_shaken_as_ctx *ctx = obj; + + ao2_cleanup(ctx->etn); + ast_channel_cleanup(ctx->chan); + ast_string_field_free_memory(ctx); + AST_VECTOR_RESET(&ctx->fingerprints, ast_free); + AST_VECTOR_FREE(&ctx->fingerprints); +} + +enum ast_stir_shaken_as_response_code + ast_stir_shaken_as_ctx_create(const char *orig_tn, + const char *dest_tn, struct ast_channel *chan, + const char *profile_name, + const char *tag, struct ast_stir_shaken_as_ctx **ctxout) +{ + RAII_VAR(struct ast_stir_shaken_as_ctx *, ctx, NULL, ao2_cleanup); + RAII_VAR(struct profile_cfg *, eprofile, NULL, ao2_cleanup); + RAII_VAR(struct attestation_cfg *, as_cfg, NULL, ao2_cleanup); + RAII_VAR(struct tn_cfg *, etn, NULL, ao2_cleanup); + SCOPE_ENTER(3, "%s: Enter\n", tag); + + if (ast_strlen_zero(orig_tn)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide caller_id/orig_tn\n", tag); + } + + if (ast_strlen_zero(dest_tn)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide dest_tn\n", tag); + } + + if (ast_strlen_zero(tag)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide tag\n", tag); + } + + if (!ctxout) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide ctxout\n", tag); + } + + if (ast_strlen_zero(profile_name)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + "%s: Disabled due to missing profile name\n", tag); + } + + as_cfg = as_get_cfg(); + if (as_cfg->global_disable) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + "%s: Globally disabled\n", tag); + } + + eprofile = eprofile_get_cfg(profile_name); + if (!eprofile) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + LOG_ERROR, "%s: No profile for profile name '%s'. Call will continue\n", tag, + profile_name); + } + + if (!PROFILE_ALLOW_ATTEST(eprofile)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + "%s: Disabled by profile\n", tag); + } + + etn = tn_get_etn(orig_tn, eprofile); + if (!etn) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + "%s: No tn for orig_tn '%s'\n", tag, orig_tn); + } + + /* We don't need eprofile or as_cfg anymore so let's clean em up */ + ao2_cleanup(as_cfg); + as_cfg = NULL; + ao2_cleanup(eprofile); + eprofile = NULL; + + + if (etn->acfg_common.attest_level == attest_level_NOT_SET) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_MISSING_PARAMETERS, + LOG_ERROR, + "'%s': No attest_level specified in tn, profile or attestation objects\n", + tag); + } + + if (ast_strlen_zero(etn->acfg_common.public_cert_url)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL, + LOG_ERROR, "%s: No public cert url in tn %s, profile or attestation objects\n", + tag, orig_tn); + } + + if (etn->acfg_common.raw_key_length == 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL, + LOG_ERROR, "%s: No private key in tn %s, profile or attestation objects\n", + orig_tn, tag); + } + + ctx = ao2_alloc_options(sizeof(*ctx), ctx_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!ctx) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + if (ast_string_field_init(ctx, 1024) != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + if (ast_string_field_set(ctx, tag, tag) != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + if (ast_string_field_set(ctx, orig_tn, orig_tn) != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + if (ast_string_field_set(ctx, dest_tn, dest_tn)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + ctx->chan = chan; + ast_channel_ref(ctx->chan); + + if (AST_VECTOR_INIT(&ctx->fingerprints, 1) != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for ctx\n", tag); + } + + /* Transfer the references */ + ctx->etn = etn; + etn = NULL; + *ctxout = ctx; + ctx = NULL; + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "%s: Done\n", tag); +} + +int ast_stir_shaken_as_ctx_wants_fingerprints(struct ast_stir_shaken_as_ctx *ctx) +{ + return ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky); +} + +enum ast_stir_shaken_as_response_code + ast_stir_shaken_as_ctx_add_fingerprint( + struct ast_stir_shaken_as_ctx *ctx, const char *alg, const char *fingerprint) +{ + char *compacted_fp = ast_alloca(strlen(fingerprint) + 1); + const char *f = fingerprint; + char *fp = compacted_fp; + char *combined; + int rc; + SCOPE_ENTER(4, "%s: Add fingerprint %s:%s\n", ctx ? ctx->tag : "", + alg, fingerprint); + + if (!ctx || ast_strlen_zero(alg) || ast_strlen_zero(fingerprint)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + "%s: Missing arguments\n", ctx->tag); + } + + if (!ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_DISABLED, + "%s: Not needed\n", ctx->tag); + } + + /* De-colonize */ + while (*f != '\0') { + if (*f != ':') { + *fp++ = *f; + } + f++; + } + *fp = '\0'; + rc = ast_asprintf(&combined, "%s:%s", alg, compacted_fp); + if (rc < 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + "%s: Can't allocate memory for comobined string\n", ctx->tag); + } + + rc = AST_VECTOR_ADD_SORTED(&ctx->fingerprints, combined, strcasecmp); + if (rc < 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + "%s: Can't add entry to vector\n", ctx->tag); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, + "%s: Done\n", ctx->tag); +} + +/* + * We have to construct the PASSporT payload manually instead of + * using ast_json_pack. These macros help make sure nothing + * leaks if there are errors creating the individual objects. + */ +#define CREATE_JSON_SET_OBJ(__val, __obj, __name) \ +({ \ + struct ast_json *__var; \ + if (!(__var = __val)) {\ + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \ + LOG_ERROR, "%s: Cannot allocate one of the JSON objects\n", \ + ctx->tag); \ + } else { \ + if (ast_json_object_set(__obj, __name, __var)) { \ + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \ + LOG_ERROR, "%s: Cannot set one of the JSON objects\n", \ + ctx->tag); \ + } \ + } \ + (__var); \ +}) + +#define CREATE_JSON_APPEND_ARRAY(__val, __obj) \ +({ \ + struct ast_json *__var; \ + if (!(__var = __val)) {\ + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \ + LOG_ERROR, "%s: Cannot allocate one of the JSON objects\n", \ + ctx->tag); \ + } else { \ + if (ast_json_array_append(__obj, __var)) { \ + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, \ + LOG_ERROR, "%s: Cannot set one of the JSON objects\n", \ + ctx->tag); \ + } \ + } \ + (__var); \ +}) + +static enum ast_stir_shaken_as_response_code pack_payload( + struct ast_stir_shaken_as_ctx *ctx, jwt_t *jwt) +{ + RAII_VAR(struct ast_json *, payload, ast_json_object_create(), ast_json_unref); + /* + * These don't need RAII because once they're added to payload, + * they'll get destroyed when payload gets unreffed. + */ + struct ast_json *dest; + struct ast_json *tns; + struct ast_json *orig; + char origid[AST_UUID_STR_LEN]; + char *payload_str = NULL; + SCOPE_ENTER(3, "%s: Enter\n", ctx->tag); + + /* + * All fields added need to be in alphabetical order + * and there must be no whitespace in the result. + * + * We can't use ast_json_pack here because the entries + * need to be kept in order and the "mky" array may + * not be present. + */ + + /* + * The order of the calls matters. We want to add an object + * to its parent as soon as it's created, then add things + * to it. This way if something later fails, the whole thing + * will get destroyed when its parent gets destroyed. + */ + CREATE_JSON_SET_OBJ(ast_json_string_create( + attest_level_to_str(ctx->etn->acfg_common.attest_level)), + payload, "attest"); + + dest = CREATE_JSON_SET_OBJ(ast_json_object_create(), payload, "dest"); + tns = CREATE_JSON_SET_OBJ(ast_json_array_create(), dest, "tn"); + CREATE_JSON_APPEND_ARRAY(ast_json_string_create(ctx->dest_tn), tns); + + CREATE_JSON_SET_OBJ(ast_json_integer_create(time(NULL)), payload, "iat"); + + if (AST_VECTOR_SIZE(&ctx->fingerprints) + && ENUM_BOOL(ctx->etn->acfg_common.send_mky, send_mky)) { + struct ast_json *mky; + int i; + + mky = CREATE_JSON_SET_OBJ(ast_json_array_create(), payload, "mky"); + + for (i = 0; i < AST_VECTOR_SIZE(&ctx->fingerprints); i++) { + struct ast_json *mk; + char *afp = AST_VECTOR_GET(&ctx->fingerprints, i); + char *fp = strchr(afp, ':'); + *fp++ = '\0'; + + mk = CREATE_JSON_APPEND_ARRAY(ast_json_object_create(), mky); + CREATE_JSON_SET_OBJ(ast_json_string_create(afp), mk, "alg"); + CREATE_JSON_SET_OBJ(ast_json_string_create(fp), mk, "dig"); + } + } + + orig = CREATE_JSON_SET_OBJ(ast_json_object_create(), payload, "orig"); + CREATE_JSON_SET_OBJ(ast_json_string_create(ctx->orig_tn), orig, "tn"); + + ast_uuid_generate_str(origid, sizeof(origid)); + CREATE_JSON_SET_OBJ(ast_json_string_create(origid), payload, "origid"); + + payload_str = ast_json_dump_string_format(payload, AST_JSON_COMPACT); + ast_trace(2, "Payload: %s\n", payload_str); + jwt_add_grants_json(jwt, payload_str); + ast_json_free(payload_str); + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "Done\n"); + +} + +enum ast_stir_shaken_as_response_code ast_stir_shaken_attest( + struct ast_stir_shaken_as_ctx *ctx, char **header) +{ + RAII_VAR(jwt_t *, jwt, NULL, jwt_free); + jwt_alg_t alg; + char *encoded = NULL; + enum ast_stir_shaken_as_response_code as_rc; + int rc = 0; + SCOPE_ENTER(3, "%s: Attestation: orig: %s dest: %s\n", + ctx ? ctx->tag : "NULL", ctx ? ctx->orig_tn : "NULL", + ctx ? ctx->dest_tn : "NULL"); + + if (!ctx) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR, + "%s: No context object!\n", "NULL"); + } + + if (header == NULL) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Header buffer was NULL\n", ctx->tag); + } + + rc = jwt_new(&jwt); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Cannot create JWT\n", ctx->tag); + } + + /* + * All headers added need to be in alphabetical order! + */ + alg = jwt_str_alg(STIR_SHAKEN_ENCRYPTION_ALGORITHM); + jwt_set_alg(jwt, alg, (const unsigned char *)ctx->etn->acfg_common.raw_key, + ctx->etn->acfg_common.raw_key_length); + jwt_add_header(jwt, "ppt", STIR_SHAKEN_PPT); + jwt_add_header(jwt, "typ", STIR_SHAKEN_TYPE); + jwt_add_header(jwt, "x5u", ctx->etn->acfg_common.public_cert_url); + + as_rc = pack_payload(ctx, jwt); + if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) { + SCOPE_EXIT_LOG_RTN_VALUE(as_rc, + LOG_ERROR, "%s: Cannot pack payload\n", ctx->tag); + } + + encoded = jwt_encode_str(jwt); + if (!encoded) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE, + LOG_ERROR, "%s: Unable to sign/encode JWT\n", ctx->tag); + } + + rc = ast_asprintf(header, "%s;info=<%s>;alg=%s;ppt=%s", + encoded, ctx->etn->acfg_common.public_cert_url, jwt_alg_str(alg), + STIR_SHAKEN_PPT); + ast_std_free(encoded); + if (rc < 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_AS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for identity header\n", + ctx->tag); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_AS_SUCCESS, "%s: Done\n", ctx->tag); +} + +int as_reload() +{ + as_config_reload(); + + return 0; +} + +int as_unload() +{ + as_config_unload(); + return 0; +} + +int as_load() +{ + if (as_config_load()) { + return AST_MODULE_LOAD_DECLINE; + } + + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_stir_shaken/attestation.h b/res/res_stir_shaken/attestation.h new file mode 100644 index 0000000000..53cfaf27c9 --- /dev/null +++ b/res/res_stir_shaken/attestation.h @@ -0,0 +1,59 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef ATTESTATION_H_ +#define ATTESTATION_H_ + +#include "common_config.h" + +struct ast_stir_shaken_as_ctx { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(tag); + AST_STRING_FIELD(orig_tn); + AST_STRING_FIELD(dest_tn); + ); + struct ast_channel *chan; + struct ast_vector_string fingerprints; + struct tn_cfg *etn; +}; + +/*! + * \brief Load the stir/shaken attestation service + * + * \retval 0 on success + * \retval -1 on error + */ +int as_load(void); + +/*! + * \brief Load the stir/shaken attestation service + * + * \retval 0 on success + * \retval -1 on error + */ +int as_reload(void); + +/*! + * \brief Load the stir/shaken attestation service + * + * \retval 0 on success + * \retval -1 on error + */ +int as_unload(void); + +#endif /* ATTESTATION_H_ */ diff --git a/res/res_stir_shaken/attestation_config.c b/res/res_stir_shaken/attestation_config.c new file mode 100644 index 0000000000..7c74fe3cd4 --- /dev/null +++ b/res/res_stir_shaken/attestation_config.c @@ -0,0 +1,326 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ +#include "asterisk.h" + +#include "asterisk/cli.h" +#include "asterisk/sorcery.h" +#include "asterisk/paths.h" + +#include "stir_shaken.h" + +#define CONFIG_TYPE "attestation" + +#define DEFAULT_global_disable 0 + +#define DEFAULT_check_tn_cert_public_url check_tn_cert_public_url_NO +#define DEFAULT_private_key_file NULL +#define DEFAULT_public_cert_url NULL +#define DEFAULT_attest_level attest_level_NOT_SET +#define DEFAULT_send_mky send_mky_NO + +static struct attestation_cfg *empty_cfg = NULL; + +struct attestation_cfg *as_get_cfg(void) +{ + struct attestation_cfg *cfg = ast_sorcery_retrieve_by_id(get_sorcery(), + CONFIG_TYPE, CONFIG_TYPE); + if (cfg) { + return cfg; + } + + return empty_cfg ? ao2_bump(empty_cfg) : NULL; +} + +int as_is_config_loaded(void) +{ + struct attestation_cfg *cfg = ast_sorcery_retrieve_by_id(get_sorcery(), + CONFIG_TYPE, CONFIG_TYPE); + ao2_cleanup(cfg); + + return !!cfg; +} + +generate_acfg_common_sorcery_handlers(attestation_cfg); + +void acfg_cleanup(struct attestation_cfg_common *acfg_common) +{ + if (!acfg_common) { + return; + } + ast_string_field_free_memory(acfg_common); + ao2_cleanup(acfg_common->raw_key); +} + +static void attestation_destructor(void *obj) +{ + struct attestation_cfg *cfg = obj; + + ast_string_field_free_memory(cfg); + acfg_cleanup(&cfg->acfg_common); +} + +static void *attestation_alloc(const char *name) +{ + struct attestation_cfg *cfg; + + cfg = ast_sorcery_generic_alloc(sizeof(*cfg), attestation_destructor); + if (!cfg) { + return NULL; + } + + if (ast_string_field_init(cfg, 1024)) { + ao2_ref(cfg, -1); + return NULL; + } + + /* + * The memory for acfg_common actually comes from cfg + * due to the weirdness of the STRFLDSET macro used with + * sorcery. We just use a token amount of memory in + * this call so the initialize doesn't fail. + */ + if (ast_string_field_init(&cfg->acfg_common, 8)) { + ao2_ref(cfg, -1); + return NULL; + } + + return cfg; +} + +int as_copy_cfg_common(const char *id, struct attestation_cfg_common *cfg_dst, + struct attestation_cfg_common *cfg_src) +{ + int rc = 0; + + if (!cfg_dst || !cfg_src) { + return -1; + } + + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, private_key_file); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, public_cert_url); + + cfg_enum_copy(cfg_dst, cfg_src, attest_level); + cfg_enum_copy(cfg_dst, cfg_src, check_tn_cert_public_url); + cfg_enum_copy(cfg_dst, cfg_src, send_mky); + + if (cfg_src->raw_key) { + /* Free and overwrite the destination */ + ao2_cleanup(cfg_dst->raw_key); + cfg_dst->raw_key = ao2_bump(cfg_src->raw_key); + cfg_dst->raw_key_length = cfg_src->raw_key_length; + } + + return rc; +} + +int as_check_common_config(const char *id, struct attestation_cfg_common *acfg_common) +{ + SCOPE_ENTER(3, "%s: Checking common config\n", id); + + if (!ast_strlen_zero(acfg_common->private_key_file) + && !ast_file_is_readable(acfg_common->private_key_file)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: default_private_key_path %s is missing or not readable\n", id, + acfg_common->private_key_file); + } + + if (ENUM_BOOL(acfg_common->check_tn_cert_public_url, + check_tn_cert_public_url) + && !ast_strlen_zero(acfg_common->public_cert_url)) { + RAII_VAR(char *, public_cert_data, NULL, ast_std_free); + X509 *public_cert; + size_t public_cert_len; + int rc = 0; + long http_code; + SCOPE_ENTER(3 , "%s: Checking public cert url '%s'\n", + id, acfg_common->public_cert_url); + + http_code = curl_download_to_memory(acfg_common->public_cert_url, + &public_cert_len, &public_cert_data, NULL); + if (http_code / 100 != 2) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: public_cert '%s' could not be downloaded\n", id, + acfg_common->public_cert_url); + } + + public_cert = crypto_load_cert_from_memory(public_cert_data, + public_cert_len); + if (!public_cert) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: public_cert '%s' could not be parsed as a certificate\n", id, + acfg_common->public_cert_url); + } + rc = crypto_is_cert_time_valid(public_cert, 0); + X509_free(public_cert); + if (!rc) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: public_cert '%s' is not valid yet or has expired\n", id, + acfg_common->public_cert_url); + } + + rc = crypto_has_private_key_from_memory(public_cert_data, public_cert_len); + if (rc) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: DANGER!!! public_cert_url '%s' has a private key in the file!!!\n", id, + acfg_common->public_cert_url); + } + SCOPE_EXIT("%s: Done\n", id); + } + + if (!ast_strlen_zero(acfg_common->private_key_file)) { + EVP_PKEY *private_key; + RAII_VAR(unsigned char *, raw_key, NULL, ast_std_free); + + private_key = crypto_load_privkey_from_file(acfg_common->private_key_file); + if (!private_key) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: Could not extract raw private key from file '%s'\n", id, + acfg_common->private_key_file); + } + + acfg_common->raw_key_length = crypto_extract_raw_privkey(private_key, &raw_key); + EVP_PKEY_free(private_key); + if (acfg_common->raw_key_length == 0 || raw_key == NULL) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "%s: Could not extract raw private key from file '%s'\n", id, + acfg_common->private_key_file); + } + + /* + * We're making this an ao2 object so it can be referenced + * by a profile instead of having to copy it. + */ + acfg_common->raw_key = ao2_alloc(acfg_common->raw_key_length, NULL); + if (!acfg_common->raw_key) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Could not allocate memory for raw private key\n", id); + } + memcpy(acfg_common->raw_key, raw_key, acfg_common->raw_key_length); + + } + + SCOPE_EXIT_RTN_VALUE(0, "%s: Done\n", id); +} + +static int attestation_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct attestation_cfg *cfg = obj; + const char *id = ast_sorcery_object_get_id(cfg); + + if (as_check_common_config(id, &cfg->acfg_common) != 0) { + return -1; + } + + return 0; +} + +static char *attestation_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct attestation_cfg *cfg; + struct config_object_cli_data data = { + .title = "Default Attestation", + .object_type = config_object_type_attestation, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show attestation"; + e->usage = + "Usage: stir_shaken show attestation\n" + " Show the stir/shaken attestation settings\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + cfg = as_get_cfg(); + config_object_cli_show(cfg, a, &data, 0); + ao2_cleanup(cfg); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry attestation_cli[] = { + AST_CLI_DEFINE(attestation_show, "Show stir/shaken attestation configuration"), +}; + +int as_config_reload(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + ast_sorcery_force_reload_object(sorcery, CONFIG_TYPE); + + if (!as_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken attestation service disabled. Either there were errors in the 'attestation' object in stir_shaken.conf or it was missing altogether.\n"); + } + if (!empty_cfg) { + empty_cfg = attestation_alloc(CONFIG_TYPE); + if (!empty_cfg) { + return -1; + } + empty_cfg->global_disable = 1; + } + + return 0; +} + +int as_config_unload(void) +{ + ast_cli_unregister_multiple(attestation_cli, + ARRAY_LEN(attestation_cli)); + ao2_cleanup(empty_cfg); + + return 0; +} + +int as_config_load(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + + ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", + "stir_shaken.conf,criteria=type=" CONFIG_TYPE ",single_object=yes,explicit_name=" CONFIG_TYPE); + + if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, attestation_alloc, + NULL, attestation_apply)) { + ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); + return -1; + } + + ast_sorcery_object_field_register_nodoc(sorcery, CONFIG_TYPE, "type", + "", OPT_NOOP_T, 0, 0); + + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "global_disable", + DEFAULT_global_disable ? "yes" : "no", + OPT_YESNO_T, 1, FLDSET(struct attestation_cfg, global_disable)); + + register_common_attestation_fields(sorcery, attestation_cfg, CONFIG_TYPE,); + + ast_sorcery_load_object(sorcery, CONFIG_TYPE); + + if (!as_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken attestation service disabled. Either there were errors in the 'attestation' object in stir_shaken.conf or it was missing altogether.\n"); + } + if (!empty_cfg) { + empty_cfg = attestation_alloc(CONFIG_TYPE); + if (!empty_cfg) { + return -1; + } + empty_cfg->global_disable = 1; + } + + ast_cli_register_multiple(attestation_cli, + ARRAY_LEN(attestation_cli)); + + return 0; +} diff --git a/res/res_stir_shaken/certificate.c b/res/res_stir_shaken/certificate.c deleted file mode 100644 index df4f38b8f7..0000000000 --- a/res/res_stir_shaken/certificate.c +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -#include "asterisk.h" - -#include - -#include "asterisk/cli.h" -#include "asterisk/sorcery.h" - -#include "stir_shaken.h" -#include "certificate.h" -#include "asterisk/res_stir_shaken.h" - -#define CONFIG_TYPE "certificate" - -struct stir_shaken_certificate { - SORCERY_OBJECT(details); - AST_DECLARE_STRING_FIELDS( - /*! Path to a directory containing certificates */ - AST_STRING_FIELD(path); - /*! URL to the public certificate */ - AST_STRING_FIELD(public_cert_url); - /*! The caller ID number associated with the certificate */ - AST_STRING_FIELD(caller_id_number); - /*! The attestation level for this certificate */ - AST_STRING_FIELD(attestation); - ); - /*! The private key for the certificate */ - EVP_PKEY *private_key; -}; - -static struct stir_shaken_certificate *stir_shaken_certificate_get(const char *id) -{ - return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, id); -} - -static struct ao2_container *stir_shaken_certificate_get_all(void) -{ - return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), CONFIG_TYPE, - AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); -} - -static void stir_shaken_certificate_destructor(void *obj) -{ - struct stir_shaken_certificate *cfg = obj; - - EVP_PKEY_free(cfg->private_key); - ast_string_field_free_memory(cfg); -} - -static void *stir_shaken_certificate_alloc(const char *name) -{ - struct stir_shaken_certificate *cfg; - - cfg = ast_sorcery_generic_alloc(sizeof(*cfg), stir_shaken_certificate_destructor); - if (!cfg) { - return NULL; - } - - if (ast_string_field_init(cfg, 512)) { - ao2_ref(cfg, -1); - return NULL; - } - - return cfg; -} - -struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number) -{ - struct ast_variable fields = { - .name = "caller_id_number", - .value = caller_id_number, - .next = NULL, - }; - - return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), - "certificate", AST_RETRIEVE_FLAG_DEFAULT, &fields); -} - -const char *stir_shaken_certificate_get_public_cert_url(struct stir_shaken_certificate *cert) -{ - return cert ? cert->public_cert_url : NULL; -} - -const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certificate *cert) -{ - return cert ? cert->attestation : NULL; -} - -EVP_PKEY *stir_shaken_certificate_get_private_key(struct stir_shaken_certificate *cert) -{ - return cert ? cert->private_key : NULL; -} - -static int stir_shaken_certificate_apply(const struct ast_sorcery *sorcery, void *obj) -{ - EVP_PKEY *private_key; - struct stir_shaken_certificate *cert = obj; - - if (ast_strlen_zero(cert->caller_id_number)) { - ast_log(LOG_ERROR, "Caller ID must be present\n"); - return -1; - } - - if (ast_strlen_zero(cert->attestation)) { - ast_log(LOG_ERROR, "Attestation must be present\n"); - return -1; - } - - private_key = stir_shaken_read_key(cert->path, 1); - if (!private_key) { - return -1; - } - - cert->private_key = private_key; - - return 0; -} - -static char *stir_shaken_certificate_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct stir_shaken_certificate *cfg; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show certificate"; - e->usage = - "Usage: stir_shaken show certificate \n" - " Show the certificate stir/shaken settings for a given id\n"; - return NULL; - case CLI_GENERATE: - if (a->pos == 3) { - return stir_shaken_tab_complete_name(a->word, stir_shaken_certificate_get_all()); - } else { - return NULL; - } - } - - if (a->argc != 4) { - return CLI_SHOWUSAGE; - } - - cfg = stir_shaken_certificate_get(a->argv[3]); - stir_shaken_cli_show(cfg, a, 0); - ao2_cleanup(cfg); - - return CLI_SUCCESS; -} - -static char *stir_shaken_certificate_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct ao2_container *container; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show certificates"; - e->usage = - "Usage: stir_shaken show certificates\n" - " Show all configured certificates for stir/shaken\n"; - return NULL; - case CLI_GENERATE: - return NULL; - } - - if (a->argc != 3) { - return CLI_SHOWUSAGE; - } - - container = stir_shaken_certificate_get_all(); - if (!container || ao2_container_count(container) == 0) { - ast_cli(a->fd, "No stir/shaken certificates found\n"); - ao2_cleanup(container); - return CLI_SUCCESS; - } - - ao2_callback(container, OBJ_NODATA, stir_shaken_cli_show, a); - ao2_ref(container, -1); - - return CLI_SUCCESS; -} - -static struct ast_cli_entry stir_shaken_certificate_cli[] = { - AST_CLI_DEFINE(stir_shaken_certificate_show, "Show stir/shaken certificate configuration by id"), - AST_CLI_DEFINE(stir_shaken_certificate_show_all, "Show all stir/shaken certificate configurations"), -}; - -static int on_load_path(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_certificate *cfg = obj; - struct stat statbuf; - - if (stat(var->value, &statbuf)) { - ast_log(LOG_ERROR, "stir/shaken - path '%s' not found\n", var->value); - return -1; - } - - if (!S_ISREG(statbuf.st_mode)) { - ast_log(LOG_ERROR, "stir/shaken - path '%s' is not a file\n", var->value); - return -1; - } - - return ast_string_field_set(cfg, path, var->value); -} - -static int path_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_certificate *cfg = obj; - - *buf = ast_strdup(cfg->path); - - return 0; -} - -static int on_load_public_cert_url(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_certificate *cfg = obj; - - if (!ast_begins_with(var->value, "http")) { - ast_log(LOG_ERROR, "stir/shaken - public_cert_url scheme must be 'http[s]'\n"); - return -1; - } - - return ast_string_field_set(cfg, public_cert_url, var->value); -} - -static int public_cert_url_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_certificate *cfg = obj; - - *buf = ast_strdup(cfg->public_cert_url); - - return 0; -} - -static int on_load_attestation(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_certificate *cfg = obj; - - if (strcmp(var->value, "A") && strcmp(var->value, "B") && strcmp(var->value, "C")) { - ast_log(LOG_ERROR, "stir/shaken - attestation level must be A, B, or C (object=%s)\n", - ast_sorcery_object_get_id(cfg)); - return -1; - } - - return ast_string_field_set(cfg, attestation, var->value); -} - -static int attestation_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_certificate *cfg = obj; - - *buf = ast_strdup(cfg->attestation); - - 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_cert_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, - ARRAY_LEN(stir_shaken_certificate_cli)); - - return 0; -} - -int stir_shaken_certificate_load(void) -{ - struct ast_sorcery *sorcery = ast_stir_shaken_sorcery(); - - ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=certificate"); - - if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, stir_shaken_certificate_alloc, - NULL, stir_shaken_certificate_apply)) { - ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); - return -1; - } - - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "path", "", - on_load_path, path_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_cert_url", "", - on_load_public_cert_url, public_cert_url_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "attestation", "", - on_load_attestation, attestation_to_str, NULL, 0, 0); - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "caller_id_number", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct stir_shaken_certificate, caller_id_number)); - - ast_cli_register_multiple(stir_shaken_certificate_cli, - ARRAY_LEN(stir_shaken_certificate_cli)); - - return 0; -} diff --git a/res/res_stir_shaken/certificate.h b/res/res_stir_shaken/certificate.h deleted file mode 100644 index 9f59afde5b..0000000000 --- a/res/res_stir_shaken/certificate.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ -#ifndef _STIR_SHAKEN_CERTIFICATE_H -#define _STIR_SHAKEN_CERTIFICATE_H - -#include - -struct ast_sorcery; - -struct stir_shaken_certificate; - -/*! - * \brief Get a STIR/SHAKEN certificate by caller ID number - * - * \param caller_id_number The caller ID number - * - * \retval NULL if not found - * \return The certificate on success - */ -struct stir_shaken_certificate *stir_shaken_certificate_get_by_caller_id_number(const char *caller_id_number); - -/*! - * \brief Get the public key URL associated with a certificate - * - * \param cert The certificate to get the public key URL from - * - * \retval NULL on failure - * \return The public key URL on success - */ -const char *stir_shaken_certificate_get_public_cert_url(struct stir_shaken_certificate *cert); - -/*! - * \brief Get the attestation level associated with a certificate - * - * \param cert The certificate - * - * \retval NULL on failure - * \retval The attestation on success - */ -const char *stir_shaken_certificate_get_attestation(struct stir_shaken_certificate *cert); - -/*! - * \brief Get the private key associated with a certificate - * - * \param cert The certificate to get the private key from - * - * \retval NULL on failure - * \return The private key on success - */ -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 - * - * \retval 0 on success - * \retval -1 on error - */ -int stir_shaken_certificate_load(void); - -/*! - * \brief Unload time cleanup for the stir/shaken 'certificate' configuration - * - * \retval 0 on success - * \retval -1 on error - */ -int stir_shaken_certificate_unload(void); - -#endif /* _STIR_SHAKEN_CERTIFICATE_H */ - diff --git a/res/res_stir_shaken/common_config.c b/res/res_stir_shaken/common_config.c new file mode 100644 index 0000000000..f7446b7cea --- /dev/null +++ b/res/res_stir_shaken/common_config.c @@ -0,0 +1,353 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" +#include "asterisk/cli.h" +#include "asterisk/cli.h" +#include "asterisk/logger.h" +#include "asterisk/module.h" +#include "asterisk/utils.h" +#include "asterisk/stasis.h" +#include "asterisk/security_events.h" + +#define AST_API_MODULE +#include "stir_shaken.h" + +static struct ast_sorcery *sorcery; +struct stasis_subscription *named_acl_changed_sub = NULL; + +struct ast_sorcery *get_sorcery(void) +{ + return sorcery; +} + +#define generate_bool_handler_functions(param_name) \ +static const char *param_name ## _map[] = { \ + [ param_name ## _NOT_SET ] = "not_set", \ + [ param_name ## _YES ] = "yes", \ + [ param_name ## _NO ] = "no", \ +}; \ +enum param_name ## _enum \ + param_name ## _from_str(const char *value) \ +{ \ + if (!strcasecmp(value, param_name ## _map[param_name ## _NOT_SET])) { \ + return param_name ## _NOT_SET; \ + } else if (ast_true(value)) { \ + return param_name ## _YES; \ + } else if (ast_false(value)) { \ + return param_name ## _NO; \ + } \ + ast_log(LOG_WARNING, "Unknown " #param_name " response value '%s'\n", value); \ + return param_name ## _UNKNOWN; \ +}\ +const char *param_name ## _to_str(enum param_name ## _enum value) \ +{ \ + return ARRAY_IN_BOUNDS(value, param_name ## _map) ? \ + param_name ## _map[value] : NULL; \ +} + +generate_bool_handler_functions(use_rfc9410_responses); +generate_bool_handler_functions(send_mky); +generate_bool_handler_functions(check_tn_cert_public_url); +generate_bool_handler_functions(relax_x5u_port_scheme_restrictions); +generate_bool_handler_functions(relax_x5u_path_restrictions); + +generate_bool_handler_functions(load_system_certs); + +struct enum_name_xref_entry { + int value; + const char *name; +}; + +#define generate_enum_string_functions(param_name, default_value, ...)\ +static struct enum_name_xref_entry param_name ## _map[] = { \ + __VA_ARGS__ \ +} ; \ +enum param_name ## _enum param_name ## _from_str( \ + const char *value) \ +{ \ + int i; \ + for (i = 0; i < ARRAY_LEN(param_name ## _map); i++) { \ + if (strcasecmp(value, param_name ##_map[i].name) == 0) { \ + return param_name ##_map[i].value; \ + } \ + } \ + return param_name ## _ ## default_value; \ +} \ +const char *param_name ## _to_str( \ + enum param_name ## _enum value) \ +{ \ + int i; \ + for (i = 0; i < ARRAY_LEN(param_name ## _map); i++) { \ + if (value == param_name ## _map[i].value) return param_name ## _map[i].name; \ + } \ + return NULL; \ +} + +generate_enum_string_functions(attest_level, UNKNOWN, + {attest_level_A, "A"}, + {attest_level_B, "B"}, + {attest_level_C, "C"}, +); + +generate_enum_string_functions(endpoint_behavior, OFF, + {endpoint_behavior_OFF, "off"}, + {endpoint_behavior_OFF, "none"}, + {endpoint_behavior_ATTEST, "attest"}, + {endpoint_behavior_VERIFY, "verify"}, + {endpoint_behavior_ON, "on"}, + {endpoint_behavior_ON, "both"} +); + +generate_enum_string_functions(stir_shaken_failure_action, CONTINUE, + {stir_shaken_failure_action_CONTINUE, "continue"}, + {stir_shaken_failure_action_REJECT_REQUEST, "reject_request"}, + {stir_shaken_failure_action_CONTINUE_RETURN_REASON, "continue_return_reason"}, +); + +static const char *translate_value(const char *val) +{ + if (val[0] == '0' + || val[0] == '\0' + || strcmp(val, "not_set") == 0) { + return ""; + } + + return val; +} + +static void print_acl(int fd, struct ast_acl_list *acl_list, const char *prefix) +{ + struct ast_acl *acl; + + AST_LIST_LOCK(acl_list); + AST_LIST_TRAVERSE(acl_list, acl, list) { + if (ast_strlen_zero(acl->name)) { + ast_cli(fd, "%s(permit/deny)\n", prefix); + } else { + ast_cli(fd, "%s%s\n", prefix, acl->name); + } + ast_ha_output(fd, acl->acl, prefix); + } + AST_LIST_UNLOCK(acl_list); +} + +#define print_acl_cert_store(cfg, a, max_name_len) \ +({ \ + if (cfg->vcfg_common.acl) { \ + ast_cli(a->fd, "x5u_acl:\n"); \ + print_acl(a->fd, cfg->vcfg_common.acl, " "); \ + } else { \ + ast_cli(a->fd, "%-*s: (none)\n", max_name_len, "x5u_acl"); \ + }\ + if (cfg->vcfg_common.tcs) { \ + int count = 0; \ + ast_cli(a->fd, "%-*s:\n", max_name_len, "Verification CA certificate store"); \ + count = crypto_show_cli_store(cfg->vcfg_common.tcs, a->fd); \ + if (count == 0 && (!ast_strlen_zero(cfg->vcfg_common.ca_path) \ + || !ast_strlen_zero(cfg->vcfg_common.crl_path))) { \ + ast_cli(a->fd, " Note: Certs in ca_path or crl_path won't show until used.\n"); \ + } \ + } else { \ + ast_cli(a->fd, "%-*s: (none)\n", max_name_len, "Verification CA certificate store"); \ + } \ +}) + +int config_object_cli_show(void *obj, void *arg, void *data, int flags) +{ + struct ast_cli_args *a = arg; + struct config_object_cli_data *cli_data = data; + struct ast_variable *options; + struct ast_variable *i; + const char *title = NULL; + const char *cfg_name = NULL; + int max_name_len = 0; + + if (!obj) { + ast_cli(a->fd, "No stir/shaken configuration found\n"); + return 0; + } + + if (!ast_strlen_zero(cli_data->title)) { + title = cli_data->title; + } else { + title = ast_sorcery_object_get_type(obj); + } + max_name_len = strlen(title); + + if (cli_data->object_type == config_object_type_profile + || cli_data->object_type == config_object_type_tn) { + cfg_name = ast_sorcery_object_get_id(obj); + max_name_len += strlen(cfg_name) + 2 /* ": " */; + } + + options = ast_variable_list_sort(ast_sorcery_objectset_create2( + get_sorcery(), obj, AST_HANDLER_ONLY_STRING)); + if (!options) { + return 0; + } + + for (i = options; i; i = i->next) { + int nlen = strlen(i->name); + max_name_len = (nlen > max_name_len) ? nlen : max_name_len; + } + + ast_cli(a->fd, "\n==============================================================================\n"); + if (ast_strlen_zero(cfg_name)) { + ast_cli(a->fd, "%s\n", title); + } else { + ast_cli(a->fd, "%s: %s\n", title, cfg_name); + } + ast_cli(a->fd, "------------------------------------------------------------------------------\n"); + + for (i = options; i; i = i->next) { + if (!ast_strings_equal(i->name, "x5u_acl")) { + ast_cli(a->fd, "%-*s: %s\n", max_name_len, i->name, + translate_value(i->value)); + } + } + + ast_variables_destroy(options); + + if (cli_data->object_type == config_object_type_profile) { + struct profile_cfg *cfg = obj; + print_acl_cert_store(cfg, a, max_name_len); + } else if (cli_data->object_type == config_object_type_verification) { + struct verification_cfg *cfg = obj; + print_acl_cert_store(cfg, a, max_name_len); + } + ast_cli(a->fd, "---------------------------------------------\n\n"); \ + + return 0; +} + +char *config_object_tab_complete_name(const char *word, struct ao2_container *container) +{ + void *obj; + struct ao2_iterator it; + int wordlen = strlen(word); + int ret; + + it = ao2_iterator_init(container, 0); + while ((obj = ao2_iterator_next(&it))) { + if (!strncasecmp(word, ast_sorcery_object_get_id(obj), wordlen)) { + ret = ast_cli_completion_add(ast_strdup(ast_sorcery_object_get_id(obj))); + if (ret) { + ao2_ref(obj, -1); + break; + } + } + ao2_ref(obj, -1); + } + ao2_iterator_destroy(&it); + + return NULL; +} + +int common_config_reload(void) +{ + SCOPE_ENTER(2, "Stir Shaken Reload\n"); + if (vs_reload()) { + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken VS Reload failed\n"); + } + + if (as_reload()) { + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken AS Reload failed\n"); + } + + if (tn_config_reload()) { + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken TN Reload failed\n"); + } + + if (profile_reload()) { + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken Profile Reload failed\n"); + } + + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Reload Done\n"); +} + +int common_config_unload(void) +{ + profile_unload(); + tn_config_unload(); + as_unload(); + vs_unload(); + + if (named_acl_changed_sub) { + stasis_unsubscribe(named_acl_changed_sub); + named_acl_changed_sub = NULL; + } + ast_sorcery_unref(sorcery); + sorcery = NULL; + + return 0; +} + +static void named_acl_changed_cb(void *data, + struct stasis_subscription *sub, struct stasis_message *message) +{ + if (stasis_message_type(message) != ast_named_acl_change_type()) { + return; + } + ast_log(LOG_NOTICE, "Named acl changed. Reloading verification and profile\n"); + common_config_reload(); +} + +int common_config_load(void) +{ + SCOPE_ENTER(2, "Stir Shaken Load\n"); + + if (!(sorcery = ast_sorcery_open())) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken sorcery load failed\n"); + } + + if (vs_load()) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken VS load failed\n"); + } + + if (as_load()) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken AS load failed\n"); + } + + if (tn_config_load()) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken TN load failed\n"); + } + + if (profile_load()) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken profile load failed\n"); + } + + if (!named_acl_changed_sub) { + named_acl_changed_sub = stasis_subscribe(ast_security_topic(), + named_acl_changed_cb, NULL); + if (!named_acl_changed_sub) { + common_config_unload(); + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_DECLINE, "Stir Shaken acl change subscribe failed\n"); + } + stasis_subscription_accept_message_type( + named_acl_changed_sub, ast_named_acl_change_type()); + } + + SCOPE_EXIT_RTN_VALUE(AST_MODULE_LOAD_SUCCESS, "Stir Shaken Load Done\n"); +} + diff --git a/res/res_stir_shaken/common_config.h b/res/res_stir_shaken/common_config.h new file mode 100644 index 0000000000..69e9b05549 --- /dev/null +++ b/res/res_stir_shaken/common_config.h @@ -0,0 +1,568 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef COMMON_CONFIG_H_ +#define COMMON_CONFIG_H_ + +#include + +#include "asterisk.h" +#include "asterisk/paths.h" +#include "asterisk/sorcery.h" +#include "asterisk/stringfields.h" + +/*! + * \brief Boolean field to/from string prototype generator + * + * Most of the boolean fields that appear in the verification and + * attestation objects can be ovrridden in the profile object; + * "use_rfc9410_responses" for instance. If they were registered as + * normal YESNO types, we couldn't tell if a "0" value in the profile + * object meant the user set it to "no" to override a value of "yes" + * in the verification object, or it just defaulted to "0". By making + * the _NOT_SET enum a non-0/1 and making it the default value, we can + * tell the difference. The _UNKNOWN enum gets set if the string value + * provided to the _from_str function wasn't recognized as one of the + * values acceptable to ast_true() or ast_false(). + * + * The result of calling the generator for a field will look like: + * + \code + enum use_rfc9410_responses_enum { + use_rfc9410_responses_UNKNOWN = -1, + use_rfc9410_responses_NO = 0, + use_rfc9410_responses_YES, + use_rfc9410_responses_NOT_SET, +}; +enum use_rfc9410_responses_enum + use_rfc9410_responses_from_str(const char *value); +const char *use_rfc9410_responses_to_str(enum use_rfc9410_responses_enum value); +\endcode + +Most of the macros that follow depend on enum values formatted +as _SOMETHING and their defaults as DEFAULT_. + */ +#define generate_bool_string_prototypes(param_name) \ +enum param_name ## _enum { \ + param_name ## _UNKNOWN = -1, \ + param_name ## _NO = 0, \ + param_name ## _YES, \ + param_name ## _NOT_SET, \ +}; \ +enum param_name ## _enum \ + param_name ## _from_str(const char *value); \ +const char *param_name ## _to_str(enum param_name ## _enum value); + +/* + * Run the generators + */ +generate_bool_string_prototypes(use_rfc9410_responses); + +generate_bool_string_prototypes(relax_x5u_port_scheme_restrictions); + +generate_bool_string_prototypes(relax_x5u_path_restrictions); + +generate_bool_string_prototypes(load_system_certs); + +generate_bool_string_prototypes(check_tn_cert_public_url); + +generate_bool_string_prototypes(send_mky); + +/*! + * \brief Enum field to/from string prototype generator + * + * This operates like the bool generator except you supply + * a list of the enum values. The first one MUST be + * param_name_UNKNOWN with a value of -1 and the rest running + * sequentially with the last being param_name_NOT_SET. + */ +#define generate_enum_string_prototypes(param_name, ...) \ +enum param_name ## _enum { \ + __VA_ARGS__ \ +}; \ +enum param_name ## _enum \ + param_name ## _from_str(const char *value); \ +const char *param_name ## _to_str(enum param_name ## _enum value); + +generate_enum_string_prototypes(endpoint_behavior, + endpoint_behavior_UNKNOWN = -1, + endpoint_behavior_OFF = 0, + endpoint_behavior_ATTEST, + endpoint_behavior_VERIFY, + endpoint_behavior_ON, + endpoint_behavior_NOT_SET +); + +generate_enum_string_prototypes(attest_level, + attest_level_UNKNOWN = -1, + attest_level_A = 0, + attest_level_B, + attest_level_C, + attest_level_NOT_SET, +); + +/* + * enum stir_shaken_failure_action is defined in + * res_stir_shaken.h because res_pjsip_stir_shaken needs it + * we we need to just declare the function prototypes. + */ + +enum stir_shaken_failure_action_enum + stir_shaken_failure_action_from_str(const char *action_str); + +const char *stir_shaken_failure_action_to_str( + enum stir_shaken_failure_action_enum action); + +/*! + * \brief Enum sorcery handler generator + * + * These macros can create the two functions needed to + * register an enum field with sorcery as long as there + * are _to_str and _from_str functions defined elsewhere. + * + */ +#define generate_sorcery_enum_to_str(__struct, __substruct, __lc_param) \ +static int sorcery_ ## __lc_param ## _to_str(const void *obj, const intptr_t *args, char **buf) \ +{ \ + const struct __struct *cfg = obj; \ + *buf = ast_strdup(__lc_param ## _to_str(cfg->__substruct __lc_param)); \ + return *buf ? 0 : -1; \ +} + +#define generate_sorcery_enum_from_str_ex(__struct, __substruct, __lc_param, __unknown) \ +static int sorcery_ ## __lc_param ## _from_str(const struct aco_option *opt, struct ast_variable *var, void *obj) \ +{ \ + struct __struct *cfg = obj; \ + cfg->__substruct __lc_param = __lc_param ## _from_str (var->value); \ + if (cfg->__substruct __lc_param == __unknown) { \ + ast_log(LOG_WARNING, "Unknown value '%s' specified for %s\n", \ + var->value, var->name); \ + return -1; \ + } \ + return 0; \ +} + +#define generate_sorcery_enum_from_str(__struct, __substruct, __lc_param, __unknown) \ + generate_sorcery_enum_from_str_ex(__struct, __substruct, __lc_param, __lc_param ## _ ## __unknown) \ + + +#define generate_sorcery_acl_to_str(__struct, __lc_param) \ +static int sorcery_acl_to_str(const void *obj, const intptr_t *args, char **buf) \ +{ \ + const struct __struct *cfg = obj; \ + struct ast_acl *first_acl; \ + if (!ast_acl_list_is_empty(cfg->vcfg_common.acl)) { \ + AST_LIST_LOCK(cfg->vcfg_common.acl); \ + first_acl = AST_LIST_FIRST(cfg->vcfg_common.acl); \ + if (ast_strlen_zero(first_acl->name)) { \ + *buf = "deny/permit"; \ + } else { \ + *buf = first_acl->name; \ + } \ + AST_LIST_UNLOCK(cfg->vcfg_common.acl); \ + } \ + *buf = ast_strdup(*buf); \ + return 0; \ +} + +#define generate_sorcery_acl_from_str(__struct, __lc_param, __unknown) \ +static int sorcery_acl_from_str(const struct aco_option *opt, struct ast_variable *var, void *obj) \ +{ \ + struct __struct *cfg = obj; \ + int error = 0; \ + int ignore; \ + const char *name = var->name + strlen("x5u_"); \ + if (ast_strlen_zero(var->value)) { \ + return 0; \ + } \ + ast_append_acl(name, var->value, &cfg->vcfg_common.acl, &error, &ignore); \ + return error; \ +} + +struct ast_acl_list *get_default_acl_list(void); + +#define EFFECTIVE_ENUM(__enum1, __enum2, __field, __default) \ + ( __enum1 != ( __field ## _ ## NOT_SET ) ? __enum1 : \ + (__enum2 != __field ## _ ## NOT_SET ? \ + __enum2 : __default )) + +#define EFFECTIVE_ENUM_BOOL(__enum1, __enum2, __field, __default) \ + (( __enum1 != ( __field ## _ ## NOT_SET ) ? __enum1 : \ + (__enum2 != __field ## _ ## NOT_SET ? \ + __enum2 : __field ## _ ## __default )) == __field ## _ ## YES) + +#define ENUM_BOOL(__enum1, __field) \ + (__enum1 == ( __field ## _ ## YES )) + +/*! + * \brief Common config copy utilities + * + * These macros are designed to be called from as_copy_cfg_common + * and vs_copy_cfg_common only. They'll only copy a field if the + * field contains a vaild value. Thus a NOT_SET value in the source + * won't override a pre-existing good value in the dest. A good + * value in the source WILL overwrite a good value in the dest. + * + */ +#define cfg_stringfield_copy(__cfg_dst, __cfg_src, __field) \ +({ \ + int __res = 0; \ + if (!ast_strlen_zero(__cfg_src->__field)) { \ + __res = ast_string_field_set(__cfg_dst, __field, __cfg_src->__field); \ + } \ + __res; \ +}) + +/*! + * \brief cfg_copy_wrapper + * + * Invoke cfg_stringfield_copy and cause the calling runction to + * return a -1 of the copy fails. + */ +#define cfg_sf_copy_wrapper(id, __cfg_dst, __cfg_src, __field) \ +{ \ + int rc = cfg_stringfield_copy(__cfg_dst, __cfg_src, __field); \ + if (rc != 0) { \ + ast_log(LOG_ERROR, "%s: Unable to copy field %s from %s to %s\n", \ + id, #__field, #__cfg_src, #__cfg_dst); \ + return -1; \ + } \ +} + +/*! + * \brief cfg_uint_copy + * + * Copy a uint from the source to the dest only if the source > 0. + * For stir-shaken, 0 isn't a valid value for any uint fields. + */ +#define cfg_uint_copy(__cfg_dst, __cfg_src, __field) \ +({ \ + if (__cfg_src->__field > 0) { \ + __cfg_dst->__field = __cfg_src->__field; \ + } \ +}) + +/*! + * \brief cfg_enum_copy + * + * Copy an enum from the source to the dest only if the source is + * neither NOT_SET nor UNKNOWN + */ +#define cfg_enum_copy(__cfg_dst, __cfg_src, __field) \ +({ \ + if (__cfg_src->__field != __field ## _NOT_SET \ + && __cfg_src->__field != __field ## _UNKNOWN) { \ + __cfg_dst->__field = __cfg_src->__field; \ + } \ +}) + +/*! + * \brief Attestation Service configuration for stir/shaken + * + * The common structure also appears in profile_cfg. + */ +struct attestation_cfg_common { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(private_key_file); + AST_STRING_FIELD(public_cert_url); + ); + enum attest_level_enum attest_level; + enum check_tn_cert_public_url_enum check_tn_cert_public_url; + enum send_mky_enum send_mky; + unsigned char *raw_key; + size_t raw_key_length; +}; + +#define generate_acfg_common_sorcery_handlers(object) \ + generate_sorcery_enum_from_str(object, acfg_common., check_tn_cert_public_url, UNKNOWN); \ + generate_sorcery_enum_to_str(object, acfg_common., check_tn_cert_public_url); \ + generate_sorcery_enum_from_str(object, acfg_common., send_mky, UNKNOWN); \ + generate_sorcery_enum_to_str(object, acfg_common., send_mky); \ + generate_sorcery_enum_from_str(object, acfg_common., attest_level, UNKNOWN); \ + generate_sorcery_enum_to_str(object, acfg_common., attest_level); + +int as_check_common_config(const char *id, + struct attestation_cfg_common *acfg_common); + +int as_copy_cfg_common(const char *id, struct attestation_cfg_common *cfg_dst, + struct attestation_cfg_common *cfg_src); + +void acfg_cleanup(struct attestation_cfg_common *cfg); + +struct attestation_cfg { + SORCERY_OBJECT(details); + /* + * We need an empty AST_DECLARE_STRING_FIELDS() here + * because when STRFLDSET is used with sorcery, the + * memory for all sub-structures that have stringfields + * is allocated from the parent's stringfield pool. + */ + AST_DECLARE_STRING_FIELDS(); + struct attestation_cfg_common acfg_common; + int global_disable; +}; + +struct attestation_cfg *as_get_cfg(void); +int as_is_config_loaded(void); +int as_config_load(void); +int as_config_reload(void); +int as_config_unload(void); + +/*! + * \brief Verification Service configuration for stir/shaken + * + * The common structure also appears in profile_cfg. + */ +struct verification_cfg_common { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(ca_file); + AST_STRING_FIELD(ca_path); + AST_STRING_FIELD(crl_file); + AST_STRING_FIELD(crl_path); + AST_STRING_FIELD(cert_cache_dir); + ); + unsigned int curl_timeout; + unsigned int max_iat_age; + unsigned int max_date_header_age; + unsigned int max_cache_entry_age; + unsigned int max_cache_size; + enum stir_shaken_failure_action_enum + stir_shaken_failure_action; + enum use_rfc9410_responses_enum use_rfc9410_responses; + enum relax_x5u_port_scheme_restrictions_enum + relax_x5u_port_scheme_restrictions; + enum relax_x5u_path_restrictions_enum + relax_x5u_path_restrictions; + enum load_system_certs_enum load_system_certs; + + struct ast_acl_list *acl; + X509_STORE *tcs; +}; + +#define generate_vcfg_common_sorcery_handlers(object) \ + generate_sorcery_enum_from_str(object, vcfg_common.,use_rfc9410_responses, UNKNOWN); \ + generate_sorcery_enum_to_str(object, vcfg_common.,use_rfc9410_responses); \ + generate_sorcery_enum_from_str(object, vcfg_common.,stir_shaken_failure_action, UNKNOWN); \ + generate_sorcery_enum_to_str(object, vcfg_common.,stir_shaken_failure_action); \ + generate_sorcery_enum_from_str(object, vcfg_common.,relax_x5u_port_scheme_restrictions, UNKNOWN); \ + generate_sorcery_enum_to_str(object, vcfg_common.,relax_x5u_port_scheme_restrictions); \ + generate_sorcery_enum_from_str(object, vcfg_common.,relax_x5u_path_restrictions, UNKNOWN); \ + generate_sorcery_enum_to_str(object, vcfg_common.,relax_x5u_path_restrictions); \ + generate_sorcery_enum_from_str(object, vcfg_common.,load_system_certs, UNKNOWN); \ + generate_sorcery_enum_to_str(object, vcfg_common.,load_system_certs); \ + generate_sorcery_acl_from_str(object, acl, NULL); \ + generate_sorcery_acl_to_str(object, acl); + +int vs_check_common_config(const char *id, + struct verification_cfg_common *vcfg_common); + +int vs_copy_cfg_common(const char *id, struct verification_cfg_common *cfg_dst, + struct verification_cfg_common *cfg_src); + +void vcfg_cleanup(struct verification_cfg_common *cfg); + +struct verification_cfg { + SORCERY_OBJECT(details); + /* + * We need an empty AST_DECLARE_STRING_FIELDS() here + * because when STRFLDSET is used with sorcery, the + * memory for all sub-structures that have stringfields + * is allocated from the parent's stringfield pool. + */ + AST_DECLARE_STRING_FIELDS(); + struct verification_cfg_common vcfg_common; + int global_disable; +}; + +struct verification_cfg *vs_get_cfg(void); +int vs_is_config_loaded(void); +int vs_config_load(void); +int vs_config_reload(void); +int vs_config_unload(void); + +/*! + * \brief Profile configuration for stir/shaken + */ +struct profile_cfg { + SORCERY_OBJECT(details); + /* + * We need an empty AST_DECLARE_STRING_FIELDS() here + * because when STRFLDSET is used with sorcery, the + * memory for all sub-structures that have stringfields + * is allocated from the parent's stringfield pool. + */ + AST_DECLARE_STRING_FIELDS(); + struct attestation_cfg_common acfg_common; + struct verification_cfg_common vcfg_common; + enum endpoint_behavior_enum endpoint_behavior; + struct profile_cfg *eprofile; +}; + +struct profile_cfg *profile_get_cfg(const char *id); +struct profile_cfg *eprofile_get_cfg(const char *id); +int profile_load(void); +int profile_reload(void); +int profile_unload(void); + +#define PROFILE_ALLOW_ATTEST(__profile) \ + (__profile->endpoint_behavior == endpoint_behavior_ON || \ + __profile->endpoint_behavior == endpoint_behavior_ATTEST) + +#define PROFILE_ALLOW_VERIFY(__profile) \ + (__profile->endpoint_behavior == endpoint_behavior_ON || \ + __profile->endpoint_behavior == endpoint_behavior_VERIFY) + +/*! + * \brief TN configuration for stir/shaken + * + * TN-specific attestation_cfg. + */ + +struct tn_cfg { + SORCERY_OBJECT(details); + /* + * We need an empty AST_DECLARE_STRING_FIELDS() here + * because when STRFLDSET is used with sorcery, the + * memory for all sub-structures that have stringfields + * is allocated from the parent's stringfield pool. + */ + AST_DECLARE_STRING_FIELDS(); + struct attestation_cfg_common acfg_common; +}; + +struct tn_cfg *tn_get_cfg(const char *tn); +struct tn_cfg *tn_get_etn(const char *tn, + struct profile_cfg *eprofile); +int tn_config_load(void); +int tn_config_reload(void); +int tn_config_unload(void); + +/*! + * \brief Sorcery fields register helpers + * + * Most of the fields on attestation_cfg and verification_cfg are also + * in profile_cfg. To prevent having to maintain duplicate sets of + * sorcery register statements, we can do this once here and call + * register_common_verification_fields() from both profile_config and + * verification_config and call register_common_attestation_fields() + * from profile_cfg and attestation_config. + * + * Most of the fields in question are in sub-structures like + * verification_cfg.vcfg_common which is why there are separate name + * and field parameters. For verification_cfg.vcfg_common.ca_file + * for instance, name would be ca_file and field would be + * vcfg_common.ca_file. + * + *\note These macros depend on default values being defined + * in the 4 _config.c files as DEFAULT_. + * + */ +#define stringfield_option_register(sorcery, CONFIG_TYPE, object, name, field, nodoc) \ + ast_sorcery_object_field_register ## nodoc(sorcery, CONFIG_TYPE, #name, \ + DEFAULT_ ## name, OPT_STRINGFIELD_T, 0, \ + STRFLDSET(struct object, field)) + +#define uint_option_register(sorcery, CONFIG_TYPE, object, name, field, nodoc) \ + ast_sorcery_object_field_register ## nodoc(sorcery, CONFIG_TYPE, #name, \ + __stringify(DEFAULT_ ## name), OPT_UINT_T, 0, \ + FLDSET(struct object, field)) + +#define enum_option_register_ex(sorcery, CONFIG_TYPE, name, field, nodoc) \ + ast_sorcery_object_field_register_custom ## nodoc(sorcery, CONFIG_TYPE, \ + #name, field ## _to_str(DEFAULT_ ## field), \ + sorcery_ ## field ## _from_str, sorcery_ ## field ## _to_str, NULL, 0, 0) + +#define enum_option_register(sorcery, CONFIG_TYPE, name, nodoc) \ + enum_option_register_ex(sorcery, CONFIG_TYPE, name, name, nodoc) + +#define register_common_verification_fields(sorcery, object, CONFIG_TYPE, nodoc) \ +({ \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, ca_file, vcfg_common.ca_file, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, ca_path, vcfg_common.ca_path, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, crl_file, vcfg_common.crl_file, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, crl_path, vcfg_common.crl_path, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, cert_cache_dir, vcfg_common.cert_cache_dir, nodoc); \ +\ + uint_option_register(sorcery, CONFIG_TYPE, object, curl_timeout, vcfg_common.curl_timeout, nodoc);\ + uint_option_register(sorcery, CONFIG_TYPE, object, max_iat_age, vcfg_common.max_iat_age, nodoc);\ + uint_option_register(sorcery, CONFIG_TYPE, object, max_date_header_age, vcfg_common.max_date_header_age, nodoc);\ + uint_option_register(sorcery, CONFIG_TYPE, object, max_cache_entry_age, vcfg_common.max_cache_entry_age, nodoc);\ + uint_option_register(sorcery, CONFIG_TYPE, object, max_cache_size, vcfg_common.max_cache_size, nodoc);\ +\ + enum_option_register_ex(sorcery, CONFIG_TYPE, failure_action, stir_shaken_failure_action, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, use_rfc9410_responses, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, \ + relax_x5u_port_scheme_restrictions, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, \ + relax_x5u_path_restrictions, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, \ + load_system_certs, nodoc); \ +\ + ast_sorcery_object_field_register_custom ## nodoc(sorcery, CONFIG_TYPE, "x5u_deny", "", sorcery_acl_from_str, NULL, NULL, 0, 0); \ + ast_sorcery_object_field_register_custom ## nodoc(sorcery, CONFIG_TYPE, "x5u_permit", "", sorcery_acl_from_str, NULL, NULL, 0, 0); \ + ast_sorcery_object_field_register_custom ## nodoc(sorcery, CONFIG_TYPE, "x5u_acl", "", sorcery_acl_from_str, sorcery_acl_to_str, NULL, 0, 0); \ +}) + +#define register_common_attestation_fields(sorcery, object, CONFIG_TYPE, nodoc) \ +({ \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, private_key_file, acfg_common.private_key_file, nodoc); \ + stringfield_option_register(sorcery, CONFIG_TYPE, object, public_cert_url, acfg_common.public_cert_url, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, attest_level, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, check_tn_cert_public_url, nodoc); \ + enum_option_register(sorcery, CONFIG_TYPE, send_mky, nodoc); \ +}) + +int common_config_load(void); +int common_config_unload(void); +int common_config_reload(void); + +enum config_object_type { + config_object_type_attestation = 0, + config_object_type_verification, + config_object_type_profile, + config_object_type_tn, +}; + +struct config_object_cli_data { + const char *title; + enum config_object_type object_type; +}; + +/*! + * \brief Output configuration settings to the Asterisk CLI + * + * \param obj A sorcery object containing configuration data + * \param arg Asterisk CLI argument object + * \param flags ao2 container flags + * + * \retval 0 + */ +int config_object_cli_show(void *obj, void *arg, void *data, int flags); + +/*! + * \brief Tab completion for name matching with STIR/SHAKEN CLI commands + * + * \param word The word to tab complete on + * \param container The sorcery container to iterate through + * + * \retval The tab completion options + */ +char *config_object_tab_complete_name(const char *word, struct ao2_container *container); + + +#endif /* COMMON_CONFIG_H_ */ diff --git a/res/res_stir_shaken/crypto_utils.c b/res/res_stir_shaken/crypto_utils.c new file mode 100644 index 0000000000..201564bcce --- /dev/null +++ b/res/res_stir_shaken/crypto_utils.c @@ -0,0 +1,525 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crypto_utils.h" + +#include "asterisk.h" +#include "asterisk/logger.h" +#include "asterisk/module.h" +#include "asterisk/stringfields.h" +#include "asterisk/utils.h" +#include "asterisk/vector.h" +#include "asterisk/cli.h" + +void __attribute__((format(printf, 5, 6))) +crypto_log_openssl(int level, char *file, int line, const char *function, + const char *fmt, ...) +{ + FILE *fp; + char *buffer; + size_t length; + va_list ap; + char *tmp_fmt; + + fp = open_memstream(&buffer, &length); + if (!fp) { + return; + } + + va_start(ap, fmt); + if (!ast_strlen_zero(fmt)) { + size_t fmt_len = strlen(fmt); + if (fmt[fmt_len - 1] == '\n') { + tmp_fmt = ast_strdupa(fmt); + tmp_fmt[fmt_len - 1] = '\0'; + fmt = tmp_fmt; + } + } + vfprintf(fp, fmt, ap); + fputs(": ", fp); + ERR_print_errors_fp(fp); + fclose(fp); + + if (length) { + ast_log(level, file, line, function, "%s\n", buffer); + } + + ast_std_free(buffer); +} + +int crypto_register_x509_extension(const char *oid, const char *short_name, + const char *long_name) +{ + int nid = 0; + + if (ast_strlen_zero(oid) || ast_strlen_zero(short_name) || + ast_strlen_zero(long_name)) { + ast_log(LOG_ERROR, "One or more of oid, short_name or long_name are NULL or empty\n"); + return -1; + } + + nid = OBJ_sn2nid(short_name); + if (nid != NID_undef) { + ast_log(LOG_NOTICE, "NID %d, object %s already registered\n", nid, short_name); + return nid; + } + + nid = OBJ_create(oid, short_name, long_name); + if (nid == NID_undef) { + crypto_log_openssl(LOG_ERROR, "Couldn't register %s X509 extension\n", short_name); + return -1; + } + ast_log(LOG_NOTICE, "Registered object %s as NID %d\n", short_name, nid); + + return nid; +} + +ASN1_OCTET_STRING *crypto_get_cert_extension_data(X509 *cert, + int nid, const char *short_name) +{ + int ex_idx; + X509_EXTENSION *ex; + + if (nid <= 0) { + nid = OBJ_sn2nid(short_name); + if (nid == NID_undef) { + ast_log(LOG_ERROR, "Extension object for %s not found\n", short_name); + return NULL; + } + } else { + const char *tmp = OBJ_nid2sn(nid); + if (!tmp) { + ast_log(LOG_ERROR, "Extension object for NID %d not found\n", nid); + return NULL; + } + } + + ex_idx = X509_get_ext_by_NID(cert, nid, -1); + if (ex_idx < 0) { + ast_log(LOG_ERROR, "Extension index not found in certificate\n"); + return NULL; + } + ex = X509_get_ext(cert, ex_idx); + if (!ex) { + ast_log(LOG_ERROR, "Extension not found in certificate\n"); + return NULL; + } + + return X509_EXTENSION_get_data(ex); +} + +EVP_PKEY *crypto_load_privkey_from_file(const char *filename) +{ + EVP_PKEY *key = NULL; + FILE *fp; + + if (ast_strlen_zero(filename)) { + ast_log(LOG_ERROR, "filename was null or empty\n"); + return NULL; + } + + fp = fopen(filename, "r"); + if (!fp) { + ast_log(LOG_ERROR, "Failed to open %s: %s\n", filename, strerror(errno)); + return NULL; + } + + key = PEM_read_PrivateKey(fp, NULL, NULL, NULL); + fclose(fp); + if (!key) { + crypto_log_openssl(LOG_ERROR, "Failed to load private key from %s\n", filename); + } + return key; +} + +X509 *crypto_load_cert_from_file(const char *filename) +{ + FILE *fp; + X509 *cert = NULL; + + if (ast_strlen_zero(filename)) { + ast_log(LOG_ERROR, "filename was null or empty\n"); + return NULL; + } + + fp = fopen(filename, "r"); + if (!fp) { + ast_log(LOG_ERROR, "Failed to open %s: %s\n", filename, strerror(errno)); + return NULL; + } + + cert = PEM_read_X509(fp, &cert, NULL, NULL); + fclose(fp); + if (!cert) { + crypto_log_openssl(LOG_ERROR, "Failed to create cert from %s\n", filename); + } + return cert; +} + +X509 *crypto_load_cert_from_memory(const char *buffer, size_t size) +{ + RAII_VAR(BIO *, bio, NULL, BIO_free_all); + X509 *cert = NULL; + + if (ast_strlen_zero(buffer) || size <= 0) { + ast_log(LOG_ERROR, "buffer was null or empty\n"); + return NULL; + } + + bio = BIO_new_mem_buf(buffer, size); + if (!bio) { + crypto_log_openssl(LOG_ERROR, "Unable to create memory BIO\n"); + return NULL; + } + + cert = PEM_read_bio_X509(bio, NULL, NULL, NULL); + if (!cert) { + crypto_log_openssl(LOG_ERROR, "Failed to create cert from BIO\n"); + } + return cert; +} + +static EVP_PKEY *load_private_key_from_memory(const char *buffer, size_t size) +{ + RAII_VAR(BIO *, bio, NULL, BIO_free_all); + EVP_PKEY *key = NULL; + + if (ast_strlen_zero(buffer) || size <= 0) { + ast_log(LOG_ERROR, "buffer was null or empty\n"); + return NULL; + } + + bio = BIO_new_mem_buf(buffer, size); + if (!bio) { + crypto_log_openssl(LOG_ERROR, "Unable to create memory BIO\n"); + return NULL; + } + + key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + + return key; +} + +EVP_PKEY *crypto_load_private_key_from_memory(const char *buffer, size_t size) +{ + EVP_PKEY *key = load_private_key_from_memory(buffer, size); + if (!key) { + crypto_log_openssl(LOG_ERROR, "Unable to load private key from memory\n"); + } + return key; +} + +int crypto_has_private_key_from_memory(const char *buffer, size_t size) +{ + RAII_VAR(EVP_PKEY *, key, load_private_key_from_memory(buffer, size), EVP_PKEY_free); + + return key ? 1 : 0; +} + +static int dump_mem_bio(BIO *bio, unsigned char **buffer) +{ + char *temp_ptr; + int raw_key_len; + + raw_key_len = BIO_get_mem_data(bio, &temp_ptr); + if (raw_key_len <= 0) { + crypto_log_openssl(LOG_ERROR, "Unable to extract raw public key\n"); + return -1; + } + *buffer = ast_malloc(raw_key_len); + if (!*buffer) { + ast_log(LOG_ERROR, "Unable to allocate memory for raw public key\n"); + return -1; + } + memcpy(*buffer, temp_ptr, raw_key_len); + + return raw_key_len; +} + +int crypto_extract_raw_pubkey(EVP_PKEY *key, unsigned char **buffer) +{ + RAII_VAR(BIO *, bio, NULL, BIO_free_all); + + bio = BIO_new(BIO_s_mem()); + + if (!bio || (PEM_write_bio_PUBKEY(bio, key) <= 0)) { + crypto_log_openssl(LOG_ERROR, "Unable to write pubkey to BIO\n"); + return -1; + } + + return dump_mem_bio(bio, buffer); +} + +int crypto_get_raw_pubkey_from_cert(X509 *cert, + unsigned char **buffer) +{ + RAII_VAR(BIO *, bio, NULL, BIO_free_all); + EVP_PKEY *public_key; + + public_key = X509_get0_pubkey(cert); + if (!public_key) { + crypto_log_openssl(LOG_ERROR, "Unable to retrieve pubkey from cert\n"); + return -1; + } + + return crypto_extract_raw_pubkey(public_key, buffer); +} + +int crypto_extract_raw_privkey(EVP_PKEY *key, unsigned char **buffer) +{ + RAII_VAR(BIO *, bio, NULL, BIO_free_all); + + bio = BIO_new(BIO_s_mem()); + + if (!bio || (PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL) <= 0)) { + crypto_log_openssl(LOG_ERROR, "Unable to write privkey to BIO\n"); + return -1; + } + + return dump_mem_bio(bio, buffer); +} + +void crypto_free_cert_store(X509_STORE *store) +{ + if (!store) { + return; + } + X509_STORE_free(store); +} + +int crypto_lock_cert_store(X509_STORE *store) +{ + if (!store) { + return -1; + } + /* lock returns 1 on success */ + return X509_STORE_lock(store) == 1 ? 0 : -1; +} + +int crypto_unlock_cert_store(X509_STORE *store) +{ + if (!store) { + return -1; + } + /* unlock returns 1 on success */ + return X509_STORE_unlock(store) == 1 ? 0 : -1; +} + +X509_STORE *crypto_create_cert_store(void) +{ + X509_STORE *store = X509_STORE_new(); + + if (!store) { + crypto_log_openssl(LOG_ERROR, "Failed to create X509_STORE\n"); + return NULL; + } + + return store; +} + +int crypto_load_cert_store(X509_STORE *store, const char *file, + const char *path) +{ + if (ast_strlen_zero(file) && ast_strlen_zero(path)) { + ast_log(LOG_ERROR, "Both file and path can't be NULL"); + return -1; + } + + if (!store) { + ast_log(LOG_ERROR, "store is NULL"); + return -1; + } + + /* + * If the file or path are empty strings, we need to pass NULL + * so openssl ignores it otherwise it'll try to open a file or + * path named ''. + */ + if (!X509_STORE_load_locations(store, S_OR(file, NULL), S_OR(path, NULL))) { + crypto_log_openssl(LOG_ERROR, "Failed to load store from file '%s' or path '%s'\n", + S_OR(file, "N/A"), S_OR(path, "N/A")); + return -1; + } + + return 0; +} + +int crypto_show_cli_store(X509_STORE *store, int fd) +{ + STACK_OF(X509_OBJECT) *certs = NULL; + int count = 0; + int i = 0; + char subj[1024]; + + certs = X509_STORE_get0_objects(store); + count = sk_X509_OBJECT_num(certs); + for (i = 0; i < count ; i++) { + X509_OBJECT *o = sk_X509_OBJECT_value(certs, i); + X509 *c = X509_OBJECT_get0_X509(o); + X509_NAME_oneline(X509_get_subject_name(c), subj, 1024); + ast_cli(fd, "%s\n", subj); + } + return count; +} +int crypto_is_cert_time_valid(X509*cert, time_t reftime) +{ + ASN1_STRING *notbefore; + ASN1_STRING *notafter; + + if (!reftime) { + reftime = time(NULL); + } + notbefore = X509_get_notBefore(cert); + notafter = X509_get_notAfter(cert); + if (!notbefore || !notafter) { + ast_log(LOG_ERROR, "Either notbefore or notafter were not present in the cert\n"); + return 0; + } + + return (X509_cmp_time(notbefore, &reftime) < 0 && + X509_cmp_time(notafter, &reftime) > 0); +} + +int crypto_is_cert_trusted(X509_STORE *store, X509 *cert, const char **err_msg) +{ + X509_STORE_CTX *verify_ctx = NULL; + int rc = 0; + + if (!(verify_ctx = X509_STORE_CTX_new())) { + crypto_log_openssl(LOG_ERROR, "Unable to create verify_ctx\n"); + return 0; + } + + if (X509_STORE_CTX_init(verify_ctx, store, cert, NULL) != 1) { + X509_STORE_CTX_cleanup(verify_ctx); + X509_STORE_CTX_free(verify_ctx); + crypto_log_openssl(LOG_ERROR, "Unable to initialize verify_ctx\n"); + return 0; + } + + rc = X509_verify_cert(verify_ctx); + if (rc != 1 && err_msg != NULL) { + int err = X509_STORE_CTX_get_error(verify_ctx); + *err_msg = X509_verify_cert_error_string(err); + } + X509_STORE_CTX_cleanup(verify_ctx); + X509_STORE_CTX_free(verify_ctx); + + return rc; +} + +#define SECS_PER_DAY 86400 +time_t crypto_asn_time_as_time_t(ASN1_TIME *at) +{ + int pday; + int psec; + time_t rt = time(NULL); + + if (!ASN1_TIME_diff(&pday, &psec, NULL, at)) { + crypto_log_openssl(LOG_ERROR, "Unable to calculate time diff\n"); + return 0; + } + + rt += ((pday * SECS_PER_DAY) + psec); + + return rt; +} +#undef SECS_PER_DAY + +char *crypto_get_cert_subject(X509 *cert, const char *short_name) +{ + size_t len = 0; + RAII_VAR(char *, buffer, NULL, ast_std_free); + char *search_buff = NULL; + char *search = NULL; + size_t search_len = 0; + char *rtn = NULL; + char *line = NULL; + /* + * If short_name was supplied, we want a multiline subject + * with each component on a separate line. This makes it easier + * to iterate over the components to find the one we want. + * Otherwise, we just want the whole subject on one line. + */ + unsigned long flags = + short_name ? XN_FLAG_FN_SN | XN_FLAG_SEP_MULTILINE : XN_FLAG_ONELINE; + FILE *fp = open_memstream(&buffer, &len); + BIO *bio = fp ? BIO_new_fp(fp, BIO_CLOSE) : NULL; + X509_NAME *subject = X509_get_subject_name(cert); + int rc = 0; + + if (!fp || !bio || !subject) { + return NULL; + } + + rc = X509_NAME_print_ex(bio, subject, 0, flags); + BIO_free(bio); + if (rc < 0) { + return NULL; + } + + if (!short_name) { + rtn = ast_malloc(len + 1); + if (rtn) { + strcpy(rtn, buffer); /* Safe */ + } + return rtn; + } + + search_len = strlen(short_name) + 1; + rc = ast_asprintf(&search, "%s=", short_name); + if (rc != search_len) { + return NULL; + } + + search_buff = buffer; + while((line = ast_read_line_from_buffer(&search_buff))) { + if (ast_begins_with(line, search)) { + rtn = ast_malloc(strlen(line) - search_len + 1); + if (rtn) { + strcpy(rtn, line + search_len); /* Safe */ + } + break; + } + } + + ast_std_free(search); + return rtn; +} + +int crypto_load(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} + +int crypto_unload(void) +{ + return 0; +} + diff --git a/res/res_stir_shaken/crypto_utils.h b/res/res_stir_shaken/crypto_utils.h new file mode 100644 index 0000000000..70538709f7 --- /dev/null +++ b/res/res_stir_shaken/crypto_utils.h @@ -0,0 +1,280 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ +#ifndef _CRYPTO_UTILS_H +#define _CRYPTO_UTILS_H + +#include "openssl/x509.h" +#include "openssl/x509_vfy.h" + +#include "asterisk.h" +#include "asterisk/logger.h" +#include "asterisk/stringfields.h" + +/*! + * \brief Print a log message with any OpenSSL errors appended + * + * \param level Type of log event + * \param file Will be provided by the AST_LOG_* macro + * \param line Will be provided by the AST_LOG_* macro + * \param function Will be provided by the AST_LOG_* macro + * \param fmt This is what is important. The format is the same as your favorite breed of printf. You know how that works, right? :-) + */ +void crypto_log_openssl(int level, char *file, int line, + const char *function, const char *fmt, ...) + __attribute__((format(printf, 5, 6))); + +/*! + * \brief Register a certificate extension to openssl + * + * \param oid The OID of the extension + * \param short_name The short name of the extension + * \param long_name The long name of the extension + * + * \retval <0 Extension was not successfully added + * \retval >= NID of the added extension + */ +int crypto_register_x509_extension(const char *oid, + const char *short_name, const char *long_name); + +/*! + * \brief Return the data from a specific extension in a cert + * + * \param cert The cert containing the extension + * \param nid The NID of the extension + * (0 to search locally registered extensions by short_name) + * \param short_name The short name of the extension + * (only for locally registered extensions) + * + * \note Either nid or short_name may be supplied. If both are, + * nid takes precedence. + * \note The extension nid may be any of the built-in values + * in openssl/obj_mac.h or a NID returned by + * ast_crypto_register_x509_extension(). + * + * \returns The data for the extension or NULL if not found + * + * \warning Do NOT attempt to free the returned buffer. + */ +ASN1_OCTET_STRING *crypto_get_cert_extension_data(X509 *cert, int nid, + const char *short_name); + +/*! + * \brief Load an X509 Cert from a file + * + * \param filename PEM file + * + * \returns X509* or NULL on error + */ +X509 *crypto_load_cert_from_file(const char *filename); + +/*! + * \brief Load a private key from memory + * + * \param buffer private key + * \param size buffer size + * + * \returns EVP_PKEY* or NULL on error + */ +EVP_PKEY *crypto_load_private_key_from_memory(const char *buffer, size_t size); + +/*! + * \brief Check if the supplied buffer has a private key + * + * \note This function can be used to check a certificate PEM file to + * see if it also has a private key in it. + * + * \param buffer arbitrary buffer + * \param size buffer size + * + * \retval 1 buffer has a private key + * \retval 0 buffer does not have a private key + */ +int crypto_has_private_key_from_memory(const char *buffer, size_t size); + +/*! + * \brief Load an X509 Cert from a NULL terminated buffer + * + * \param buffer containing the cert + * \param size size of the buffer. + * May be -1 if the buffer is NULL terminated. + * + * \returns X509* or NULL on error + */ +X509 *crypto_load_cert_from_memory(const char *buffer, size_t size); + +/*! + * \brief Retrieve RAW public key from cert + * + * \param cert The cert containing the extension + * \param raw_key Address of char * to place the raw key. + * Must be freed with ast_free after use + * + * \retval <=0 An error has occurred + * \retval >0 Length of raw key + */ +int crypto_get_raw_pubkey_from_cert(X509 *cert, + unsigned char **raw_key); + +/*! + * \brief Extract raw public key from EVP_PKEY + * + * \param key Key to extract from + * + * \param buffer Pointer to unsigned char * to receive raw key + * Must be freed with ast_free after use + * + * \retval <=0 An error has occurred + * \retval >0 Length of raw key + */ +int crypto_extract_raw_pubkey(EVP_PKEY *key, unsigned char **buffer); + +/*! + * \brief Extract raw private key from EVP_PKEY + * + * \param key Key to extract from + * \param buffer Pointer to unsigned char * to receive raw key + * Must be freed with ast_free after use + * + * \retval <=0 An error has occurred + * \retval >0 Length of raw key + */ +int crypto_extract_raw_privkey(EVP_PKEY *key, unsigned char **buffer); + +/*! + * \brief Load a private key from a file + * + * \param filename File to load from + * + * \returns EVP_PKEY *key or NULL on error + */ +EVP_PKEY *crypto_load_privkey_from_file(const char *filename); + +/*! + * \brief Free an X509 store + * + * \param store X509 Store to free + * + */ +void crypto_free_cert_store(X509_STORE *store); + +/*! + * \brief Create an empty X509 store + * + * \returns X509_STORE* or NULL on error + */ +X509_STORE *crypto_create_cert_store(void); + +/*! + * \brief Dump a cert store to the asterisk CLI + * + * \param store X509 Store to dump + * \param fd The CLI fd to print to + + * \retval Count of objects printed + */ +int crypto_show_cli_store(X509_STORE *store, int fd); + +/*! + * \brief Load an X509 Store with either certificates or CRLs + * + * \param store X509 Store to load + * \param file Certificate or CRL file to load or NULL + * \param path Path to directory with hashed certs or CRLs to load or NULL + * + * \note At least 1 file or path must be specified. + * + * \retval <= 0 failure + * \retval 0 success + */ +int crypto_load_cert_store(X509_STORE *store, const char *file, + const char *path); + +/*! + * \brief Locks an X509 Store + * + * \param store X509 Store to lock + * + * \retval <= 0 failure + * \retval 0 success + */ +int crypto_lock_cert_store(X509_STORE *store); + +/*! + * \brief Unlocks an X509 Store + * + * \param store X509 Store to unlock + * + * \retval <= 0 failure + * \retval 0 success + */ +int crypto_unlock_cert_store(X509_STORE *store); + +/*! + * \brief Check if the reftime is within the cert's valid dates + * + * \param cert The cert to check + * \param reftime to use or 0 to use current time + * + * \retval 1 Cert is valid + * \retval 0 Cert is not valid + */ +int crypto_is_cert_time_valid(X509 *cert, time_t reftime); + +/*! + * \brief Check if the cert is trusted + * + * \param store The CA store to check against + * \param cert The cert to check + * \param err_msg Optional pointer to a const char * + * + * \retval 1 Cert is trusted + * \retval 0 Cert is not trusted + */ +int crypto_is_cert_trusted(X509_STORE *store, X509 *cert, const char **err_msg); + +/*! + * \brief Return a time_t for an ASN1_TIME + * + * \param at ASN1_TIME + * + * \returns time_t corresponding to the ASN1_TIME + */ +time_t crypto_asn_time_as_time_t(ASN1_TIME *at); + + +/*! + * \brief Returns the Subject (or component of Subject) from a certificate + * + * \param cert The X509 certificate + * \param short_name The upper case short name of the component to extract. + * May be NULL to extract the entire subject. + * \returns Entire subject or component. Must be freed with ast_free(); + */ +char *crypto_get_cert_subject(X509 *cert, const char *short_name); + +/*! + * \brief Initialize the crypto utils + */ +int crypto_load(void); + +/*! + * \brief Clean up the crypto utils + */ +int crypto_unload(void); + +#endif /* CRYPTO_UTILS */ diff --git a/res/res_stir_shaken/curl.c b/res/res_stir_shaken/curl.c deleted file mode 100644 index fb06de59ca..0000000000 --- a/res/res_stir_shaken/curl.c +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Ben Ford - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -#include "asterisk.h" - -#include "asterisk/utils.h" -#include "asterisk/logger.h" -#include "asterisk/file.h" -#include "asterisk/acl.h" - -#include "curl.h" -#include "general.h" -#include "stir_shaken.h" -#include "profile.h" - -#include -#include - -/* Used to check CURL headers */ -#define MAX_HEADER_LENGTH 1023 - -/* Used to limit download size */ -#define MAX_DOWNLOAD_SIZE 8192 - -/* Used to limit how many bytes we get from CURL per write */ -#define MAX_BUF_SIZE_PER_WRITE 1024 - -/* Certificates should begin with this */ -#define BEGIN_CERTIFICATE_STR "-----BEGIN CERTIFICATE-----" - -/* CURL callback data to avoid storing useless info in AstDB */ -struct curl_cb_data { - char *cache_control; - char *expires; -}; - -struct curl_cb_write_buf { - char buf[MAX_DOWNLOAD_SIZE + 1]; - size_t size; - const char *url; -}; - -struct curl_cb_open_socket { - const struct ast_acl_list *acl; - curl_socket_t *sockfd; -}; - -struct curl_cb_data *curl_cb_data_create(void) -{ - struct curl_cb_data *data; - - data = ast_calloc(1, sizeof(*data)); - - return data; -} - -void curl_cb_data_free(struct curl_cb_data *data) -{ - if (!data) { - return; - } - - ast_free(data->cache_control); - ast_free(data->expires); - - ast_free(data); -} - -static void curl_cb_open_socket_free(struct curl_cb_open_socket *data) -{ - if (!data) { - return; - } - - close(*data->sockfd); - - /* We don't need to free the ACL since we just use a reference */ - ast_free(data); -} - -char *curl_cb_data_get_cache_control(const struct curl_cb_data *data) -{ - if (!data) { - return NULL; - } - - return data->cache_control; -} - -char *curl_cb_data_get_expires(const struct curl_cb_data *data) -{ - if (!data) { - return NULL; - } - - return data->expires; -} - -/*! - * \brief Called when a CURL request completes - * - * \param buffer, size, nitems - * \param data The curl_cb_data structure to store expiration info - */ -static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data) -{ - struct curl_cb_data *cb_data = data; - size_t realsize; - char *header; - char *value; - - realsize = size * nitems; - - if (realsize > MAX_HEADER_LENGTH) { - ast_log(LOG_WARNING, "CURL header length is too large (size: '%zu' | max: '%d')\n", - realsize, MAX_HEADER_LENGTH); - return 0; - } - - header = ast_alloca(realsize + 1); - memcpy(header, buffer, realsize); - header[realsize] = '\0'; - value = strchr(header, ':'); - if (!value) { - return realsize; - } - *value++ = '\0'; - value = ast_trim_blanks(ast_skip_blanks(value)); - - if (!strcasecmp(header, "Cache-Control")) { - cb_data->cache_control = ast_strdup(value); - } else if (!strcasecmp(header, "Expires")) { - cb_data->expires = ast_strdup(value); - } - - return realsize; -} - -/*! - * \brief Prepare a CURL instance to use - * - * \param data The CURL callback data - * - * \retval NULL on failure - * \return CURL instance on success - */ -static CURL *get_curl_instance(struct curl_cb_data *data) -{ - CURL *curl; - struct stir_shaken_general *cfg; - unsigned int curl_timeout; - - cfg = stir_shaken_general_get(); - curl_timeout = ast_stir_shaken_curl_timeout(cfg); - ao2_cleanup(cfg); - - curl = curl_easy_init(); - if (!curl) { - return NULL; - } - - curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, curl_timeout); - curl_easy_setopt(curl, CURLOPT_USERAGENT, AST_CURL_USER_AGENT); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, data); - - return curl; -} - -/*! - * \brief Write callback passed to libcurl - * - * \note If this function returns anything other than the size of the data - * libcurl expected us to process, the request will cancel. That's why we return - * 0 on error, otherwise the amount of data we were given - * - * \param curl_data The data from libcurl - * \param size Always 1 according to libcurl - * \param actual_size The actual size of the data - * \param our_data The data we passed to libcurl - * - * \retval The size of the data we processed - * \retval 0 if there was an error - */ -static size_t curl_write_cb(void *curl_data, size_t size, size_t actual_size, void *our_data) -{ - /* Just in case size is NOT always 1 or if it's changed in the future, let's go ahead - * and do the math for the actual size */ - size_t real_size = size * actual_size; - struct curl_cb_write_buf *buf = our_data; - size_t new_size = buf->size + real_size; - - if (new_size > MAX_DOWNLOAD_SIZE) { - ast_log(LOG_WARNING, "Attempted to retrieve certificate from %s failed " - "because it's size exceeds the maximum %d bytes\n", buf->url, MAX_DOWNLOAD_SIZE); - return 0; - } - - memcpy(&(buf->buf[buf->size]), curl_data, real_size); - buf->size += real_size; - buf->buf[buf->size] = 0; - - return real_size; -} - -static curl_socket_t stir_shaken_curl_open_socket_callback(void *our_data, curlsocktype purpose, struct curl_sockaddr *address) -{ - struct curl_cb_open_socket *data = our_data; - - if (!ast_acl_list_is_empty((struct ast_acl_list *)data->acl)) { - struct ast_sockaddr ast_address = { {0,} }; - - ast_sockaddr_copy_sockaddr(&ast_address, &address->addr, address->addrlen); - - if (ast_apply_acl((struct ast_acl_list *)data->acl, &ast_address, NULL) != AST_SENSE_ALLOW) { - return CURLE_COULDNT_CONNECT; - } - } - - *data->sockfd = socket(address->family, address->socktype, address->protocol); - - return *data->sockfd; -} - -char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data, const struct ast_acl_list *acl) -{ - FILE *public_key_file; - char *filename; - char *serial; - long http_code; - CURL *curl; - char curl_errbuf[CURL_ERROR_SIZE + 1]; - struct curl_cb_write_buf *buf; - struct curl_cb_open_socket *open_socket_data; - curl_socket_t sockfd; - - curl_errbuf[CURL_ERROR_SIZE] = '\0'; - - buf = ast_calloc(1, sizeof(*buf)); - if (!buf) { - ast_log(LOG_ERROR, "Failed to allocate memory for CURL write buffer for %s\n", public_cert_url); - return NULL; - } - - open_socket_data = ast_calloc(1, sizeof(*open_socket_data)); - if (!open_socket_data) { - ast_log(LOG_ERROR, "Failed to allocate memory for open socket callback\n"); - return NULL; - } - open_socket_data->acl = acl; - open_socket_data->sockfd = &sockfd; - - buf->url = public_cert_url; - curl_errbuf[CURL_ERROR_SIZE] = '\0'; - - curl = get_curl_instance(data); - if (!curl) { - ast_log(LOG_ERROR, "Failed to set up CURL instance for '%s'\n", public_cert_url); - ast_free(buf); - return NULL; - } - - curl_easy_setopt(curl, CURLOPT_URL, public_cert_url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf); - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); - curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, MAX_BUF_SIZE_PER_WRITE); - curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, stir_shaken_curl_open_socket_callback); - curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, open_socket_data); - - if (curl_easy_perform(curl)) { - ast_log(LOG_ERROR, "%s\n", curl_errbuf); - curl_easy_cleanup(curl); - ast_free(buf); - curl_cb_open_socket_free(open_socket_data); - return NULL; - } - - curl_cb_open_socket_free(open_socket_data); - - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - - curl_easy_cleanup(curl); - - if (http_code / 100 != 2) { - ast_log(LOG_ERROR, "Failed to retrieve URL '%s': code %ld\n", public_cert_url, http_code); - ast_free(buf); - return NULL; - } - - if (!ast_begins_with(buf->buf, BEGIN_CERTIFICATE_STR)) { - ast_log(LOG_WARNING, "Certificate from %s does not begin with what we expect\n", public_cert_url); - ast_free(buf); - return NULL; - } - - serial = stir_shaken_get_serial_number_x509(buf->buf, buf->size); - if (!serial) { - ast_log(LOG_ERROR, "Failed to get serial from CURL buffer from %s\n", public_cert_url); - ast_free(buf); - return NULL; - } - - if (ast_asprintf(&filename, "%s/%s.pem", path, serial) < 0) { - ast_log(LOG_ERROR, "Failed to allocate memory for filename after CURL from %s\n", public_cert_url); - ast_free(serial); - ast_free(buf); - return NULL; - } - - ast_free(serial); - - public_key_file = fopen(filename, "w"); - if (!public_key_file) { - ast_log(LOG_ERROR, "Failed to open file '%s' to write public key from '%s': %s (%d)\n", - filename, public_cert_url, strerror(errno), errno); - ast_free(buf); - ast_free(filename); - return NULL; - } - - if (fputs(buf->buf, public_key_file) == EOF) { - ast_log(LOG_ERROR, "Failed to write string to file from URL %s\n", public_cert_url); - fclose(public_key_file); - ast_free(buf); - ast_free(filename); - return NULL; - } - - fclose(public_key_file); - ast_free(buf); - - return filename; -} diff --git a/res/res_stir_shaken/curl.h b/res/res_stir_shaken/curl.h deleted file mode 100644 index 2dbd5d2346..0000000000 --- a/res/res_stir_shaken/curl.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Ben Ford - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ -#ifndef _STIR_SHAKEN_CURL_H -#define _STIR_SHAKEN_CURL_H - -struct ast_acl_list; - -/* Forward declaration for CURL callback data */ -struct curl_cb_data; - -/*! - * \brief Allocate memory for a curl_cb_data struct - * - * \note This will need to be freed by the consumer using curl_cb_data_free - * - * \retval NULL on failure - * \retval curl_cb_struct on success - */ -struct curl_cb_data *curl_cb_data_create(void); - -/*! - * \brief Free a curl_cb_data struct - * - * \param data The curl_cb_data struct to free - */ -void curl_cb_data_free(struct curl_cb_data *data); - -/*! - * \brief Get the cache_control field from a curl_cb_data struct - * - * \param data The curl_cb_data - * - * \retval cache_control on success - * \retval NULL otherwise - */ -char *curl_cb_data_get_cache_control(const struct curl_cb_data *data); - -/*! - * \brief Get the expires field from a curl_cb_data struct - * - * \param data The curl_cb_data - * - * \retval expires on success - * \retval NULL otherwise - */ -char *curl_cb_data_get_expires(const struct curl_cb_data *data); - -/*! - * \brief CURL the public key from the provided URL to the specified path - * - * \note The returned string will need to be freed by the caller - * - * \param public_cert_url The public cert URL - * \param path The path to download the file to - * \param data The curl_cb_data - * \param acl The ACL to use for cURL (if not NULL) - * - * \retval NULL on failure - * \retval full path filename on success - */ -char *curl_public_key(const char *public_cert_url, const char *path, struct curl_cb_data *data, const struct ast_acl_list *acl); - -#endif /* _STIR_SHAKEN_CURL_H */ diff --git a/res/res_stir_shaken/curl_utils.c b/res/res_stir_shaken/curl_utils.c new file mode 100644 index 0000000000..b4503f02b9 --- /dev/null +++ b/res/res_stir_shaken/curl_utils.c @@ -0,0 +1,344 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include + +#include "asterisk.h" +#include "asterisk/config.h" + +#include "curl_utils.h" + +void curl_header_data_free(void *obj) +{ + struct curl_header_data *cb_data = obj; + if (!cb_data) { + return; + } + ast_variables_destroy(cb_data->headers); + if (cb_data->debug_info) { + ast_free(cb_data->debug_info); + } + ast_free(cb_data); +} + +size_t curl_header_cb(char *data, size_t size, + size_t nitems, void *client_data) +{ + struct curl_header_data *cb_data = client_data; + size_t realsize = size * nitems; + size_t adjusted_size = realsize; + char *debug_info = S_OR(cb_data->debug_info, ""); + char *start = data; + char *colon = NULL; + struct ast_variable *h; + char *header; + char *value; + SCOPE_ENTER(5, "'%s': Header received with %zu bytes\n", + debug_info, realsize); + + if (cb_data->max_header_len == 0) { + cb_data->max_header_len = AST_CURL_DEFAULT_MAX_HEADER_LEN; + } + + if (realsize > cb_data->max_header_len) { + /* + * Silently ignore any header over the length limit. + */ + SCOPE_EXIT_RTN_VALUE(realsize, "oversize header: %zu > %zu\n", + realsize, cb_data->max_header_len); + } + + /* Per CURL: buffer may not be NULL terminated. */ + + /* Skip blanks */ + while (*start && ((unsigned char) *start) < 33 && start < data + realsize) { + start++; + adjusted_size--; + } + + if (adjusted_size < strlen("HTTP/") + 1) { + /* this is probably the \r\n\r\n sequence that ends the headers */ + cb_data->_capture = 0; + SCOPE_EXIT_RTN_VALUE(realsize, "undersized header. probably end-of-headers marker: %zu\n", + adjusted_size); + } + + /* + * We only want headers from a 2XX response + * so don't start capturing until we see + * the 2XX. + */ + if (ast_begins_with(start, "HTTP/")) { + int code; + /* + * HTTP/1.1 200 OK + * We want there to be a version after the HTTP/ + * and reason text after the code but we don't care + * what they are. + */ + int rc = sscanf(start, "HTTP/%*s %d %*s", &code); + if (rc == 1) { + if (code / 100 == 2) { + cb_data->_capture = 1; + } + } + SCOPE_EXIT_RTN_VALUE(realsize, "HTTP response code: %d\n", + code); + } + + if (!cb_data->_capture) { + SCOPE_EXIT_RTN_VALUE(realsize, "not capturing\n"); + } + + header = ast_alloca(adjusted_size + 1); + ast_copy_string(header, start, adjusted_size + 1); + + /* We have a NULL terminated string now */ + + colon = strchr(header, ':'); + if (!colon) { + SCOPE_EXIT_RTN_VALUE(realsize, "No colon in the header. Weird\n"); + } + + *colon++ = '\0'; + value = colon; + value = ast_skip_blanks(ast_trim_blanks(value)); + + h = ast_variable_new(header, value, __FILE__); + if (!h) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_WRITEFUNC_ERROR, LOG_WARNING, + "'%s': Unable to allocate memory for header '%s'\n", + debug_info, header); + } + ast_variable_list_append(&cb_data->headers, h); + + SCOPE_EXIT_RTN_VALUE(realsize, "header: <%s> value: <%s>", + header, value); +} + +void curl_write_data_free(void *obj) +{ + struct curl_write_data *cb_data = obj; + if (!cb_data) { + return; + } + if (cb_data->output) { + fclose(cb_data->output); + } + if (cb_data->debug_info) { + ast_free(cb_data->debug_info); + } + ast_std_free(cb_data->stream_buffer); + ast_free(cb_data); +} + +size_t curl_write_cb(char *data, size_t size, + size_t nmemb, void *client_data) +{ + struct curl_write_data *cb_data = client_data; + size_t realsize = size * nmemb; + size_t bytes_written = 0; + char *debug_info = S_OR(cb_data->debug_info, ""); + SCOPE_ENTER(5, "'%s': Writing data chunk of %zu bytes\n", + debug_info, realsize); + + if (!cb_data->output) { + cb_data->output = open_memstream( + &cb_data->stream_buffer, + &cb_data->stream_bytes_downloaded); + if (!cb_data->output) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_WRITEFUNC_ERROR, LOG_WARNING, + "'%s': Xfer failed. " + "open_memstream failed: %s\n", debug_info, strerror(errno)); + } + cb_data->_internal_memstream = 1; + } + + if (cb_data->max_download_bytes > 0 && + cb_data->stream_bytes_downloaded + realsize > + cb_data->max_download_bytes) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_WRITEFUNC_ERROR, LOG_WARNING, + "'%s': Xfer failed. " + "Exceeded maximum %zu bytes transferred\n", debug_info, + cb_data->max_download_bytes); + } + + bytes_written = fwrite(data, 1, realsize, cb_data->output); + cb_data->bytes_downloaded += bytes_written; + if (bytes_written != realsize) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_WRITEFUNC_ERROR, LOG_WARNING, + "'%s': Xfer failed. " + "Expected to write %zu bytes but wrote %zu\n", + debug_info, realsize, bytes_written); + } + + SCOPE_EXIT_RTN_VALUE(realsize, "Wrote %zu bytes\n", bytes_written); +} + +void curl_open_socket_data_free(void *obj) +{ + struct curl_open_socket_data *cb_data = obj; + if (!cb_data) { + return; + } + if (cb_data->debug_info) { + ast_free(cb_data->debug_info); + } + ast_free(cb_data); +} + +curl_socket_t curl_open_socket_cb(void *client_data, + curlsocktype purpose, struct curl_sockaddr *address) +{ + struct curl_open_socket_data *cb_data = client_data; + char *debug_info = S_OR(cb_data->debug_info, ""); + SCOPE_ENTER(5, "'%s': Opening socket\n", debug_info); + + if (!ast_acl_list_is_empty((struct ast_acl_list *)cb_data->acl)) { + struct ast_sockaddr ast_address = { {0,} }; + + ast_sockaddr_copy_sockaddr(&ast_address, &address->addr, address->addrlen); + + if (ast_apply_acl((struct ast_acl_list *)cb_data->acl, &ast_address, NULL) != AST_SENSE_ALLOW) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_SOCKET_BAD, LOG_WARNING, + "'%s': Unable to apply acl\n", debug_info); + } + } + + cb_data->sockfd = socket(address->family, address->socktype, address->protocol); + if (cb_data->sockfd < 0) { + SCOPE_EXIT_LOG_RTN_VALUE(CURL_SOCKET_BAD, LOG_WARNING, + "'%s': Failed to open socket: %s\n", debug_info, strerror(errno)); + } + + SCOPE_EXIT_RTN_VALUE(cb_data->sockfd, "Success"); +} + +long curler(const char *url, int request_timeout, + struct curl_write_data *write_data, + struct curl_header_data *header_data, + struct curl_open_socket_data *open_socket_data) +{ + RAII_VAR(CURL *, curl, NULL, curl_easy_cleanup); + long http_code = 0; + CURLcode rc; + + SCOPE_ENTER(1, "'%s': Retrieving\n", url); + + if (ast_strlen_zero(url)) { + SCOPE_EXIT_LOG_RTN_VALUE(500, LOG_ERROR, "'missing': url is missing\n"); + } + + if (!write_data) { + SCOPE_EXIT_LOG_RTN_VALUE(500, LOG_ERROR, "'%s': Either wite_cb and write_data are missing\n", url); + } + + curl = curl_easy_init(); + if (!curl) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "'%s': Failed to set up CURL instance\n", url); + } + + curl_easy_setopt(curl, CURLOPT_URL, url); + if (request_timeout) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT, request_timeout); + } + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, write_data); + + if (header_data) { + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_cb); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, header_data); + } + + curl_easy_setopt(curl, CURLOPT_USERAGENT, AST_CURL_USER_AGENT); + + if (open_socket_data) { + curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, curl_open_socket_cb); + curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, open_socket_data); + } + + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + /* + * ATIS-1000074 specifically says to NOT follow redirections. + */ + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0); + + rc = curl_easy_perform(curl); + if (rc != CURLE_OK) { + char *err = ast_strdupa(curl_easy_strerror(rc)); + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, "'%s': %s\n", url, err); + } + + fflush(write_data->output); + if (write_data->_internal_memstream) { + fclose(write_data->output); + write_data->output = NULL; + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + curl_easy_cleanup(curl); + curl = NULL; + + SCOPE_EXIT_RTN_VALUE(http_code, "'%s': Done: %ld\n", url, http_code); +} + +long curl_download_to_memory(const char *url, size_t *returned_length, + char **returned_data, struct ast_variable **headers) +{ + struct curl_write_data data = { + .debug_info = ast_strdupa(url), + }; + struct curl_header_data hdata = { + .debug_info = ast_strdupa(url), + }; + + long rc = curler(url, 0, &data, headers ? &hdata : NULL, NULL); + + *returned_length = data.stream_bytes_downloaded; + *returned_data = data.stream_buffer; + if (headers) { + *headers = hdata.headers; + } + + return rc; +} + +long curl_download_to_file(const char *url, char *filename) +{ + FILE *fp = NULL; + long rc = 0; + struct curl_write_data data = { + .debug_info = ast_strdup(url), + }; + + if (ast_strlen_zero(url) || ast_strlen_zero(filename)) { + ast_log(LOG_ERROR,"url or filename was NULL\n"); + return -1; + } + data.output = fopen(filename, "w"); + if (!fp) { + ast_log(LOG_ERROR,"Unable to open file '%s': %s\n", filename, + strerror(errno)); + return -1; + } + rc = curler(url, 0, &data, NULL, NULL); + fclose(data.output); + ast_free(data.debug_info); + return rc; +} + diff --git a/res/res_stir_shaken/curl_utils.h b/res/res_stir_shaken/curl_utils.h new file mode 100644 index 0000000000..2d23e87b64 --- /dev/null +++ b/res/res_stir_shaken/curl_utils.h @@ -0,0 +1,489 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef _CURL_UTILS_H +#define _CURL_UTILS_H + +#include +#include "asterisk/acl.h" + +#define AST_CURL_DEFAULT_MAX_HEADER_LEN 2048 + +#ifndef CURL_WRITEFUNC_ERROR +#define CURL_WRITEFUNC_ERROR 0 +#endif + +/*! \defgroup curl_wrappers CURL Convenience Wrappers + * @{ + +\section Overwiew Overview + +While libcurl is extremely flexible in what it allows you to do, +that flexibility comes at complexity price. The convenience wrappers +defined here aim to take away some of that complexity for run-of-the-mill +requests. + +\par A Basic Example + +If all you need to do is receive a document into a buffer... + +\code + char *url = "https://someurl"; + size_t returned_length; + char *returned_data = NULL; + + long rc = ast_curler_simple(url, &returned_length, &returned_data, NULL); + + ast_log(LOG_ERROR, "rc: %ld size: %zu doc: %.*s \n", + rc, returned_length, + (int)returned_length, returned_data); + ast_free(returned_data); +\endcode + +If you need the headers as well... + +\code + char *url = "https://someurl"; + size_t returned_length; + char *returned_data = NULL; + struct ast_variable *headers; + + long rc = ast_curler_simple(url, &returned_length, &returned_data, + &headers); + + ast_log(LOG_ERROR, "rc: %ld size: %zu doc: %.*s \n", + rc, returned_length, + (int)returned_length, returned_data); + + ast_free(returned_data); + ast_variables_destroy(headers); +\endcode + +\par A More Complex Example + +If you need more control, you can specify callbacks to capture +the response headers, do something other than write the data +to a memory buffer, or do some special socket manipulation like +check that the server's IP address matched an acl. + +Let's write the data to a file, capture the headers, +and make sure the server's IP address is whitelisted. + +The default callbacks can do that so all we need to do is +supply the data. + +\code + char *url = "http://something"; + + struct ast_curl_write_data data = { + .output = fopen("myfile.txt", "w"); + .debug_info = url, + }; + struct ast_curl_header_data hdata = { + .debug_info = url, + }; + struct ast_curl_open_socket_data osdata = { + .acl = my_acl_list, + .debug_info = url, + }; + struct ast_curl_optional_data opdata = { + .open_socket_cb = ast_curl_open_socket_cb, + .open_socket_data = &osdata, + }; + + long rc = ast_curler(url, 0, ast_curl_write_default_cb, &data, + ast_curl_header_default_cb, &hdata, &opdata); + + fclose(data.output); + ast_variables_destroy(hdata.headers); + +\endcode + +If you need even more control, you can supply your own +callbacks as well. This is a silly example of providing +your own write callback. It's basically what +ast_curler_write_to_file() does. + +\code +static size_t my_write_cb(char *data, size_t size, + size_t nmemb, void *client_data) +{ + FILE *fp = (FILE *)client_data; + return fwrite(data, size, nmemb, fp); +} + +static long myfunc(char *url, char *file) +{ + FILE *fp = fopen(file, "w"); + long rc = ast_curler(url, 0, my_write_cb, fp, NULL, NULL, NULL); + fclose(fp); + return rc; +} +\endcode + */ + +/*! + * \defgroup HeaderCallback Header Callback + * \ingroup curl_wrappers + * @{ + * + * If you need to access the headers returned on the response, + * you can define a callback that curl will call for every + * header it receives. + * + * Your callback must follow the specification defined for + * CURLOPT_HEADERFUNCTION and implement the curl_write_callback + * prototype. + * + * The following ast_curl_headers objects compose a default + * implementation that will accumulate the headers in an + * ast_variable list. + */ + +/*! + * + * \brief Context structure passed to \ref ast_curl_header_default_cb + * + */ +struct curl_header_data { + /*! + * curl's default max header length is 100k but we rarely + * need that much. It's also possible that a malicious remote + * server could send tons of 100k headers in an attempt to + * cause an out-of-memory condition. Setting this value + * will cause us to simply ignore any header with a length + * that exceeds it. If not set, the length defined in + * #AST_CURL_DEFAULT_MAX_HEADER_LEN will be used. + */ + size_t max_header_len; + /*! + * Identifying info placed at the start of log and trace messages. + */ + char *debug_info; + /*! + * This list will contain all the headers received. + * \note curl converts all header names to lower case. + */ + struct ast_variable *headers; + /*! + * \internal + * Private flag used to keep track of whether we're + * capturing headers or not. We only want them after + * we've seen an HTTP response code in the 2XX range + * and before the blank line that separaes the headers + * from the body. + */ + int _capture; +}; + +/*! + * \brief A default implementation of a header callback. + * + * This is an implementation of #CURLOPT_HEADERFUNCTION that performs + * basic sanity checks and saves headers in the + * ast_curl_header_data.headers ast_variable list. + * + * The curl prototype for this function is \ref curl_write_callback + * + * \warning If you decide to write your own callback, curl doesn't + * guarantee a terminating NULL in data passed to the callbacks! + * + * \param data Will contain a header line that may not be NULL terminated. + * \param size Always 1. + * \param nitems The number of bytes in data. + * \param client_data A pointer to whatever structure you passed to + * \ref ast_curler in the \p curl_header_data parameter. + * + * \return Number of bytes handled. Must be (size * nitems) or an + * error is signalled. + */ +size_t curl_header_cb(char *data, size_t size, + size_t nitems, void *client_data); + +void curl_header_data_free(void *obj); + +/*! + * @} + */ + +/*! + * \defgroup DataCallback Received Data Callback + * \ingroup curl_wrappers + * @{ + * + * If you need to do something with the data received other than + * save it in a memory buffer, you can define a callback that curl + * will call for each "chunk" of data it receives from the server. + * + * Your callback must follow the specification defined for + * CURLOPT_WRITEFUNCTION and implement the 'curl_write_callback' + * prototype. + * + * The following ast_curl_write objects compose a default + * implementation that will write the data to any FILE * + * descriptor you choose. + */ + +/*! + * \brief Context structure passed to \ref ast_curl_write_default_cb. + */ +struct curl_write_data { + /*! + * If this value is > 0, the request will be cancelled when + * \a bytes_downloaded exceeds it. + */ + size_t max_download_bytes; + /*! + * Where to write to. Could be anything you can get a FILE* for. + * A file opened with fopen, a buffer opened with open_memstream(), etc. + * Required by \ref ast_curl_write_default_cb. + */ + FILE *output; + /*! + * Identifying info placed at the start of log and trace messages. + */ + char *debug_info; + /*! + * Keeps track of the number of bytes read so far. + * This is updated by the callback regardless of + * whether the output stream is updating + * \ref stream_bytes_downloaded. + */ + size_t bytes_downloaded; + /*! + * A buffer to be used for anything the output stream needs. + * For instance, the address of this member can be passed to + * open_memstream which will update it as it reads data. When + * the memstream is flushed/closed, this will contain all of + * the data read so far. You must free this yourself with + * ast_std_free(). + */ + char *stream_buffer; + /*! + * Keeps track of the number of bytes read so far. + * Can be used by memstream. + */ + size_t stream_bytes_downloaded; + /*! + * \internal + * Set if we automatically opened a memstream + */ + int _internal_memstream; +}; + +/*! + * \brief A default implementation of a write data callback. + + * This is a default implementation of the function described + * by CURLOPT_WRITEFUNCTION that writes data received to a + * user-provided FILE *. This function is called by curl itself + * when it determines it has enough data to warrant a write. + * This may be influenced by the value of + * ast_curl_optional_data.per_write_buffer_size. + * See the CURLOPT_WRITEFUNCTION documentation for more info. + * + * The curl prototype for this function is 'curl_write_callback' + * + * \param data Data read by curl. + * \param size Always 1. + * \param nitems The number of bytes read. + * \param client_data A pointer to whatever structure you passed to + * \ref ast_curler in the \p curl_write_data parameter. + * + * \return Number of bytes handled. Must be (size * nitems) or an + * error is signalled. + */ +size_t curl_write_cb(char *data, size_t size, size_t nmemb, void *clientp); + +void curl_write_data_free(void *obj); + +/*! + * @} + */ + +/*! + * \defgroup OpenSocket Open Socket Callback + * \ingroup curl_wrappers + * @{ + * + * If you need to allocate the socket curl uses to make the + * request yourself or you need to do some checking on the + * request's resolved IP address, this is the callback for you. + * + * Your callback must follow the specification defined for + * CURLOPT_OPENSOCKETFUNCTION and implement the + * 'curl_opensocket_callback' prototype. + * + * The following ast_open_socket objects compose a default + * implementation that will not allow requests to servers + * not whitelisted in the provided ast_acl_list. + * + */ + +/*! + * \brief Context structure passed to \ref ast_curl_open_socket_default_cb + */ +struct curl_open_socket_data { + /*! + * The acl should provide a whitelist. Request to servers + * with addresses not allowed by the acl will be rejected. + */ + const struct ast_acl_list *acl; + /*! + * Identifying info placed at the start of log and trace messages. + */ + char *debug_info; + /*! + * \internal + * Set by the callback and passed to curl. + */ + curl_socket_t sockfd; +}; + +/*! + * \brief A default implementation of an open socket callback. + + * This is an implementation of the function described + * by CURLOPT_OPENSOCKETFUNCTION that checks the request's IP + * address against a user-supplied ast_acl_list and either rejects + * the request if the IP address isn't allowed, or opens a socket + * and returns it to curl. + * See the CURLOPT_OPENSOCKETFUNCTION documentation for more info. + * + * \param client_data A pointer to whatever structure you passed to + * \ref ast_curler in the \p curl_write_data parameter. + * \param purpose Will always be CURLSOCKTYPE_IPCXN + * \param address The request server's resolved IP address + * + * \return A socket opened by socket() or -1 to signal an error. + */ +curl_socket_t curl_open_socket_cb(void *client_data, + curlsocktype purpose, struct curl_sockaddr *address); + +void curl_open_socket_data_free(void *obj); + +/*! + * @} + */ + +/*! + * \defgroup OptionalData Optional Data + * \ingroup curl_wrappers + * @{ + + * \brief Structure pased to \ref ast_curler with infrequenty used + * control data. + */ +struct curl_optional_data { + /*! + * If not set, AST_CURL_USER_AGENT + * (defined in asterisk.h) will be used. + */ + const char *user_agent; + /*! + * Set this to limit the amount of data in each call to + * ast_curl_write_cb_t. + */ + size_t per_write_buffer_size; + /*! + * Set this to a custom function that has a matching + * prototype, set it to \ref ast_curl_open_socket_default_cb + * to use the default callback, or leave it at NULL + * to not use any callback. + * \note Will not be called if open_socket_data is NULL. + */ + curl_opensocket_callback curl_open_socket_cb; + /*! + * Set this to whatever your curl_open_socket_cb needs. + * If using \ref ast_curl_open_socket_default_cb, this MUST + * be set to an \ref ast_curl_open_socket_data structure. + * If set to NULL, curl_open_socket_cb will not be called. + */ + void *curl_open_socket_data; +}; + +/*! + * @} + */ + +/*! + * \defgroup requests Making Requests + * \ingroup curl_wrappers + * @{ + */ + +/*! + * \brief Perform a curl request. + * + * \param url The URL to request. + * \param request_timeout If > 0, timeout after this number of seconds. + * \param curl_write_data A pointer to a \ref curl_write_data structure. If + * curl_write_data.output is NULL, open_memstream will be called to + * provide one and the resulting data will be available in + * curl_write_data.stream_buffer with the number of bytes + * retrieved in curl_write_data.stream_bytes_downloaded. + * You must free curl_write_data.stream_buffer yourself with + * ast_std_free() when you no longer need it. + * \param curl_header_data A pointer to a \ref ast_curl_header_data structure. + * The headers read will be in the curl_header_data.headers + * ast_variable list which you must free with ast_variables_destroy() + * when you're done with them. + * \param curl_open_socket_data A pointer to an \ref curl_open_socket_data + * structure or NULL if you don't need it. + * \retval An HTTP response code. + * \retval -1 for internal error. + */ +long curler(const char *url, int request_timeout, + struct curl_write_data *write_data, + struct curl_header_data *header_data, + struct curl_open_socket_data *open_socket_data); + +/*! + * \brief Really simple document retrieval to memory + * + * \param url The URL to retrieve + * \param returned_length Pointer to a size_t to hold document length. + * \param returned_data Pointer to a buffer which will be updated to + * point to the data. Must be freed with ast_std_free() after use. + * \param headers Pointer to an ast_variable * that will contain + * the response headers. Must be freed with ast_variables_destroy() + * Set to NULL if you don't need the headers. + * \retval An HTTP response code. + * \retval -1 for internal error. + */ +long curl_download_to_memory(const char *url, size_t *returned_length, + char **returned_data, struct ast_variable **headers); + +/*! + * \brief Really simple document retrieval to file + * + * \param url The URL to retrieve. + * \param filename The filename to save it to. + * \retval An HTTP response code. + * \retval -1 for internal error. + */ +long curl_download_to_file(const char *url, char *filename); + +/*! + * @} + */ + +/*! + * @} + */ +#endif /* _CURL_UTILS_H */ diff --git a/res/res_stir_shaken/general.c b/res/res_stir_shaken/general.c deleted file mode 100644 index d241082411..0000000000 --- a/res/res_stir_shaken/general.c +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -#include "asterisk.h" - -#include "asterisk/cli.h" -#include "asterisk/sorcery.h" - -#include "stir_shaken.h" -#include "general.h" -#include "asterisk/res_stir_shaken.h" - -#define CONFIG_TYPE "general" - -#define DEFAULT_CA_FILE "" -#define DEFAULT_CA_PATH "" -#define DEFAULT_CACHE_MAX_SIZE 1000 -#define DEFAULT_CURL_TIMEOUT 2 -#define DEFAULT_SIGNATURE_TIMEOUT 15 - -struct stir_shaken_general { - SORCERY_OBJECT(details); - AST_DECLARE_STRING_FIELDS( - /*! File path to a certificate authority */ - AST_STRING_FIELD(ca_file); - /*! File path to a chain of trust */ - AST_STRING_FIELD(ca_path); - ); - /*! Maximum size of public keys cache */ - unsigned int cache_max_size; - /*! Maximum time to wait to CURL certificates */ - unsigned int curl_timeout; - /*! Amount of time a signature is valid for */ - unsigned int signature_timeout; -}; - -static struct stir_shaken_general *default_config = NULL; - -struct stir_shaken_general *stir_shaken_general_get() -{ - struct stir_shaken_general *cfg; - struct ao2_container *container; - - container = ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), CONFIG_TYPE, - AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); - if (!container || ao2_container_count(container) == 0) { - ao2_cleanup(container); - return ao2_bump(default_config); - } - - cfg = ao2_find(container, NULL, 0); - ao2_ref(container, -1); - - return cfg; -} - -const char *ast_stir_shaken_ca_file(const struct stir_shaken_general *cfg) -{ - return cfg ? cfg->ca_file : DEFAULT_CA_FILE; -} - -const char *ast_stir_shaken_ca_path(const struct stir_shaken_general *cfg) -{ - return cfg ? cfg->ca_path : DEFAULT_CA_PATH; -} - -unsigned int ast_stir_shaken_cache_max_size(const struct stir_shaken_general *cfg) -{ - return cfg ? cfg->cache_max_size : DEFAULT_CACHE_MAX_SIZE; -} - -unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg) -{ - return cfg ? cfg->curl_timeout : DEFAULT_CURL_TIMEOUT; -} - -unsigned int ast_stir_shaken_signature_timeout(const struct stir_shaken_general *cfg) -{ - return cfg ? cfg->signature_timeout : DEFAULT_SIGNATURE_TIMEOUT; -} - -static void stir_shaken_general_destructor(void *obj) -{ - struct stir_shaken_general *cfg = obj; - - ast_string_field_free_memory(cfg); -} - -static void *stir_shaken_general_alloc(const char *name) -{ - struct stir_shaken_general *cfg; - - cfg = ast_sorcery_generic_alloc(sizeof(*cfg), stir_shaken_general_destructor); - if (!cfg) { - return NULL; - } - - if (ast_string_field_init(cfg, 512)) { - ao2_ref(cfg, -1); - return NULL; - } - - return cfg; -} - -static int stir_shaken_general_apply(const struct ast_sorcery *sorcery, void *obj) -{ - return 0; -} - -static void stir_shaken_general_loaded(const char *name, const struct ast_sorcery *sorcery, - const char *object_type, int reloaded) -{ - struct stir_shaken_general *cfg; - - if (strcmp(object_type, CONFIG_TYPE)) { - /* Not interested */ - return; - } - - if (default_config) { - ao2_ref(default_config, -1); - default_config = NULL; - } - - cfg = stir_shaken_general_get(); - if (cfg) { - ao2_ref(cfg, -1); - return; - } - - /* Use the default configuration if on is not specified */ - default_config = ast_sorcery_alloc(sorcery, CONFIG_TYPE, NULL); - if (default_config) { - stir_shaken_general_apply(sorcery, default_config); - } -} - -static const struct ast_sorcery_instance_observer stir_shaken_general_observer = { - .object_type_loaded = stir_shaken_general_loaded, -}; - -static char *stir_shaken_general_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct stir_shaken_general *cfg; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show general"; - e->usage = - "Usage: stir_shaken show general\n" - " Show the general stir/shaken settings\n"; - return NULL; - case CLI_GENERATE: - return NULL; - } - - if (a->argc != 3) { - return CLI_SHOWUSAGE; - } - - cfg = stir_shaken_general_get(); - stir_shaken_cli_show(cfg, a, 0); - ao2_cleanup(cfg); - - return CLI_SUCCESS; -} - -static struct ast_cli_entry stir_shaken_general_cli[] = { - AST_CLI_DEFINE(stir_shaken_general_show, "Show stir/shaken general configuration"), -}; - -static int on_load_ca_file(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_general *cfg = obj; - - if (!ast_file_is_readable(var->value)) { - ast_log(LOG_ERROR, "stir/shaken - %s '%s' not found, or is unreadable\n", - var->name, var->value); - return -1; - } - - return ast_string_field_set(cfg, ca_file, var->value); -} - -static int ca_file_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_general *cfg = obj; - - *buf = ast_strdup(cfg->ca_file); - - return 0; -} - -static int on_load_ca_path(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_general *cfg = obj; - - if (!ast_file_is_readable(var->value)) { - ast_log(LOG_ERROR, "stir/shaken - %s '%s' not found, or is unreadable\n", - var->name, var->value); - return -1; - } - - return ast_string_field_set(cfg, ca_path, var->value); -} - -static int ca_path_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_general *cfg = obj; - - *buf = ast_strdup(cfg->ca_path); - - return 0; -} - -int stir_shaken_general_unload(void) -{ - ast_cli_unregister_multiple(stir_shaken_general_cli, - ARRAY_LEN(stir_shaken_general_cli)); - - ast_sorcery_instance_observer_remove(ast_stir_shaken_sorcery(), - &stir_shaken_general_observer); - - if (default_config) { - ao2_ref(default_config, -1); - default_config = NULL; - } - - return 0; -} - -int stir_shaken_general_load(void) -{ - struct ast_sorcery *sorcery = ast_stir_shaken_sorcery(); - - ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", - "stir_shaken.conf,criteria=type=general,single_object=yes,explicit_name=general"); - - if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, stir_shaken_general_alloc, - NULL, stir_shaken_general_apply)) { - ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); - return -1; - } - - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "ca_file", - DEFAULT_CA_FILE, on_load_ca_file, ca_file_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "ca_path", - DEFAULT_CA_PATH, on_load_ca_path, ca_path_to_str, NULL, 0, 0); - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "cache_max_size", - __stringify(DEFAULT_CACHE_MAX_SIZE), OPT_UINT_T, 0, - FLDSET(struct stir_shaken_general, cache_max_size)); - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "curl_timeout", - __stringify(DEFAULT_CURL_TIMEOUT), OPT_UINT_T, 0, - FLDSET(struct stir_shaken_general, curl_timeout)); - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "signature_timeout", - __stringify(DEFAULT_SIGNATURE_TIMEOUT), OPT_UINT_T, 0, - FLDSET(struct stir_shaken_general, signature_timeout)); - - if (ast_sorcery_instance_observer_add(sorcery, &stir_shaken_general_observer)) { - ast_log(LOG_ERROR, "stir/shaken - failed to register loaded observer for '%s' " - "sorcery object type\n", CONFIG_TYPE); - return -1; - } - - ast_cli_register_multiple(stir_shaken_general_cli, - ARRAY_LEN(stir_shaken_general_cli)); - - return 0; -} diff --git a/res/res_stir_shaken/general.h b/res/res_stir_shaken/general.h deleted file mode 100644 index 3ea1d693f4..0000000000 --- a/res/res_stir_shaken/general.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ -#ifndef _STIR_SHAKEN_GENERAL_H -#define _STIR_SHAKEN_GENERAL_H - -struct ast_sorcery; - -/*! - * \brief General configuration for stir/shaken - */ -struct stir_shaken_general; - -/*! - * \brief Retrieve the stir/shaken 'general' configuration object - * - * A default configuration object is returned if no configuration was specified. - * As well, NULL can be returned if there is no configuration, and a problem - * occurred while loading the defaults. - * - * \note Object is returned with a reference that the caller is responsible - * for de-referencing. - * - * \retval A 'general' configuration object, or NULL - */ -struct stir_shaken_general *stir_shaken_general_get(void); - -/*! - * \brief Retrieve the 'ca_file' general configuration option value - * - * \note If a NULL configuration is given, then the default value is returned - * - * \param cfg A 'general' configuration object - * - * \retval The 'ca_file' value - */ -const char *ast_stir_shaken_ca_file(const struct stir_shaken_general *cfg); - -/*! - * \brief Retrieve the 'ca_path' general configuration option value - * - * \note If a NULL configuration is given, then the default value is returned - * - * \param cfg A 'general' configuration object - * - * \retval The 'ca_path' value - */ -const char *ast_stir_shaken_ca_path(const struct stir_shaken_general *cfg); - -/*! - * \brief Retrieve the 'cache_max_size' general configuration option value - * - * \note If a NULL configuration is given, then the default value is returned - * - * \param cfg A 'general' configuration object - * - * \retval The 'cache_max_size' value - */ -unsigned int ast_stir_shaken_cache_max_size(const struct stir_shaken_general *cfg); - -/*! - * \brief Retrieve the 'curl_timeout' general configuration option value - * - * \note If a NULL configuration is given, then the default value is returned - * - * \param cfg A 'general' configuration object - * - * \retval The 'curl_timeout' value - */ -unsigned int ast_stir_shaken_curl_timeout(const struct stir_shaken_general *cfg); - -/*! - * \brief Retrieve the 'signature_timeout' general configuration option value - * - * \note if a NULL configuration is given, then the default value is returned - * - * \param cfg A 'general' configuration object - * - * \retval The 'signature_timeout' value - */ -unsigned int ast_stir_shaken_signature_timeout(const struct stir_shaken_general *cfg); - -/*! - * \brief Load time initialization for the stir/shaken 'general' configuration - * - * \retval 0 on success, -1 on error - */ -int stir_shaken_general_load(void); - -/*! - * \brief Unload time cleanup for the stir/shaken 'general' configuration - * - * \retval 0 on success, -1 on error - */ -int stir_shaken_general_unload(void); - -#endif /* _STIR_SHAKEN_GENERAL_H */ diff --git a/res/res_stir_shaken/profile.c b/res/res_stir_shaken/profile.c deleted file mode 100644 index 5d4fa9b838..0000000000 --- a/res/res_stir_shaken/profile.c +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2022, Sangoma Technologies Corporation - * - * Ben Ford - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -#include "asterisk.h" - -#include "asterisk/cli.h" -#include "asterisk/sorcery.h" - -#include "stir_shaken.h" -#include "profile.h" -#include "asterisk/res_stir_shaken.h" - -#define CONFIG_TYPE "profile" - -static void stir_shaken_profile_destructor(void *obj) -{ - struct stir_shaken_profile *cfg = obj; - - ast_free_acl_list(cfg->acl); - - return; -} - -static void *stir_shaken_profile_alloc(const char *name) -{ - struct stir_shaken_profile *cfg; - - cfg = ast_sorcery_generic_alloc(sizeof(*cfg), stir_shaken_profile_destructor); - if (!cfg) { - return NULL; - } - - return cfg; -} - -static struct stir_shaken_profile *stir_shaken_profile_get(const char *id) -{ - return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, id); -} - -static struct ao2_container *stir_shaken_profile_get_all(void) -{ - return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), CONFIG_TYPE, - AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); -} - -struct stir_shaken_profile *ast_stir_shaken_get_profile_by_name(const char *name) -{ - return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, name); -} - -static int stir_shaken_profile_apply(const struct ast_sorcery *sorcery, void *obj) -{ - return 0; -} - -static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_profile *cfg = obj; - - if (!strcasecmp("attest", var->value)) { - cfg->stir_shaken = STIR_SHAKEN_ATTEST; - } else if (!strcasecmp("verify", var->value)) { - cfg->stir_shaken = STIR_SHAKEN_VERIFY; - } else if (!strcasecmp("on", var->value)) { - cfg->stir_shaken = STIR_SHAKEN_ON; - } else { - ast_log(LOG_WARNING, "'%s' is not a valid value for option " - "'stir_shaken' for %s %s\n", - var->value, CONFIG_TYPE, ast_sorcery_object_get_id(cfg)); - return -1; - } - - return 0; -} - -static const char *stir_shaken_map[] = { - [STIR_SHAKEN_ATTEST] = "attest", - [STIR_SHAKEN_VERIFY] = "verify", - [STIR_SHAKEN_ON] = "on", -}; - -static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_profile *cfg = obj; - if (ARRAY_IN_BOUNDS(cfg->stir_shaken, stir_shaken_map)) { - *buf = ast_strdup(stir_shaken_map[cfg->stir_shaken]); - } - return 0; -} - -static int stir_shaken_acl_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_profile *cfg = obj; - int error = 0; - int ignore; - - if (ast_strlen_zero(var->value)) { - return 0; - } - - ast_append_acl(var->name, var->value, &cfg->acl, &error, &ignore); - - return error; -} - -static int acl_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_profile *cfg = obj; - struct ast_acl_list *acl_list; - struct ast_acl *first_acl; - - if (cfg && !ast_acl_list_is_empty(acl_list=cfg->acl)) { - AST_LIST_LOCK(acl_list); - first_acl = AST_LIST_FIRST(acl_list); - if (ast_strlen_zero(first_acl->name)) { - *buf = "deny/permit"; - } else { - *buf = first_acl->name; - } - AST_LIST_UNLOCK(acl_list); - } - - *buf = ast_strdup(*buf); - return 0; -} - -static char *stir_shaken_profile_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct stir_shaken_profile *cfg; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show profile"; - e->usage = - "Usage: stir_shaken show profile \n" - " Show the stir/shaken profile settings for a given id\n"; - return NULL; - case CLI_GENERATE: - if (a->pos == 3) { - return stir_shaken_tab_complete_name(a->word, stir_shaken_profile_get_all()); - } else { - return NULL; - } - } - - if (a->argc != 4) { - return CLI_SHOWUSAGE; - } - - cfg = stir_shaken_profile_get(a->argv[3]); - stir_shaken_cli_show(cfg, a, 0); - ast_acl_output(a->fd, cfg->acl, NULL); - ao2_cleanup(cfg); - - return CLI_SUCCESS; -} - -static char *stir_shaken_profile_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct ao2_container *container; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show profiles"; - e->usage = - "Usage: stir_shaken show profiles\n" - " Show all profiles for stir/shaken\n"; - return NULL; - case CLI_GENERATE: - return NULL; - } - - if (a->argc != 3) { - return CLI_SHOWUSAGE; - } - - container = stir_shaken_profile_get_all(); - if (!container || ao2_container_count(container) == 0) { - ast_cli(a->fd, "No stir/shaken ACLs found\n"); - ao2_cleanup(container); - return CLI_SUCCESS; - } - - ao2_callback(container, OBJ_NODATA, stir_shaken_cli_show, a); - ao2_ref(container, -1); - - return CLI_SUCCESS; -} - -static struct ast_cli_entry stir_shaken_profile_cli[] = { - AST_CLI_DEFINE(stir_shaken_profile_show, "Show stir/shaken profile by id"), - AST_CLI_DEFINE(stir_shaken_profile_show_all, "Show all stir/shaken profiles"), -}; - -int stir_shaken_profile_unload(void) -{ - ast_cli_unregister_multiple(stir_shaken_profile_cli, - ARRAY_LEN(stir_shaken_profile_cli)); - - return 0; -} - -int stir_shaken_profile_load(void) -{ - struct ast_sorcery *sorcery = ast_stir_shaken_sorcery(); - - ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=profile"); - - if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, stir_shaken_profile_alloc, - NULL, stir_shaken_profile_apply)) { - ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); - return -1; - } - - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "stir_shaken", "on", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "deny", "", stir_shaken_acl_handler, NULL, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "permit", "", stir_shaken_acl_handler, NULL, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "acllist", "", stir_shaken_acl_handler, acl_to_str, NULL, 0, 0); - - ast_cli_register_multiple(stir_shaken_profile_cli, - ARRAY_LEN(stir_shaken_profile_cli)); - - return 0; -} diff --git a/res/res_stir_shaken/profile.h b/res/res_stir_shaken/profile.h deleted file mode 100644 index 5617e9ad1e..0000000000 --- a/res/res_stir_shaken/profile.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2022, Sangoma Technologies Corporation - * - * Ben Ford - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ -#ifndef _STIR_SHAKEN_PROFILE_H -#define _STIR_SHAKEN_PROFILE_H - -#include "profile_private.h" - -struct stir_shaken_profile *ast_stir_shaken_get_profile_by_name(const char *name); - -/*! - * \brief Load time initialization for the stir/shaken 'profile' object - * - * \retval 0 on success, -1 on error - */ -int stir_shaken_profile_load(void); - -/*! - * \brief Unload time cleanup for the stir/shaken 'profile' - * - * \retval 0 on success, -1 on error - */ -int stir_shaken_profile_unload(void); - -#endif /* _STIR_SHAKEN_PROFILE_H */ diff --git a/res/res_stir_shaken/profile_config.c b/res/res_stir_shaken/profile_config.c new file mode 100644 index 0000000000..e892fb9991 --- /dev/null +++ b/res/res_stir_shaken/profile_config.c @@ -0,0 +1,471 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2022, Sangoma Technologies Corporation + * + * Ben Ford + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include "asterisk/cli.h" +#include "asterisk/sorcery.h" +#include "asterisk/acl.h" +#include "asterisk/stasis.h" +#include "asterisk/security_events.h" + +#include "stir_shaken.h" + +#define CONFIG_TYPE "profile" + +#define DEFAULT_endpoint_behavior endpoint_behavior_OFF + +#define DEFAULT_ca_file NULL +#define DEFAULT_ca_path NULL +#define DEFAULT_crl_file NULL +#define DEFAULT_crl_path NULL +#define DEFAULT_cert_cache_dir NULL + +#define DEFAULT_curl_timeout 0 +#define DEFAULT_max_iat_age 0 +#define DEFAULT_max_date_header_age 0 +#define DEFAULT_max_cache_entry_age 0 +#define DEFAULT_max_cache_size 0 + +#define DEFAULT_stir_shaken_failure_action stir_shaken_failure_action_NOT_SET +#define DEFAULT_use_rfc9410_responses use_rfc9410_responses_NOT_SET +#define DEFAULT_relax_x5u_port_scheme_restrictions relax_x5u_port_scheme_restrictions_NOT_SET +#define DEFAULT_relax_x5u_path_restrictions relax_x5u_path_restrictions_NOT_SET +#define DEFAULT_load_system_certs load_system_certs_NOT_SET + +#define DEFAULT_check_tn_cert_public_url check_tn_cert_public_url_NOT_SET +#define DEFAULT_private_key_file NULL +#define DEFAULT_public_cert_url NULL +#define DEFAULT_attest_level attest_level_NOT_SET +#define DEFAULT_send_mky send_mky_NOT_SET + +static void profile_destructor(void *obj) +{ + struct profile_cfg *cfg = obj; + ast_string_field_free_memory(cfg); + + acfg_cleanup(&cfg->acfg_common); + vcfg_cleanup(&cfg->vcfg_common); + + ao2_cleanup(cfg->eprofile); + + return; +} + +static void *profile_alloc(const char *name) +{ + struct profile_cfg *profile; + + profile = ast_sorcery_generic_alloc(sizeof(*profile), profile_destructor); + if (!profile) { + return NULL; + } + + if (ast_string_field_init(profile, 2048)) { + ao2_ref(profile, -1); + return NULL; + } + + /* + * The memory for the commons actually comes from cfg + * due to the weirdness of the STRFLDSET macro used with + * sorcery. We just use a token amount of memory in + * this call so the initialize doesn't fail. + */ + if (ast_string_field_init(&profile->acfg_common, 8)) { + ao2_ref(profile, -1); + return NULL; + } + + if (ast_string_field_init(&profile->vcfg_common, 8)) { + ao2_ref(profile, -1); + return NULL; + } + + return profile; +} + +static struct ao2_container *profile_get_all(void) +{ + return ast_sorcery_retrieve_by_fields(get_sorcery(), CONFIG_TYPE, + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); +} + +struct profile_cfg *profile_get_cfg(const char *id) +{ + if (ast_strlen_zero(id)) { + return NULL; + } + return ast_sorcery_retrieve_by_id(get_sorcery(), CONFIG_TYPE, id); +} + +static struct ao2_container *eprofile_get_all(void) +{ + return ast_sorcery_retrieve_by_fields(get_sorcery(), "eprofile", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); +} + +struct profile_cfg *eprofile_get_cfg(const char *id) +{ + if (ast_strlen_zero(id)) { + return NULL; + } + return ast_sorcery_retrieve_by_id(get_sorcery(), "eprofile", id); +} + +static struct profile_cfg *create_effective_profile( + struct profile_cfg *base_profile) +{ + struct profile_cfg *eprofile; + struct profile_cfg *existing_eprofile; + RAII_VAR(struct attestation_cfg*, acfg, as_get_cfg(), ao2_cleanup); + RAII_VAR(struct verification_cfg*, vcfg, vs_get_cfg(), ao2_cleanup); + const char *id = ast_sorcery_object_get_id(base_profile); + int rc = 0; + + eprofile = ast_sorcery_alloc(get_sorcery(), "eprofile", id); + if (!eprofile) { + ast_log(LOG_ERROR, "%s: Unable to allocate memory for effective profile\n", id); + return NULL; + } + + rc = vs_copy_cfg_common(id, &eprofile->vcfg_common, + &vcfg->vcfg_common); + if (rc != 0) { + ao2_cleanup(eprofile); + return NULL; + } + + rc = vs_copy_cfg_common(id, &eprofile->vcfg_common, + &base_profile->vcfg_common); + if (rc != 0) { + ao2_cleanup(eprofile); + return NULL; + } + + rc = as_copy_cfg_common(id, &eprofile->acfg_common, + &acfg->acfg_common); + if (rc != 0) { + ao2_cleanup(eprofile); + return NULL; + } + + rc = as_copy_cfg_common(id, &eprofile->acfg_common, + &base_profile->acfg_common); + if (rc != 0) { + ao2_cleanup(eprofile); + return NULL; + } + + eprofile->endpoint_behavior = base_profile->endpoint_behavior; + + if (eprofile->endpoint_behavior == endpoint_behavior_ON) { + if (acfg->global_disable && vcfg->global_disable) { + eprofile->endpoint_behavior = endpoint_behavior_OFF; + } else if (acfg->global_disable && !vcfg->global_disable) { + eprofile->endpoint_behavior = endpoint_behavior_VERIFY; + } else if (!acfg->global_disable && vcfg->global_disable) { + eprofile->endpoint_behavior = endpoint_behavior_ATTEST; + } + } else if (eprofile->endpoint_behavior == endpoint_behavior_ATTEST + && acfg->global_disable) { + eprofile->endpoint_behavior = endpoint_behavior_OFF; + } else if (eprofile->endpoint_behavior == endpoint_behavior_VERIFY + && vcfg->global_disable) { + eprofile->endpoint_behavior = endpoint_behavior_OFF; + } + + existing_eprofile = ast_sorcery_retrieve_by_id(get_sorcery(), "eprofile", id); + if (existing_eprofile) { + ao2_cleanup(existing_eprofile); + ast_sorcery_update(get_sorcery(), eprofile); + } else { + ast_sorcery_create(get_sorcery(), eprofile); + } + + /* + * This triggers eprofile_apply. We _could_ just call + * eprofile_apply directly but this seems more keeping + * with how sorcery works. + */ + ast_sorcery_objectset_apply(get_sorcery(), eprofile, NULL); + + return eprofile; +} + +static int profile_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct profile_cfg *cfg = obj; + const char *id = ast_sorcery_object_get_id(cfg); + + if (PROFILE_ALLOW_ATTEST(cfg) + && as_check_common_config(id, &cfg->acfg_common) != 0) { + return -1; + } + + if (PROFILE_ALLOW_VERIFY(cfg) + && vs_check_common_config(id, &cfg->vcfg_common) !=0) { + return -1; + } + + cfg->eprofile = create_effective_profile(cfg); + if (!cfg->eprofile) { + return -1; + } + + return 0; +} + +static int eprofile_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct profile_cfg *cfg = obj; + const char *id = ast_sorcery_object_get_id(cfg); + + if (PROFILE_ALLOW_VERIFY(cfg) && !cfg->vcfg_common.tcs) { + ast_log(LOG_ERROR, "%s: Neither this profile nor default" + " verification options specify ca_file or ca_path\n", id); + return -1; + } + + return 0; +} +generate_acfg_common_sorcery_handlers(profile_cfg); +generate_vcfg_common_sorcery_handlers(profile_cfg); + +generate_sorcery_enum_from_str(profile_cfg, , endpoint_behavior, UNKNOWN); +generate_sorcery_enum_to_str(profile_cfg, , endpoint_behavior); + +static char *cli_profile_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct profile_cfg *profile; + struct config_object_cli_data data = { + .title = "Profile", + .object_type = config_object_type_profile, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show profile"; + e->usage = + "Usage: stir_shaken show profile \n" + " Show the stir/shaken profile settings for a given id\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 3) { + return config_object_tab_complete_name(a->word, profile_get_all()); + } else { + return NULL; + } + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + profile = profile_get_cfg(a->argv[3]); + if (!profile) { + ast_log(LOG_ERROR,"Profile %s doesn't exist\n", a->argv[3]); + return CLI_FAILURE; + } + config_object_cli_show(profile, a, &data, 0); + + ao2_cleanup(profile); + + return CLI_SUCCESS; +} + +static char *cli_profile_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_container *container; + struct config_object_cli_data data = { + .title = "Profile", + .object_type = config_object_type_profile, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show profiles"; + e->usage = + "Usage: stir_shaken show profiles\n" + " Show all profiles for stir/shaken\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + container = profile_get_all(); + if (!container || ao2_container_count(container) == 0) { + ast_cli(a->fd, "No stir/shaken profiles found\n"); + ao2_cleanup(container); + return CLI_SUCCESS; + } + + ao2_callback_data(container, OBJ_NODATA, config_object_cli_show, a, &data); + ao2_ref(container, -1); + + return CLI_SUCCESS; +} + +static char *cli_eprofile_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct profile_cfg *profile; + struct config_object_cli_data data = { + .title = "Effective Profile", + .object_type = config_object_type_profile, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show eprofile"; + e->usage = + "Usage: stir_shaken show eprofile \n" + " Show the stir/shaken eprofile settings for a given id\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 3) { + return config_object_tab_complete_name(a->word, eprofile_get_all()); + } else { + return NULL; + } + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + profile = eprofile_get_cfg(a->argv[3]); + if (!profile) { + ast_log(LOG_ERROR,"Effective Profile %s doesn't exist\n", a->argv[3]); + return CLI_FAILURE; + } + config_object_cli_show(profile, a, &data, 0); + + ao2_cleanup(profile); + + return CLI_SUCCESS; +} + +static char *cli_eprofile_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_container *container; + struct config_object_cli_data data = { + .title = "Effective Profile", + .object_type = config_object_type_profile, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show eprofiles"; + e->usage = + "Usage: stir_shaken show eprofiles\n" + " Show all eprofiles for stir/shaken\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + container = eprofile_get_all(); + if (!container || ao2_container_count(container) == 0) { + ast_cli(a->fd, "No stir/shaken eprofiles found\n"); + ao2_cleanup(container); + return CLI_SUCCESS; + } + + ao2_callback_data(container, OBJ_NODATA, config_object_cli_show, a, &data); + ao2_ref(container, -1); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry stir_shaken_profile_cli[] = { + AST_CLI_DEFINE(cli_profile_show, "Show stir/shaken profile by id"), + AST_CLI_DEFINE(cli_profile_show_all, "Show all stir/shaken profiles"), + AST_CLI_DEFINE(cli_eprofile_show, "Show stir/shaken eprofile by id"), + AST_CLI_DEFINE(cli_eprofile_show_all, "Show all stir/shaken eprofiles"), +}; + +int profile_reload(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + ast_sorcery_force_reload_object(sorcery, CONFIG_TYPE); + ast_sorcery_force_reload_object(sorcery, "eprofile"); + return 0; +} + +int profile_unload(void) +{ + ast_cli_unregister_multiple(stir_shaken_profile_cli, + ARRAY_LEN(stir_shaken_profile_cli)); + + return 0; +} + +int profile_load(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + enum ast_sorcery_apply_result apply_rc; + + /* + * eprofile MUST be registered first because profile needs it. + */ + apply_rc = ast_sorcery_apply_default(sorcery, "eprofile", "memory", NULL); + if (apply_rc != AST_SORCERY_APPLY_SUCCESS) { + abort(); + } + if (ast_sorcery_internal_object_register(sorcery, "eprofile", + profile_alloc, NULL, eprofile_apply)) { + ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", "eprofile"); + return -1; + } + + ast_sorcery_object_field_register_nodoc(sorcery, "eprofile", "type", "", OPT_NOOP_T, 0, 0); + enum_option_register(sorcery, "eprofile", endpoint_behavior, _nodoc); + register_common_verification_fields(sorcery, profile_cfg, "eprofile", _nodoc); + register_common_attestation_fields(sorcery, profile_cfg, "eprofile", _nodoc); + + /* + * Now we can do profile + */ + ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=profile"); + if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, profile_alloc, + NULL, profile_apply)) { + ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); + return -1; + } + + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0); + enum_option_register(sorcery, CONFIG_TYPE, endpoint_behavior,); + register_common_verification_fields(sorcery, profile_cfg, CONFIG_TYPE,); + register_common_attestation_fields(sorcery, profile_cfg, CONFIG_TYPE,); + + ast_sorcery_load_object(sorcery, CONFIG_TYPE); + ast_sorcery_load_object(sorcery, "eprofile"); + + ast_cli_register_multiple(stir_shaken_profile_cli, + ARRAY_LEN(stir_shaken_profile_cli)); + + return 0; +} diff --git a/res/res_stir_shaken/profile_private.h b/res/res_stir_shaken/profile_private.h deleted file mode 100644 index 536a0fe1a2..0000000000 --- a/res/res_stir_shaken/profile_private.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2022, Sangoma Technologies Corporation - * - * Ben Ford - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ -#ifndef _STIR_SHAKEN_PROFILE_PRIVATE_H -#define _STIR_SHAKEN_PROFILE_PRIVATE_H - -#include "asterisk/sorcery.h" - -#include "asterisk/acl.h" - -enum stir_shaken_profile_behavior { - /*! Only do STIR/SHAKEN attestation */ - STIR_SHAKEN_ATTEST = 1, - /*! Only do STIR/SHAKEN verification */ - STIR_SHAKEN_VERIFY = 2, - /*! Do STIR/SHAKEN attestation and verification */ - STIR_SHAKEN_ON = 3, -}; - -struct stir_shaken_profile { - SORCERY_OBJECT(details); - unsigned int stir_shaken; - struct ast_acl_list *acl; -}; - -#endif /* _STIR_SHAKEN_PROFILE_PRIVATE_H */ diff --git a/res/res_stir_shaken/stir_shaken.c b/res/res_stir_shaken/stir_shaken.c deleted file mode 100644 index 9e17f7c56f..0000000000 --- a/res/res_stir_shaken/stir_shaken.c +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -/*! \file - * - * \brief Internal stir/shaken utilities - */ - -#include "asterisk.h" - -#include -#include - -#include "asterisk/cli.h" -#include "asterisk/sorcery.h" - -#include "stir_shaken.h" -#include "asterisk/res_stir_shaken.h" - -int stir_shaken_cli_show(void *obj, void *arg, int flags) -{ - struct ast_cli_args *a = arg; - struct ast_variable *options; - struct ast_variable *i; - - if (!obj) { - ast_cli(a->fd, "No stir/shaken configuration found\n"); - return 0; - } - - options = ast_variable_list_sort(ast_sorcery_objectset_create2( - ast_stir_shaken_sorcery(), obj, AST_HANDLER_ONLY_STRING)); - if (!options) { - return 0; - } - - ast_cli(a->fd, "%s: %s\n", ast_sorcery_object_get_type(obj), - ast_sorcery_object_get_id(obj)); - - for (i = options; i; i = i->next) { - ast_cli(a->fd, "\t%s: %s\n", i->name, i->value); - } - - ast_cli(a->fd, "\n"); - - ast_variables_destroy(options); - - return 0; -} - -char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *container) -{ - void *obj; - struct ao2_iterator it; - int wordlen = strlen(word); - int ret; - - it = ao2_iterator_init(container, 0); - while ((obj = ao2_iterator_next(&it))) { - if (!strncasecmp(word, ast_sorcery_object_get_id(obj), wordlen)) { - ret = ast_cli_completion_add(ast_strdup(ast_sorcery_object_get_id(obj))); - if (ret) { - ao2_ref(obj, -1); - break; - } - } - ao2_ref(obj, -1); - } - ao2_iterator_destroy(&it); - - return NULL; -} - -EVP_PKEY *stir_shaken_read_key(const char *path, int priv) -{ - EVP_PKEY *key = NULL; - FILE *fp; - X509 *cert = NULL; - - fp = fopen(path, "r"); - if (!fp) { - ast_log(LOG_ERROR, "Failed to read %s key file '%s'\n", priv ? "private" : "public", path); - return NULL; - } - - /* If this is to get the private key, the file will be ECDSA or RSA, with the former eventually - * replacing the latter. For the public key, the file will be X.509. - */ - if (priv) { - key = PEM_read_PrivateKey(fp, NULL, NULL, NULL); - } else { - cert = PEM_read_X509(fp, NULL, NULL, NULL); - if (!cert) { - ast_log(LOG_ERROR, "Failed to read X.509 cert from file '%s'\n", path); - fclose(fp); - return NULL; - } - key = X509_get_pubkey(cert); - /* It's fine to free the cert after we get the key because they are 2 - * independent objects; you don't need a X509 object to be in memory - * in order to have an EVP_PKEY, and it doesn't rely on it being there. - */ - X509_free(cert); - } - - if (!key) { - ast_log(LOG_ERROR, "Failed to read %s key from file '%s'\n", priv ? "private" : "public", path); - fclose(fp); - return NULL; - } - - if (EVP_PKEY_id(key) != EVP_PKEY_EC && EVP_PKEY_id(key) != EVP_PKEY_RSA) { - ast_log(LOG_ERROR, "%s key from '%s' must be of type EVP_PKEY_EC or EVP_PKEY_RSA\n", - priv ? "Private" : "Public", path); - fclose(fp); - EVP_PKEY_free(key); - return NULL; - } - - fclose(fp); - - return key; -} - -char *stir_shaken_get_serial_number_x509(const char *buf, size_t buf_size) -{ - BIO *certBIO; - X509 *cert; - ASN1_INTEGER *serial; - BIGNUM *bignum; - char *serial_hex; - char *ret; - - certBIO = BIO_new(BIO_s_mem()); - BIO_write(certBIO, buf, buf_size); - cert = PEM_read_bio_X509(certBIO, NULL, NULL, NULL); - BIO_free(certBIO); - if (!cert) { - ast_log(LOG_ERROR, "Failed to read X.509 cert from buffer\n"); - return NULL; - } - - serial = X509_get_serialNumber(cert); - if (!serial) { - ast_log(LOG_ERROR, "Failed to get serial number from certificate\n"); - X509_free(cert); - return NULL; - } - - bignum = ASN1_INTEGER_to_BN(serial, NULL); - if (bignum == NULL) { - ast_log(LOG_ERROR, "Failed to convert serial to bignum for certificate\n"); - X509_free(cert); - return NULL; - } - - /* This will return a string with memory allocated. After we get the string, - * we don't need the cert, file, or bignum references anymore, so free them - * and return the string, if BN_bn2hex was a success. - */ - serial_hex = BN_bn2hex(bignum); - X509_free(cert); - BN_free(bignum); - - if (!serial_hex) { - ast_log(LOG_ERROR, "Failed to convert bignum to hex for certificate\n"); - return NULL; - } - - ret = ast_strdup(serial_hex); - OPENSSL_free(serial_hex); - if (!ret) { - ast_log(LOG_ERROR, "Failed to dup serial from openssl for certificate\n"); - return NULL; - } - - return ret; -} diff --git a/res/res_stir_shaken/stir_shaken.h b/res/res_stir_shaken/stir_shaken.h index a707c3bf19..dee3b1287f 100644 --- a/res/res_stir_shaken/stir_shaken.h +++ b/res/res_stir_shaken/stir_shaken.h @@ -18,51 +18,59 @@ #ifndef _STIR_SHAKEN_H #define _STIR_SHAKEN_H -#include +#include "asterisk/res_stir_shaken.h" +#include "common_config.h" +#include "crypto_utils.h" +#include "curl_utils.h" +#include "attestation.h" +#include "verification.h" + +#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256" +#define STIR_SHAKEN_PPT "shaken" +#define STIR_SHAKEN_TYPE "passport" /*! - * \brief Output configuration settings to the Asterisk CLI + * \brief Retrieve the stir/shaken sorcery context * - * \param obj A sorcery object containing configuration data - * \param arg Asterisk CLI argument object - * \param flags ao2 container flags - * - * \retval 0 + * \retval The stir/shaken sorcery context */ -int stir_shaken_cli_show(void *obj, void *arg, int flags); +struct ast_sorcery *get_sorcery(void); + /*! - * \brief Tab completion for name matching with STIR/SHAKEN CLI commands + * \brief Return string version of VS response code * - * \param word The word to tab complete on - * \param container The sorcery container to iterate through - * - * \retval The tab completion options + * \param vs_rc + * \return Response string */ -char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *container); +const char *vs_response_code_to_str( + enum ast_stir_shaken_vs_response_code vs_rc); /*! - * \brief Reads the public (or private) key from the specified path + * \brief Return string version of AS response code * - * \param path The path to the file containing the private key - * \param priv Specify 0 for public, 1 for private - * - * \retval NULL on failure - * \retval The public/private key on success + * \param as_rc + * \return Response string */ -EVP_PKEY *stir_shaken_read_key(const char *path, int priv); +const char *as_response_code_to_str( + enum ast_stir_shaken_as_response_code as_rc); /*! - * \brief Gets the serial number in hex form from the buffer (for X509) - * - * \note The returned string will need to be freed by the caller - * - * \param buf The BASE64 encoded buffer - * \param buf_size The size of the data in buf - * - * \retval NULL on failure - * \retval serial number on success + * \brief Retrieves the OpenSSL NID for the TN Auth list extension + * \retval The NID */ -char *stir_shaken_get_serial_number_x509(const char *buf, size_t buf_size); +int get_tn_auth_nid(void); + +struct trusted_cert_store { + X509_STORE *store; + ast_rwlock_t store_lock; +}; + +/*! + * \brief Retrieves the OpenSSL trusted cert store + * \retval The store + */ +struct trusted_cert_store *get_trusted_cert_store(void); + #endif /* _STIR_SHAKEN_H */ diff --git a/res/res_stir_shaken/stir_shaken_doc.xml b/res/res_stir_shaken/stir_shaken_doc.xml new file mode 100644 index 0000000000..e14d1d2d08 --- /dev/null +++ b/res/res_stir_shaken/stir_shaken_doc.xml @@ -0,0 +1,294 @@ + + + + + STIR/SHAKEN module for Asterisk + + + STIR/SHAKEN attestation options + + Globally disable verification + + + File path to a certificate + + + URL to the public certificate + + Must be a valid http, or https, URL. + + + + Attestation level + + + On load, Retrieve all TN's certificates and validate their dates + + + Send a media key (mky) grant in the attestation for DTLS calls. + (not common) + + + + STIR/SHAKEN TN options + + Must be of type 'tn'. + + + File path to a certificate + + + URL to the public certificate + + Must be a valid http, or https, URL. + + + + Attestation level + + + On load, Retrieve all TN's certificates and validate their dates + + + Send a media key (mky) grant in the attestation for DTLS calls. + (not common) + + + + STIR/SHAKEN verification options + + Globally disable verification + + + A boolean indicating whether trusted CA certificates should be loaded from the system + + + Path to a file containing one or more CA certs + + + Path to a directory containing one or more hashed CA certs + + + Path to a file containing a CRL + + + Path to a directory containing one or more hashed CRLs + + + Directory to cache retrieved verification certs + + + Maximum time to wait to CURL certificates + + + Number of seconds a cache entry may be behind current time + + + Maximum size to use for caching public keys + + + Number of seconds an iat grant may be behind current time + + + Number of seconds a SIP Date header may be behind current time + + + The default failure action when not set on a profile + + + + If set to continue, continue and let + the dialplan decide what action to take. + + + If set to reject_request, reject the incoming + request with response codes defined in RFC8224. + + + + If set to return_reason, continue to the + dialplan but add a Reason header to the sender in + the next provisional response. + + + + + + RFC9410 uses the STIR protocol on Reason headers + instead of the SIP protocol + + + Relaxes check for "https" and port 443 or 8443 + in incoming Identity header x5u URLs. + + + Relaxes check for query parameters, user/password, etc. + in incoming Identity header x5u URLs. + + + An existing ACL from acl.conf to use when checking + hostnames in incoming Identity header x5u URLs. + + + An IP or subnet to permit when checking + hostnames in incoming Identity header x5u URLs. + + + An IP or subnet to deny checking + hostnames in incoming Identity header x5u URLs. + + + + STIR/SHAKEN profile configuration options + + Must be of type 'profile'. + + + A boolean indicating whether trusted CA certificates should be loaded from the system + + + Path to a file containing one or more CA certs + + + Path to a directory containing one or more hashed CA certs + + + Path to a file containing a CRL + + + Path to a directory containing one or more hashed CRLs + + + Directory to cache retrieved verification certs + + + Maximum time to wait to CURL certificates + + + Number of seconds an iat grant may be behind current time + + + Number of seconds a SIP Date header may be behind current time + + + Number of seconds a cache entry may be behind current time + + + Maximum size to use for caching public keys + + + Actions performed when an endpoint references this profile + + + + Don't do any STIR/SHAKEN processing. + + + Attest on outgoing calls. + + + Verify incoming calls. + + + Attest outgoing calls and verify incoming calls. + + + + + + What do do when a verification fails + + + + If set to continue, continue and let + the dialplan decide what action to take. + + + If set to reject_request, reject the incoming + request with response codes defined in RFC8224. + + + + If set to return_reason, continue to the + dialplan but add a Reason header to the sender in + the next provisional response. + + + + + + RFC9410 uses the STIR protocol on Reason headers + instead of the SIP protocol + + + Relaxes check for "https" and port 443 or 8443 + in incoming Identity header x5u URLs. + + + Relaxes check for query parameters, user/password, etc. + in incoming Identity header x5u URLs. + + + An existing ACL from acl.conf to use when checking + hostnames in incoming Identity header x5u URLs. + + + An IP or subnet to permit when checking + hostnames in incoming Identity header x5u URLs. + + + An IP or subnet to deny checking + hostnames in incoming Identity header x5u URLs. + + + On load, Retrieve all TN's certificates and validate their dates + + + File path to a certificate + + + URL to the public certificate + + Must be a valid http, or https, URL. + + + + Attestation level + + + Send a media key (mky) grant in the attestation for DTLS calls. + (not common) + + + + + + + Gets the number of STIR/SHAKEN results or a specific STIR/SHAKEN value from a result on the channel. + + + + The index of the STIR/SHAKEN result to get. If only 'count' is passed in, gets the number of STIR/SHAKEN results instead. + + + The value to get from the STIR/SHAKEN result. Only used when an index is passed in (instead of 'count'). Allowable values: + + + + + + + + + This function will either return the number of STIR/SHAKEN identities, or return information on the specified identity. + To get the number of identities, just pass 'count' as the only parameter to the function. If you want to get information on a + specific STIR/SHAKEN identity, you can get the number of identities and then pass an index as the first parameter and one of + the values you would like to retrieve as the second parameter. + + + same => n,NoOp(Number of STIR/SHAKEN identities: ${STIR_SHAKEN(count)}) + same => n,NoOp(Identity ${STIR_SHAKEN(0, identity)} has attestation level ${STIR_SHAKEN(0, attestation)}) + + + + \ No newline at end of file diff --git a/res/res_stir_shaken/store.c b/res/res_stir_shaken/store.c deleted file mode 100644 index 30bc63aed0..0000000000 --- a/res/res_stir_shaken/store.c +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 2020, Sangoma Technologies Corporation - * - * Kevin Harwell - * - * See http://www.asterisk.org for more information about - * the Asterisk project. Please do not directly contact - * any of the maintainers of this project for assistance; - * the project provides a web site, mailing lists and IRC - * channels for your use. - * - * This program is free software, distributed under the terms of - * the GNU General Public License Version 2. See the LICENSE file - * at the top of the source tree. - */ - -#include "asterisk.h" - -#include - -#include "asterisk/cli.h" -#include "asterisk/sorcery.h" - -#include "stir_shaken.h" -#include "store.h" -#include "asterisk/res_stir_shaken.h" - -#define CONFIG_TYPE "store" - -#define VARIABLE_SUBSTITUTE "${CERTIFICATE}" - -struct stir_shaken_store { - SORCERY_OBJECT(details); - AST_DECLARE_STRING_FIELDS( - /*! Path to a directory containing certificates */ - AST_STRING_FIELD(path); - /*! URL to the public certificate */ - AST_STRING_FIELD(public_cert_url); - ); -}; - -static struct stir_shaken_store *stir_shaken_store_get(const char *id) -{ - return ast_sorcery_retrieve_by_id(ast_stir_shaken_sorcery(), CONFIG_TYPE, id); -} - -static struct ao2_container *stir_shaken_store_get_all(void) -{ - return ast_sorcery_retrieve_by_fields(ast_stir_shaken_sorcery(), CONFIG_TYPE, - AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); -} - -static void stir_shaken_store_destructor(void *obj) -{ - struct stir_shaken_store *cfg = obj; - - ast_string_field_free_memory(cfg); -} - -static void *stir_shaken_store_alloc(const char *name) -{ - struct stir_shaken_store *cfg; - - cfg = ast_sorcery_generic_alloc(sizeof(*cfg), stir_shaken_store_destructor); - if (!cfg) { - return NULL; - } - - if (ast_string_field_init(cfg, 512)) { - ao2_ref(cfg, -1); - return NULL; - } - - return cfg; -} - -static int stir_shaken_store_apply(const struct ast_sorcery *sorcery, void *obj) -{ - return 0; -} - -static char *stir_shaken_store_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct stir_shaken_store *cfg; - - switch(cmd) { - case CLI_INIT: - e->command = "stir_shaken show store"; - e->usage = - "Usage: stir_shaken show store \n" - " Show the store stir/shaken settings for a given id\n"; - return NULL; - case CLI_GENERATE: - if (a->pos == 3) { - return stir_shaken_tab_complete_name(a->word, stir_shaken_store_get_all()); - } else { - return NULL; - }; - } - - if (a->argc != 4) { - return CLI_SHOWUSAGE; - } - - cfg = stir_shaken_store_get(a->argv[3]); - stir_shaken_cli_show(cfg, a, 0); - ao2_cleanup(cfg); - - return CLI_SUCCESS; -} - -static struct ast_cli_entry stir_shaken_store_cli[] = { - AST_CLI_DEFINE(stir_shaken_store_show, "Show stir/shaken store configuration by id"), -}; - -static int on_load_path(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_store *cfg = obj; - struct stat statbuf; - - if (stat(var->value, &statbuf)) { - ast_log(LOG_ERROR, "stir/shaken - path '%s' not found\n", var->value); - return -1; - } - - if (!S_ISDIR(statbuf.st_mode)) { - ast_log(LOG_ERROR, "stir/shaken - path '%s' is not a directory\n", var->value); - return -1; - } - - return ast_string_field_set(cfg, path, var->value); -} - -static int path_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_store *cfg = obj; - - *buf = ast_strdup(cfg->path); - - return 0; -} - -static int on_load_public_cert_url(const struct aco_option *opt, struct ast_variable *var, void *obj) -{ - struct stir_shaken_store *cfg = obj; - - if (!ast_begins_with(var->value, "http")) { - ast_log(LOG_ERROR, "stir/shaken - public_cert_url scheme must be 'http[s]'\n"); - return -1; - } - - if (!strstr(var->value, VARIABLE_SUBSTITUTE)) { - ast_log(LOG_ERROR, "stir/shaken - public_cert_url must contain variable '%s' " - "used for substitution\n", VARIABLE_SUBSTITUTE); - return -1; - } - - return ast_string_field_set(cfg, public_cert_url, var->value); -} - -static int public_cert_url_to_str(const void *obj, const intptr_t *args, char **buf) -{ - const struct stir_shaken_store *cfg = obj; - - *buf = ast_strdup(cfg->public_cert_url); - - return 0; -} - -int stir_shaken_store_unload(void) -{ - ast_cli_unregister_multiple(stir_shaken_store_cli, - ARRAY_LEN(stir_shaken_store_cli)); - - return 0; -} - -int stir_shaken_store_load(void) -{ - struct ast_sorcery *sorcery = ast_stir_shaken_sorcery(); - - ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=store"); - - if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, stir_shaken_store_alloc, - NULL, stir_shaken_store_apply)) { - ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); - return -1; - } - - ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", OPT_NOOP_T, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "path", "", - on_load_path, path_to_str, NULL, 0, 0); - ast_sorcery_object_field_register_custom(sorcery, CONFIG_TYPE, "public_cert_url", "", - on_load_public_cert_url, public_cert_url_to_str, NULL, 0, 0); - - ast_cli_register_multiple(stir_shaken_store_cli, - ARRAY_LEN(stir_shaken_store_cli)); - - return 0; -} diff --git a/res/res_stir_shaken/tn_config.c b/res/res_stir_shaken/tn_config.c new file mode 100644 index 0000000000..123b0519d8 --- /dev/null +++ b/res/res_stir_shaken/tn_config.c @@ -0,0 +1,280 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include + +#include "asterisk/cli.h" +#include "asterisk/module.h" +#include "asterisk/sorcery.h" + +#include "stir_shaken.h" + +#define CONFIG_TYPE "tn" + +#define DEFAULT_check_tn_cert_public_url check_tn_cert_public_url_NO +#define DEFAULT_private_key_file NULL +#define DEFAULT_public_cert_url NULL +#define DEFAULT_attest_level attest_level_NOT_SET +#define DEFAULT_send_mky send_mky_NO + +struct tn_cfg *tn_get_cfg(const char *id) +{ + return ast_sorcery_retrieve_by_id(get_sorcery(), CONFIG_TYPE, id); +} + +static struct ao2_container *get_tn_all(void) +{ + return ast_sorcery_retrieve_by_fields(get_sorcery(), CONFIG_TYPE, + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); +} + +generate_sorcery_enum_from_str(tn_cfg, acfg_common., check_tn_cert_public_url, UNKNOWN) +generate_sorcery_enum_to_str(tn_cfg, acfg_common., check_tn_cert_public_url) + +generate_sorcery_enum_from_str(tn_cfg, acfg_common., attest_level, UNKNOWN) +generate_sorcery_enum_to_str(tn_cfg, acfg_common., attest_level) + +generate_sorcery_enum_from_str(tn_cfg, acfg_common., send_mky, UNKNOWN) +generate_sorcery_enum_to_str(tn_cfg, acfg_common., send_mky) + +static void tn_destructor(void *obj) +{ + struct tn_cfg *cfg = obj; + + ast_string_field_free_memory(cfg); + acfg_cleanup(&cfg->acfg_common); +} + +static int init_tn(struct tn_cfg *cfg) +{ + if (ast_string_field_init(cfg, 1024)) { + return -1; + } + + /* + * The memory for the commons actually comes from cfg + * due to the weirdness of the STRFLDSET macro used with + * sorcery. We just use a token amount of memory in + * this call so the initialize doesn't fail. + */ + if (ast_string_field_init(&cfg->acfg_common, 8)) { + return -1; + } + + return 0; +} + +static void *tn_alloc(const char *name) +{ + struct tn_cfg *cfg; + + cfg = ast_sorcery_generic_alloc(sizeof(*cfg), tn_destructor); + if (!cfg) { + return NULL; + } + + if (init_tn(cfg) != 0) { + ao2_cleanup(cfg); + cfg = NULL; + } + return cfg; +} + +static void *etn_alloc(const char *name) +{ + struct tn_cfg *cfg; + + cfg = ao2_alloc_options(sizeof(*cfg), tn_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg) { + return NULL; + } + + if (init_tn(cfg) != 0) { + ao2_cleanup(cfg); + cfg = NULL; + } + return cfg; +} + +struct tn_cfg *tn_get_etn(const char *id, struct profile_cfg *eprofile) +{ + RAII_VAR(struct tn_cfg *, tn, + ast_sorcery_retrieve_by_id(get_sorcery(), CONFIG_TYPE, S_OR(id, "")), + ao2_cleanup); + struct tn_cfg *etn = etn_alloc(id); + int rc = 0; + + if (!tn || !eprofile || !etn) { + return NULL; + } + + /* Initialize with the acfg from the eprofile first */ + rc = as_copy_cfg_common(id, &etn->acfg_common, + &eprofile->acfg_common); + if (rc != 0) { + ao2_cleanup(etn); + return NULL; + } + + /* Overwrite with anything in the TN itself */ + rc = as_copy_cfg_common(id, &etn->acfg_common, + &tn->acfg_common); + if (rc != 0) { + ao2_cleanup(etn); + return NULL; + } + + /* + * Unlike profile, we're not going to actually add a + * new object to sorcery because, although unlikely, + * the same TN could be used with multiple profiles. + */ + + return etn; +} + +static int tn_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct tn_cfg *cfg = obj; + const char *id = ast_sorcery_object_get_id(cfg); + int rc = 0; + + if (as_check_common_config(id, &cfg->acfg_common) != 0) { + return -1; + } + + return rc; +} + +static char *cli_tn_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_container *container; + struct config_object_cli_data data = { + .title = "TN", + .object_type = config_object_type_tn, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show tns"; + e->usage = + "Usage: stir_shaken show tns\n" + " Show all attestation TNs\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + container = get_tn_all(); + if (!container || ao2_container_count(container) == 0) { + ast_cli(a->fd, "No stir/shaken TNs found\n"); + ao2_cleanup(container); + return CLI_SUCCESS; + } + + ao2_callback_data(container, OBJ_NODATA, config_object_cli_show, a,&data); + ao2_ref(container, -1); + + return CLI_SUCCESS; +} + +static char *cli_tn_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct tn_cfg *cfg; + struct config_object_cli_data data = { + .title = "TN", + .object_type = config_object_type_tn, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show tn"; + e->usage = + "Usage: stir_shaken show tn \n" + " Show the settings for a given TN\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 3) { + return config_object_tab_complete_name(a->word, get_tn_all()); + } else { + return NULL; + } + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + cfg = tn_get_cfg(a->argv[3]); + config_object_cli_show(cfg, a, &data, 0); + ao2_cleanup(cfg); + + return CLI_SUCCESS; +} + + +static struct ast_cli_entry stir_shaken_certificate_cli[] = { + AST_CLI_DEFINE(cli_tn_show, "Show stir/shaken TN configuration by id"), + AST_CLI_DEFINE(cli_tn_show_all, "Show all stir/shaken attestation TN configurations"), +}; + +int tn_config_reload(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + ast_sorcery_force_reload_object(sorcery, CONFIG_TYPE); + return AST_MODULE_LOAD_SUCCESS; +} + +int tn_config_unload(void) +{ + ast_cli_unregister_multiple(stir_shaken_certificate_cli, + ARRAY_LEN(stir_shaken_certificate_cli)); + + return 0; +} + +int tn_config_load(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + + ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", "stir_shaken.conf,criteria=type=tn"); + + if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, tn_alloc, + NULL, tn_apply)) { + ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "type", "", + OPT_NOOP_T, 0, 0); + + register_common_attestation_fields(sorcery, tn_cfg, CONFIG_TYPE,); + + ast_sorcery_load_object(sorcery, CONFIG_TYPE); + + ast_cli_register_multiple(stir_shaken_certificate_cli, + ARRAY_LEN(stir_shaken_certificate_cli)); + + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_stir_shaken/verification.c b/res/res_stir_shaken/verification.c new file mode 100644 index 0000000000..f8d21cfdf4 --- /dev/null +++ b/res/res_stir_shaken/verification.c @@ -0,0 +1,1121 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ +#include +#include + +#include +#include +#include + +#include "asterisk.h" + +#define _TRACE_PREFIX_ "v",__LINE__, "" + +#include "asterisk/channel.h" +#include "asterisk/cli.h" +#include "asterisk/config.h" +#include "asterisk/module.h" +#include "asterisk/sorcery.h" +#include "asterisk/astdb.h" +#include "asterisk/conversions.h" +#include "asterisk/utils.h" +#include "asterisk/paths.h" +#include "asterisk/logger.h" +#include "asterisk/acl.h" +#include "asterisk/time.h" +#include "asterisk/localtime.h" +#include "asterisk/crypto.h" +#include "asterisk/json.h" + +#include "stir_shaken.h" + +#define AST_DB_FAMILY "STIR_SHAKEN" + +static regex_t url_match_regex; + +/* Certificates should begin with this */ +#define BEGIN_CERTIFICATE_STR "-----BEGIN CERTIFICATE-----" + +static const char *vs_rc_map[] = { + [AST_STIR_SHAKEN_VS_SUCCESS] = "success", + [AST_STIR_SHAKEN_VS_DISABLED] = "disabled", + [AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS] = "invalid_arguments", + [AST_STIR_SHAKEN_VS_INTERNAL_ERROR] = "internal_error", + [AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR] = "missing_identity_hdr", + [AST_STIR_SHAKEN_VS_NO_DATE_HDR] = "missing_date_hdr", + [AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE] = "date_hdr_parse_failure", + [AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED] = "date_hdr_range_error", + [AST_STIR_SHAKEN_VS_NO_JWT_HDR] = "missing_jwt_hdr", + [AST_STIR_SHAKEN_VS_CERT_CACHE_MISS] = "cert_cache_miss", + [AST_STIR_SHAKEN_VS_CERT_CACHE_INVALID] = "cert_cache_invalid", + [AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED] = "cert_cache_expired", + [AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE] = "cert_retrieval_failure", + [AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID] = "cert_contents_invalid", + [AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED] = "cert_not_trusted", + [AST_STIR_SHAKEN_VS_CERT_DATE_INVALID] = "cert_date_failure", + [AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT] = "cert_no_tn_auth_ext", + [AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT] = "cert_no_spc_in_auth_ext", + [AST_STIR_SHAKEN_VS_NO_RAW_KEY] = "no_raw_key", + [AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION] = "signature_validation", + [AST_STIR_SHAKEN_VS_NO_IAT] = "missing_iat", + [AST_STIR_SHAKEN_VS_IAT_EXPIRED] = "iat_range_error", + [AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT] = "invalid_or_no_ppt", + [AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG] = "invalid_or_no_alg", + [AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP] = "invalid_or_no_typ", + [AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS] = "invalid_or_no_grants", + [AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST] = "invalid_or_no_attest", + [AST_STIR_SHAKEN_VS_NO_ORIGID] = "missing_origid", + [AST_STIR_SHAKEN_VS_NO_ORIG_TN] = "missing_orig_tn", + [AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH] = "cid_orig_tn_mismatch", + [AST_STIR_SHAKEN_VS_NO_DEST_TN] = "missing_dest_tn", + [AST_STIR_SHAKEN_VS_INVALID_HEADER] = "invalid_header", + [AST_STIR_SHAKEN_VS_INVALID_GRANT] = "invalid_grant", +}; + +const char *vs_response_code_to_str( + enum ast_stir_shaken_vs_response_code vs_rc) +{ + return ARRAY_IN_BOUNDS(vs_rc, vs_rc_map) ? + vs_rc_map[vs_rc] : NULL; +} + +static void cleanup_cert_from_astdb_and_fs( + struct ast_stir_shaken_vs_ctx *ctx) +{ + if (ast_db_exists(ctx->hash_family, "path") || ast_db_exists(ctx->hash_family, "expiration")) { + ast_db_deltree(ctx->hash_family, NULL); + } + + if (ast_db_exists(ctx->url_family, ctx->public_url)) { + ast_db_del(ctx->url_family, ctx->public_url); + } + + /* Remove the actual file from the system */ + remove(ctx->filename); +} + +static int add_cert_expiration_to_astdb(struct ast_stir_shaken_vs_ctx *cert, + const char *cache_control_header, const char *expires_header) +{ + RAII_VAR(struct verification_cfg *, cfg, vs_get_cfg(), ao2_cleanup); + + char time_buf[32]; + time_t current_time = time(NULL); + time_t max_age_hdr = 0; + time_t expires_hdr = 0; + ASN1_TIME *notAfter = NULL; + time_t cert_expires = 0; + time_t config_expires = 0; + time_t expires = 0; + int rc = 0; + + config_expires = current_time + cfg->vcfg_common.max_cache_entry_age; + + if (!ast_strlen_zero(cache_control_header)) { + char *str_max_age; + + str_max_age = strstr(cache_control_header, "s-maxage"); + if (!str_max_age) { + str_max_age = strstr(cache_control_header, "max-age"); + } + + if (str_max_age) { + unsigned int m; + char *equal = strchr(str_max_age, '='); + if (equal && !ast_str_to_uint(equal + 1, &m)) { + max_age_hdr = current_time + m; + } + } + } + + if (!ast_strlen_zero(expires_header)) { + struct ast_tm expires_time; + + ast_strptime(expires_header, "%a, %d %b %Y %T %z", &expires_time); + expires_time.tm_isdst = -1; + expires_hdr = ast_mktime(&expires_time, "GMT").tv_sec; + } + + notAfter = X509_get_notAfter(cert->xcert); + cert_expires = crypto_asn_time_as_time_t(notAfter); + + /* + * ATIS-1000074 says: + * The STI-VS shall implement the cache behavior described in + * [Ref 10]. If the HTTP response does not include any recognized + * caching directives or indicates caching for less than 24 hours, + * then the STI-VS should cache the HTTP response for 24 hours. + * + * Basically, they're saying "cache for 24 hours unless the HTTP + * response says to cache for longer." Instead of the fixed 24 + * hour minumum, however, we'll use max_cache_entry_age instead. + * + * We got all the possible values of expires so let's find the + * highest value greater than the configured max_cache_entry_age. + */ + + /* The default */ + expires = config_expires; + + if (max_age_hdr > expires) { + expires = max_age_hdr; + } + + if (expires_hdr > expires) { + expires = expires_hdr; + } + + /* + * However... Don't cache for longer than the + * certificate is actually valid. + */ + if (cert_expires && cert_expires < expires) { + expires = cert_expires; + } + + snprintf(time_buf, sizeof(time_buf), "%ld", expires); + + rc = ast_db_put(cert->hash_family, "expiration", time_buf); + if (rc == 0) { + strcpy(cert->expiration, time_buf); /* safe */ + } + + return rc; +} + +static int add_cert_key_to_astdb(struct ast_stir_shaken_vs_ctx *cert, + const char *cache_control_hdr, const char *expires_hdr) +{ + int rc = 0; + + rc = ast_db_put(cert->url_family, cert->public_url, cert->hash); + if (rc) { + return rc; + } + rc = ast_db_put(cert->hash_family, "path", cert->filename); + if (rc) { + ast_db_del(cert->url_family, cert->public_url); + return rc; + } + + rc = add_cert_expiration_to_astdb(cert, cache_control_hdr, expires_hdr); + if (rc) { + ast_db_del(cert->url_family, cert->public_url); + ast_db_del(cert->hash_family, "path"); + } + + return rc; +} + +static int is_cert_cache_entry_expired(char *expiration) +{ + struct timeval current_time = ast_tvnow(); + struct timeval expires = { .tv_sec = 0, .tv_usec = 0 }; + int res = 0; + SCOPE_ENTER(3, "Checking for cache expiration: %s\n", expiration); + + if (ast_strlen_zero(expiration)) { + SCOPE_EXIT_RTN_VALUE(1, "No expiration date provided\n"); + } + + if (ast_str_to_ulong(expiration, (unsigned long *)&expires.tv_sec)) { + SCOPE_EXIT_RTN_VALUE(1, "Couldn't convert expiration string '%s' to ulong", + expiration); + } + ast_trace(2, "Expiration comparison: exp: %" PRIu64 " curr: %" PRIu64 " Diff: %" PRIu64 ".\n", + expires.tv_sec, current_time.tv_sec, expires.tv_sec - current_time.tv_sec); + + res = (ast_tvcmp(current_time, expires) == -1 ? 0 : 1); + SCOPE_EXIT_RTN_VALUE(res , "entry was %sexpired\n", res ? "" : "not "); +} + +#define ASN1_TAG_TNAUTH_SPC 0 +#define ASN1_TAG_TNAUTH_TN_RANGE 1 +#define ASN1_TAG_TNAUTH_TN 2 + +#define IS_GET_OBJ_ERR(ret) (ret & 0x80) + +static enum ast_stir_shaken_vs_response_code + check_tn_auth_list(struct ast_stir_shaken_vs_ctx * ctx) +{ + ASN1_OCTET_STRING *tn_exten; + const unsigned char* octet_str_data = NULL; + long xlen; + int tag, xclass; + int ret; + SCOPE_ENTER(3, "%s: Checking TNAuthList in cert '%s'\n", ctx->tag, ctx->public_url); + + tn_exten = crypto_get_cert_extension_data(ctx->xcert, get_tn_auth_nid(), NULL); + if (!tn_exten) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT, + LOG_ERROR, "%s: Cert '%s' doesn't have a TNAuthList extension\n", + ctx->tag, ctx->public_url); + } + octet_str_data = tn_exten->data; + + /* The first call to ASN1_get_object should return a SEQUENCE */ + ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length); + if (IS_GET_OBJ_ERR(ret)) { + crypto_log_openssl(LOG_ERROR, "%s: Cert '%s' has malformed TNAuthList extension\n", + ctx->tag, ctx->public_url); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT); + } + + if (ret != V_ASN1_CONSTRUCTED || tag != V_ASN1_SEQUENCE) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT, + LOG_ERROR, "%s: Cert '%s' has malformed TNAuthList extension (tag %d != V_ASN1_SEQUENCE)\n", + ctx->tag, ctx->public_url, tag); + } + + /* + * The second call to ASN1_get_object should return one of + * the following tags defined in RFC8226 section 9: + * + * ASN1_TAG_TNAUTH_SPC 0 + * ASN1_TAG_TNAUTH_TN_RANGE 1 + * ASN1_TAG_TNAUTH_TN 2 + * + * ATIS-1000080 however limits this to only ASN1_TAG_TNAUTH_SPC + * + */ + ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length); + if (IS_GET_OBJ_ERR(ret)) { + crypto_log_openssl(LOG_ERROR, "%s: Cert '%s' has malformed TNAuthList extension\n", + ctx->tag, ctx->public_url); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT); + } + + if (ret != V_ASN1_CONSTRUCTED || tag != ASN1_TAG_TNAUTH_SPC) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT, + LOG_ERROR, "%s: Cert '%s' has malformed TNAuthList extension (tag %d != ASN1_TAG_TNAUTH_SPC(0))\n", + ctx->tag, ctx->public_url, tag); + } + + /* The third call to ASN1_get_object should contain the SPC */ + ret = ASN1_get_object(&octet_str_data, &xlen, &tag, &xclass, tn_exten->length); + if (ret != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT, + LOG_ERROR, "%s: Cert '%s' has malformed TNAuthList extension (no SPC)\n", + ctx->tag, ctx->public_url); + } + + if (ast_string_field_set(ctx, cert_spc, (char *)octet_str_data) != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, "%s: Cert '%s' with SPC: %s CN: %s has valid TNAuthList\n", + ctx->tag, ctx->public_url, ctx->cert_spc, ctx->cert_cn); +} +#undef IS_GET_OBJ_ERR + +static enum ast_stir_shaken_vs_response_code check_cert( + struct ast_stir_shaken_vs_ctx * ctx) +{ + RAII_VAR(char *, CN, NULL, ast_free); + int res = 0; + const char *err_msg; + SCOPE_ENTER(3, "%s: Validating cert '%s'\n", ctx->tag, ctx->public_url); + + CN = crypto_get_cert_subject(ctx->xcert, "CN"); + if (!CN) { + CN = crypto_get_cert_subject(ctx->xcert, NULL); + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, + LOG_ERROR, "%s: Cert '%s' has no commonName(CN) in Subject '%s'\n", + ctx->tag, ctx->public_url, CN); + } + + res = ast_string_field_set(ctx, cert_cn, CN); + if (res != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + + ast_trace(3,"%s: Checking ctx against CA ctx\n", ctx->tag); + res = crypto_is_cert_trusted(ctx->eprofile->vcfg_common.tcs, ctx->xcert, &err_msg); + if (!res) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED, + LOG_ERROR, "%s: Cert '%s' not trusted: %s\n", + ctx->tag, ctx->public_url, err_msg); + } + + ast_trace(3,"%s: Attempting to get the raw pubkey\n", ctx->tag); + ctx->raw_key_len = crypto_get_raw_pubkey_from_cert(ctx->xcert, + &ctx->raw_key); + if (ctx->raw_key_len <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_RAW_KEY, + LOG_ERROR, "%s: Unable to extract raw public key from '%s'\n", + ctx->tag, ctx->public_url); + } + + ast_trace(3,"%s: Checking cert '%s' validity dates\n", + ctx->tag, ctx->public_url); + if (!crypto_is_cert_time_valid(ctx->xcert, ctx->validity_check_time)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_DATE_INVALID, + LOG_ERROR, "%s: Cert '%s' dates not valid\n", + ctx->tag, ctx->public_url); + } + + SCOPE_EXIT_RTN_VALUE(check_tn_auth_list(ctx), + "%s: Cert '%s' with SPC: %s CN: %s is valid\n", + ctx->tag, ctx->public_url, ctx->cert_spc, ctx->cert_cn); +} + +static enum ast_stir_shaken_vs_response_code retrieve_cert_from_url( + struct ast_stir_shaken_vs_ctx *ctx) +{ + FILE *cert_file; + long http_code; + int rc = 0; + enum ast_stir_shaken_vs_response_code vs_rc; + RAII_VAR(struct curl_header_data *, header_data, + ast_calloc(1, sizeof(*header_data)), curl_header_data_free); + RAII_VAR(struct curl_write_data *, write_data, + ast_calloc(1, sizeof(*write_data)), curl_write_data_free); + RAII_VAR(struct curl_open_socket_data *, open_socket_data, + ast_calloc(1, sizeof(*open_socket_data)), curl_open_socket_data_free); + + const char *cache_control; + const char *expires; + SCOPE_ENTER(2, "%s: Attempting to retrieve '%s' from net\n", + ctx->tag, ctx->public_url); + + if (!header_data || !write_data || !open_socket_data) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for curl '%s' transaction\n", + ctx->tag, ctx->public_url); + } + + header_data->debug_info = ast_strdup(ctx->public_url); + write_data->debug_info = ast_strdup(ctx->public_url); + write_data->max_download_bytes = 8192; + write_data->stream_buffer = NULL; + open_socket_data->debug_info = ast_strdup(ctx->public_url); + open_socket_data->acl = ctx->eprofile->vcfg_common.acl; + + if (!header_data->debug_info || !write_data->debug_info || + !open_socket_data->debug_info) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to allocate memory for curl '%s' transaction\n", + ctx->tag, ctx->public_url); + } + + http_code = curler(ctx->public_url, + ctx->eprofile->vcfg_common.curl_timeout, + write_data, header_data, open_socket_data); + + if (http_code / 100 != 2) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE, + LOG_ERROR, "%s: Failed to retrieve cert %s: code %ld\n", + ctx->tag, ctx->public_url, http_code); + } + + if (!ast_begins_with(write_data->stream_buffer, BEGIN_CERTIFICATE_STR)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, + LOG_ERROR, "%s: Cert '%s' contains invalid data\n", + ctx->tag, ctx->public_url); + } + + ctx->xcert = crypto_load_cert_from_memory(write_data->stream_buffer, + write_data->stream_bytes_downloaded); + if (!ctx->xcert) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, + LOG_ERROR, "%s: Cert '%s' was not parseable as an X509 certificate\n", + ctx->tag, ctx->public_url); + } + + vs_rc = check_cert(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + X509_free(ctx->xcert); + ctx->xcert = NULL; + SCOPE_EXIT_RTN_VALUE(vs_rc, "%s: Cert '%s' failed validity checks\n", + ctx->tag, ctx->public_url); + } + + cert_file = fopen(ctx->filename, "w"); + if (!cert_file) { + X509_free(ctx->xcert); + ctx->xcert = NULL; + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + LOG_ERROR, "%s: Failed to write cert %s: file '%s' %s (%d)\n", + ctx->tag, ctx->public_url, ctx->filename, strerror(errno), errno); + } + + rc = fputs(write_data->stream_buffer, cert_file); + fclose(cert_file); + if (rc == EOF) { + X509_free(ctx->xcert); + ctx->xcert = NULL; + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + LOG_ERROR, "%s: Failed to write cert %s: file '%s' %s (%d)\n", + ctx->tag, ctx->public_url, ctx->filename, strerror(errno), errno); + } + + ast_trace(2, "%s: Cert '%s' written to file '%s'\n", + ctx->tag, ctx->public_url, ctx->filename); + + ast_trace(2, "%s: Adding cert '%s' to astdb", + ctx->tag, ctx->public_url); + cache_control = ast_variable_find_in_list(header_data->headers, "cache-control"); + expires = ast_variable_find_in_list(header_data->headers, "expires"); + + rc = add_cert_key_to_astdb(ctx, cache_control, expires); + if (rc) { + X509_free(ctx->xcert); + ctx->xcert = NULL; + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, + LOG_ERROR, "%s: Unable to add cert '%s' to ASTDB\n", + ctx->tag, ctx->public_url); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, + "%s: Cert '%s' successfully retrieved from internet and cached\n", + ctx->tag, ctx->public_url); +} + +static enum ast_stir_shaken_vs_response_code + retrieve_cert_from_cache(struct ast_stir_shaken_vs_ctx *ctx) +{ + int rc = 0; + enum ast_stir_shaken_vs_response_code vs_rc; + + SCOPE_ENTER(2, "%s: Attempting to retrieve cert '%s' from cache\n", + ctx->tag, ctx->public_url); + + if (!ast_db_exists(ctx->hash_family, "path")) { + cleanup_cert_from_astdb_and_fs(ctx); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS, + "%s: No cert found in astdb for '%s'\n", + ctx->tag, ctx->public_url); + } + + rc = ast_db_get(ctx->hash_family, "expiration", ctx->expiration, sizeof(ctx->expiration)); + if (rc) { + cleanup_cert_from_astdb_and_fs(ctx); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS, + "%s: No cert found in astdb for '%s'\n", + ctx->tag, ctx->public_url); + } + + if (!ast_file_is_readable(ctx->filename)) { + cleanup_cert_from_astdb_and_fs(ctx); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_MISS, + "%s: Cert file '%s' was not found or was not readable for '%s'\n", + ctx->tag, ctx->filename, ctx->public_url); + } + + if (is_cert_cache_entry_expired(ctx->expiration)) { + cleanup_cert_from_astdb_and_fs(ctx); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED, + "%s: Cert file '%s' cache entry was expired for '%s'\n", + ctx->tag, ctx->filename, ctx->public_url); + } + + ctx->xcert = crypto_load_cert_from_file(ctx->filename); + if (!ctx->xcert) { + cleanup_cert_from_astdb_and_fs(ctx); + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID, + "%s: Cert file '%s' was not parseable as an X509 certificate for '%s'\n", + ctx->tag, ctx->filename, ctx->public_url); + } + + vs_rc = check_cert(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + X509_free(ctx->xcert); + ctx->xcert = NULL; + SCOPE_EXIT_RTN_VALUE(vs_rc, "%s: Cert '%s' failed validity checks\n", + ctx->tag, ctx->public_url); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, + "%s: Cert '%s' successfully retrieved from cache\n", + ctx->tag, ctx->public_url); +} + +static enum ast_stir_shaken_vs_response_code ctx_populate( + struct ast_stir_shaken_vs_ctx *ctx) +{ + char hash[41]; + + ast_sha1_hash(hash, ctx->public_url); + if (ast_string_field_set(ctx, hash, hash) != 0) { + return AST_STIR_SHAKEN_VS_INTERNAL_ERROR; + } + + if (ast_string_field_build(ctx, filename, "%s/%s.pem", + ctx->eprofile->vcfg_common.cert_cache_dir, hash) != 0) { + return AST_STIR_SHAKEN_VS_INTERNAL_ERROR; + } + + if (ast_string_field_build(ctx, hash_family, "%s/hash/%s", + AST_DB_FAMILY, hash) != 0) { + return AST_STIR_SHAKEN_VS_INTERNAL_ERROR; + } + + if (ast_string_field_build(ctx, url_family, "%s/url", AST_DB_FAMILY) != 0) { + return AST_STIR_SHAKEN_VS_INTERNAL_ERROR; + } + + return AST_STIR_SHAKEN_VS_SUCCESS; +} + +static enum ast_stir_shaken_vs_response_code + retrieve_verification_cert(struct ast_stir_shaken_vs_ctx *ctx) +{ + enum ast_stir_shaken_vs_response_code rc = AST_STIR_SHAKEN_VS_SUCCESS; + SCOPE_ENTER(3, "%s: Retrieving cert '%s'\n", ctx->tag, ctx->public_url); + + ast_trace(1, "%s: Checking cache for cert '%s'\n", ctx->tag, ctx->public_url); + rc = retrieve_cert_from_cache(ctx); + if (rc == AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_RTN_VALUE(rc, "%s: Using cert '%s' from cache\n", + ctx->tag, ctx->public_url);; + } + + ast_trace(1, "%s: No valid cert for '%s' available in cache\n", + ctx->tag, ctx->public_url); + ast_trace(1, "%s: Retrieving cert directly from url '%s'\n", + ctx->tag, ctx->public_url); + + rc = retrieve_cert_from_url(ctx); + if (rc == AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_RTN_VALUE(rc, "%s: Using cert '%s' from internet\n", + ctx->tag, ctx->public_url); + } + + SCOPE_EXIT_LOG_RTN_VALUE(rc, LOG_ERROR, + "%s: Unable to retrieve cert '%s' from cache or internet\n", + ctx->tag, ctx->public_url); +} + +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_add_identity_hdr( + struct ast_stir_shaken_vs_ctx * ctx, const char *identity_hdr) +{ + return ast_string_field_set(ctx, identity_hdr, identity_hdr) == 0 ? + AST_STIR_SHAKEN_VS_SUCCESS : AST_STIR_SHAKEN_VS_INTERNAL_ERROR; +} + +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_add_date_hdr(struct ast_stir_shaken_vs_ctx * ctx, + const char *date_hdr) +{ + return ast_string_field_set(ctx, date_hdr, date_hdr) == 0 ? + AST_STIR_SHAKEN_VS_SUCCESS : AST_STIR_SHAKEN_VS_INTERNAL_ERROR; +} + +enum stir_shaken_failure_action_enum + ast_stir_shaken_vs_get_failure_action( + struct ast_stir_shaken_vs_ctx *ctx) +{ + return ctx->eprofile->vcfg_common.stir_shaken_failure_action; +} + +int ast_stir_shaken_vs_get_use_rfc9410_responses( + struct ast_stir_shaken_vs_ctx *ctx) +{ + return ctx->eprofile->vcfg_common.use_rfc9410_responses; +} + +void ast_stir_shaken_vs_ctx_set_response_code( + struct ast_stir_shaken_vs_ctx *ctx, + enum ast_stir_shaken_vs_response_code vs_rc) +{ + ctx->failure_reason = vs_rc; +} + +static void ctx_destructor(void *obj) +{ + struct ast_stir_shaken_vs_ctx *ctx = obj; + + ao2_cleanup(ctx->eprofile); + ast_free(ctx->raw_key); + ast_string_field_free_memory(ctx); + X509_free(ctx->xcert); +} + +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_ctx_create(const char *caller_id, + struct ast_channel *chan, const char *profile_name, + const char *tag, struct ast_stir_shaken_vs_ctx **ctxout) +{ + RAII_VAR(struct ast_stir_shaken_vs_ctx *, ctx, NULL, ao2_cleanup); + RAII_VAR(struct profile_cfg *, profile, NULL, ao2_cleanup); + RAII_VAR(struct verification_cfg *, vs, NULL, ao2_cleanup); + const char *t = S_OR(tag, S_COR(chan, ast_channel_name(chan), "")); + SCOPE_ENTER(3, "%s: Enter\n", t); + + if (ast_strlen_zero(tag)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide tag\n", t); + } + + if (ast_strlen_zero(caller_id)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS, + LOG_ERROR, "%s: Must provide caller_id\n", t); + } + + if (ast_strlen_zero(profile_name)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED, + "%s: Disabled due to missing profile name\n", t); + } + + vs = vs_get_cfg(); + if (vs->global_disable) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED, + "%s: Globally disabled\n", t); + } + + profile = eprofile_get_cfg(profile_name); + if (!profile) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED, + LOG_ERROR, "%s: No profile for profile name '%s'. Call will continue\n", tag, + profile_name); + } + + if (!PROFILE_ALLOW_VERIFY(profile)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_DISABLED, + "%s: Disabled by profile\n", t); + } + + ctx = ao2_alloc_options(sizeof(*ctx), ctx_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!ctx) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + if (ast_string_field_init(ctx, 1024) != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + + if (ast_string_field_set(ctx, tag, tag) != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + + ctx->chan = chan; + if (ast_string_field_set(ctx, caller_id, caller_id) != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR); + } + + /* Transfer references to ctx */ + ctx->eprofile = profile; + profile = NULL; + + ao2_ref(ctx, +1); + *ctxout = ctx; + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, "%s: Done\n", t); +} + +static enum ast_stir_shaken_vs_response_code check_date_header( + struct ast_stir_shaken_vs_ctx * ctx) +{ + struct ast_tm date_hdr_tm; + struct timeval date_hdr_timeval; + struct timeval current_timeval; + char *remainder; + char timezone[80] = { 0 }; + int64_t time_diff; + SCOPE_ENTER(3, "%s: Checking date header: '%s'\n", + ctx->tag, ctx->date_hdr); + + if (!(remainder = ast_strptime(ctx->date_hdr, "%a, %d %b %Y %T", &date_hdr_tm))) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE, + LOG_ERROR, "%s: Failed to parse: '%s'\n", + ctx->tag, ctx->date_hdr); + } + + sscanf(remainder, "%79s", timezone); + + if (ast_strlen_zero(timezone)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE, + LOG_ERROR, "%s: A timezone is required: '%s'\n", + ctx->tag, ctx->date_hdr); + } + + date_hdr_timeval = ast_mktime(&date_hdr_tm, timezone); + ctx->date_hdr_time = date_hdr_timeval.tv_sec; + current_timeval = ast_tvnow(); + + time_diff = ast_tvdiff_ms(current_timeval, date_hdr_timeval); + ast_trace(3, "%zu %zu %zu %d\n", current_timeval.tv_sec, + date_hdr_timeval.tv_sec, + (current_timeval.tv_sec - date_hdr_timeval.tv_sec), (int)time_diff); + if (time_diff < 0) { + /* An INVITE from the future! */ + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED, + LOG_ERROR, "%s: Future date: '%s'\n", + ctx->tag, ctx->date_hdr); + } else if (time_diff > (ctx->eprofile->vcfg_common.max_date_header_age * 1000)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED, + LOG_ERROR, "%s: More than %u seconds old: '%s'\n", + ctx->tag, ctx->eprofile->vcfg_common.max_date_header_age, ctx->date_hdr); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, + "%s: Success: '%s'\n", ctx->tag, ctx->date_hdr); +} + +#define FULL_URL_REGEX "^([a-zA-Z]+)://(([^@]+@[^:]+):)?(([^:/?]+)|([0-9.]+)|([[][0-9a-fA-F:]+[]]))(:([0-9]+))?(/([^#\\?]+))?(\\?([^#]+))?(#(.*))?" +#define FULL_URL_REGEX_GROUPS 15 +/* + * Broken down... + * ^([a-zA-Z]+) must start with scheme group 1 + * :// + * (([^@]+@[^:]+):)? optional user@pass group 3 + * ( start hostname group group 4 + * ([^:/?]+) normal fqdn group 5 + * |([0-9.]+) OR IPv4 address group 6 + * |([[][0-9a-fA-F:]+[]]) OR IPv6 address group 7 + * ) end hostname group + * (:([0-9]+))? optional port group 9 + * (/([^#\?]+))? optional path group 11 + * (\?([^#]+))? optional query string group 13 + * (#([^?]+))? optional fagment group 15 + * + * If you change the regex, make sure FULL_URL_REGEX_GROUPS is updated. + */ +#define URL_MATCH_SCHEME 1 +#define URL_MATCH_USERPASS 3 +#define URL_MATCH_HOST 4 +#define URL_MATCH_PORT 9 +#define URL_MATCH_PATH 11 +#define URL_MATCH_QUERY 13 +#define URL_MATCH_FRAGMENT 15 + +#define get_match_string(__x5u, __pmatch, __i) \ +({ \ + char *__match = NULL; \ + if (__pmatch[__i].rm_so >= 0) { \ + regoff_t __len = __pmatch[__i].rm_eo - __pmatch[__i].rm_so; \ + const char *__start = __x5u + __pmatch[__i].rm_so; \ + __match = ast_alloca(__len + 1); \ + ast_copy_string(__match, __start, __len + 1); \ + } \ + __match; \ +}) + +#define DUMP_X5U_MATCH() \ +{\ + int i; \ + if (TRACE_ATLEAST(4)) { \ + ast_trace(-1, "%s: x5u: %s\n", ctx->tag, x5u); \ + for (i=0;itag, i, m); \ + } \ + } \ + } \ +} + +static int check_x5u_url(struct ast_stir_shaken_vs_ctx * ctx, + const char *x5u) +{ + int max_groups = url_match_regex.re_nsub + 1; + regmatch_t pmatch[max_groups]; + int rc; + SCOPE_ENTER(3, "%s: Checking x5u '%s'\n", ctx->tag, x5u); + + rc = regexec(&url_match_regex, x5u, max_groups, pmatch, 0); + if (rc) { + char regex_error[512]; + regerror(rc, &url_match_regex, regex_error, sizeof(regex_error)); + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR, + "%s: x5u '%s' in Identity header failed basic URL validation: %s\n", + ctx->tag, x5u, regex_error); + } + + if (ctx->eprofile->vcfg_common.relax_x5u_port_scheme_restrictions + != relax_x5u_port_scheme_restrictions_YES) { + const char *scheme = get_match_string(x5u, pmatch, URL_MATCH_SCHEME); + const char *port = get_match_string(x5u, pmatch, URL_MATCH_PORT); + + if (!ast_strings_equal(scheme, "https")) { + DUMP_X5U_MATCH(); + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR, + "%s: x5u '%s': scheme '%s' not https\n", + ctx->tag, x5u, scheme); + } + if (!ast_strlen_zero(port)) { + if (!ast_strings_equal(port, "443") + || !ast_strings_equal(port, "8443")) { + DUMP_X5U_MATCH(); + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR, + "%s: x5u '%s': port '%s' not port 443 or 8443\n", + ctx->tag, x5u, port); + } + } + } + + if (ctx->eprofile->vcfg_common.relax_x5u_path_restrictions + != relax_x5u_path_restrictions_YES) { + const char *userpass = get_match_string(x5u, pmatch, URL_MATCH_USERPASS); + const char *qs = get_match_string(x5u, pmatch, URL_MATCH_QUERY); + const char *frag = get_match_string(x5u, pmatch, URL_MATCH_FRAGMENT); + + if (!ast_strlen_zero(userpass) || !ast_strlen_zero(qs) + || !ast_strlen_zero(frag)) { + DUMP_X5U_MATCH(); + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR, + "%s: x5u '%s' contains user:password, query parameters or fragment\n", + ctx->tag, x5u); + } + } + + return 0; +} + +enum ast_stir_shaken_vs_response_code + ast_stir_shaken_vs_verify(struct ast_stir_shaken_vs_ctx * ctx) +{ + RAII_VAR(char *, jwt_encoded, NULL, ast_free); + RAII_VAR(jwt_t *, jwt, NULL, jwt_free); + RAII_VAR(struct ast_json *, grants, NULL, ast_json_unref); + char *p = NULL; + char *grants_str = NULL; + const char *x5u; + const char *ppt_header = NULL; + const char *grant = NULL; + time_t now_s = time(NULL); + time_t iat; + struct ast_json *grant_obj = NULL; + int len; + int rc; + enum ast_stir_shaken_vs_response_code vs_rc; + SCOPE_ENTER(3, "%s: Verifying\n", ctx ? ctx->tag : "NULL"); + + if (!ctx) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR, + "%s: No context object!\n", "NULL"); + } + + if (ast_strlen_zero(ctx->identity_hdr)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR, + "%s: No identity header in ctx\n", ctx->tag); + } + + p = strchr(ctx->identity_hdr, ';'); + len = p - ctx->identity_hdr + 1; + jwt_encoded = ast_malloc(len); + if (!jwt_encoded) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR, + "%s: Failed to allocate memory for encoded jwt\n", ctx->tag); + } + + memcpy(jwt_encoded, ctx->identity_hdr, len); + jwt_encoded[len - 1] = '\0'; + + jwt_decode(&jwt, jwt_encoded, NULL, 0); + + ppt_header = jwt_get_header(jwt, "ppt"); + if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_PPT)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT, "%s: %s\n", + ctx->tag, vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT)); + } + + vs_rc = check_date_header(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR, + "%s: Date header verification failed\n", ctx->tag); + } + + x5u = jwt_get_header(jwt, "x5u"); + if (ast_strlen_zero(x5u)) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U, LOG_ERROR, + "%s: No x5u in Identity header\n", ctx->tag); + } + + rc = check_x5u_url(ctx, x5u); + if (rc != AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_RTN_VALUE(vs_rc, + "%s: x5u URL verification failed\n", ctx->tag); + } + + ast_trace(3, "%s: Decoded enough to get x5u: '%s'\n", ctx->tag, x5u); + if (ast_string_field_set(ctx, public_url, x5u) != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_INTERNAL_ERROR, LOG_ERROR, + "%s: Failed to set public_url '%s'\n", ctx->tag, x5u); + } + + iat = jwt_get_grant_int(jwt, "iat"); + if (iat == 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_IAT, LOG_ERROR, + "%s: No 'iat' in Identity header\n", ctx->tag); + } + ast_trace(1, "date_hdr: %zu iat: %zu diff: %zu\n", + ctx->date_hdr_time, iat, ctx->date_hdr_time - iat); + if (iat + ctx->eprofile->vcfg_common.max_iat_age < now_s) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_IAT_EXPIRED, + "%s: iat %ld older than %u seconds\n", ctx->tag, + iat, ctx->eprofile->vcfg_common.max_iat_age); + } + ctx->validity_check_time = iat; + + vs_rc = ctx_populate(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR, + "%s: Unable to populate ctx\n", ctx->tag); + } + + vs_rc = retrieve_verification_cert(ctx); + if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) { + SCOPE_EXIT_LOG_RTN_VALUE(vs_rc, LOG_ERROR, + "%s: Could not get valid cert from '%s'\n", ctx->tag, ctx->public_url); + } + + jwt_free(jwt); + jwt = NULL; + + rc = jwt_decode(&jwt, jwt_encoded, ctx->raw_key, ctx->raw_key_len); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION, + LOG_ERROR, "%s: Signature validation failed for '%s'\n", + ctx->tag, ctx->public_url); + } + + ast_trace(1, "%s: Decoding succeeded\n", ctx->tag); + + ppt_header = jwt_get_header(jwt, "alg"); + if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG, + "%s: %s\n", ctx->tag, + vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG)); + } + + ppt_header = jwt_get_header(jwt, "ppt"); + if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_PPT)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT, + "%s: %s\n", ctx->tag, + vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT)); + } + + ppt_header = jwt_get_header(jwt, "typ"); + if (!ppt_header || strcmp(ppt_header, STIR_SHAKEN_TYPE)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP, + "%s: %s\n", ctx->tag, + vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP)); + } + + grants_str = jwt_get_grants_json(jwt, NULL); + if (ast_strlen_zero(grants_str)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS, + "%s: %s\n", ctx->tag, + vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS)); + } + ast_trace(1, "grants: %s\n", grants_str); + grants = ast_json_load_string(grants_str, NULL); + ast_std_free(grants_str); + if (!grants) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS, + "%s: %s\n", ctx->tag, + vs_response_code_to_str(AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS)); + } + + grant = ast_json_object_string_get(grants, "attest"); + if (ast_strlen_zero(grant)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST, + "%s: No 'attest' in Identity header\n", ctx->tag); + } + if (grant[0] < 'A' || grant[0] > 'C') { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST, + "%s: Invalid attest value '%s'\n", ctx->tag, grant); + } + ast_string_field_set(ctx, attestation, grant); + ast_trace(1, "got attest: %s\n", grant); + + grant_obj = ast_json_object_get(grants, "dest"); + if (!grant_obj) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_DEST_TN, + "%s: No 'dest' in Identity header\n", ctx->tag); + } + if (TRACE_ATLEAST(3)) { + char *otn = ast_json_dump_string(grant_obj); + ast_trace(1, "got dest: %s\n", otn); + ast_json_free(otn); + } + + grant_obj = ast_json_object_get(grants, "orig"); + if (!grant_obj) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIG_TN, + "%s: No 'orig' in Identity header\n", ctx->tag); + } + if (TRACE_ATLEAST(3)) { + char *otn = ast_json_dump_string(grant_obj); + ast_trace(1, "got orig: %s\n", otn); + ast_json_free(otn); + } + grant = ast_json_object_string_get(grant_obj, "tn"); + if (!grant) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIG_TN, + "%s: No 'orig.tn' in Indentity header\n", ctx->tag); + } + ast_string_field_set(ctx, orig_tn, grant); + if (strcmp(ctx->caller_id, ctx->orig_tn) != 0) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH, + "%s: Mismatched cid '%s' and orig_tn '%s'\n", ctx->tag, + ctx->caller_id, grant); + } + + grant = ast_json_object_string_get(grants, "origid"); + if (ast_strlen_zero(grant)) { + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_NO_ORIGID, + "%s: No 'origid' in Identity header\n", ctx->tag); + } + + SCOPE_EXIT_RTN_VALUE(AST_STIR_SHAKEN_VS_SUCCESS, + "%s: verification succeeded\n", ctx->tag); +} + +int vs_reload() +{ + vs_config_reload(); + + return 0; +} + +int vs_unload() +{ + vs_config_unload(); + if (url_match_regex.re_nsub > 0) { + regfree(&url_match_regex); + } + + return 0; +} + +int vs_load() +{ + int rc = 0; + + if (vs_config_load()) { + return AST_MODULE_LOAD_DECLINE; + } + + rc = regcomp(&url_match_regex, FULL_URL_REGEX, REG_EXTENDED); + if (rc) { + char regex_error[512]; + regerror(rc, &url_match_regex, regex_error, sizeof(regex_error)); + ast_log(LOG_ERROR, "Verification service URL regex failed to compile: %s\n", regex_error); + vs_unload(); + return AST_MODULE_LOAD_DECLINE; + } + if (url_match_regex.re_nsub != FULL_URL_REGEX_GROUPS) { + ast_log(LOG_ERROR, "The verification service URL regex was updated without updating FULL_URL_REGEX_GROUPS\n"); + vs_unload(); + return AST_MODULE_LOAD_DECLINE; + } + + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_stir_shaken/verification.h b/res/res_stir_shaken/verification.h new file mode 100644 index 0000000000..65f5fb6970 --- /dev/null +++ b/res/res_stir_shaken/verification.h @@ -0,0 +1,75 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#ifndef VERIFICATION_H_ +#define VERIFICATION_H_ + +#include "common_config.h" + +struct ast_stir_shaken_vs_ctx { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(tag); + AST_STRING_FIELD(caller_id); + AST_STRING_FIELD(orig_tn); + AST_STRING_FIELD(identity_hdr); + AST_STRING_FIELD(date_hdr); + AST_STRING_FIELD(filename); + AST_STRING_FIELD(public_url); + AST_STRING_FIELD(hash); + AST_STRING_FIELD(hash_family); + AST_STRING_FIELD(url_family); + AST_STRING_FIELD(attestation); + AST_STRING_FIELD(cert_spc); + AST_STRING_FIELD(cert_cn); + ); + struct profile_cfg *eprofile; + struct ast_channel *chan; + time_t date_hdr_time; + time_t validity_check_time; + long raw_key_len; + unsigned char *raw_key; + char expiration[32]; + X509 *xcert; + enum ast_stir_shaken_vs_response_code failure_reason; +}; + +/*! + * \brief Load the stir/shaken verification service + * + * \retval 0 on success + * \retval -1 on error + */ +int vs_load(void); + +/*! + * \brief Reload the stir/shaken verification service + * + * \retval 0 on success + * \retval -1 on error + */ +int vs_reload(void); + +/*! + * \brief Unload the stir/shaken verification service + * + * \retval 0 on success + * \retval -1 on error + */ +int vs_unload(void); + +#endif /* VERIFICATION_H_ */ diff --git a/res/res_stir_shaken/verification_config.c b/res/res_stir_shaken/verification_config.c new file mode 100644 index 0000000000..4f44995b75 --- /dev/null +++ b/res/res_stir_shaken/verification_config.c @@ -0,0 +1,440 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2023, Sangoma Technologies Corporation + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include "asterisk/cli.h" +#include "stir_shaken.h" + +#define CONFIG_TYPE "verification" + +#define DEFAULT_global_disable 0 + +#define DEFAULT_ca_file NULL +#define DEFAULT_ca_path NULL +#define DEFAULT_crl_file NULL +#define DEFAULT_crl_path NULL +static char DEFAULT_cert_cache_dir[PATH_MAX]; + +#define DEFAULT_curl_timeout 2 +#define DEFAULT_max_iat_age 15 +#define DEFAULT_max_date_header_age 15 +#define DEFAULT_max_cache_entry_age 3600 +#define DEFAULT_max_cache_size 1000 +#define DEFAULT_stir_shaken_failure_action stir_shaken_failure_action_CONTINUE +#define DEFAULT_use_rfc9410_responses use_rfc9410_responses_NO +#define DEFAULT_relax_x5u_port_scheme_restrictions relax_x5u_port_scheme_restrictions_NO +#define DEFAULT_relax_x5u_path_restrictions relax_x5u_path_restrictions_NO +#define DEFAULT_load_system_certs load_system_certs_NO + +static struct verification_cfg *empty_cfg = NULL; + +#define STIR_SHAKEN_DIR_NAME "stir_shaken" + +struct verification_cfg *vs_get_cfg(void) +{ + struct verification_cfg *cfg = ast_sorcery_retrieve_by_id(get_sorcery(), + CONFIG_TYPE, CONFIG_TYPE); + if (cfg) { + return cfg; + } + + return empty_cfg ? ao2_bump(empty_cfg) : NULL; +} + +int vs_is_config_loaded(void) +{ + struct verification_cfg *cfg = ast_sorcery_retrieve_by_id(get_sorcery(), + CONFIG_TYPE, CONFIG_TYPE); + ao2_cleanup(cfg); + + return !!cfg; +} + +generate_vcfg_common_sorcery_handlers(verification_cfg); + +void vcfg_cleanup(struct verification_cfg_common *vcfg_common) +{ + if (!vcfg_common) { + return; + } + ast_string_field_free_memory(vcfg_common); + if (vcfg_common->tcs) { + crypto_free_cert_store(vcfg_common->tcs); + } + ast_free_acl_list(vcfg_common->acl); +} + +static void verification_destructor(void *obj) +{ + struct verification_cfg *cfg = obj; + ast_string_field_free_memory(cfg); + vcfg_cleanup(&cfg->vcfg_common); +} + +static void *verification_alloc(const char *name) +{ + struct verification_cfg *cfg; + + cfg = ast_sorcery_generic_alloc(sizeof(*cfg), verification_destructor); + if (!cfg) { + return NULL; + } + + if (ast_string_field_init(cfg, 1024)) { + ao2_ref(cfg, -1); + return NULL; + } + + /* + * The memory for vcfg_common actually comes from cfg + * due to the weirdness of the STRFLDSET macro used with + * sorcery. We just use a token amount of memory in + * this call so the initialize doesn't fail. + */ + if (ast_string_field_init(&cfg->vcfg_common, 8)) { + ao2_ref(cfg, -1); + return NULL; + } + + return cfg; +} + +int vs_copy_cfg_common(const char *id, struct verification_cfg_common *cfg_dst, + struct verification_cfg_common *cfg_src) +{ + int rc = 0; + + if (!cfg_dst || !cfg_src) { + return -1; + } + + if (!cfg_dst->tcs && cfg_src->tcs) { + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, ca_file); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, ca_path); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, crl_file); + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, crl_path); + X509_STORE_up_ref(cfg_src->tcs); + cfg_dst->tcs = cfg_src->tcs; + } + + cfg_sf_copy_wrapper(id, cfg_dst, cfg_src, cert_cache_dir); + + cfg_uint_copy(cfg_dst, cfg_src, curl_timeout); + cfg_uint_copy(cfg_dst, cfg_src, max_iat_age); + cfg_uint_copy(cfg_dst, cfg_src, max_date_header_age); + cfg_uint_copy(cfg_dst, cfg_src, max_cache_entry_age); + cfg_uint_copy(cfg_dst, cfg_src, max_cache_size); + + cfg_enum_copy(cfg_dst, cfg_src, stir_shaken_failure_action); + cfg_enum_copy(cfg_dst, cfg_src, use_rfc9410_responses); + cfg_enum_copy(cfg_dst, cfg_src, relax_x5u_port_scheme_restrictions); + cfg_enum_copy(cfg_dst, cfg_src, relax_x5u_path_restrictions); + cfg_enum_copy(cfg_dst, cfg_src, load_system_certs); + + if (cfg_src->acl) { + ast_free_acl_list(cfg_dst->acl); + cfg_dst->acl = ast_duplicate_acl_list(cfg_src->acl); + } + + return rc; +} + +int vs_check_common_config(const char *id, + struct verification_cfg_common *vcfg_common) +{ + SCOPE_ENTER(3, "%s: Checking common config\n", id); + + if (!ast_strlen_zero(vcfg_common->ca_file) + && !ast_file_is_readable(vcfg_common->ca_file)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: ca_file '%s' not found, or is unreadable\n", + id, vcfg_common->ca_file); + } + + if (!ast_strlen_zero(vcfg_common->ca_path) + && !ast_file_is_readable(vcfg_common->ca_path)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: ca_path '%s' not found, or is unreadable\n", + id, vcfg_common->ca_path); + } + + if (!ast_strlen_zero(vcfg_common->crl_file) + && !ast_file_is_readable(vcfg_common->crl_file)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: crl_file '%s' not found, or is unreadable\n", + id, vcfg_common->crl_file); + } + + if (!ast_strlen_zero(vcfg_common->crl_path) + && !ast_file_is_readable(vcfg_common->crl_path)) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: crl_path '%s' not found, or is unreadable\n", + id, vcfg_common->crl_path); + } + + if (!ast_strlen_zero(vcfg_common->ca_file) + || !ast_strlen_zero(vcfg_common->ca_path)) { + int rc = 0; + + if (!vcfg_common->tcs) { + vcfg_common->tcs = crypto_create_cert_store(); + if (!vcfg_common->tcs) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to create CA cert store\n", id); + } + } + rc = crypto_load_cert_store(vcfg_common->tcs, + vcfg_common->ca_file, vcfg_common->ca_path); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to load CA cert store from '%s' or '%s'\n", + id, vcfg_common->ca_file, vcfg_common->ca_path); + } + } + + if (!ast_strlen_zero(vcfg_common->crl_file) + || !ast_strlen_zero(vcfg_common->crl_path)) { + int rc = 0; + + if (!vcfg_common->tcs) { + vcfg_common->tcs = crypto_create_cert_store(); + if (!vcfg_common->tcs) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to create CA cert store\n", id); + } + } + rc = crypto_load_cert_store(vcfg_common->tcs, + vcfg_common->crl_file, vcfg_common->crl_path); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to load CA CRL store from '%s' or '%s'\n", + id, vcfg_common->crl_file, vcfg_common->crl_path); + } + } + + if (vcfg_common->tcs) { + if (ENUM_BOOL(vcfg_common->load_system_certs, load_system_certs)) { + X509_STORE_set_default_paths(vcfg_common->tcs); + } + + if (!ast_strlen_zero(vcfg_common->crl_file) + || !ast_strlen_zero(vcfg_common->crl_path)) { + X509_STORE_set_flags(vcfg_common->tcs, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); + } + } + + if (!ast_strlen_zero(vcfg_common->cert_cache_dir)) { + FILE *fp; + char *testfile; + + if (ast_asprintf(&testfile, "%s/testfile", vcfg_common->cert_cache_dir) <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: Unable to allocate memory for testfile\n", id); + } + + fp = fopen(testfile, "w+"); + if (!fp) { + ast_free(testfile); + SCOPE_EXIT_LOG_RTN_VALUE(-1, LOG_ERROR, + "%s: cert_cache_dir '%s' was not writable\n", + id, vcfg_common->cert_cache_dir); + } + fclose(fp); + remove(testfile); + ast_free(testfile); + } + + SCOPE_EXIT_RTN_VALUE(0, "%s: Done\n", id); +} + +static char *special_addresses[] = { + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.0.0/29", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "240.0.0.0/4", + "255.255.255.255/32", + "::1/128", + "::/128", +/* "64:ff9b::/96", IPv4-IPv6 translation addresses should probably not be blocked by default */ +/* "::ffff:0:0/96", IPv4 mapped addresses should probably not be blocked by default */ + "100::/64", + "2001::/23", + "2001::/32", + "2001:2::/48", + "2001:db8::/32", + "2001:10::/28", +/* "2002::/16", 6to4 should problably not be blocked by default */ + "fc00::/7", + "fe80::/10", +}; + +static int verification_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct verification_cfg *cfg = obj; + const char *id = ast_sorcery_object_get_id(cfg); + + if (vs_check_common_config("verification", &cfg->vcfg_common) !=0) { + return -1; + } + + if (!cfg->vcfg_common.acl) { + int error = 0; + int ignore; + int i; + + ast_append_acl("permit", "0.0.0.0/0", &cfg->vcfg_common.acl, &error, &ignore); + if (error) { + ast_free_acl_list(cfg->vcfg_common.acl); + cfg->vcfg_common.acl = NULL; + ast_log(LOG_ERROR, "%s: Unable to create default acl rule for '%s: %s'\n", + id, "permit", "0.0.0.0/0"); + return -1; + } + + for (i = 0; i < ARRAY_LEN(special_addresses); i++) { + ast_append_acl("deny", special_addresses[i], &cfg->vcfg_common.acl, &error, &ignore); + if (error) { + ast_free_acl_list(cfg->vcfg_common.acl); + cfg->vcfg_common.acl = NULL; + ast_log(LOG_ERROR, "%s: Unable to create default acl rule for '%s: %s'\n", + id, "deny", special_addresses[i]); + return -1; + } + } + } + + return 0; +} + +static char *cli_verification_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct verification_cfg *cfg; + struct config_object_cli_data data = { + .title = "Default Verification", + .object_type = config_object_type_verification, + }; + + switch(cmd) { + case CLI_INIT: + e->command = "stir_shaken show verification"; + e->usage = + "Usage: stir_shaken show verification\n" + " Show the stir/shaken verification settings\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + cfg = vs_get_cfg(); + config_object_cli_show(cfg, a, &data, 0); + + ao2_cleanup(cfg); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry verification_cli[] = { + AST_CLI_DEFINE(cli_verification_show, "Show stir/shaken verification configuration"), +}; + +int vs_config_reload(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + ast_sorcery_force_reload_object(sorcery, CONFIG_TYPE); + + if (!vs_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken verification service disabled. Either there were errors in the 'verification' object in stir_shaken.conf or it was missing altogether.\n"); + } + if (!empty_cfg) { + empty_cfg = verification_alloc(CONFIG_TYPE); + if (!empty_cfg) { + return -1; + } + empty_cfg->global_disable = 1; + } + + return 0; +} + +int vs_config_unload(void) +{ + ast_cli_unregister_multiple(verification_cli, + ARRAY_LEN(verification_cli)); + ao2_cleanup(empty_cfg); + + return 0; +} + +int vs_config_load(void) +{ + struct ast_sorcery *sorcery = get_sorcery(); + + snprintf(DEFAULT_cert_cache_dir, sizeof(DEFAULT_cert_cache_dir), "%s/keys/%s/cache", + ast_config_AST_DATA_DIR, STIR_SHAKEN_DIR_NAME); + + ast_sorcery_apply_default(sorcery, CONFIG_TYPE, "config", + "stir_shaken.conf,criteria=type=" CONFIG_TYPE ",single_object=yes,explicit_name=" CONFIG_TYPE); + + if (ast_sorcery_object_register(sorcery, CONFIG_TYPE, verification_alloc, + NULL, verification_apply)) { + ast_log(LOG_ERROR, "stir/shaken - failed to register '%s' sorcery object\n", CONFIG_TYPE); + return -1; + } + + ast_sorcery_object_field_register_nodoc(sorcery, CONFIG_TYPE, "type", "", + OPT_NOOP_T, 0, 0); + + ast_sorcery_object_field_register(sorcery, CONFIG_TYPE, "global_disable", + DEFAULT_global_disable ? "yes" : "no", + OPT_YESNO_T, 1, FLDSET(struct verification_cfg, global_disable)); + + register_common_verification_fields(sorcery, verification_cfg, CONFIG_TYPE,); + + ast_sorcery_load_object(sorcery, CONFIG_TYPE); + + if (!vs_is_config_loaded()) { + ast_log(LOG_WARNING,"Stir/Shaken verification service disabled. Either there were errors in the 'verification' object in stir_shaken.conf or it was missing altogether.\n"); + } + if (!empty_cfg) { + empty_cfg = verification_alloc(CONFIG_TYPE); + if (!empty_cfg) { + return -1; + } + empty_cfg->global_disable = 1; + } + + ast_cli_register_multiple(verification_cli, + ARRAY_LEN(verification_cli)); + + return 0; +}