Stir/Shaken Refactor

Why do we need a refactor?

The original stir/shaken implementation was started over 3 years ago
when little was understood about practical implementation.  The
result was an implementation that wouldn't actually interoperate
with any other stir-shaken implementations.

There were also a number of stir-shaken features and RFC
requirements that were never implemented such as TNAuthList
certificate validation, sending Reason headers in SIP responses
when verification failed but we wished to continue the call, and
the ability to send Media Key(mky) grants in the Identity header
when the call involved DTLS.

Finally, there were some performance concerns around outgoing
calls and selection of the correct certificate and private key.
The configuration was keyed by an arbitrary name which meant that
for every outgoing call, we had to scan the entire list of
configured TNs to find the correct cert to use.  With only a few
TNs configured, this wasn't an issue but if you have a thousand,
it could be.

What's changed?

* Configuration objects have been refactored to be clearer about
  their uses and to fix issues.
    * The "general" object was renamed to "verification" since it
      contains parameters specific to the incoming verification
      process.  It also never handled ca_path and crl_path
      correctly.
    * A new "attestation" object was added that controls the
      outgoing attestation process.  It sets default certificates,
      keys, etc.
    * The "certificate" object was renamed to "tn" and had it's key
      change to telephone number since outgoing call attestation
      needs to look up certificates by telephone number.
    * The "profile" object had more parameters added to it that can
      override default parameters specified in the "attestation"
      and "verification" objects.
    * The "store" object was removed altogther as it was never
      implemented.

* We now use libjwt to create outgoing Identity headers and to
  parse and validate signatures on incoming Identiy headers.  Our
  previous custom implementation was much of the source of the
  interoperability issues.

* General code cleanup and refactor.
    * Moved things to better places.
    * Separated some of the complex functions to smaller ones.
    * Using context objects rather than passing tons of parameters
      in function calls.
    * Removed some complexity and unneeded encapsuation from the
      config objects.

Resolves: #351
Resolves: #46

UserNote: Asterisk's stir-shaken feature has been refactored to
correct interoperability, RFC compliance, and performance issues.
See https://docs.asterisk.org/Deployment/STIR-SHAKEN for more
information.

UpgradeNote: The stir-shaken refactor is a breaking change but since
it's not working now we don't think it matters. The
stir_shaken.conf file has changed significantly which means that
existing ones WILL need to be changed.  The stir_shaken.conf.sample
file in configs/samples/ has quite a bit more information.  This is
also an ABI breaking change since some of the existing objects
needed to be changed or removed, and new ones added.  Additionally,
if res_stir_shaken is enabled in menuselect, you'll need to either
have the development package for libjwt v1.15.3 installed or use
the --with-libjwt-bundled option with ./configure.
This commit is contained in:
George Joseph 2023-10-26 10:27:35 -06:00 committed by asterisk-org-access-app[bot]
parent ea8ead4e13
commit 628f8d7a43
44 changed files with 7500 additions and 4299 deletions

View File

@ -84,6 +84,7 @@
;ps_outbound_publishes => odbc,asterisk
;ps_inbound_publications = odbc,asterisk
;ps_asterisk_publications = odbc,asterisk
;stir_tn => odbc,asterisk
;voicemail => odbc,asterisk
;extensions => odbc,asterisk
;meetme => mysql,general

View File

@ -76,3 +76,6 @@ test=memory
;[res_pjsip_publish_asterisk]
;asterisk-publication=realtime,ps_asterisk_publications
;[res_stir_shaken]
;tn=realtime,stir_tn

View File

@ -1,103 +1,459 @@
;
; This file is used by the res_stir_shaken module to configure parameters
; used for STIR/SHAKEN.
;
; There are 2 sides to STIR/SHAKEN: attestation and verification.
;
; Attestation is done on outgoing calls and makes use out of the certificate
; objects. The cert located at path will be used to sign, and the cert
; located at public_cert_url will be placed in the Identity header to let the
; remote side know where to download the public cert from. These 2 certs must
; match; that is, the cert located at public_cert_url must be the public cert
; derived from the private cert located at path.
;
; Verification is done on incoming calls and doesn't rely on cert objects
; defined in this file.
;
; The general section applies to all STIR/SHAKEN operations. However,
; cache_max_size, curl_timeout, and signature_timeout only apply to the
; verification side.
;
; It's important to note that downloaded certificates are stored in
; <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

View File

@ -0,0 +1,35 @@
"""Create STIR/SHAKEN TN table
Revision ID: bd335bae5d33
Revises: 24c12d8e9014
Create Date: 2024-01-09 12:17:47.353533
"""
# revision identifiers, used by Alembic.
revision = 'bd335bae5d33'
down_revision = '24c12d8e9014'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import ENUM
AST_BOOL_NAME = 'ast_bool_values'
AST_BOOL_VALUES = [ '0', '1',
'off', 'on',
'false', 'true',
'no', 'yes' ]
def upgrade():
ast_bool_values = ENUM(*AST_BOOL_VALUES, name=AST_BOOL_NAME, create_type=False)
op.create_table(
'stir_tn',
sa.Column('id', sa.String(80), nullable=False, primary_key=True),
sa.Column('private_key_file', sa.String(1024), nullable=True),
sa.Column('public_cert_url', sa.String(1024), nullable=True),
sa.Column('attest_level', sa.String(1), nullable=True),
sa.Column('send_mky', ast_bool_values)
)
def downgrade():
op.drop_table('stir_tn')

View File

@ -50,6 +50,16 @@ int ast_db_get(const char *family, const char *key, char *value, int valuelen);
*/
int ast_db_get_allocated(const char *family, const char *key, char **out);
/*!
* \brief Check if family/key exitsts
*
* \param family
* \param key
* \retval 1 if family/key exists
* \retval 0 if family/key does not exist or an error occurred
*/
int ast_db_exists(const char *family, const char *key);
/*! \brief Store value addressed by family/key */
int ast_db_put(const char *family, const char *key, const char *value);

View File

@ -71,22 +71,6 @@
#define PJSTR_PRINTF_SPEC "%.*s"
#define PJSTR_PRINTF_VAR(_v) ((int)(_v).slen), ((_v).ptr)
/* Response codes from RFC8224 */
#define AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE 403
#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER 428
#define AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT 428
#define AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO 436
#define AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL 437
#define AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER 438
/* Response strings from RFC8224 */
#define AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE "Stale Date"
#define AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER "Use Identity Header"
#define AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT "Use Supported PASSporT Format"
#define AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO "Bad Identity Info"
#define AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL "Unsupported Credential"
#define AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER "Invalid Identity Header"
#define AST_SIP_AUTH_MAX_REALM_LENGTH 255 /* From the auth/realm realtime column size */
/* ":12345" */
@ -666,17 +650,6 @@ enum ast_sip_session_redirect {
AST_SIP_REDIRECT_URI_PJSIP,
};
enum ast_sip_stir_shaken_behavior {
/*! Don't do any STIR/SHAKEN operations */
AST_SIP_STIR_SHAKEN_OFF = 0,
/*! Only do STIR/SHAKEN attestation */
AST_SIP_STIR_SHAKEN_ATTEST = 1,
/*! Only do STIR/SHAKEN verification */
AST_SIP_STIR_SHAKEN_VERIFY = 2,
/*! Do STIR/SHAKEN attestation and verification */
AST_SIP_STIR_SHAKEN_ON = 3,
};
/*!
* \brief Incoming/Outgoing call offer/answer joint codec preference.
*

View File

@ -963,4 +963,29 @@ struct ast_sip_session_media *ast_sip_session_media_get_transport(struct ast_sip
*/
const char *ast_sip_session_get_name(const struct ast_sip_session *session);
/*!
* \brief Determines if the Connected Line info can be presented for this session
*
* \param session The session
* \param id The Connected Line info to evaluate
*
* \retval 1 The Connected Line info can be presented
* \retval 0 The Connected Line info cannot be presented
*/
int ast_sip_can_present_connected_id(const struct ast_sip_session *session, const struct ast_party_id *id);
/*!
* \brief Adds a Reason header in the next reponse to an incoming INVITE
*
* \param session The session
* \param protocol Usually "SIP" but may be "STIR" for stir-shaken
* \param code SIP response code
* \param text Reason string
*
* \retval 0 the header is accepted
* \retval -1 the header is rejected
*/
int ast_sip_session_add_reason_header(struct ast_sip_session *session,
const char *protocol, int code, const char *text);
#endif /* _RES_PJSIP_SESSION_H */

View File

