;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
;[res_pjsip_publish_asterisk]
;asterisk-publication=realtime,ps_asterisk_publications
+
+;[res_stir_shaken]
+;tn=realtime,stir_tn
-;
-; 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
-; <ast_config_AST_DATA_DIR>/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: <astvarlibdir>/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
+
--- /dev/null
+"""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')
*/
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);
#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" */
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.
*
*/
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 */
#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"
-
-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 */
-};
+#include "asterisk/sorcery.h"
-/*! 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_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
};
-struct ast_stir_shaken_payload;
+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_acl_list;
+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_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
+ *
+ * \param ctx AS context
+ * \param alg Fingerprint algorithm ("sha-1" or "sha-256")
+ * \param fingerprint Fingerprint
*
- * \retval The signature timeout
+ * \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 a STIR/SHAKEN verification result to a channel
+ * \brief Add the received Identity header value to the VS context
*
- * \param chan The channel
- * \param identity The identity
- * \param attestation The attestation
- * \param result The verification result
+ * \param ctx VS context
+ * \param identity_hdr Identity header value
*
- * \retval -1 on failure
- * \retval 0 on success
+ * \retval AST_STIR_SHAKEN_VS_SUCCESS if successful
+ * \retval Other AST_STIR_SHAKEN_VS errors.
*/
-int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation,
- enum ast_stir_shaken_verification_result result);
+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 Verify a JSON STIR/SHAKEN payload
+ * \brief Add the received Date header value to the VS context
*
- * \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
+ * \param date_hdr Date header value
*
- * \retval ast_stir_shaken_payload on success
- * \retval NULL on failure
+ * \retval AST_STIR_SHAKEN_VS_SUCCESS if successful
+ * \retval Other AST_STIR_SHAKEN_VS 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);
+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 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
+ * \brief Get failure_action from context
*
- * \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
+ * \param ctx VS context
*
- * \retval ast_stir_shaken_payload on success
- * \retval NULL on failure
+ * \retval ast_stir_shaken_failure_action
*/
-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);
+enum stir_shaken_failure_action_enum
+ ast_stir_shaken_vs_get_failure_action(
+ struct ast_stir_shaken_vs_ctx *ctx);
/*!
- * \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
+ * \brief Get use_rfc9410_responses from context
*
- * \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
+ * \param ctx VS context
*
- * \retval ast_stir_shaken_payload on success
- * \retval NULL on failure
+ * \retval 1 if true
+ * \retval 0 if false
*/
-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);
+int ast_stir_shaken_vs_get_use_rfc9410_responses(
+ struct ast_stir_shaken_vs_ctx *ctx);
/*!
- * \brief Retrieve the stir/shaken sorcery context
+ * \brief Add a STIR/SHAKEN verification result to a channel
*
- * \retval The stir/shaken sorcery context
- */
-struct ast_sorcery *ast_stir_shaken_sorcery(void);
-
-/*!
- * \brief Free a STIR/SHAKEN payload
+ * \param ctx VS context
+ *
+ * \retval -1 on failure
+ * \retval 0 on success
*/
-void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload);
+int ast_stir_shaken_add_result_to_channel(
+ struct ast_stir_shaken_vs_ctx *ctx);
/*!
- * \brief Sign a JSON STIR/SHAKEN payload
- *
- * \note This function will automatically add the "attest", "iat", and "origid" fields.
+ * \brief Perform incoming call verification
*
- * \param json The JWT to sign
+ * \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_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 */
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")
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);
/* 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))
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];
#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
{
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;
}
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));
*/
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) {
#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"
}
}
+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;
{
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;
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 */
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);
static int unload_module(void)
{
+ pjsip_reason_header_unload();
+
#ifdef TEST_FRAMEWORK
AST_TEST_UNREGISTER(test_resolve_refresh_media_states);
#endif
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:
*;
};
/*
* Asterisk -- An open source telephony toolkit.
*
- * Copyright (C) 2020, Sangoma Technologies Corporation
+ * Copyright (C) 2023, Sangoma Technologies Corporation
*
- * Kevin Harwell <kharwell@sangoma.com>
+ * George Joseph <gjoseph@sangoma.com>
*
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* 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_ */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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,"<missing protocol>"),
+ code, S_OR(text, "<missing 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);
+}
#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 };
+
+/* 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,
+};
-/*!
- * \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)
+#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)
-{
- 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"));
+#define translate_code(_vs_rc, _sip_rc) \
+case AST_STIR_SHAKEN_VS_ ## _vs_rc: \
+ return SIP_RESPONSE_CODE_ ## _sip_rc;
- if (strcmp(caller_id, caller_id_other)) {
- return -1;
- }
-
- return 0;
-}
-
-/*!
- * \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)
+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);
- 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;
+ /*
+ * 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 500;
}
-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;
- }
+enum process_failure_rc {
+ PROCESS_FAILURE_CONTINUE = 0,
+ PROCESS_FAILURE_REJECT,
+ PROCESS_FAILURE_SYSTEM_FAILURE,
+};
- return 0;
+static void reject_incoming_call(struct ast_sip_session *session,
+ enum sip_response_code response_code)
+{
+ ast_sip_session_terminate(session, response_code);
+ ast_hangup(session->channel);
}
-/* 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)
+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)
{
- 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);
+ 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);
}
- ast_hangup(session->channel);
+ SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_CONTINUE,
+ "%s: Continuing\n", tag);
}
/*!
*/
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);
- 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;
+ 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);
}
- /* 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;
+ identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_hdr_str);
+ if (ast_strlen_zero(identity_hdr_val)) {
+ 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);
}
- /* 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;
+ 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);
}
- 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;
+ 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);
}
+ SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: No Date 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;
- }
-
- 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;
+ 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);
}
- 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);
+ 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);
- return 1;
}
- ast_stir_shaken_payload_free(ss_payload);
- mismatch |= compare_caller_id(caller_id, payload);
- mismatch |= compare_timestamp(payload);
+ ast_stir_shaken_add_result_to_channel(ctx);
- if (mismatch) {
- ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_MISMATCH);
- return 0;
- }
+ SCOPE_EXIT_RTN_VALUE(0, "Passed\n");
+}
- ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_PASSED);
+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);
- return 0;
+ 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 int add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
+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
s++;
}
*new_tn = '\0';
- ast_debug(4, "Canonicalized telephone number %.*s -> %s\n", (int) uri->user.slen, uri->user.ptr, 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;
+ ast_trace(2, "Canonicalized telephone number " PJSTR_PRINTF_SPEC " -> %s\n",
+ PJSTR_PRINTF_VAR(uri->user), dest_tn);
}
- 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=<public_cert_url>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);
+
+ 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");
+ }
- 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;
+ 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 (ast_strlen_zero(session->id.number.str) && session->id.number.valid) {
- 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");
}
- /* 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;
+ 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 = {
*/
/*** MODULEINFO
- <depend>crypto</depend>
<depend>curl</depend>
<depend>res_curl</depend>
+ <depend>libjwt</depend>
<support_level>core</support_level>
***/
-#include "asterisk.h"
+#define _TRACE_PREFIX_ "rss",__LINE__, ""
-#include <openssl/evp.h>
+#include "asterisk.h"
-#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/app.h"
+#include "asterisk/cli.h"
#include "asterisk/conversions.h"
-#include "asterisk/pbx.h"
+#include "asterisk/module.h"
#include "asterisk/global_datastores.h"
-#include "asterisk/app.h"
-#include "asterisk/test.h"
-#include "asterisk/acl.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
- <configInfo name="res_stir_shaken" language="en_US">
- <synopsis>STIR/SHAKEN module for Asterisk</synopsis>
- <configFile name="stir_shaken.conf">
- <configObject name="general">
- <synopsis>STIR/SHAKEN general options</synopsis>
- <configOption name="type">
- <synopsis>Must be of type 'general'.</synopsis>
- </configOption>
- <configOption name="ca_file" default="">
- <synopsis>File path to the certificate authority certificate</synopsis>
- </configOption>
- <configOption name="ca_path" default="">
- <synopsis>File path to a chain of trust</synopsis>
- </configOption>
- <configOption name="cache_max_size" default="1000">
- <synopsis>Maximum size to use for caching public keys</synopsis>
- </configOption>
- <configOption name="curl_timeout" default="2">
- <synopsis>Maximum time to wait to CURL certificates</synopsis>
- </configOption>
- <configOption name="signature_timeout" default="15">
- <synopsis>Amount of time a signature is valid for</synopsis>
- </configOption>
- </configObject>
- <configObject name="store">
- <synopsis>STIR/SHAKEN certificate store options</synopsis>
- <configOption name="type">
- <synopsis>Must be of type 'store'.</synopsis>
- </configOption>
- <configOption name="path" default="">
- <synopsis>Path to a directory containing certificates</synopsis>
- </configOption>
- <configOption name="public_cert_url" default="">
- <synopsis>URL to the public certificate(s)</synopsis>
- <description><para>
- 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
- </para></description>
- </configOption>
- </configObject>
- <configObject name="certificate">
- <synopsis>STIR/SHAKEN certificate options</synopsis>
- <configOption name="type">
- <synopsis>Must be of type 'certificate'.</synopsis>
- </configOption>
- <configOption name="path" default="">
- <synopsis>File path to a certificate</synopsis>
- </configOption>
- <configOption name="public_cert_url" default="">
- <synopsis>URL to the public certificate</synopsis>
- <description><para>
- Must be a valid http, or https, URL.
- </para></description>
- </configOption>
- <configOption name="attestation">
- <synopsis>Attestation level</synopsis>
- </configOption>
- <configOption name="caller_id_number" default="">
- <synopsis>The caller ID number to match on.</synopsis>
- </configOption>
- </configObject>
- <configObject name="profile">
- <synopsis>STIR/SHAKEN profile configuration options</synopsis>
- <configOption name="type">
- <synopsis>Must be of type 'profile'.</synopsis>
- </configOption>
- <configOption name="stir_shaken" default="on">
- <synopsis>STIR/SHAKEN configuration settings</synopsis>
- <description><para>
- 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.</para>
- </description>
- </configOption>
- <configOption name="acllist" default="">
- <synopsis>An existing ACL from acl.conf to use</synopsis>
- </configOption>
- <configOption name="permit" default="">
- <synopsis>An IP or subnet to permit</synopsis>
- </configOption>
- <configOption name="deny" default="">
- <synopsis>An IP or subnet to deny</synopsis>
- </configOption>
- </configObject>
- </configFile>
- </configInfo>
- <function name="STIR_SHAKEN" language="en_US">
- <synopsis>
- Gets the number of STIR/SHAKEN results or a specific STIR/SHAKEN value from a result on the channel.
- </synopsis>
- <syntax>
- <parameter name="index" required="true">
- <para>The index of the STIR/SHAKEN result to get. If only 'count' is passed in, gets the number of STIR/SHAKEN results instead.</para>
- </parameter>
- <parameter name="value" required="false">
- <para>The value to get from the STIR/SHAKEN result. Only used when an index is passed in (instead of 'count'). Allowable values:</para>
- <enumlist>
- <enum name = "identity" />
- <enum name = "attestation" />
- <enum name = "verify_result" />
- </enumlist>
- </parameter>
- </syntax>
- <description>
- <para>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.
- </para>
- <example title="Get count and retrieve value">
- 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)})
- </example>
- </description>
- </function>
- ***/
-
-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)
-{
- 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);
-}
+static int tn_auth_list_nid;
-/*!
- * \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)
+int get_tn_auth_nid(void)
{
- 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;
};
/*!
*
* \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;
*
* \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:
* \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;
/* 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)) {
}
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++;
/* 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;
}
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;
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)
+static int reload_module(void)
{
- 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);
+ return common_config_reload();
}
-/*!
- * \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)
+static int unload_module(void)
{
- 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;
- }
+ int res = 0;
- 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;
- }
+ common_config_unload();
+ crypto_unload();
- fclose(file);
+ res |= ast_custom_function_unregister(&stir_shaken_function);
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;
-}
+#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"
-AST_TEST_DEFINE(test_stir_shaken_verify)
+static int check_for_old_config(void)
{
- 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);
+ 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;
- /* 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;
+ 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;
}
-
- 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);
+ 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 0;
-}
-
-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;
-
- res |= ast_custom_function_unregister(&stir_shaken_function);
-
- AST_TEST_UNREGISTER(test_stir_shaken_sign);
- AST_TEST_UNREGISTER(test_stir_shaken_verify);
-
- return res;
+ 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;
}
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <jwt.h>
+
+#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;
+}
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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_ */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@digium.com>
+ *
+ * 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;
+}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@digium.com>
- *
- * 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 <sys/stat.h>
-
-#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 <id>\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;
-}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@sangoma.com>
- *
- * 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 <openssl/evp.h>
-
-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 */
-
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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");
+}
+
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <openssl/evp.h>
+
+#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 <param_name>_SOMETHING and their defaults as DEFAULT_<param_name>.
+ */
+#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_<field_name>.
+ *
+ */
+#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_ */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <openssl/err.h>
+#include <openssl/ssl.h>
+#include <openssl/evp.h>
+#include <openssl/md5.h>
+#include <openssl/sha.h>
+#include <openssl/bio.h>
+#include <openssl/obj_mac.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+#include <openssl/x509_vfy.h>
+
+#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;
+}
+
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 */
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Ben Ford <bford@sangoma.com>
- *
- * 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 <curl/curl.h>
-#include <sys/stat.h>
-
-/* 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;
-}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Ben Ford <bford@sangoma.com>
- *
- * 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 */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <curl/curl.h>
+
+#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;
+}
+
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <curl/curl.h>
+#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 */
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@digium.com>
- *
- * 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;
-}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@sangoma.com>
- *
- * 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 */
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2022, Sangoma Technologies Corporation
- *
- * Ben Ford <bford@sangoma.com>
- *
- * 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 <id>\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;
-}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2022, Sangoma Technologies Corporation
- *
- * Ben Ford <bford@sangoma.com>
- *
- * 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 */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * Ben Ford <bford@sangoma.com>
+ *
+ * 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 <id>\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 <id>\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;
+}
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2022, Sangoma Technologies Corporation
- *
- * Ben Ford <bford@sangoma.com>
- *
- * 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 */
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@digium.com>
- *
- * 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 <openssl/evp.h>
-#include <openssl/pem.h>
-
-#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;
-}
#ifndef _STIR_SHAKEN_H
#define _STIR_SHAKEN_H
-#include <openssl/evp.h>
+#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
- *
- * \param obj A sorcery object containing configuration data
- * \param arg Asterisk CLI argument object
- * \param flags ao2 container flags
+ * \brief Retrieve the stir/shaken sorcery context
*
- * \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
- *
- * \param word The word to tab complete on
- * \param container The sorcery container to iterate through
+ * \brief Return string version of VS response code
*
- * \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
+ */
+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
*/
-char *stir_shaken_get_serial_number_x509(const char *buf, size_t buf_size);
+struct trusted_cert_store *get_trusted_cert_store(void);
+
#endif /* _STIR_SHAKEN_H */
--- /dev/null
+<!DOCTYPE docs SYSTEM "appdocsxml.dtd">
+<?xml-stylesheet type="text/xsl" href="appdocsxml.xslt"?>
+<docs xmlns:xi="http://www.w3.org/2001/XInclude">
+ <configInfo name="res_stir_shaken" language="en_US">
+ <synopsis>STIR/SHAKEN module for Asterisk</synopsis>
+ <configFile name="stir_shaken.conf">
+ <configObject name="attestation">
+ <synopsis>STIR/SHAKEN attestation options</synopsis>
+ <configOption name="global_disable" default="false">
+ <synopsis>Globally disable verification</synopsis>
+ </configOption>
+ <configOption name="private_key_file" default="">
+ <synopsis>File path to a certificate</synopsis>
+ </configOption>
+ <configOption name="public_cert_url" default="">
+ <synopsis>URL to the public certificate</synopsis>
+ <description><para>
+ Must be a valid http, or https, URL.
+ </para></description>
+ </configOption>
+ <configOption name="attest_level">
+ <synopsis>Attestation level</synopsis>
+ </configOption>
+ <configOption name="check_tn_cert_public_url" default="false">
+ <synopsis>On load, Retrieve all TN's certificates and validate their dates</synopsis>
+ </configOption>
+ <configOption name="send_mky" default="no">
+ <synopsis>Send a media key (mky) grant in the attestation for DTLS calls.
+ (not common)</synopsis>
+ </configOption>
+ </configObject>
+ <configObject name="tn">
+ <synopsis>STIR/SHAKEN TN options</synopsis>
+ <configOption name="type">
+ <synopsis>Must be of type 'tn'.</synopsis>
+ </configOption>
+ <configOption name="private_key_file" default="">
+ <synopsis>File path to a certificate</synopsis>
+ </configOption>
+ <configOption name="public_cert_url" default="">
+ <synopsis>URL to the public certificate</synopsis>
+ <description><para>
+ Must be a valid http, or https, URL.
+ </para></description>
+ </configOption>
+ <configOption name="attest_level">
+ <synopsis>Attestation level</synopsis>
+ </configOption>
+ <configOption name="check_tn_cert_public_url" default="false">
+ <synopsis>On load, Retrieve all TN's certificates and validate their dates</synopsis>
+ </configOption>
+ <configOption name="send_mky" default="no">
+ <synopsis>Send a media key (mky) grant in the attestation for DTLS calls.
+ (not common)</synopsis>
+ </configOption>
+ </configObject>
+ <configObject name="verification">
+ <synopsis>STIR/SHAKEN verification options</synopsis>
+ <configOption name="global_disable" default="false">
+ <synopsis>Globally disable verification</synopsis>
+ </configOption>
+ <configOption name="load_system_certs" default="">
+ <synopsis>A boolean indicating whether trusted CA certificates should be loaded from the system</synopsis>
+ </configOption>
+ <configOption name="ca_file" default="">
+ <synopsis>Path to a file containing one or more CA certs</synopsis>
+ </configOption>
+ <configOption name="ca_path" default="">
+ <synopsis>Path to a directory containing one or more hashed CA certs</synopsis>
+ </configOption>
+ <configOption name="crl_file" default="">
+ <synopsis>Path to a file containing a CRL</synopsis>
+ </configOption>
+ <configOption name="crl_path" default="">
+ <synopsis>Path to a directory containing one or more hashed CRLs</synopsis>
+ </configOption>
+ <configOption name="cert_cache_dir" default="">
+ <synopsis>Directory to cache retrieved verification certs</synopsis>
+ </configOption>
+ <configOption name="curl_timeout" default="2">
+ <synopsis>Maximum time to wait to CURL certificates</synopsis>
+ </configOption>
+ <configOption name="max_cache_entry_age" default="60">
+ <synopsis>Number of seconds a cache entry may be behind current time</synopsis>
+ </configOption>
+ <configOption name="max_cache_size" default="1000">
+ <synopsis>Maximum size to use for caching public keys</synopsis>
+ </configOption>
+ <configOption name="max_iat_age" default="15">
+ <synopsis>Number of seconds an iat grant may be behind current time</synopsis>
+ </configOption>
+ <configOption name="max_date_header_age" default="15">
+ <synopsis>Number of seconds a SIP Date header may be behind current time</synopsis>
+ </configOption>
+ <configOption name="failure_action" default="continue">
+ <synopsis>The default failure action when not set on a profile</synopsis>
+ <description>
+ <enumlist>
+ <enum name="continue">
+ <para>If set to <literal>continue</literal>, continue and let
+ the dialplan decide what action to take.</para>
+ </enum>
+ <enum name="reject_request">
+ <para>If set to <literal>reject_request</literal>, reject the incoming
+ request with response codes defined in RFC8224.
+ </para>
+ </enum>
+ <enum name="continue_return_reason">
+ <para>If set to <literal>return_reason</literal>, continue to the
+ dialplan but add a <literal>Reason</literal> header to the sender in
+ the next provisional response.</para>
+ </enum>
+ </enumlist>
+ </description>
+ </configOption>
+ <configOption name="use_rfc9410_responses" default="no">
+ <synopsis>RFC9410 uses the STIR protocol on Reason headers
+ instead of the SIP protocol</synopsis>
+ </configOption>
+ <configOption name="relax_x5u_port_scheme_restrictions" default="no">
+ <synopsis>Relaxes check for "https" and port 443 or 8443
+ in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="relax_x5u_path_restrictions" default="no">
+ <synopsis>Relaxes check for query parameters, user/password, etc.
+ in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_acl" default="">
+ <synopsis>An existing ACL from acl.conf to use when checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_permit" default="">
+ <synopsis>An IP or subnet to permit when checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_deny" default="">
+ <synopsis>An IP or subnet to deny checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ </configObject>
+ <configObject name="profile">
+ <synopsis>STIR/SHAKEN profile configuration options</synopsis>
+ <configOption name="type">
+ <synopsis>Must be of type 'profile'.</synopsis>
+ </configOption>
+ <configOption name="load_system_certs" default="">
+ <synopsis>A boolean indicating whether trusted CA certificates should be loaded from the system</synopsis>
+ </configOption>
+ <configOption name="ca_file" default="">
+ <synopsis>Path to a file containing one or more CA certs</synopsis>
+ </configOption>
+ <configOption name="ca_path" default="">
+ <synopsis>Path to a directory containing one or more hashed CA certs</synopsis>
+ </configOption>
+ <configOption name="crl_file" default="">
+ <synopsis>Path to a file containing a CRL</synopsis>
+ </configOption>
+ <configOption name="crl_path" default="">
+ <synopsis>Path to a directory containing one or more hashed CRLs</synopsis>
+ </configOption>
+ <configOption name="cert_cache_dir" default="">
+ <synopsis>Directory to cache retrieved verification certs</synopsis>
+ </configOption>
+ <configOption name="curl_timeout" default="2">
+ <synopsis>Maximum time to wait to CURL certificates</synopsis>
+ </configOption>
+ <configOption name="max_iat_age" default="15">
+ <synopsis>Number of seconds an iat grant may be behind current time</synopsis>
+ </configOption>
+ <configOption name="max_date_header_age" default="15">
+ <synopsis>Number of seconds a SIP Date header may be behind current time</synopsis>
+ </configOption>
+ <configOption name="max_cache_entry_age" default="60">
+ <synopsis>Number of seconds a cache entry may be behind current time</synopsis>
+ </configOption>
+ <configOption name="max_cache_size" default="1000">
+ <synopsis>Maximum size to use for caching public keys</synopsis>
+ </configOption>
+ <configOption name="endpoint_behavior" default="off">
+ <synopsis>Actions performed when an endpoint references this profile</synopsis>
+ <description>
+ <enumlist>
+ <enum name="off">
+ <para>Don't do any STIR/SHAKEN processing.</para>
+ </enum>
+ <enum name="attest">
+ <para>Attest on outgoing calls.</para>
+ </enum>
+ <enum name="verify">
+ <para>Verify incoming calls.</para>
+ </enum>
+ <enum name="on">
+ <para>Attest outgoing calls and verify incoming calls.</para>
+ </enum>
+ </enumlist>
+ </description>
+ </configOption>
+ <configOption name="failure_action" default="continue">
+ <synopsis>What do do when a verification fails</synopsis>
+ <description>
+ <enumlist>
+ <enum name="continue">
+ <para>If set to <literal>continue</literal>, continue and let
+ the dialplan decide what action to take.</para>
+ </enum>
+ <enum name="reject_request">
+ <para>If set to <literal>reject_request</literal>, reject the incoming
+ request with response codes defined in RFC8224.
+ </para>
+ </enum>
+ <enum name="return_reason">
+ <para>If set to <literal>return_reason</literal>, continue to the
+ dialplan but add a <literal>Reason</literal> header to the sender in
+ the next provisional response.</para>
+ </enum>
+ </enumlist>
+ </description>
+ </configOption>
+ <configOption name="use_rfc9410_responses" default="no">
+ <synopsis>RFC9410 uses the STIR protocol on Reason headers
+ instead of the SIP protocol</synopsis>
+ </configOption>
+ <configOption name="relax_x5u_port_scheme_restrictions" default="no">
+ <synopsis>Relaxes check for "https" and port 443 or 8443
+ in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="relax_x5u_path_restrictions" default="no">
+ <synopsis>Relaxes check for query parameters, user/password, etc.
+ in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_acl" default="">
+ <synopsis>An existing ACL from acl.conf to use when checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_permit" default="">
+ <synopsis>An IP or subnet to permit when checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="x5u_deny" default="">
+ <synopsis>An IP or subnet to deny checking
+ hostnames in incoming Identity header x5u URLs.</synopsis>
+ </configOption>
+ <configOption name="check_tn_cert_public_url" default="false">
+ <synopsis>On load, Retrieve all TN's certificates and validate their dates</synopsis>
+ </configOption>
+ <configOption name="private_key_file" default="">
+ <synopsis>File path to a certificate</synopsis>
+ </configOption>
+ <configOption name="public_cert_url" default="">
+ <synopsis>URL to the public certificate</synopsis>
+ <description><para>
+ Must be a valid http, or https, URL.
+ </para></description>
+ </configOption>
+ <configOption name="attest_level">
+ <synopsis>Attestation level</synopsis>
+ </configOption>
+ <configOption name="send_mky" default="no">
+ <synopsis>Send a media key (mky) grant in the attestation for DTLS calls.
+ (not common)</synopsis>
+ </configOption>
+ </configObject>
+ </configFile>
+ </configInfo>
+ <function name="STIR_SHAKEN" language="en_US">
+ <synopsis>
+ Gets the number of STIR/SHAKEN results or a specific STIR/SHAKEN value from a result on the channel.
+ </synopsis>
+ <syntax>
+ <parameter name="index" required="true">
+ <para>The index of the STIR/SHAKEN result to get. If only 'count' is passed in, gets the number of STIR/SHAKEN results instead.</para>
+ </parameter>
+ <parameter name="value" required="false">
+ <para>The value to get from the STIR/SHAKEN result. Only used when an index is passed in (instead of 'count'). Allowable values:</para>
+ <enumlist>
+ <enum name = "identity" />
+ <enum name = "attestation" />
+ <enum name = "verify_result" />
+ </enumlist>
+ </parameter>
+ </syntax>
+ <description>
+ <para>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.
+ </para>
+ <example title="Get count and retrieve value">
+ 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)})
+ </example>
+ </description>
+ </function>
+</docs>
\ No newline at end of file
+++ /dev/null
-/*
- * Asterisk -- An open source telephony toolkit.
- *
- * Copyright (C) 2020, Sangoma Technologies Corporation
- *
- * Kevin Harwell <kharwell@digium.com>
- *
- * 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 <sys/stat.h>
-
-#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 <id>\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;
-}
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@digium.com>
+ *
+ * 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 <sys/stat.h>
+
+#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 <id>\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;
+}
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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 <curl/curl.h>
+#include <sys/stat.h>
+
+#include <jwt.h>
+#include <jansson.h>
+#include <regex.h>
+
+#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;i<FULL_URL_REGEX_GROUPS;i++) { \
+ const char *m = get_match_string(x5u, pmatch, i); \
+ if (m) { \
+ ast_trace(-1, "%s: %2d %s\n", ctx->tag, 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;
+}
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * 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_ */
--- /dev/null
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2023, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@digium.com>
+ *
+ * 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;
+}