@ -18,176 +18,241 @@
#ifndef _RES_STIR_SHAKEN_H
#define _RES_STIR_SHAKEN_H
#define STIR_SHAKEN_ENCRYPTION_ALGORITHM "ES256"
#define STIR_SHAKEN_PPT "shaken"
#define STIR_SHAKEN_TYPE "passport"
#include "asterisk/sorcery.h"
enum ast_stir_shaken_verification_result {
AST_STIR_SHAKEN_VERIFY_NOT_PRESENT, /*! No STIR/SHAKEN information was available */
AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED, /*! Signature verification failed */
AST_STIR_SHAKEN_VERIFY_MISMATCH, /*! Contents of the signaling and the STIR/SHAKEN payload did not match */
AST_STIR_SHAKEN_VERIFY_PASSED, /*! Signature verified and contents match signaling */
enum ast_stir_shaken_vs_response_code {
AST_STIR_SHAKEN_VS_SUCCESS = 0,
AST_STIR_SHAKEN_VS_DISABLED,
AST_STIR_SHAKEN_VS_INVALID_ARGUMENTS,
AST_STIR_SHAKEN_VS_INTERNAL_ERROR,
AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR,
AST_STIR_SHAKEN_VS_NO_DATE_HDR,
AST_STIR_SHAKEN_VS_DATE_HDR_PARSE_FAILURE,
AST_STIR_SHAKEN_VS_DATE_HDR_EXPIRED,
AST_STIR_SHAKEN_VS_NO_JWT_HDR,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_X5U,
AST_STIR_SHAKEN_VS_CERT_CACHE_MISS,
AST_STIR_SHAKEN_VS_CERT_CACHE_INVALID,
AST_STIR_SHAKEN_VS_CERT_CACHE_EXPIRED,
AST_STIR_SHAKEN_VS_CERT_RETRIEVAL_FAILURE,
AST_STIR_SHAKEN_VS_CERT_CONTENTS_INVALID,
AST_STIR_SHAKEN_VS_CERT_NOT_TRUSTED,
AST_STIR_SHAKEN_VS_CERT_DATE_INVALID,
AST_STIR_SHAKEN_VS_CERT_NO_TN_AUTH_EXT,
AST_STIR_SHAKEN_VS_CERT_NO_SPC_IN_TN_AUTH_EXT,
AST_STIR_SHAKEN_VS_NO_RAW_KEY,
AST_STIR_SHAKEN_VS_SIGNATURE_VALIDATION,
AST_STIR_SHAKEN_VS_NO_IAT,
AST_STIR_SHAKEN_VS_IAT_EXPIRED,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_PPT,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_ALG,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_TYP,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_GRANTS,
AST_STIR_SHAKEN_VS_INVALID_OR_NO_ATTEST,
AST_STIR_SHAKEN_VS_NO_ORIGID,
AST_STIR_SHAKEN_VS_NO_ORIG_TN,
AST_STIR_SHAKEN_VS_CID_ORIG_TN_MISMATCH,
AST_STIR_SHAKEN_VS_NO_DEST_TN,
AST_STIR_SHAKEN_VS_INVALID_HEADER,
AST_STIR_SHAKEN_VS_INVALID_GRANT,
AST_STIR_SHAKEN_VS_RESPONSE_CODE_MAX
};
/*! Different from ast_stir_shaken_verification_result. Used to determine why ast_stir_shaken_verify returned NULL */
enum ast_stir_shaken_verify_failure_reason {
AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC, /*! Memory allocation failure */
AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT, /*! Failed to get the credentials to verify */
AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION, /*! Failed validating the signature */
enum ast_stir_shaken_as_response_code {
AST_STIR_SHAKEN_AS_SUCCESS = 0,
AST_STIR_SHAKEN_AS_DISABLED,
AST_STIR_SHAKEN_AS_INVALID_ARGUMENTS,
AST_STIR_SHAKEN_AS_MISSING_PARAMETERS,
AST_STIR_SHAKEN_AS_INTERNAL_ERROR,
AST_STIR_SHAKEN_AS_NO_TN_FOR_CALLERID,
AST_STIR_SHAKEN_AS_NO_PRIVATE_KEY_AVAIL,
AST_STIR_SHAKEN_AS_NO_PUBLIC_CERT_URL_AVAIL,
AST_STIR_SHAKEN_AS_NO_ATTEST_LEVEL,
AST_STIR_SHAKEN_AS_IDENTITY_HDR_EXISTS,
AST_STIR_SHAKEN_AS_NO_TO_HDR,
AST_STIR_SHAKEN_AS_TO_HDR_BAD_URI,
AST_STIR_SHAKEN_AS_SIGN_ENCODE_FAILURE,
AST_STIR_SHAKEN_AS_RESPONSE_CODE_MAX
};
struct ast_stir_shaken_payload;
enum stir_shaken_failure_action_enum {
/*! Unknown value */
stir_shaken_failure_action_UNKNOWN = -1,
/*! Continue and let dialplan decide action */
stir_shaken_failure_action_CONTINUE = 0,
/*! Reject request with respone codes defined in RFC8224 */
stir_shaken_failure_action_REJECT_REQUEST,
/*! Continue but return a Reason header in next provisional response */
stir_shaken_failure_action_CONTINUE_RETURN_REASON,
/*! Not set in config */
stir_shaken_failure_action_NOT_SET,
};
struct ast_acl_list;
struct ast_json;
struct ast_stir_shaken_as_ctx;
/*!
* \brief Retrieve the value for 'signature' from an ast_stir_shaken_payload
* \brief Create Attestation Service Context
*
* \param payload The payload
*
* \retval The signature
* \param caller_id The caller_id for the outgoing call
* \param dest_tn Canonicalized destination tn
* \param chan The outgoing channel
* \param profile_name The profile name on the endpoint
* May be NULL.
* \param tag Identifying string to output in log and trace messages.
* \param ctxout Receives a pointer to the newly created context
* The caller must release with ao2_ref or ao2_cleanup.
* \retval AST_STIR_SHAKEN_AS_SUCCESS if successful.
* \retval AST_STIR_SHAKEN_AS_DISABLED if attestation is disabled
* by the endpoint itself, the profile or globally.
* \retval Other AST_STIR_SHAKEN_AS errors.
*/
unsigned char *ast_stir_shaken_payload_get_signature(const struct ast_stir_shaken_payload *payload);
enum ast_stir_shaken_as_response_code
ast_stir_shaken_as_ctx_create(const char *caller_id,
const char *dest_tn, struct ast_channel *chan,
const char *profile_name,
const char *tag, struct ast_stir_shaken_as_ctx **ctxout);
/*!
* \brief Retrieve the value for 'public_cert_url' from an ast_stir_shaken_payload
* \brief Indicates if the AS context needs DTLS fingerprints
*
* \param payload The payload
* \param ctx AS Context
*
* \retval The public key URL
* \retval 0 Not needed
* \retval 1 Needed
*/
char *ast_stir_shaken_payload_get_public_cert_url(const struct ast_stir_shaken_payload *payload);
int ast_stir_shaken_as_ctx_wants_fingerprints(struct ast_stir_shaken_as_ctx *ctx);
/*!
* \brief Retrieve the value for 'signature_timeout' from 'general' config object
* \brief Add DTLS fingerprints to AS context
*
* \retval The signature timeout
* \param ctx AS context
* \param alg Fingerprint algorithm ("sha-1" or "sha-256")
* \param fingerprint Fingerprint
*
* \retval AST_STIR_SHAKEN_AS_SUCCESS if successful
* \retval Other AST_STIR_SHAKEN_AS errors.
*/
unsigned int ast_stir_shaken_get_signature_timeout(void);
enum ast_stir_shaken_as_response_code ast_stir_shaken_as_ctx_add_fingerprint(
struct ast_stir_shaken_as_ctx *ctx, const char *alg, const char *fingerprint);
/*!
* \brief Retrieve a stir_shaken_profile by id
* \brief Attest and return Identity header value
*
* \note The profile will need to be unref'd when not needed anymore
* \param ctx AS Context
* \param header Pointer to buffer to receive the header value
* Must be freed with ast_free when done
*
* \param id The id of the stir_shaken_profile to get
*
* \retval stir_shaken_profile on success
* \retval NULL on failure
* \retval AST_STIR_SHAKEN_AS_SUCCESS if successful
* \retval Other AST_STIR_SHAKEN_AS errors.
*/
struct stir_shaken_profile *ast_stir_shaken_get_profile(const char *id);
enum ast_stir_shaken_as_response_code ast_stir_shaken_attest(
struct ast_stir_shaken_as_ctx *ctx, char **header);
struct ast_stir_shaken_vs_ctx;
/*!
* \brief Check if a stir_shaken_profile supports attestation
* \brief Create Verification Service context
*
* \param profile The stir_shaken_profile to test
* \param caller_id Incoming caller id
* \param chan Incoming channel
* \param profile_name The profile name on the endpoint
* May be NULL.
* \param endpoint_behavior Behavior associated to the specific
* endpoint
* \param tag Identifying string to output in log and trace messages.
* \param ctxout Receives a pointer to the newly created context
* The caller must release with ao2_ref or ao2_cleanup.
*
* \retval 0 if not supported
* \retval 1 if supported
* \retval AST_STIR_SHAKEN_VS_SUCCESS if successful.
* \retval AST_STIR_SHAKEN_VS_DISABLED if verification is disabled
* by the endpoint itself, the profile or globally.
* \retval Other AST_STIR_SHAKEN_VS errors.
*/
unsigned int ast_stir_shaken_profile_supports_attestation(const struct stir_shaken_profile *profile);
enum ast_stir_shaken_vs_response_code
ast_stir_shaken_vs_ctx_create(const char *caller_id,
struct ast_channel *chan, const char *profile_name,
const char *tag, struct ast_stir_shaken_vs_ctx **ctxout);
/*!
* \brief Check if a stir_shaken_profile supports verification
* \brief Sets response code on VS context
*
* \param profile The stir_shaken_profile to test
*
* \retval 0 if not supported
* \retval 1 if supported
* \param ctx VS context
* \param vs_rc ast_stir_shaken_vs_response_code to set
*/
unsigned int ast_stir_shaken_profile_supports_verification(const struct stir_shaken_profile *profile);
void ast_stir_shaken_vs_ctx_set_response_code(
struct ast_stir_shaken_vs_ctx *ctx,
enum ast_stir_shaken_vs_response_code vs_rc);
/*!
* \brief Add the received Identity header value to the VS context
*
* \param ctx VS context
* \param identity_hdr Identity header value
*
* \retval AST_STIR_SHAKEN_VS_SUCCESS if successful
* \retval Other AST_STIR_SHAKEN_VS errors.
*/
enum ast_stir_shaken_vs_response_code
ast_stir_shaken_vs_ctx_add_identity_hdr(struct ast_stir_shaken_vs_ctx * ctx,
const char *identity_hdr);
/*!
* \brief Add the received Date header value to the VS context
*
* \param ctx VS context
* \param date_hdr Date header value
*
* \retval AST_STIR_SHAKEN_VS_SUCCESS if successful
* \retval Other AST_STIR_SHAKEN_VS errors.
*/
enum ast_stir_shaken_vs_response_code
ast_stir_shaken_vs_ctx_add_date_hdr(struct ast_stir_shaken_vs_ctx * ctx,
const char *date_hdr);
/*!
* \brief Get failure_action from context
*
* \param ctx VS context
*
* \retval ast_stir_shaken_failure_action
*/
enum stir_shaken_failure_action_enum
ast_stir_shaken_vs_get_failure_action(
struct ast_stir_shaken_vs_ctx *ctx);
/*!
* \brief Get use_rfc9410_responses from context
*
* \param ctx VS context
*
* \retval 1 if true
* \retval 0 if false
*/
int ast_stir_shaken_vs_get_use_rfc9410_responses(
struct ast_stir_shaken_vs_ctx *ctx);
/*!
* \brief Add a STIR/SHAKEN verification result to a channel
*
* \param chan The channel
* \param identity The identity
* \param attestation The attestation
* \param result The verification result
* \param ctx VS context
*
* \retval -1 on failure
* \retval 0 on success
*/
int ast_stir_shaken_add_verification(struct ast_channel *chan, const char *identity, const char *attestation,
enum ast_stir_shaken_verification_result result);
int ast_stir_shaken_add_result_to_channel(
struct ast_stir_shaken_vs_ctx *ctx);
/*!
* \brief Verify a JSON STIR/SHAKEN payload
* \brief Perform incoming call verification
*
* \param header The payload header
* \param payload The payload section
* \param signature The payload signature
* \param algorithm The signature algorithm
* \param public_cert_url The public key URL
* \param ctx VS context
*
* \retval ast_stir_shaken_payload on success
* \retval NULL on failure
* \retval AST_STIR_SHAKEN_AS_SUCCESS if successful
* \retval Other AST_STIR_SHAKEN_AS errors.
*/
struct ast_stir_shaken_payload *ast_stir_shaken_verify(const char *header, const char *payload, const char *signature,
const char *algorithm, const char *public_cert_url);
/*!
* \brief Same as ast_stir_shaken_verify, but will populate a struct with additional information on failure
*
* \note failure_code will be written to in this function
*
* \param header The payload header
* \param payload The payload section
* \param signature The payload signature
* \param algorithm The signature algorithm
* \param public_cert_url The public key URL
* \param failure_code Additional failure information
*
* \retval ast_stir_shaken_payload on success
* \retval NULL on failure
*/
struct ast_stir_shaken_payload *ast_stir_shaken_verify2(const char *header, const char *payload, const char *signature,
const char *algorithm, const char *public_cert_url, int *failure_code);
/*!
* \brief Same as ast_stir_shaken_verify2, but passes in a stir_shaken_profile with additional configuration
*
* \note failure_code will be written to in this function
*
* \param header The payload header
* \param payload The payload section
* \param signature The payload signature
* \param algorithm The signature algorithm
* \param public_cert_url The public key URL
* \param failure Additional failure information
* \param profile The stir_shaken_profile
*
* \retval ast_stir_shaken_payload on success
* \retval NULL on failure
*/
struct ast_stir_shaken_payload *ast_stir_shaken_verify_with_profile(const char *header, const char *payload,
const char *signature, const char *algorithm, const char *public_cert_url, int *failure,
const struct stir_shaken_profile *profile);
/*!
* \brief Retrieve the stir/shaken sorcery context
*
* \retval The stir/shaken sorcery context
*/
struct ast_sorcery *ast_stir_shaken_sorcery(void);
/*!
* \brief Free a STIR/SHAKEN payload
*/
void ast_stir_shaken_payload_free(struct ast_stir_shaken_payload *payload);
/*!
* \brief Sign a JSON STIR/SHAKEN payload
*
* \note This function will automatically add the "attest", "iat", and "origid" fields.
*
* \param json The JWT to sign
*
* \retval ast_stir_shaken_payload on success
* \retval NULL on failure
*/
struct ast_stir_shaken_payload *ast_stir_shaken_sign(struct ast_json *json);
enum ast_stir_shaken_vs_response_code
ast_stir_shaken_vs_verify(struct ast_stir_shaken_vs_ctx * ctx);
#endif /* _RES_STIR_SHAKEN_H */

View File

@ -131,6 +131,7 @@ static void db_sync(void);
DEFINE_SQL_STATEMENT(put_stmt, "INSERT OR REPLACE INTO astdb (key, value) VALUES (?, ?)")
DEFINE_SQL_STATEMENT(get_stmt, "SELECT value FROM astdb WHERE key=?")
DEFINE_SQL_STATEMENT(exists_stmt, "SELECT CAST(COUNT(1) AS INTEGER) AS 'exists' FROM astdb WHERE key=?")
DEFINE_SQL_STATEMENT(del_stmt, "DELETE FROM astdb WHERE key=?")
DEFINE_SQL_STATEMENT(deltree_stmt, "DELETE FROM astdb WHERE key || '/' LIKE ? || '/' || '%'")
DEFINE_SQL_STATEMENT(deltree_all_stmt, "DELETE FROM astdb")
@ -188,6 +189,7 @@ static int clean_stmt(sqlite3_stmt **stmt, const char *sql)
static void clean_statements(void)
{
clean_stmt(&get_stmt, get_stmt_sql);
clean_stmt(&exists_stmt, exists_stmt_sql);
clean_stmt(&del_stmt, del_stmt_sql);
clean_stmt(&deltree_stmt, deltree_stmt_sql);
clean_stmt(&deltree_all_stmt, deltree_all_stmt_sql);
@ -204,6 +206,7 @@ static int init_statements(void)
/* Don't initialize create_astdb_statement here as the astdb table needs to exist
* brefore these statements can be initialized */
return init_stmt(&get_stmt, get_stmt_sql, sizeof(get_stmt_sql))
|| init_stmt(&exists_stmt, exists_stmt_sql, sizeof(exists_stmt_sql))
|| init_stmt(&del_stmt, del_stmt_sql, sizeof(del_stmt_sql))
|| init_stmt(&deltree_stmt, deltree_stmt_sql, sizeof(deltree_stmt_sql))
|| init_stmt(&deltree_all_stmt, deltree_all_stmt_sql, sizeof(deltree_all_stmt_sql))
@ -438,6 +441,38 @@ int ast_db_get_allocated(const char *family, const char *key, char **out)
return db_get_common(family, key, out, -1);
}
int ast_db_exists(const char *family, const char *key)
{
int result;
char fullkey[MAX_DB_FIELD];
size_t fullkey_len;
int res = 0;
fullkey_len = snprintf(fullkey, sizeof(fullkey), "/%s/%s", family, key);
if (fullkey_len >= sizeof(fullkey)) {
ast_log(LOG_WARNING, "Family and key length must be less than %zu bytes\n", sizeof(fullkey) - 3);
return -1;
}
ast_mutex_lock(&dblock);
res = sqlite3_bind_text(exists_stmt, 1, fullkey, fullkey_len, SQLITE_STATIC);
if (res != SQLITE_OK) {
ast_log(LOG_WARNING, "Couldn't bind key to stmt: %d:%s\n", res, sqlite3_errmsg(astdb));
res = 0;
} else if (sqlite3_step(exists_stmt) != SQLITE_ROW) {
res = 0;
} else if (!(result = sqlite3_column_int(exists_stmt, 0))) {
res = 0;
} else {
res = result;
}
sqlite3_reset(exists_stmt);
ast_mutex_unlock(&dblock);
return res;
}
int ast_db_del(const char *family, const char *key)
{
char fullkey[MAX_DB_FIELD];

View File

@ -37,6 +37,7 @@
#include "asterisk/stream.h"
#include "asterisk/stasis.h"
#include "asterisk/security_events.h"
#include "asterisk/res_stir_shaken.h"
/*! \brief Number of buckets for persistent endpoint information */
#define PERSISTENT_BUCKETS 53
@ -800,37 +801,17 @@ static int stir_shaken_handler(const struct aco_option *opt, struct ast_variable
{
struct ast_sip_endpoint *endpoint = obj;
if (!strcasecmp("off", var->value)) {
endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_OFF;
} else if (!strcasecmp("attest", var->value)) {
endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ATTEST;
} else if (!strcasecmp("verify", var->value)) {
endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_VERIFY;
} else if (!strcasecmp("on", var->value)) {
endpoint->stir_shaken = AST_SIP_STIR_SHAKEN_ON;
} else {
ast_log(LOG_WARNING, "'%s' is not a valid value for option "
"'stir_shaken' for endpoint %s\n",
var->value, ast_sorcery_object_get_id(endpoint));
return -1;
}
ast_log(LOG_WARNING, "Endpoint %s: Option 'stir_shaken' is no longer supported. Use 'stir_shaken_profile' instead.\n",
ast_sorcery_object_get_id(endpoint));
endpoint->stir_shaken = 0;
return 0;
}
static const char *stir_shaken_map[] = {
[AST_SIP_STIR_SHAKEN_OFF] = "off",
[AST_SIP_STIR_SHAKEN_ATTEST] = "attest",
[AST_SIP_STIR_SHAKEN_VERIFY] = "verify",
[AST_SIP_STIR_SHAKEN_ON] = "on",
};
static int stir_shaken_to_str(const void *obj, const intptr_t *args, char **buf)
{
const struct ast_sip_endpoint *endpoint = obj;
if (ARRAY_IN_BOUNDS(endpoint->stir_shaken, stir_shaken_map)) {
*buf = ast_strdup(stir_shaken_map[endpoint->stir_shaken]);
}
*buf = ast_strdup("no");
return 0;
}
@ -2308,7 +2289,8 @@ int ast_res_pjsip_initialize_configuration(void)
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "codec_prefs_outgoing_answer",
"prefer: pending, operation: intersect, keep: all",
codec_prefs_handler, outgoing_answer_codec_prefs_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "stir_shaken", "off", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint",
"stir_shaken", 0, stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0);
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, stir_shaken_profile));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_unauthenticated_options", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, allow_unauthenticated_options));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "geoloc_incoming_call_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, geoloc_incoming_call_profile));

View File

@ -521,9 +521,7 @@ static void add_rpid_header(const struct ast_sip_session *session, pjsip_tx_data
*/
static void add_id_headers(const struct ast_sip_session *session, pjsip_tx_data *tdata, const struct ast_party_id *id)
{
if (!id->number.valid
|| (!session->endpoint->id.trust_outbound
&& (ast_party_id_presentation(id) & AST_PRES_RESTRICTION) != AST_PRES_ALLOWED)) {
if (!ast_sip_can_present_connected_id(session, id)) {
return;
}
if (session->endpoint->id.send_pai) {

View File

@ -52,6 +52,8 @@
#include "asterisk/stream.h"
#include "asterisk/vector.h"
#include "res_pjsip_session/pjsip_session.h"
#define SDP_HANDLER_BUCKETS 11
#define MOD_DATA_ON_RESPONSE "on_response"
@ -126,6 +128,13 @@ const char *ast_sip_session_get_name(const struct ast_sip_session *session)
}
}
int ast_sip_can_present_connected_id(const struct ast_sip_session *session, const struct ast_party_id *id)
{
return id->number.valid
&& (session->endpoint->id.trust_outbound
|| (ast_party_id_presentation(id) & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED);
}
static int sdp_handler_list_cmp(void *obj, void *arg, int flags)
{
struct sdp_handler_list *handler_list1 = obj;
@ -4122,11 +4131,6 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
{
RAII_VAR(struct ast_sip_endpoint *, endpoint,
ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup);
static const pj_str_t identity_str = { "Identity", 8 };
const pj_str_t use_identity_header_str = {
AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_IDENTITY_HEADER)
};
pjsip_inv_session *inv_session = NULL;
struct ast_sip_session *session;
struct new_invite invite;
@ -4136,14 +4140,6 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
ast_assert(endpoint != NULL);
if ((endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) &&
!ast_sip_rdata_get_header_value(rdata, identity_str)) {
pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata,
AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER, &use_identity_header_str, NULL, NULL);
ast_debug(3, "No Identity header when we require one\n");
return;
}
inv_session = pre_session_setup(rdata, endpoint);
if (!inv_session) {
/* pre_session_setup() returns a response on failure */
@ -6243,6 +6239,8 @@ static int load_module(void)
ast_sip_register_service(&session_reinvite_module);
ast_sip_register_service(&outbound_invite_auth_module);
pjsip_reason_header_load();
ast_module_shutdown_ref(ast_module_info->self);
#ifdef TEST_FRAMEWORK
AST_TEST_REGISTER(test_resolve_refresh_media_states);
@ -6252,6 +6250,8 @@ static int load_module(void)
static int unload_module(void)
{
pjsip_reason_header_unload();
#ifdef TEST_FRAMEWORK
AST_TEST_UNREGISTER(test_resolve_refresh_media_states);
#endif

View File

@ -5,6 +5,7 @@
LINKER_SYMBOL_PREFIXast_sip_dialog_get_session;
LINKER_SYMBOL_PREFIXast_sip_channel_pvt_alloc;
LINKER_SYMBOL_PREFIXast_sip_create_joint_call_cap;
LINKER_SYMBOL_PREFIXast_sip_can_present_connected_id;
local:
*;
};

View File

@ -1,9 +1,9 @@
/*
* 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
@ -15,23 +15,20 @@
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
#ifndef _STIR_SHAKEN_STORE_H
#define _STIR_SHAKEN_STORE_H
struct ast_sorcery;
#ifndef PJSIP_SESSION_H_
#define PJSIP_SESSION_H_
/*!
* \brief Load time initialization for the stir/shaken 'store' configuration
*
* \retval 0 on success, -1 on error
* \internal
* \brief Unregisters the session supplement
*/
int stir_shaken_store_load(void);
void pjsip_reason_header_unload(void);
/*!
* \brief Unload time cleanup for the stir/shaken 'store' configuration
*
* \retval 0 on success, -1 on error
* \internal
* \brief Registers the session supplement
*/
int stir_shaken_store_unload(void);
void pjsip_reason_header_load(void);
#endif /* _STIR_SHAKEN_STORE_H */
#endif /* PJSIP_SESSION_H_ */

View File

@ -0,0 +1,168 @@
/*
* 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);
}

View File

@ -26,149 +26,167 @@
#include "asterisk.h"
#define _TRACE_PREFIX_ "pjss",__LINE__, ""
#include "asterisk/callerid.h"
#include "asterisk/res_pjsip.h"
#include "asterisk/res_pjsip_session.h"
#include "asterisk/module.h"
#include "asterisk/rtp_engine.h"
#include "asterisk/res_stir_shaken.h"
/*! The Date header will not be valid after this many milliseconds (60 seconds recommended) */
#define STIR_SHAKEN_DATE_HEADER_TIMEOUT 60000
static const pj_str_t identity_hdr_str = { "Identity", 8 };
static const pj_str_t date_hdr_str = { "Date", 4 };
/*!
* \brief Get the attestation from the payload
*
* \param json_str The JSON string representation of the payload
*
* \retval Empty string on failure
* \retval The attestation on success
*/
static char *get_attestation_from_payload(const char *json_str)
/* Response codes from RFC8224 */
enum sip_response_code {
SIP_RESPONSE_CODE_OK = 200,
SIP_RESPONSE_CODE_STALE_DATE = 403,
SIP_RESPONSE_CODE_USE_IDENTITY_HEADER = 428,
SIP_RESPONSE_CODE_BAD_IDENTITY_INFO = 436,
SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL = 437,
SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER = 438,
SIP_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT = 428,
SIP_RESPONSE_CODE_INTERNAL_ERROR = 500,
};
#define SIP_RESPONSE_CODE_OK_STR "OK"
/* Response strings from RFC8224 */
#define SIP_RESPONSE_CODE_STALE_DATE_STR "Stale Date"
#define SIP_RESPONSE_CODE_USE_IDENTITY_HEADER_STR "Use Identity Header"
#define SIP_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT_STR "Use Supported PASSporT Format"
#define SIP_RESPONSE_CODE_BAD_IDENTITY_INFO_STR "Bad Identity Info"
#define SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL_STR "Unsupported Credential"
#define SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER_STR "Invalid Identity Header"
#define SIP_RESPONSE_CODE_INTERNAL_ERROR_STR "Internal Error"
#define response_to_str(_code) \
case _code: \
return _code ## _STR;
static const char *sip_response_code_to_str(enum sip_response_code code)
{
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
char *attestation;
json = ast_json_load_string(json_str, NULL);
attestation = (char *)ast_json_string_get(ast_json_object_get(json, "attest"));
if (!ast_strlen_zero(attestation)) {
return attestation;
switch (code) {
response_to_str(SIP_RESPONSE_CODE_OK)
response_to_str(SIP_RESPONSE_CODE_STALE_DATE)
response_to_str(SIP_RESPONSE_CODE_USE_IDENTITY_HEADER)
response_to_str(SIP_RESPONSE_CODE_BAD_IDENTITY_INFO)
response_to_str(SIP_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL)
response_to_str(SIP_RESPONSE_CODE_INVALID_IDENTITY_HEADER)
default:
break;
}
return "";
}
/*!
* \brief Compare the caller ID from the INVITE with the one in the payload
*
* \param caller_id
* \param json_str The JSON string representation of the payload
*
* \retval -1 on failure
* \retval 0 on success
*/
static int compare_caller_id(char *caller_id, const char *json_str)
#define translate_code(_vs_rc, _sip_rc) \
case AST_STIR_SHAKEN_VS_ ## _vs_rc: \
return SIP_RESPONSE_CODE_ ## _sip_rc;
static enum sip_response_code vs_code_to_sip_code(
enum ast_stir_shaken_vs_response_code vs_rc)
{
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
char *caller_id_other;
json = ast_json_load_string(json_str, NULL);
caller_id_other = (char *)ast_json_string_get(ast_json_object_get(
ast_json_object_get(json, "orig"), "tn"));
if (strcmp(caller_id, caller_id_other)) {
return -1;
/*
* We want to use a switch/case statement here because
* it'll spit out an error if VS codes are added to the
* enum but aren't present here.
*/
switch (vs_rc) {
translate_code(SUCCESS, OK)
translate_code(DISABLED, OK)
translate_code(INVALID_ARGUMENTS, INTERNAL_ERROR)
translate_code(INTERNAL_ERROR, INTERNAL_ERROR)
translate_code(NO_IDENTITY_HDR, USE_IDENTITY_HEADER)
translate_code(NO_DATE_HDR, STALE_DATE)
translate_code(DATE_HDR_PARSE_FAILURE, STALE_DATE)
translate_code(DATE_HDR_EXPIRED, STALE_DATE)
translate_code(NO_JWT_HDR, INVALID_IDENTITY_HEADER)
translate_code(INVALID_OR_NO_X5U, INVALID_IDENTITY_HEADER)
translate_code(CERT_CACHE_MISS, INVALID_IDENTITY_HEADER)
translate_code(CERT_CACHE_INVALID, INVALID_IDENTITY_HEADER)
translate_code(CERT_CACHE_EXPIRED, INVALID_IDENTITY_HEADER)
translate_code(CERT_RETRIEVAL_FAILURE, BAD_IDENTITY_INFO)
translate_code(CERT_CONTENTS_INVALID, UNSUPPORTED_CREDENTIAL)
translate_code(CERT_NOT_TRUSTED, UNSUPPORTED_CREDENTIAL)
translate_code(CERT_DATE_INVALID, UNSUPPORTED_CREDENTIAL)
translate_code(CERT_NO_TN_AUTH_EXT, UNSUPPORTED_CREDENTIAL)
translate_code(CERT_NO_SPC_IN_TN_AUTH_EXT, UNSUPPORTED_CREDENTIAL)
translate_code(NO_RAW_KEY, UNSUPPORTED_CREDENTIAL)
translate_code(SIGNATURE_VALIDATION, INVALID_IDENTITY_HEADER)
translate_code(NO_IAT, INVALID_IDENTITY_HEADER)
translate_code(IAT_EXPIRED, STALE_DATE)
translate_code(INVALID_OR_NO_PPT, INVALID_IDENTITY_HEADER)
translate_code(INVALID_OR_NO_ALG, INVALID_IDENTITY_HEADER)
translate_code(INVALID_OR_NO_TYP, INVALID_IDENTITY_HEADER)
translate_code(INVALID_OR_NO_ATTEST, INVALID_IDENTITY_HEADER)
translate_code(NO_ORIGID, INVALID_IDENTITY_HEADER)
translate_code(NO_ORIG_TN, INVALID_IDENTITY_HEADER)
translate_code(NO_DEST_TN, INVALID_IDENTITY_HEADER)
translate_code(INVALID_HEADER, INVALID_IDENTITY_HEADER)
translate_code(INVALID_GRANT, INVALID_IDENTITY_HEADER)
translate_code(INVALID_OR_NO_GRANTS, INVALID_IDENTITY_HEADER)
translate_code(CID_ORIG_TN_MISMATCH, INVALID_IDENTITY_HEADER)
translate_code(RESPONSE_CODE_MAX, INVALID_IDENTITY_HEADER)
}
return 0;
return 500;
}
/*!
* \brief Compare the current timestamp with the one in the payload. If the difference
* is greater than the signature timeout, it's not valid anymore
*
* \param json_str The JSON string representation of the payload
*
* \retval -1 on failure
* \retval 0 on success
*/
static int compare_timestamp(const char *json_str)
enum process_failure_rc {
PROCESS_FAILURE_CONTINUE = 0,
PROCESS_FAILURE_REJECT,
PROCESS_FAILURE_SYSTEM_FAILURE,
};
static void reject_incoming_call(struct ast_sip_session *session,
enum sip_response_code response_code)
{
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
long int timestamp;
struct timeval now = ast_tvnow();
#ifdef TEST_FRAMEWORK
ast_debug(3, "Ignoring STIR/SHAKEN timestamp\n");
return 0;
#endif
json = ast_json_load_string(json_str, NULL);
timestamp = ast_json_integer_get(ast_json_object_get(json, "iat"));
if (now.tv_sec - timestamp > ast_stir_shaken_get_signature_timeout()) {
return -1;
}
return 0;
}
static int check_date_header(pjsip_rx_data *rdata)
{
static const pj_str_t date_hdr_str = { "Date", 4 };
char *date_hdr_val;
struct ast_tm date_hdr_tm;
struct timeval date_hdr_timeval;
struct timeval current_timeval;
char *remainder;
char timezone[80] = { 0 };
int64_t time_diff;
date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str);
if (ast_strlen_zero(date_hdr_val)) {
ast_log(LOG_ERROR, "Failed to get Date header from incoming INVITE for STIR/SHAKEN\n");
return -1;
}
if (!(remainder = ast_strptime(date_hdr_val, "%a, %d %b %Y %T", &date_hdr_tm))) {
ast_log(LOG_ERROR, "Failed to parse Date header\n");
return -1;
}
sscanf(remainder, "%79s", timezone);
if (ast_strlen_zero(timezone)) {
ast_log(LOG_ERROR, "A timezone is required for STIR/SHAKEN Date header, but we didn't get one\n");
return -1;
}
date_hdr_timeval = ast_mktime(&date_hdr_tm, timezone);
current_timeval = ast_tvnow();
time_diff = ast_tvdiff_ms(current_timeval, date_hdr_timeval);
if (time_diff < 0) {
/* An INVITE from the future! */
ast_log(LOG_ERROR, "STIR/SHAKEN Date header has a future date\n");
return -1;
} else if (time_diff > STIR_SHAKEN_DATE_HEADER_TIMEOUT) {
ast_log(LOG_ERROR, "STIR/SHAKEN Date header was outside of the allowable range (60 seconds)\n");
return -1;
}
return 0;
}
/* Send a response back and end the session */
static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_rx_data *rdata, int response_code, const pj_str_t response_str)
{
pjsip_tx_data *tdata;
if (pjsip_inv_end_session(session->inv_session, response_code, &response_str, &tdata) == PJ_SUCCESS) {
pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL);
}
ast_sip_session_terminate(session, response_code);
ast_hangup(session->channel);
}
static enum process_failure_rc process_failure(struct ast_stir_shaken_vs_ctx *ctx,
const char *caller_id, struct ast_sip_session *session,
pjsip_rx_data *rdata, enum ast_stir_shaken_vs_response_code vs_rc)
{
enum sip_response_code response_code = vs_code_to_sip_code(vs_rc);
pj_str_t response_str;
const char *response_string =
sip_response_code_to_str(response_code);
enum stir_shaken_failure_action_enum failure_action =
ast_stir_shaken_vs_get_failure_action(ctx);
const char *tag = ast_sip_session_get_name(session);
SCOPE_ENTER(1, "%s: FA: %d RC: %d\n", tag,
failure_action, response_code);
pj_cstr(&response_str, response_string);
if (failure_action == stir_shaken_failure_action_REJECT_REQUEST) {
reject_incoming_call(session, response_code);
SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_REJECT,
"%s: Rejecting request and terminating session\n",
tag);
}
ast_stir_shaken_vs_ctx_set_response_code(ctx, vs_rc);
ast_stir_shaken_add_result_to_channel(ctx);
if (failure_action == stir_shaken_failure_action_CONTINUE_RETURN_REASON) {
int rc = ast_sip_session_add_reason_header(session,
ast_stir_shaken_vs_get_use_rfc9410_responses(ctx) ? "STIR" : "SIP",
response_code, response_str.ptr);
if (rc != 0) {
SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_SYSTEM_FAILURE,
"%s: Failed to add Reason header\n", tag);
}
SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_CONTINUE,
"%s: Attaching reason code to session\n", tag);
}
SCOPE_EXIT_RTN_VALUE(PROCESS_FAILURE_CONTINUE,
"%s: Continuing\n", tag);
}
/*!
* \internal
* \brief Session supplement callback on an incoming INVITE request
@ -181,222 +199,155 @@ static void stir_shaken_inv_end_session(struct ast_sip_session *session, pjsip_r
*/
static int stir_shaken_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata)
{
static const pj_str_t identity_str = { "Identity", 8 };
const pj_str_t bad_identity_info_str = {
AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_BAD_IDENTITY_INFO)
};
const pj_str_t unsupported_credential_str = {
AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_UNSUPPORTED_CREDENTIAL)
};
const pj_str_t stale_date_str = {
AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_STALE_DATE)
};
const pj_str_t use_supported_passport_format_str = {
AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_USE_SUPPORTED_PASSPORT_FORMAT)
};
const pj_str_t invalid_identity_hdr_str = {
AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER,
strlen(AST_STIR_SHAKEN_RESPONSE_STR_INVALID_IDENTITY_HEADER)
};
const pj_str_t server_internal_error_str = { "Server Internal Error", 21 };
char *identity_hdr_val;
char *encoded_val;
struct ast_channel *chan = session->channel;
char *caller_id = session->id.number.str;
RAII_VAR(struct ast_stir_shaken_vs_ctx *, ctx, NULL, ao2_cleanup);
RAII_VAR(char *, header, NULL, ast_free);
RAII_VAR(char *, payload, NULL, ast_free);
char *signature;
char *algorithm;
char *public_cert_url;
char *attestation;
char *ppt;
int mismatch = 0;
struct ast_stir_shaken_payload *ss_payload;
int failure_code = 0;
RAII_VAR(struct stir_shaken_profile *, profile, NULL, ao2_cleanup);
char *identity_hdr_val;
char *date_hdr_val;
char *caller_id = session->id.number.str;
const char *session_name = ast_sip_session_get_name(session);
struct ast_channel *chan = session->channel;
enum ast_stir_shaken_vs_response_code vs_rc;
enum process_failure_rc p_rc;
SCOPE_ENTER(1, "%s: Enter\n", session_name);
/* Check if this is a reinvite. If it is, we don't need to do anything */
if (rdata->msg_info.to->tag.slen) {
return 0;
SCOPE_EXIT_RTN_VALUE(0, "%s: Reinvite. No action needed\n", session_name);
}
profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile);
/* Profile should be checked first as it takes priority over anything else.
* If there is a profile and it doesn't have verification enabled, do nothing.
* If there is no profile and the stir_shaken option is either not set or does
* not support verification, do nothing.
/*
* Shortcut: If there's no callerid or profile name,
* just bail now.
*/
if ((profile && !ast_stir_shaken_profile_supports_verification(profile))
|| (!profile && (session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_VERIFY) == 0)) {
return 0;
if (ast_strlen_zero(caller_id)
|| ast_strlen_zero(session->endpoint->stir_shaken_profile)) {
SCOPE_EXIT_RTN_VALUE(0, "%s: No callerid or profile name. No action needed\n", session_name);
}
identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_str);
vs_rc = ast_stir_shaken_vs_ctx_create(caller_id, chan,
session->endpoint->stir_shaken_profile,
session_name, &ctx);
if (vs_rc == AST_STIR_SHAKEN_VS_DISABLED) {
SCOPE_EXIT_RTN_VALUE(0, "%s: VS Disabled\n", session_name);
} else if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
reject_incoming_call(session, 500);
SCOPE_EXIT_RTN_VALUE(1, "%s: Unable to create context. Call terminated\n",
session_name);
}
identity_hdr_val = ast_sip_rdata_get_header_value(rdata, identity_hdr_str);
if (ast_strlen_zero(identity_hdr_val)) {
ast_stir_shaken_add_verification(chan, caller_id, "", AST_STIR_SHAKEN_VERIFY_NOT_PRESENT);
return 0;
}
encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
header = ast_base64url_decode_string(encoded_val);
if (ast_strlen_zero(header)) {
ast_debug(3, "STIR/SHAKEN INVITE for %s is missing header\n",
ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
return 1;
}
encoded_val = strtok_r(identity_hdr_val, ".", &identity_hdr_val);
payload = ast_base64url_decode_string(encoded_val);
if (ast_strlen_zero(payload)) {
ast_debug(3, "STIR/SHAKEN INVITE for %s is missing payload\n",
ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
return 1;
}
/* It's fine to leave the signature encoded */
signature = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
if (ast_strlen_zero(signature)) {
ast_debug(3, "STIR/SHAKEN INVITE for %s is missing signature\n",
ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
return 1;
}
/* Trim "info=<" to get public cert URL */
strtok_r(identity_hdr_val, "<", &identity_hdr_val);
public_cert_url = strtok_r(identity_hdr_val, ">", &identity_hdr_val);
/* Make sure the public URL is actually a URL */
if (ast_strlen_zero(public_cert_url) || !ast_begins_with(public_cert_url, "http")) {
/* RFC8224 states that if we can't acquire the credentials needed
* by the verification service, we should send a 436 */
ast_debug(3, "STIR/SHAKEN INVITE for %s did not have valid URL (%s)\n",
ast_sorcery_object_get_id(session->endpoint), public_cert_url);
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_BAD_IDENTITY_INFO, bad_identity_info_str);
return 1;
}
algorithm = strtok_r(identity_hdr_val, ";", &identity_hdr_val);
if (ast_strlen_zero(algorithm)) {
/* RFC8224 states that if the algorithm is not specified, use ES256 */
algorithm = STIR_SHAKEN_ENCRYPTION_ALGORITHM;
} else {
strtok_r(algorithm, "=", &algorithm);
if (strcmp(algorithm, STIR_SHAKEN_ENCRYPTION_ALGORITHM)) {
/* RFC8224 states that if we don't support the algorithm, send a 437 */
ast_debug(3, "STIR/SHAKEN INVITE for %s uses an unsupported algorithm (%s)\n",
ast_sorcery_object_get_id(session->endpoint), algorithm);
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str);
return 1;
p_rc = process_failure(ctx, caller_id, session, rdata,
AST_STIR_SHAKEN_VS_NO_IDENTITY_HDR);
if (p_rc == PROCESS_FAILURE_CONTINUE) {
SCOPE_EXIT_RTN_VALUE(0, "%s: No Identity header found. Call continuing\n",
session_name);
}
SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: No Identity header found. Call terminated\n",
session_name);
}
/* The only thing left should be ppt=shaken (which could have more values later),
* unless using the compact PASSport form */
strtok_r(identity_hdr_val, "=", &identity_hdr_val);
ppt = ast_strip(identity_hdr_val);
if (!ast_strlen_zero(ppt) && strcmp(ppt, STIR_SHAKEN_PPT)) {
ast_log(LOG_ERROR, "STIR/SHAKEN INVITE for %s has unsupported ppt (%s)\n",
ast_sorcery_object_get_id(session->endpoint), ppt);
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_USE_SUPPORTED_PASSPORT_FORMAT, use_supported_passport_format_str);
return 1;
vs_rc = ast_stir_shaken_vs_ctx_add_identity_hdr(ctx, identity_hdr_val);
if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
reject_incoming_call(session, 500);
SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Unable to add Identity header. Call terminated.\n",
session_name);
}
if (check_date_header(rdata)) {
ast_debug(3, "STIR/SHAKEN INVITE for %s has old Date header\n",
ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE, stale_date_str);
return 1;
}
attestation = get_attestation_from_payload(payload);
ss_payload = ast_stir_shaken_verify_with_profile(header, payload, signature, algorithm, public_cert_url, &failure_code, profile);
if (!ss_payload) {
if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_TO_GET_CERT) {
/* RFC8224 states that if we can't get the credentials we need, send a 437 */
ast_debug(3, "STIR/SHAKEN INVITE for %s failed to acquire cert during verification process\n",
ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_UNSUPPORTED_CREDENTIAL, unsupported_credential_str);
} else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_MEMORY_ALLOC) {
ast_log(LOG_ERROR, "Failed to allocate memory during STIR/SHAKEN verification"
" for %s\n", ast_sorcery_object_get_id(session->endpoint));
stir_shaken_inv_end_session(session, rdata, 500, server_internal_error_str);
} else if (failure_code == AST_STIR_SHAKEN_VERIFY_FAILED_SIGNATURE_VALIDATION) {
/* RFC8224 states that if we can't validate the signature, send a 438 */
ast_debug(3, "STIR/SHAKEN INVITE for %s failed signature validation during verification process\n",
ast_sorcery_object_get_id(session->endpoint));
ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_SIGNATURE_FAILED);
stir_shaken_inv_end_session(session, rdata, AST_STIR_SHAKEN_RESPONSE_CODE_INVALID_IDENTITY_HEADER, invalid_identity_hdr_str);
date_hdr_val = ast_sip_rdata_get_header_value(rdata, date_hdr_str);
if (ast_strlen_zero(date_hdr_val)) {
p_rc = process_failure(ctx, caller_id, session, rdata,
AST_STIR_SHAKEN_VS_NO_DATE_HDR);
if (p_rc == PROCESS_FAILURE_CONTINUE) {
SCOPE_EXIT_RTN_VALUE(0, "%s: No Date header found. Call continuing\n",
session_name);
}
return 1;
}
ast_stir_shaken_payload_free(ss_payload);
mismatch |= compare_caller_id(caller_id, payload);
mismatch |= compare_timestamp(payload);
if (mismatch) {
ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_MISMATCH);
return 0;
SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: No Date header found. Call terminated\n",
session_name);
}
ast_stir_shaken_add_verification(chan, caller_id, attestation, AST_STIR_SHAKEN_VERIFY_PASSED);
ast_stir_shaken_vs_ctx_add_date_hdr(ctx, date_hdr_val);
if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
reject_incoming_call(session, 500);
SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Unable to add Date header. Call terminated.\n",
session_name);
}
return 0;
vs_rc = ast_stir_shaken_vs_verify(ctx);
if (vs_rc != AST_STIR_SHAKEN_VS_SUCCESS) {
p_rc = process_failure(ctx, caller_id, session, rdata, vs_rc);
if (p_rc == PROCESS_FAILURE_CONTINUE) {
SCOPE_EXIT_RTN_VALUE(0, "%s: Verification failed. Call continuing\n",
session_name);
}
SCOPE_EXIT_LOG_RTN_VALUE(1, LOG_ERROR, "%s: Verification failed. Call terminated\n",
session_name);
}
ast_stir_shaken_add_result_to_channel(ctx);
SCOPE_EXIT_RTN_VALUE(0, "Passed\n");
}
static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_data *tdata)
static void add_fingerprints_if_present(struct ast_sip_session *session,
struct ast_stir_shaken_as_ctx *ctx)
{
struct ast_sip_session_media_state *ms = session->pending_media_state;
struct ast_sip_session_media *m = NULL;
struct ast_rtp_engine_dtls *d = NULL;
enum ast_rtp_dtls_hash h;
int i;
const char *tag = ast_sip_session_get_name(session);
size_t count = AST_VECTOR_SIZE(&ms->sessions);
SCOPE_ENTER(4, "%s: Check %zu media sessions for fingerprints\n",
tag, count);
if (!ast_stir_shaken_as_ctx_wants_fingerprints(ctx)) {
SCOPE_EXIT_RTN("%s: Fingerprints not needed\n", tag);
}
for (i = 0; i < count; i++) {
const char *f;
m = AST_VECTOR_GET(&ms->sessions, i);
if (!m|| !m->rtp) {
ast_trace(1, "Session: %d: No session or rtp instance\n", i);
continue;
}
d = ast_rtp_instance_get_dtls(m->rtp);
h = d->get_fingerprint_hash(m->rtp);
f = d->get_fingerprint(m->rtp);
ast_stir_shaken_as_ctx_add_fingerprint(ctx,
h == AST_RTP_DTLS_HASH_SHA256 ? "sha-256" : "sha-1", f);
}
SCOPE_EXIT_RTN("%s: Done\n", tag);
}
static char *get_dest_tn(pjsip_tx_data *tdata, const char *tag)
{
static const pj_str_t identity_str = { "Identity", 8 };
pjsip_generic_string_hdr *identity_hdr;
pj_str_t identity_val;
pjsip_fromto_hdr *old_identity;
pjsip_fromto_hdr *to;
pjsip_sip_uri *uri;
char *signature;
char *public_cert_url;
struct ast_json *header;
struct ast_json *payload;
char *dumped_string;
RAII_VAR(char *, dest_tn, NULL, ast_free);
RAII_VAR(struct ast_json *, json, NULL, ast_json_free);
RAII_VAR(struct ast_stir_shaken_payload *, ss_payload, NULL, ast_stir_shaken_payload_free);
RAII_VAR(char *, encoded_header, NULL, ast_free);
RAII_VAR(char *, encoded_payload, NULL, ast_free);
RAII_VAR(char *, combined_str, NULL, ast_free);
size_t combined_size;
old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_str, NULL);
if (old_identity) {
return 0;
}
char *dest_tn = NULL;
SCOPE_ENTER(4, "%s: Enter\n", tag);
to = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_TO, NULL);
if (!to) {
ast_log(LOG_ERROR, "Failed to find To header while adding STIR/SHAKEN Identity header\n");
return -1;
SCOPE_EXIT_RTN_VALUE(NULL, "%s: Failed to find To header\n", tag);
}
uri = pjsip_uri_get_uri(to->uri);
if (!uri) {
ast_log(LOG_ERROR, "Failed to retrieve URI from To header while adding STIR/SHAKEN Identity header\n");
return -1;
SCOPE_EXIT_RTN_VALUE(NULL,
"%s: Failed to retrieve URI from To header\n", tag);
}
dest_tn = ast_malloc(uri->user.slen + 1);
if (!dest_tn) {
ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN dest->tn\n");
return -1;
SCOPE_EXIT_RTN_VALUE(NULL,
"%s: Failed to allocate memory for dest_tn\n", tag);
}
/* Remove everything except 0-9, *, and # in telephone number according to RFC 8224
@ -413,114 +364,105 @@ static int add_identity_header(const struct ast_sip_session *session, pjsip_tx_d
s++;
}
*new_tn = '\0';
ast_debug(4, "Canonicalized telephone number %.*s -> %s\n", (int) uri->user.slen, uri->user.ptr, dest_tn);
ast_trace(2, "Canonicalized telephone number " PJSTR_PRINTF_SPEC " -> %s\n",
PJSTR_PRINTF_VAR(uri->user), dest_tn);
}
/* x5u (public key URL), attestation, and origid will be added by ast_stir_shaken_sign */
json = ast_json_pack("{s: {s: s, s: s, s: s}, s: {s: {s: [s]}, s: {s: s}}}",
"header", "alg", "ES256", "ppt", "shaken", "typ", "passport",
"payload", "dest", "tn", dest_tn, "orig", "tn",
session->id.number.str);
if (!json) {
ast_log(LOG_ERROR, "Failed to allocate memory for STIR/SHAKEN JSON\n");
return -1;
}
ss_payload = ast_stir_shaken_sign(json);
if (!ss_payload) {
ast_log(LOG_ERROR, "Failed to sign STIR/SHAKEN payload\n");
return -1;
}
header = ast_json_object_get(json, "header");
dumped_string = ast_json_dump_string(header);
encoded_header = ast_base64url_encode_string(dumped_string);
ast_json_free(dumped_string);
if (!encoded_header) {
ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN header\n");
return -1;
}
payload = ast_json_object_get(json, "payload");
/* Fields must appear in lexiographic order: https://www.rfc-editor.org/rfc/rfc8588.html#section-6
* https://www.rfc-editor.org/rfc/rfc8225.html#section-9 */
dumped_string = ast_json_dump_string_sorted(payload);
encoded_payload = ast_base64url_encode_string(dumped_string);
ast_json_free(dumped_string);
if (!encoded_payload) {
ast_log(LOG_ERROR, "Failed to encode STIR/SHAKEN payload\n");
return -1;
}
signature = (char *)ast_stir_shaken_payload_get_signature(ss_payload);
public_cert_url = ast_stir_shaken_payload_get_public_cert_url(ss_payload);
/* The format for the identity header:
* header.payload.signature;info=<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);
profile = ast_stir_shaken_get_profile(session->endpoint->stir_shaken_profile);
/* Profile should be checked first as it takes priority over anything else.
* If there is a profile and it doesn't have attestation enabled, do nothing.
* If there is no profile and the stir_shaken option is either not set or does
* not support attestation, do nothing.
*/
if ((profile && !ast_stir_shaken_profile_supports_attestation(profile))
|| (!profile && (session->endpoint->stir_shaken & AST_SIP_STIR_SHAKEN_ATTEST) == 0)) {
return;
old_identity = pjsip_msg_find_hdr_by_name(tdata->msg, &identity_hdr_str, NULL);
if (old_identity) {
SCOPE_EXIT_RTN("Found an existing Identity header\n");
}
if (ast_strlen_zero(session->id.number.str) && session->id.number.valid) {
return;
dest_tn = get_dest_tn(tdata, session_name);
if (!dest_tn) {
SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Unable to find destination tn\n",
session_name);
}
/* If adding the Identity header fails for some reason, there's no point
* adding the Date header.
*/
if ((add_identity_header(session, tdata)) != 0) {
return;
ast_party_id_init(&connected_id);
ast_channel_lock(session->channel);
effective_id = ast_channel_connected_effective_id(session->channel);
ast_party_id_copy(&connected_id, &effective_id);
ast_channel_unlock(session->channel);
if (!ast_sip_can_present_connected_id(session, &connected_id)) {
ast_free(dest_tn);
ast_party_id_free(&connected_id);
SCOPE_EXIT_RTN("Unable to get caller id\n");
}
as_rc = ast_stir_shaken_as_ctx_create(connected_id.number.str,
dest_tn, session->channel,
session->endpoint->stir_shaken_profile,
session_name, &ctx);
ast_free(dest_tn);
ast_party_id_free(&connected_id);
if (as_rc == AST_STIR_SHAKEN_AS_DISABLED) {
SCOPE_EXIT_RTN("%s: AS Disabled\n", session_name);
} else if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) {
SCOPE_EXIT_RTN("%s: Unable to create context\n",
session_name);
}
add_date_header(session, tdata);
add_fingerprints_if_present(session, ctx);
as_rc = ast_stir_shaken_attest(ctx, &identity_str);
if (as_rc != AST_STIR_SHAKEN_AS_SUCCESS) {
ao2_cleanup(ctx);
SCOPE_EXIT_LOG(LOG_ERROR,
"%s: Failed to create attestation\n", session_name);
}
ast_trace(1, "%s: Identity header: %s\n", session_name, identity_str);
identity_val = pj_str(identity_str);
identity_hdr = pjsip_generic_string_hdr_create(tdata->pool, &identity_hdr_str, &identity_val);
ast_free(identity_str);
if (!identity_hdr) {
ao2_cleanup(ctx);
SCOPE_EXIT_LOG_RTN(LOG_ERROR,
"%s: Unable to create Identity header\n", session_name);
}
pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)identity_hdr);
ao2_cleanup(ctx);
SCOPE_EXIT_RTN("Done\n");
}
static struct ast_sip_session_supplement stir_shaken_supplement = {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,443 @@
/*
* 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;
}

View File

@ -0,0 +1,59 @@
/*
* 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_ */

View File

@ -0,0 +1,326 @@
/*
* 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;
}

View File

@ -1,380 +0,0 @@
/*
* 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;
}

View File

@ -1,111 +0,0 @@
/*
* 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 */

View File

@ -0,0 +1,353 @@
/*
* 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");
}

View File

@ -0,0 +1,568 @@
/*
* 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_ */

View File

@ -0,0 +1,525 @@
/*
* 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;
}

View File

@ -0,0 +1,280 @@
/*
* 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 */

View File

@ -1,351 +0,0 @@
/*
* 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;
}

View File

@ -1,78 +0,0 @@
/*
* 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 */

View File

@ -0,0 +1,344 @@
/*
* 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;
}

View File

@ -0,0 +1,489 @@
/*
* 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 */

View File

@ -1,286 +0,0 @@
/*
* 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;
}

View File

@ -1,111 +0,0 @@
/*
* 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 */

View File

@ -1,241 +0,0 @@
/*
* 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;
}

View File

@ -1,39 +0,0 @@
/*
* 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 */

View File

@ -0,0 +1,471 @@
/*
* 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;
}

View File

@ -1,40 +0,0 @@
/*
* 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 */

View File

@ -1,193 +0,0 @@
/*
* 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;
}

View File

@ -18,51 +18,59 @@
#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
* \brief Retrieve the stir/shaken sorcery context
*
* \param obj A sorcery object containing configuration data
* \param arg Asterisk CLI argument object
* \param flags ao2 container flags
*
* \retval 0
* \retval The stir/shaken sorcery context
*/
int stir_shaken_cli_show(void *obj, void *arg, int flags);
struct ast_sorcery *get_sorcery(void);
/*!
* \brief Tab completion for name matching with STIR/SHAKEN CLI commands
* \brief Return string version of VS response code
*
* \param word The word to tab complete on
* \param container The sorcery container to iterate through
*
* \retval The tab completion options
* \param vs_rc
* \return Response string
*/
char *stir_shaken_tab_complete_name(const char *word, struct ao2_container *container);
const char *vs_response_code_to_str(
enum ast_stir_shaken_vs_response_code vs_rc);
/*!
* \brief Reads the public (or private) key from the specified path
* \brief Return string version of AS response code
*
* \param path The path to the file containing the private key
* \param priv Specify 0 for public, 1 for private
*
* \retval NULL on failure
* \retval The public/private key on success
* \param as_rc
* \return Response string
*/
EVP_PKEY *stir_shaken_read_key(const char *path, int priv);
const char *as_response_code_to_str(
enum ast_stir_shaken_as_response_code as_rc);
/*!
* \brief Gets the serial number in hex form from the buffer (for X509)
*
* \note The returned string will need to be freed by the caller
*
* \param buf The BASE64 encoded buffer
* \param buf_size The size of the data in buf
*
* \retval NULL on failure
* \retval serial number on success
* \brief Retrieves the OpenSSL NID for the TN Auth list extension
* \retval The NID
*/
char *stir_shaken_get_serial_number_x509(const char *buf, size_t buf_size);
int get_tn_auth_nid(void);
struct trusted_cert_store {
X509_STORE *store;
ast_rwlock_t store_lock;
};
/*!
* \brief Retrieves the OpenSSL trusted cert store
* \retval The store
*/
struct trusted_cert_store *get_trusted_cert_store(void);
#endif /* _STIR_SHAKEN_H */

View File

@ -0,0 +1,294 @@
<!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>

View File

@ -1,202 +0,0 @@
/*
* 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;
}

View File

@ -0,0 +1,280 @@
/*
* 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;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
/*
* 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_ */

View File

@ -0,0 +1,440 @@
/*
* 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;
}