diff --git a/subprojects/gst-plugins-base/gst-libs/gst/sdp/gstmikey.c b/subprojects/gst-plugins-base/gst-libs/gst/sdp/gstmikey.c index af0f2b3480..e71117c067 100644 --- a/subprojects/gst-plugins-base/gst-libs/gst/sdp/gstmikey.c +++ b/subprojects/gst-plugins-base/gst-libs/gst/sdp/gstmikey.c @@ -2258,7 +2258,7 @@ gst_mikey_message_new_from_caps (GstCaps * caps) GstStructure *s; GstMapInfo info; GstBuffer *srtpkey; - const GValue *val; + const GValue *val, *mki_val; const gchar *cipher, *auth; const gchar *srtpcipher, *srtpauth, *srtcpcipher, *srtcpauth; @@ -2280,12 +2280,8 @@ gst_mikey_message_new_from_caps (GstCaps * caps) srtcpcipher = gst_structure_get_string (s, "srtcp-cipher"); srtcpauth = gst_structure_get_string (s, "srtcp-auth"); - /* we need srtp cipher/auth or srtcp cipher/auth */ - if ((srtpcipher == NULL || srtpauth == NULL) - && (srtcpcipher == NULL || srtcpauth == NULL)) { - GST_WARNING ("could not find the right SRTP parameters in caps"); - return NULL; - } + /* Some mikey messages are intended to only set the SPI/MKI, + * in which case cipher or auth can be absent. */ /* prefer srtp cipher over srtcp */ cipher = srtpcipher; @@ -2298,10 +2294,12 @@ gst_mikey_message_new_from_caps (GstCaps * caps) auth = srtcpauth; /* get cipher and auth values */ - if (!enc_alg_from_cipher_name (cipher, &enc_alg) || - !auth_alg_from_cipher_name (cipher, &auth_alg) || - !enc_key_length_from_cipher_name (cipher, &enc_key_length) || - !auth_key_length_from_auth_cipher_name (auth, cipher, &auth_key_length)) { + if (cipher && (!enc_alg_from_cipher_name (cipher, &enc_alg) || + !auth_alg_from_cipher_name (cipher, &auth_alg) || + !enc_key_length_from_cipher_name (cipher, &enc_key_length) || + (auth + && !auth_key_length_from_auth_cipher_name (auth, cipher, + &auth_key_length)))) { return NULL; } @@ -2310,37 +2308,47 @@ gst_mikey_message_new_from_caps (GstCaps * caps) gst_mikey_message_set_info (msg, GST_MIKEY_VERSION, GST_MIKEY_TYPE_PSK_INIT, FALSE, GST_MIKEY_PRF_MIKEY_1, g_random_int (), GST_MIKEY_MAP_TYPE_SRTP); - /* timestamp is now */ - gst_mikey_message_add_t_now_ntp_utc (msg); - /* add some random data */ - gst_mikey_message_add_rand_len (msg, 16); + if (cipher || auth) { + /* timestamp is now */ + gst_mikey_message_add_t_now_ntp_utc (msg); + /* add some random data */ + gst_mikey_message_add_rand_len (msg, 16); - /* the policy '0' is SRTP */ - payload = gst_mikey_payload_new (GST_MIKEY_PT_SP); - gst_mikey_payload_sp_set (payload, 0, GST_MIKEY_SEC_PROTO_SRTP); + /* the policy '0' is SRTP */ + payload = gst_mikey_payload_new (GST_MIKEY_PT_SP); + gst_mikey_payload_sp_set (payload, 0, GST_MIKEY_SEC_PROTO_SRTP); - /* AES-CM or AES-GCM is supported */ - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_ENC_ALG, 1, - &enc_alg); - /* encryption key length */ - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_ENC_KEY_LEN, 1, - &enc_key_length); - /* HMAC-SHA1 or NULL in case of GCM */ - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_AUTH_ALG, 1, - &auth_alg); - /* authentication key length */ - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_AUTH_KEY_LEN, 1, - &auth_key_length); - /* we enable encryption on RTP and RTCP */ - byte = 1; - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTP_ENC, 1, - &byte); - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTCP_ENC, 1, - &byte); - /* we enable authentication on RTP and RTCP */ - gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTP_AUTH, 1, - &byte); - gst_mikey_message_add_payload (msg, payload); + if (cipher) { + /* AES-CM or AES-GCM is supported */ + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_ENC_ALG, 1, + &enc_alg); + /* encryption key length */ + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_ENC_KEY_LEN, 1, + &enc_key_length); + } + if (auth) { + /* HMAC-SHA1 or NULL in case of GCM */ + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_AUTH_ALG, 1, + &auth_alg); + /* authentication key length */ + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_AUTH_KEY_LEN, + 1, &auth_key_length); + } + /* we enable encryption on RTP and RTCP */ + byte = 1; + if (cipher) { + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTP_ENC, 1, + &byte); + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTCP_ENC, 1, + &byte); + } + if (auth) { + /* we enable authentication on RTP and RTCP */ + gst_mikey_payload_sp_add_param (payload, GST_MIKEY_SP_SRTP_SRTP_AUTH, 1, + &byte); + } + gst_mikey_message_add_payload (msg, payload); + } /* make unencrypted KEMAC */ payload = gst_mikey_payload_new (GST_MIKEY_PT_KEMAC); @@ -2351,6 +2359,22 @@ gst_mikey_message_new_from_caps (GstCaps * caps) gst_mikey_payload_key_data_set_key (pkd, GST_MIKEY_KD_TEK, info.size, info.data); gst_buffer_unmap (srtpkey, &info); + /* add optional SPI/MKI + * See: https://www.rfc-editor.org/rfc/rfc3830.html#section-6.14 + */ + mki_val = gst_structure_get_value (s, "mki"); + if (mki_val) { + GstBuffer *mki = gst_value_get_buffer (mki_val); + if (mki && GST_IS_BUFFER (mki) && gst_buffer_get_size (mki) > 0) { + gst_buffer_map (mki, &info, GST_MAP_READ); + gst_mikey_payload_key_data_set_spi (pkd, info.size, info.data); + gst_buffer_unmap (mki, &info); + } else { + GST_WARNING + ("Failed to get 'mki' from caps as a buffer or the buffer is empty"); + } + } + gst_mikey_payload_kemac_add_sub (payload, pkd); gst_mikey_message_add_payload (msg, payload); @@ -2511,6 +2535,16 @@ gst_mikey_message_to_caps (const GstMIKEYMessage * msg, GstCaps * caps) gst_buffer_unref (buf); gst_caps_set_simple (caps, "roc", G_TYPE_UINT, srtp->roc, NULL); + + /* Get optional SPI/MKI + * See: https://www.rfc-editor.org/rfc/rfc3830.html#section-6.13 + */ + if (pkd->kv_type == GST_MIKEY_KV_SPI && pkd->kv_len[0] > 0) { + GstBuffer *mki = + gst_buffer_new_memdup (pkd->kv_data[0], (gsize) pkd->kv_len[0]); + gst_caps_set_simple (caps, "mki", GST_TYPE_BUFFER, mki, NULL); + gst_buffer_unref (mki); + } } gst_caps_set_simple (caps, diff --git a/subprojects/gst-plugins-good/docs/gst_plugins_cache.json b/subprojects/gst-plugins-good/docs/gst_plugins_cache.json index 5eae3ea5ab..20ab0dd120 100644 --- a/subprojects/gst-plugins-good/docs/gst_plugins_cache.json +++ b/subprojects/gst-plugins-good/docs/gst_plugins_cache.json @@ -21760,6 +21760,18 @@ "type": "GstRTSPSrcBufferMode", "writable": true }, + "client-managed-mikey": { + "blurb": "Enable client-managed MIKEY mode", + "conditionally-available": false, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "false", + "mutable": "null", + "readable": true, + "type": "gboolean", + "writable": true + }, "connection-speed": { "blurb": "Network connection speed in kbps (0 = unknown)", "conditionally-available": false, @@ -22415,6 +22427,16 @@ ], "return-type": "void" }, + "hard-limit": { + "args": [ + { + "name": "arg0", + "type": "guint" + } + ], + "return-type": "void", + "when": "last" + }, "new-manager": { "args": [ { @@ -22464,6 +22486,17 @@ "return-type": "GstFlowReturn", "when": "last" }, + "remove-key": { + "action": true, + "args": [ + { + "name": "arg0", + "type": "guint" + } + ], + "return-type": "gboolean", + "when": "last" + }, "request-rtcp-key": { "args": [ { @@ -22474,6 +22507,15 @@ "return-type": "GstCaps", "when": "last" }, + "request-rtp-key": { + "args": [ + { + "name": "arg0", + "type": "guint" + } + ], + "return-type": "GstCaps" + }, "select-stream": { "args": [ { @@ -22488,6 +22530,25 @@ "return-type": "gboolean", "when": "last" }, + "set-mikey-parameter": { + "action": true, + "args": [ + { + "name": "arg0", + "type": "guint" + }, + { + "name": "arg1", + "type": "GstCaps" + }, + { + "name": "arg2", + "type": "GstPromise" + } + ], + "return-type": "gboolean", + "when": "last" + }, "set-parameter": { "action": true, "args": [ @@ -22510,6 +22571,16 @@ ], "return-type": "gboolean", "when": "last" + }, + "soft-limit": { + "args": [ + { + "name": "arg0", + "type": "guint" + } + ], + "return-type": "void", + "when": "last" } } } diff --git a/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.c b/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.c index b3dd255264..8f09658b44 100644 --- a/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.c +++ b/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.c @@ -93,6 +93,30 @@ * rtspsrc does not know the difference and will send a PAUSE when you wanted * a TEARDOWN. The workaround is to hook into the `before-send` signal and * return FALSE in this case. + * + * Some servers (e.g. Axis cameras) expect the client to propose the encryption + * key(s) to be used for SRTP / SRTCP. This is required to allow re-keying so + * as to evade cryptanalysis. Note that the behaviour is not specified by the + * RFCs. By setting the 'client-managed-mikey-mode' property to 'true', rtspsrc + * acts as follows: + * + * * For a secured profile (RTP/SAVP or RTP/SAVPF), any media in the SDP + * returned by the server for which MIKEY key management applies is + * elligible for client managed mode. The MIKEY from the server is then + * ignored. + * * rtspsrc sends a SETUP with a MIKEY payload proposed by the user. The + * payload is formed by calling the 'request-rtp-key' signal for each + * elligible stream. During initialisation, 'request-rtcp-key' is also + * called as usual. The keys returned by both signals should be the same + * for a single stream, but the mechanism allows a different approach. + * * The user can start re-keying of a stream by calling SET_PARAMETER. + * The convenience signal 'set-mikey-parameter' can be used to build a + * 'KeyMgmt' parameter with a MIKEY payload. + * * After the server accepts the new parameter, the user can call + * 'remove-key' and prepare for the new key(s) to be served by signals + * 'request-rtp-key' & 'request-rtcp-key'. + * * The signals 'soft-limit' & 'hard-limit' are called when a key + * reaches the limits of its utilisation. */ #ifdef HAVE_CONFIG_H @@ -147,6 +171,9 @@ enum SIGNAL_SELECT_STREAM, SIGNAL_NEW_MANAGER, SIGNAL_REQUEST_RTCP_KEY, + SIGNAL_REQUEST_RTP_KEY, + SIGNAL_SOFT_LIMIT, + SIGNAL_HARD_LIMIT, SIGNAL_ACCEPT_CERTIFICATE, SIGNAL_BEFORE_SEND, SIGNAL_PUSH_BACKCHANNEL_BUFFER, @@ -154,6 +181,8 @@ enum SIGNAL_GET_PARAMETERS, SIGNAL_SET_PARAMETER, SIGNAL_PUSH_BACKCHANNEL_SAMPLE, + SIGNAL_SET_MIKEY_PARAMETER, + SIGNAL_REMOVE_KEY, LAST_SIGNAL }; @@ -316,6 +345,7 @@ gst_rtsp_backchannel_get_type (void) #define DEFAULT_IGNORE_X_SERVER_REPLY FALSE #define DEFAULT_TCP_TIMESTAMP FALSE #define DEFAULT_FORCE_NON_COMPLIANT_URL FALSE +#define DEFAULT_CLIENT_MANAGED_MIKEY FALSE enum { @@ -369,6 +399,7 @@ enum PROP_EXTRA_HTTP_REQUEST_HEADERS, PROP_TCP_TIMESTAMP, PROP_FORCE_NON_COMPLIANT_URL, + PROP_CLIENT_MANAGED_MIKEY, }; #define GST_TYPE_RTSP_NAT_METHOD (gst_rtsp_nat_method_get_type()) @@ -480,6 +511,17 @@ static GstFlowReturn gst_rtspsrc_push_backchannel_sample (GstRTSPSrc * src, static void gst_rtspsrc_reset_flows (GstRTSPSrc * src); +static GstCaps *signal_get_srtcp_params (GstRTSPSrc * src, + GstRTSPStream * stream); + +static GstCaps *signal_get_srtp_params (GstRTSPSrc * src, + GstRTSPStream * stream); + +static gboolean set_mikey_parameter (GstRTSPSrc * src, const guint id, + GstCaps * mikey, GstPromise * promise); + +static gboolean remove_key (GstRTSPSrc * src, const guint id); + typedef struct { guint8 pt; @@ -1172,6 +1214,36 @@ gst_rtspsrc_class_init (GstRTSPSrcClass * klass) DEFAULT_FORCE_NON_COMPLIANT_URL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + /** + * GstRTSPSrc:client-managed-mikey + * + * Some servers (e.g. Axis Cameras) advertise a `key-mgmt:mikey` attribute in + * the SDP from the reply to DESCRIBE, but expect the client to provide the + * key for both the client (SRTCP/RR) and the server (SRTP/SRTCP SR). + * + * When this mode is enabled, for each eligible media, the server crypto + * policy is discarded. The crypto policy for this media is built from the + * key returned by the signals 'request-rtp-key' & 'request-rtcp-key'. + * + * A media is eligible for this mode if: + * + * * No session level MIKEY key-mgmt are defined and the media level key-mgmt + * is MIKEY. + * * A session level MIKEY key-mgmt is defined and no media level key-mgmts + * are defined. + * * A session level MIKEY key-mgmt is defined and the media level key-mgmt is + * also MIKEY. + * + * Since: 1.26 + */ + g_object_class_install_property (gobject_class, + PROP_CLIENT_MANAGED_MIKEY, + g_param_spec_boolean ("client-managed-mikey", + "Client-managed MIKEY", + "Enable client-managed MIKEY mode", + DEFAULT_CLIENT_MANAGED_MIKEY, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + /** * GstRTSPSrc::handle-request: * @rtspsrc: a #GstRTSPSrc @@ -1263,6 +1335,58 @@ gst_rtspsrc_class_init (GstRTSPSrcClass * klass) g_signal_new ("request-rtcp-key", G_TYPE_FROM_CLASS (klass), 0, 0, NULL, NULL, NULL, GST_TYPE_CAPS, 1, G_TYPE_UINT); + /** + * GstRTSPSrc::request-rtp-key: + * @rtspsrc: a #GstRTSPSrc + * @num: the stream number + * + * Signal emitted to get the crypto parameters relevant to the RTP + * stream. User should provide the key and the RTP encryption ciphers + * and authentication, and return them wrapped in a GstCaps. + * + * Applications should connect to this signal when 'client-managed-mikey' + * mode is enabled. The crypto parameters are usually the same as those + * use by the 'request-rtcp-key' signal. + * + * Since: 1.26 + */ + gst_rtspsrc_signals[SIGNAL_REQUEST_RTP_KEY] = + g_signal_new ("request-rtp-key", G_TYPE_FROM_CLASS (klass), + 0, 0, NULL, NULL, NULL, GST_TYPE_CAPS, 1, G_TYPE_UINT); + + /** + * GstRTSPSrc::soft-limit: + * @rtspsrc: a #GstRTSPSrc + * @id: The id of the stream + * + * When the 'client-managed-mikey' mode is enabled, this signal + * is emitted when the stream with @id has reached the + * soft limit of utilisation of it's master encryption key. + * User should start a re-keying procedure. + * + * Since: 1.26 + */ + gst_rtspsrc_signals[SIGNAL_SOFT_LIMIT] = + g_signal_new ("soft-limit", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT); + + /** + * GstRTSPSrc::hard-limit: + * @rtspsrc: a #GstRTSPSrc + * @id: The id of the stream + * + * When the 'client-managed-mikey' mode is enabled, this signal + * is emitted when the stream with @id has reached the + * hard limit of utilisation of it's master encryption key. + * User should start a re-keying procedure. Failure to do so, + * will lead to buffers of this stream being dropped. + * + * Since: 1.26 + */ + gst_rtspsrc_signals[SIGNAL_HARD_LIMIT] = + g_signal_new ("hard-limit", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT); + /** * GstRTSPSrc::accept-certificate: * @rtspsrc: a #GstRTSPSrc @@ -1384,6 +1508,12 @@ gst_rtspsrc_class_init (GstRTSPSrcClass * klass) * * Handle the SET_PARAMETER signal. * + * The #GstPromise reply consists in the following fields: + * + * * 'rtsp-result': set to 0 if the HTTP request could be processed. + * * 'rtsp-code': the HTTP status code returned by the server. + * * 'rtsp-reason': a human-readable version of the HTTP status code. + * * Returns: %TRUE when the command could be issued, %FALSE otherwise * */ @@ -1393,6 +1523,55 @@ gst_rtspsrc_class_init (GstRTSPSrcClass * klass) set_parameter), NULL, NULL, NULL, G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, GST_TYPE_PROMISE); + /** + * GstRTSPSrc::set-mikey-parameter: + * @rtspsrc: a #GstRTSPSrc + * @parameter: the id of the stream + * @parameter: the caps for the MIKEY payload + * @parameter: a pointer to #GstPromise + * + * Sends a SET_PARAMETER to the server with a MIKEY payload. + * + * This will call SET_PARAMETER with parameter 'KeyMgmt' and a MIKEY + * payload formed from the provided stream's ssrc and MIKEY caps. + * + * The #GstPromise reply consists in the following fields: + * + * * 'rtsp-result': set to 0 if the HTTP request could be processed. + * * 'rtsp-code': the HTTP status code returned by the server. + * * 'rtsp-reason': a human-readable version of the HTTP status code. + * + * Returns: %TRUE when the command could be issued, %FALSE otherwise + * + * Since: 1.26 + */ + gst_rtspsrc_signals[SIGNAL_SET_MIKEY_PARAMETER] = + g_signal_new ("set-mikey-parameter", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GstRTSPSrcClass, + set_mikey_parameter), NULL, NULL, NULL, G_TYPE_BOOLEAN, 3, + G_TYPE_UINT, GST_TYPE_CAPS, GST_TYPE_PROMISE); + + /** + * GstRTSPSrc::remove-key: + * @rtspsrc: a #GstRTSPSrc + * @parameter: the id of the stream for which to remove the key. + * + * Removes the key for a specific stream. + * + * When the 'client-managed-mikey' mode is enabled, this can be used + * after informing the server of the new crypto params (see signal + * 'set-mikey-parameter') to remove previous keys and force srtpdec + * to request new keys. + * + * Returns: %TRUE when the command could be issued, %FALSE otherwise + * + * Since: 1.26 + */ + gst_rtspsrc_signals[SIGNAL_REMOVE_KEY] = + g_signal_new ("remove-key", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GstRTSPSrcClass, + remove_key), NULL, NULL, NULL, G_TYPE_BOOLEAN, 1, G_TYPE_UINT); + gstelement_class->send_event = gst_rtspsrc_send_event; gstelement_class->provide_clock = gst_rtspsrc_provide_clock; gstelement_class->change_state = gst_rtspsrc_change_state; @@ -1413,6 +1592,8 @@ gst_rtspsrc_class_init (GstRTSPSrcClass * klass) klass->get_parameter = GST_DEBUG_FUNCPTR (get_parameter); klass->get_parameters = GST_DEBUG_FUNCPTR (get_parameters); klass->set_parameter = GST_DEBUG_FUNCPTR (set_parameter); + klass->set_mikey_parameter = GST_DEBUG_FUNCPTR (set_mikey_parameter); + klass->remove_key = GST_DEBUG_FUNCPTR (remove_key); gst_rtsp_ext_list_init (); @@ -1971,6 +2152,9 @@ gst_rtspsrc_set_property (GObject * object, guint prop_id, const GValue * value, case PROP_FORCE_NON_COMPLIANT_URL: rtspsrc->force_non_compliant_url = g_value_get_boolean (value); break; + case PROP_CLIENT_MANAGED_MIKEY: + rtspsrc->client_managed_mikey = g_value_get_boolean (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -2153,6 +2337,9 @@ gst_rtspsrc_get_property (GObject * object, guint prop_id, GValue * value, case PROP_FORCE_NON_COMPLIANT_URL: g_value_set_boolean (value, rtspsrc->force_non_compliant_url); break; + case PROP_CLIENT_MANAGED_MIKEY: + g_value_set_boolean (value, rtspsrc->client_managed_mikey); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -3753,6 +3940,25 @@ stream_get_caps_for_pt (GstRTSPStream * stream, guint pt) return NULL; } +/* @caps: (transfer-full) */ +static void +stream_set_caps_for_pt (GstRTSPStream * stream, guint pt, GstCaps * caps) +{ + guint i, len; + + len = stream->ptmap->len; + for (i = 0; i < len; i++) { + PtMapItem *item = &g_array_index (stream->ptmap, PtMapItem, i); + if (item->pt == pt) { + if (item->caps) + gst_caps_unref (item->caps); + + item->caps = caps; + return; + } + } +} + static GstCaps * request_pt_map (GstElement * manager, guint session, guint pt, GstRTSPSrc * src) { @@ -3941,17 +4147,87 @@ set_manager_buffer_mode (GstRTSPSrc * src) } } +static void +update_srtcp_params (GstRTSPStream * stream) +{ + GstStructure *s = gst_caps_get_structure (stream->srtcpparams, 0); + if (s) { + GstBuffer *buf; + const gchar *str; + GType ciphertype, authtype; + GValue rtcp_cipher = G_VALUE_INIT, rtcp_auth = G_VALUE_INIT; + + ciphertype = g_type_from_name ("GstSrtpCipherType"); + authtype = g_type_from_name ("GstSrtpAuthType"); + g_value_init (&rtcp_cipher, ciphertype); + g_value_init (&rtcp_auth, authtype); + + str = gst_structure_get_string (s, "srtcp-cipher"); + gst_value_deserialize (&rtcp_cipher, str); + str = gst_structure_get_string (s, "srtcp-auth"); + gst_value_deserialize (&rtcp_auth, str); + gst_structure_get (s, "srtp-key", GST_TYPE_BUFFER, &buf, NULL); + + g_object_set_property (G_OBJECT (stream->srtpenc), "rtp-cipher", + &rtcp_cipher); + g_object_set_property (G_OBJECT (stream->srtpenc), "rtp-auth", &rtcp_auth); + g_object_set_property (G_OBJECT (stream->srtpenc), "rtcp-cipher", + &rtcp_cipher); + g_object_set_property (G_OBJECT (stream->srtpenc), "rtcp-auth", &rtcp_auth); + g_object_set (stream->srtpenc, "key", buf, NULL); + + g_value_unset (&rtcp_cipher); + g_value_unset (&rtcp_auth); + gst_buffer_unref (buf); + } +} + static GstCaps * request_key (GstElement * srtpdec, guint ssrc, GstRTSPStream * stream) { guint i; - GstCaps *caps; - GstMIKEYMessage *msg = stream->mikey; + GstCaps *key_caps; + GstMIKEYMessage *msg = NULL; + GstRTSPSrc *rtspsrc = GST_RTSPSRC (stream->parent); - GST_DEBUG ("request key SSRC %u", ssrc); + GST_DEBUG_OBJECT (rtspsrc, "request key stream with id %u SSRC %u", + stream->id, ssrc); - caps = gst_caps_ref (stream_get_caps_for_pt (stream, stream->default_pt)); - caps = gst_caps_make_writable (caps); + if (stream->mikey) { + key_caps = + gst_caps_ref (stream_get_caps_for_pt (stream, stream->default_pt)); + } else if (rtspsrc->client_managed_mikey) { + key_caps = signal_get_srtp_params (rtspsrc, stream); + if (key_caps == NULL) { + GST_ERROR_OBJECT (rtspsrc, "no key caps returned for stream with id %u", + stream->id); + return NULL; + } + + stream->mikey = gst_mikey_message_new_from_caps (key_caps); + if (stream->mikey == NULL) { + GST_ERROR_OBJECT (rtspsrc, "failed to create MIKEY for stream with id %u", + stream->id); + gst_caps_unref (key_caps); + + return NULL; + } + + stream_set_caps_for_pt (stream, stream->default_pt, + gst_caps_ref (key_caps)); + + /* also renew RTCP crypto params in case we are re-keying */ + if (stream->srtcpparams) + gst_caps_unref (stream->srtcpparams); + + stream->srtcpparams = signal_get_srtcp_params (rtspsrc, stream); + update_srtcp_params (stream); + } else { + GST_ERROR_OBJECT (rtspsrc, "No MIKEYs for stream with id %u", stream->id); + return NULL; + } + + key_caps = gst_caps_make_writable (key_caps); /* parse crypto sessions and look for the SSRC rollover counter */ msg = stream->mikey; @@ -3959,12 +4235,44 @@ request_key (GstElement * srtpdec, guint ssrc, GstRTSPStream * stream) const GstMIKEYMapSRTP *map = gst_mikey_message_get_cs_srtp (msg, i); if (ssrc == map->ssrc) { - gst_caps_set_simple (caps, "roc", G_TYPE_UINT, map->roc, NULL); + gst_caps_set_simple (key_caps, "roc", G_TYPE_UINT, map->roc, NULL); break; } } - return caps; + return key_caps; +} + +static GstCaps * +on_soft_limit (GstElement * srtpdec, guint ssrc, gpointer user_data) +{ + GstRTSPStream *stream = (GstRTSPStream *) user_data; + GstRTSPSrc *rtspsrc = GST_RTSPSRC_CAST (stream->parent); + + GST_DEBUG_OBJECT (rtspsrc, + "Emitting 'soft-limit' for stream with id %u SSRC %u", stream->id, ssrc); + + g_signal_emit (rtspsrc, gst_rtspsrc_signals[SIGNAL_SOFT_LIMIT], 0, + stream->id); + + /* don't return CAPS, user needs to start a re-keying procedure */ + return NULL; +} + +static GstCaps * +on_hard_limit (GstElement * srtpdec, guint ssrc, gpointer user_data) +{ + GstRTSPStream *stream = (GstRTSPStream *) user_data; + GstRTSPSrc *rtspsrc = GST_RTSPSRC_CAST (stream->parent); + + GST_DEBUG_OBJECT (rtspsrc, + "Emitting 'hard-limit' for stream with id %u SSRC %u", stream->id, ssrc); + + g_signal_emit (rtspsrc, gst_rtspsrc_signals[SIGNAL_HARD_LIMIT], 0, + stream->id); + + /* don't return CAPS, user needs to start a re-keying procedure */ + return NULL; } static GstElement * @@ -3980,6 +4288,7 @@ request_rtp_decoder (GstElement * rtpbin, guint session, GstRTSPStream * stream) if (stream->srtpdec == NULL) { gchar *name; + GstRTSPSrc *rtspsrc; name = g_strdup_printf ("srtpdec_%u", session); stream->srtpdec = gst_element_factory_make ("srtpdec", name); @@ -3992,6 +4301,15 @@ request_rtp_decoder (GstElement * rtpbin, guint session, GstRTSPStream * stream) } g_signal_connect (stream->srtpdec, "request-key", (GCallback) request_key, stream); + + rtspsrc = GST_RTSPSRC_CAST (stream->parent); + if (rtspsrc->client_managed_mikey) { + g_signal_connect (stream->srtpdec, "soft-limit", + (GCallback) on_soft_limit, stream); + + g_signal_connect (stream->srtpdec, "hard-limit", + (GCallback) on_hard_limit, stream); + } } return gst_object_ref (stream->srtpdec); } @@ -4012,8 +4330,6 @@ request_rtcp_encoder (GstElement * rtpbin, guint session, return NULL; if (stream->srtpenc == NULL) { - GstStructure *s; - name = g_strdup_printf ("srtpenc_%u", session); stream->srtpenc = gst_element_factory_make ("srtpenc", name); g_free (name); @@ -4025,38 +4341,7 @@ request_rtcp_encoder (GstElement * rtpbin, guint session, } /* get RTCP crypto parameters from caps */ - s = gst_caps_get_structure (stream->srtcpparams, 0); - if (s) { - GstBuffer *buf; - const gchar *str; - GType ciphertype, authtype; - GValue rtcp_cipher = G_VALUE_INIT, rtcp_auth = G_VALUE_INIT; - - ciphertype = g_type_from_name ("GstSrtpCipherType"); - authtype = g_type_from_name ("GstSrtpAuthType"); - g_value_init (&rtcp_cipher, ciphertype); - g_value_init (&rtcp_auth, authtype); - - str = gst_structure_get_string (s, "srtcp-cipher"); - gst_value_deserialize (&rtcp_cipher, str); - str = gst_structure_get_string (s, "srtcp-auth"); - gst_value_deserialize (&rtcp_auth, str); - gst_structure_get (s, "srtp-key", GST_TYPE_BUFFER, &buf, NULL); - - g_object_set_property (G_OBJECT (stream->srtpenc), "rtp-cipher", - &rtcp_cipher); - g_object_set_property (G_OBJECT (stream->srtpenc), "rtp-auth", - &rtcp_auth); - g_object_set_property (G_OBJECT (stream->srtpenc), "rtcp-cipher", - &rtcp_cipher); - g_object_set_property (G_OBJECT (stream->srtpenc), "rtcp-auth", - &rtcp_auth); - g_object_set (stream->srtpenc, "key", buf, NULL); - - g_value_unset (&rtcp_cipher); - g_value_unset (&rtcp_auth); - gst_buffer_unref (buf); - } + update_srtcp_params (stream); } name = g_strdup_printf ("rtcp_sink_%d", session); pad = gst_element_request_pad_simple (stream->srtpenc, name); @@ -7407,6 +7692,20 @@ signal_get_srtcp_params (GstRTSPSrc * src, GstRTSPStream * stream) g_signal_emit (src, gst_rtspsrc_signals[SIGNAL_REQUEST_RTCP_KEY], 0, stream->id, &caps); + if (caps != NULL) + GST_DEBUG_OBJECT (src, "SRTCP parameters received"); + + return caps; +} + +static GstCaps * +signal_get_srtp_params (GstRTSPSrc * src, GstRTSPStream * stream) +{ + GstCaps *caps = NULL; + + g_signal_emit (src, gst_rtspsrc_signals[SIGNAL_REQUEST_RTP_KEY], 0, + stream->id, &caps); + if (caps != NULL) GST_DEBUG_OBJECT (src, "SRTP parameters received"); @@ -8093,6 +8392,127 @@ cleanup_error: } } +static gchar * +gst_rtspsrc_stream_make_renew_keymgmt (GstRTSPSrc * src, GstRTSPStream * stream, + GstCaps * mikey) +{ + gchar *base64, *result = NULL; + GstMIKEYMessage *mikey_msg; + + mikey_msg = gst_mikey_message_new_from_caps (mikey); + if (mikey_msg == NULL) + return NULL; + + /* add policy '0' for our SSRC */ + gst_mikey_message_add_cs_srtp (mikey_msg, 0, stream->ssrc, 0); + + base64 = gst_mikey_message_base64_encode (mikey_msg); + gst_mikey_message_unref (mikey_msg); + + if (base64) { + result = gst_sdp_make_keymgmt (stream->conninfo.location, base64); + g_free (base64); + } + + return result; +} + +static gboolean +set_mikey_parameter (GstRTSPSrc * src, const guint id, GstCaps * mikey_caps, + GstPromise * promise) +{ + gboolean res; + GstRTSPStream *stream; + gchar *keymgmt; + + GST_LOG_OBJECT (src, + "setting MIKEY parameter for stream with id %u: %" GST_PTR_FORMAT, id, + mikey_caps); + + if (mikey_caps == NULL) { + GST_ERROR_OBJECT (src, "invalid caps"); + return FALSE; + } + + if (src->state == GST_RTSP_STATE_INVALID) { + GST_ERROR_OBJECT (src, "invalid state"); + return FALSE; + } + + GST_OBJECT_LOCK (src); + + stream = find_stream (src, &id, (gpointer) find_stream_by_id); + + if (stream == NULL) { + GST_OBJECT_UNLOCK (src); + GST_ERROR_OBJECT (src, "no streams with id %u", id); + return FALSE; + } + + if (stream->profile != GST_RTSP_PROFILE_SAVP && + stream->profile != GST_RTSP_PROFILE_SAVPF) { + GST_OBJECT_UNLOCK (src); + GST_WARNING_OBJECT (src, "stream with id %u, is not encrypted", id); + return FALSE; + } + + keymgmt = gst_rtspsrc_stream_make_renew_keymgmt (src, stream, mikey_caps); + + GST_OBJECT_UNLOCK (src); + + if (keymgmt == NULL) { + GST_ERROR_OBJECT (src, + "failed to build MIKEY for stream with id %u: %" + GST_PTR_FORMAT, id, mikey_caps); + return FALSE; + } + + res = set_parameter (src, "KeyMgmt", keymgmt, NULL, promise); + g_free (keymgmt); + + return res; +} + +static gboolean +remove_key (GstRTSPSrc * src, const guint id) +{ + GstRTSPStream *stream; + + GST_LOG_OBJECT (src, "Removing key for stream with id %u", id); + + if (src->state == GST_RTSP_STATE_INVALID) { + GST_ERROR_OBJECT (src, "invalid state"); + return FALSE; + } + + GST_OBJECT_LOCK (src); + + stream = find_stream (src, &id, (gpointer) find_stream_by_id); + + if (stream == NULL) { + GST_ERROR_OBJECT (src, "no streams with id %u", id); + GST_OBJECT_UNLOCK (src); + return FALSE; + } + + if (stream->profile != GST_RTSP_PROFILE_SAVP && + stream->profile != GST_RTSP_PROFILE_SAVPF) { + GST_OBJECT_UNLOCK (src); + GST_WARNING_OBJECT (src, "stream with id %u, is not encrypted", id); + return FALSE; + } + + g_signal_emit_by_name (stream->srtpdec, "remove-key", stream->ssrc, NULL); + if (stream->mikey) { + gst_mikey_message_unref (stream->mikey); + stream->mikey = NULL; + } + + GST_OBJECT_UNLOCK (src); + + return TRUE; +} + static gboolean gst_rtspsrc_parse_range (GstRTSPSrc * src, const gchar * range, GstSegment * segment, gboolean update_duration) @@ -8336,6 +8756,99 @@ gst_rtspsrc_open_from_sdp (GstRTSPSrc * src, GstSDPMessage * sdp, } } + if (src->client_managed_mikey) { + guint len = gst_sdp_message_attributes_len (sdp); + + GST_DEBUG_OBJECT (src, "client-managed-mikey mode enabled"); + + /* Remove key-mgmt:mikey attributes from incoming sdp. + * The key parameters will be requested by srtpdec when needed. + */ + + /* RFC 4567 ยง 3.1: + * + * > The attribute MAY be used at session level, media level, or at both + * > levels. An attribute defined at media level overrides an attribute + * > defined at session level. + * + * See: https://www.rfc-editor.org/rfc/rfc4567.html#section-3.1 + */ + for (i = 0; i < len; i++) { + const GstSDPAttribute *attr = gst_sdp_message_get_attribute (sdp, i); + + if (g_str_equal (attr->key, "key-mgmt")) { + if (g_str_has_prefix (attr->value, "mikey")) { + GST_DEBUG_OBJECT (src, + "client-managed-mikey mode enabled, " + "removing session level key-mgmt:mikey"); + gst_sdp_message_remove_attribute (sdp, i); + } else { + GST_DEBUG_OBJECT (src, "Keeping session level key-mgmt:%s", + attr->value); + } + + /* Not expecting more than one key-mgmt attribute at the session level + * aternatively, we could keep iterating and error out if several are + * found */ + break; + } + } + + len = gst_sdp_message_medias_len (sdp); + for (i = 0; i < len; i++) { + guint32 ssrc = 0; + guint j, jlen, mikey_id = 0; + gboolean mikey_found, other_keymgmt_found, ssrc_found = FALSE; + + const GstSDPMedia *media = gst_sdp_message_get_media (sdp, i); + + jlen = gst_sdp_media_attributes_len (media); + for (j = 0; j < jlen; j++) { + const GstSDPAttribute *attr = gst_sdp_media_get_attribute (media, j); + + if (g_str_equal (attr->key, "key-mgmt")) { + /* Not expecting more than one key-mgmt attribute at the media level + * aternatively, we could keep iterating and error out if several are + * found */ + + if (g_str_has_prefix (attr->value, "mikey")) { + mikey_id = j; + mikey_found = TRUE; + } else { + other_keymgmt_found = TRUE; + } + + if (ssrc_found) + break; + } else if (g_str_equal (attr->key, "ssrc")) { + if (!sscanf (attr->value, "%u", &ssrc)) { + GST_ERROR_OBJECT (src, "Unexpected ssrc value: %s", attr->value); + res = GST_RTSP_ERROR; + goto setup_failed; + } + + ssrc_found = TRUE; + + if (mikey_found) + break; + } + } + + if (ssrc_found && mikey_found) { + GST_DEBUG_OBJECT (src, + "replacing media level key-mgmt:mikey for ssrc: %" + G_GUINT32_FORMAT, ssrc); + + gst_sdp_media_remove_attribute ((GstSDPMedia *) media, mikey_id); + } else if (ssrc_found && other_keymgmt_found) { + GST_INFO_OBJECT (src, + "Found non-MIKEY media level key-mgmt for ssrc: %" + G_GUINT32_FORMAT ", client-managed-mikey mode will not be used", + ssrc); + } + } + } + /* create streams */ n_streams = gst_sdp_message_medias_len (sdp); for (i = 0; i < n_streams; i++) { diff --git a/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.h b/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.h index 0d3e318225..93b53c58c7 100644 --- a/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.h +++ b/subprojects/gst-plugins-good/gst/rtsp/gstrtspsrc.h @@ -282,6 +282,7 @@ struct _GstRTSPSrc { GstStructure *prop_extra_http_request_headers; gboolean tcp_timestamp; gboolean force_non_compliant_url; + gboolean client_managed_mikey; /* state */ GstRTSPState state; @@ -340,6 +341,8 @@ struct _GstRTSPSrcClass { gboolean (*get_parameter) (GstRTSPSrc *rtsp, const gchar *parameter, const gchar *content_type, GstPromise *promise); gboolean (*get_parameters) (GstRTSPSrc *rtsp, gchar **parameters, const gchar *content_type, GstPromise *promise); gboolean (*set_parameter) (GstRTSPSrc *rtsp, const gchar *name, const gchar *value, const gchar *content_type, GstPromise *promise); + gboolean (*set_mikey_parameter) (GstRTSPSrc *rtsp, guint id, GstCaps *mikey, GstPromise *promise); + gboolean (*remove_key) (GstRTSPSrc *rtsp, guint id); GstFlowReturn (*push_backchannel_buffer) (GstRTSPSrc *src, guint id, GstSample *sample); GstFlowReturn (*push_backchannel_sample) (GstRTSPSrc *src, guint id, GstSample *sample); }; diff --git a/subprojects/gst-plugins-good/tests/examples/rtsp/meson.build b/subprojects/gst-plugins-good/tests/examples/rtsp/meson.build index 747d690366..32dadba4bc 100644 --- a/subprojects/gst-plugins-good/tests/examples/rtsp/meson.build +++ b/subprojects/gst-plugins-good/tests/examples/rtsp/meson.build @@ -1,5 +1,12 @@ -executable('test-onvif', 'test-onvif.c', - dependencies: [gst_dep], - c_args : gst_plugins_good_args, - include_directories : [configinc], - install: false) +examples = [ + 'test-client-managed-mikey', + 'test-onvif', +] + +foreach example : examples + executable(example, '@0@.c'.format(example), + dependencies: [gst_dep], + c_args : gst_plugins_good_args, + include_directories : [configinc], + install: false) +endforeach diff --git a/subprojects/gst-plugins-good/tests/examples/rtsp/test-client-managed-mikey.c b/subprojects/gst-plugins-good/tests/examples/rtsp/test-client-managed-mikey.c new file mode 100644 index 0000000000..1214951df6 --- /dev/null +++ b/subprojects/gst-plugins-good/tests/examples/rtsp/test-client-managed-mikey.c @@ -0,0 +1,773 @@ +#include +#include + +GST_DEBUG_CATEGORY_STATIC (srtp_client_debug); +#define GST_CAT_DEFAULT srtp_client_debug + +#define ERROR_STR_NULL(err) \ + ((err) != NULL ? ((err)->message != NULL ? (err->message) : "(NULL)") \ + : "(NULL)") + +#define MAKE_AND_ADD(var, pipe, name, out_label, elem_name) \ + G_STMT_START { \ + if (G_UNLIKELY(!(var = (gst_element_factory_make(name, elem_name))))) { \ + GST_ERROR("Could not create element %s", name); \ + goto out_label; \ + } \ + if (G_UNLIKELY(!gst_bin_add(GST_BIN_CAST(pipe), var))) { \ + GST_ERROR("Could not add element %s", name); \ + goto out_label; \ + } \ + } \ + G_STMT_END + +static GMainLoop *loop = NULL; +static GstElement *pipeline = NULL; +static GstElement *rtspsrc = NULL; +static GList *streams = NULL; + +typedef struct +{ + GMutex lock; + guint key_size; + guint32 mki; + GstCaps *key_caps; + GstCaps *rekey_caps; +} KeyParam; + +static KeyParam * +key_param_new (guint key_size, guint32 mki) +{ + KeyParam *key_param = NULL; + guint8 *data, *mki_data; + guint data_size = GST_ROUND_UP_4 (key_size); + GstBuffer *srtp_key, *mki_buf; + guint i; + + key_param = g_malloc0 (sizeof (KeyParam)); + + g_mutex_init (&key_param->lock); + + key_param->key_size = data_size; + key_param->mki = mki; + + data = g_malloc (data_size); + for (i = 0; i < data_size; i += 4) { + GST_WRITE_UINT32_BE (data + i, g_random_int ()); + } + srtp_key = gst_buffer_new_wrapped (data, key_size); + + mki_data = g_malloc (sizeof (guint32)); + GST_WRITE_UINT32_BE (mki_data, mki); + mki_buf = gst_buffer_new_wrapped (mki_data, sizeof (guint32)); + + /* parameters for MIKEY SETUP and srtpdec */ + key_param->key_caps = + gst_caps_new_static_str_simple ("application/x-srtp", "srtp-key", + GST_TYPE_BUFFER, srtp_key, "srtp-cipher", G_TYPE_STRING, "aes-128-icm", + "srtp-auth", G_TYPE_STRING, "hmac-sha1-80", "mki", GST_TYPE_BUFFER, + mki_buf, "srtcp-cipher", G_TYPE_STRING, "aes-128-icm", "srtcp-auth", + G_TYPE_STRING, "hmac-sha1-80", NULL); + + /* parameters for re-keying */ + key_param->rekey_caps = + gst_caps_new_static_str_simple ("application/x-srtp", "srtp-key", + GST_TYPE_BUFFER, srtp_key, "mki", GST_TYPE_BUFFER, mki_buf, NULL); + + gst_buffer_unref (mki_buf); + gst_buffer_unref (srtp_key); + + return key_param; +} + +static void +key_param_free (KeyParam * key_param) +{ + g_mutex_lock (&key_param->lock); + + gst_caps_unref (key_param->rekey_caps); + gst_caps_unref (key_param->key_caps); + + g_mutex_unlock (&key_param->lock); + + g_free (key_param); +} + +static GstCaps * +key_param_get_srtp_param (KeyParam * key_param) +{ + GstCaps *caps; + + g_mutex_lock (&key_param->lock); + caps = gst_caps_ref (key_param->key_caps); + g_mutex_unlock (&key_param->lock); + + return caps; +} + +static GstCaps * +key_param_get_rekey_mikey (KeyParam * key_param) +{ + GstCaps *caps; + + g_mutex_lock (&key_param->lock); + caps = gst_caps_ref (key_param->rekey_caps); + g_mutex_unlock (&key_param->lock); + + return caps; +} + +static void +key_param_inc_mki (KeyParam * key_param) +{ + GstBuffer *mki_buf; + guint8 *mki_data = g_malloc (sizeof (guint32)); + + g_mutex_lock (&key_param->lock); + + key_param->mki += 1; + GST_INFO ("Incrementing mki to: %u", key_param->mki); + + GST_WRITE_UINT32_BE (mki_data, key_param->mki); + mki_buf = gst_buffer_new_wrapped (mki_data, sizeof (guint32)); + + key_param->key_caps = gst_caps_make_writable (key_param->key_caps); + gst_caps_set_simple_static_str (key_param->key_caps, "mki", GST_TYPE_BUFFER, + mki_buf, NULL); + + key_param->rekey_caps = gst_caps_make_writable (key_param->rekey_caps); + gst_caps_set_simple_static_str (key_param->rekey_caps, "mki", GST_TYPE_BUFFER, + mki_buf, NULL); + + g_mutex_unlock (&key_param->lock); + + gst_buffer_unref (mki_buf); +} + +/* Called when a key is required: + * + * * When configuring srtpenc for RTCP. + * * When preparing the KeyMgmt parameter for the SETUP request. + * * When srtpdec needs a key to decrypt an incoming packet. + * * After 'remove-key', which we call when re-keying. + */ +static GstCaps * +request_key (G_GNUC_UNUSED GstElement * src, + G_GNUC_UNUSED guint stream, gpointer user_data) +{ + GstCaps *caps; + KeyParam *key_param = (KeyParam *) user_data; + + caps = key_param_get_srtp_param (key_param); + GST_DEBUG ("Got key: %" GST_PTR_FORMAT, caps); + + return caps; +} + +typedef struct +{ + KeyParam *key_param; + GList *streams_to_rekey; +} RekeyData; + +static RekeyData * +rekey_data_new (KeyParam * key_param, GList * streams_to_renew) +{ + RekeyData *this = g_malloc0 (sizeof (RekeyData)); + this->key_param = key_param; + this->streams_to_rekey = streams_to_renew; + + return this; +} + +static void +rekey_data_free (RekeyData * data) +{ + g_list_free (data->streams_to_rekey); + /* key_param lifetime is handled elsewhere */ + + g_free (data); +} + +static void on_rekey_reply (GstPromise * promise, gpointer user_data); + +static gboolean +rekey_next_stream (gpointer user_data) +{ + RekeyData *data = (RekeyData *) user_data; + GList *first; + guint stream_id; + GstCaps *mikey; + GstPromise *promise; + gboolean res; + + first = g_list_first (data->streams_to_rekey); + if (!first) { + GST_DEBUG ("No more streams to re-key"); + rekey_data_free (data); + goto out; + } + + stream_id = GPOINTER_TO_UINT (first->data); + GST_INFO ("Re-keying stream with id %u", stream_id); + + promise = gst_promise_new_with_change_func (on_rekey_reply, data, NULL); + + mikey = key_param_get_rekey_mikey (data->key_param); + g_signal_emit_by_name (rtspsrc, "set-mikey-parameter", stream_id, mikey, + promise, &res); + + if (!res) { + GST_ERROR ("Failed to emit set-mikey-parameter for stream with id %u", + stream_id); + rekey_data_free (data); + gst_promise_unref (promise); + } + +out: + /* next stream will be processed when the promise is complete */ + return G_SOURCE_REMOVE; +} + +static void +on_rekey_reply (GstPromise * promise, gpointer user_data) +{ + RekeyData *data = (RekeyData *) user_data; + GList *first; + guint stream_id; + const GstStructure *reply; + gint result, code; + gboolean res; + + first = g_list_first (data->streams_to_rekey); + if (!first) { + GST_WARNING ("on_rekey_reply called but there are no more streams"); + goto unrecoverable_err; + } + stream_id = GPOINTER_TO_UINT (first->data); + + if (gst_promise_wait (promise) != GST_PROMISE_RESULT_REPLIED) { + GST_WARNING ("set-mikey-parameter interrupted or expired"); + /* will try again */ + goto next; + } + + /* First stream was either processed or there was an unrecoverable error */ + data->streams_to_rekey = + g_list_remove (data->streams_to_rekey, GUINT_TO_POINTER (stream_id)); + + reply = gst_promise_get_reply (promise); + GST_DEBUG ("renew-mikey replied %" GST_PTR_FORMAT, reply); + + if (!gst_structure_get_int (reply, "rtsp-result", &result) || result != 0) { + GST_ERROR ("Failed to send MIKEY parameter to server: %" GST_PTR_FORMAT, + reply); + goto unrecoverable_err; + } + if (!gst_structure_get_int (reply, "rtsp-code", &code) || code != 200) { + GST_ERROR ("Setting MIKEY failed for stream with id %u. Reply from server: " + "%" GST_PTR_FORMAT, stream_id, reply); + goto next; + } + + g_signal_emit_by_name (rtspsrc, "remove-key", stream_id, &res); + if (!res) { + GST_ERROR ("Failed to remove key from client for stream with id %u", + stream_id); + goto next; + } + + GST_DEBUG ("Re-keying complete for stream with id %u", stream_id); +next: + if (data->streams_to_rekey) + g_idle_add (rekey_next_stream, data); + + gst_promise_unref (promise); + return; + +unrecoverable_err: + rekey_data_free (data); + gst_promise_unref (promise); +} + +static gboolean +rekey_all (gpointer user_data) +{ + KeyParam *key_param = (KeyParam *) user_data; + RekeyData *data; + + if (!rtspsrc) { + GST_DEBUG ("Skipping rekey_all because rtspsrc is not ready yet"); + goto out; + } + + key_param_inc_mki (key_param); + + /* rtspsrc can only process one SET_PARAMETER at once. + * We will chain SET_PARAMETER then remove-key for each stream. + */ + data = rekey_data_new (key_param, g_list_copy (streams)); + rekey_next_stream (data); + +out: + return G_SOURCE_CONTINUE; +} + +static void +on_soft_limit (GstElement * rtspsrc, guint stream_id, gpointer user_data) +{ + GST_INFO ("Reached soft-limit for stream with id %u", stream_id); + + /* this is where we should re-new the key + * in this test, we wait for hard-limit though to show both signals. + */ +} + +static void +on_hard_limit (GstElement * rtspsrc, guint stream_id, gpointer user_data) +{ + KeyParam *key_param = (KeyParam *) user_data; + GList *list = NULL; + RekeyData *data; + + GST_INFO ("Reached hard-limit for stream with id %u", stream_id); + + key_param_inc_mki (key_param); + + list = g_list_append (list, GUINT_TO_POINTER (stream_id)); + data = rekey_data_new (key_param, list); + + g_idle_add (rekey_next_stream, data); +} + +static gboolean +setup_h264_pipeline (GstElement * element, GstPad * pad, GstPad ** decode_pad) +{ + gboolean ret = FALSE; + GstObject *parent = NULL; + GstElement *depay = NULL; + GstElement *decode = NULL; + GstPad *sinkpad = NULL; + + parent = gst_object_get_parent (GST_OBJECT (element)); + + MAKE_AND_ADD (depay, parent, "rtph264depay", out, NULL); + MAKE_AND_ADD (decode, parent, "avdec_h264", out, NULL); + + if (!gst_element_link (depay, decode)) { + GST_ERROR ("failed linking h264 elements"); + goto out; + } + + sinkpad = gst_element_get_static_pad (depay, "sink"); + if (gst_pad_link (pad, sinkpad) != GST_PAD_LINK_OK) { + GST_ERROR ("failed linking video depayloader"); + goto out; + } + + (void) gst_element_sync_state_with_parent (decode); + (void) gst_element_sync_state_with_parent (depay); + + *decode_pad = gst_element_get_static_pad (decode, "src"); + + ret = TRUE; + +out: + g_clear_object (&sinkpad); + gst_object_unref (parent); + + return ret; +} + +static gboolean +setup_h265_pipeline (GstElement * element, GstPad * pad, GstPad ** decode_pad) +{ + gboolean ret = FALSE; + GstObject *parent = NULL; + GstElement *depay = NULL; + GstElement *decode = NULL; + GstPad *sinkpad = NULL; + + parent = gst_object_get_parent (GST_OBJECT (element)); + + MAKE_AND_ADD (depay, parent, "rtph265depay", out, NULL); + MAKE_AND_ADD (decode, parent, "avdec_h265", out, NULL); + + if (!gst_element_link (depay, decode)) { + GST_ERROR ("failed linking h265 elements"); + goto out; + } + + sinkpad = gst_element_get_static_pad (depay, "sink"); + if (gst_pad_link (pad, sinkpad) != GST_PAD_LINK_OK) { + GST_ERROR ("failed linking video depayloader"); + goto out; + } + + (void) gst_element_sync_state_with_parent (decode); + (void) gst_element_sync_state_with_parent (depay); + + *decode_pad = gst_element_get_static_pad (decode, "src"); + + ret = TRUE; + +out: + g_clear_object (&sinkpad); + gst_object_unref (parent); + + return ret; +} + +static void +setup_video_sink (GstElement * element, GstPad * pad, GstStructure * st) +{ + GstObject *parent = NULL; + GstElement *scale = NULL; + GstElement *convert = NULL; + GstElement *queue = NULL; + GstElement *sink = NULL; + GstPad *decode_pad = NULL; + GstPad *sinkpad = NULL; + const gchar *encoding; + + encoding = gst_structure_get_string (st, "encoding-name"); + if (g_str_equal (encoding, "H264")) { + if (!setup_h264_pipeline (element, pad, &decode_pad)) { + GST_WARNING ("skipping H264 stream"); + goto out; + } + } else if (g_str_equal (encoding, "H265")) { + if (!setup_h265_pipeline (element, pad, &decode_pad)) { + GST_WARNING ("skipping H265 stream"); + goto out; + } + } else { + /* TODO: add more formats */ + GST_FIXME ("unhandled encoding: %s", encoding); + goto out; + } + + parent = gst_object_get_parent (GST_OBJECT (element)); + + MAKE_AND_ADD (scale, parent, "videoscale", out, NULL); + MAKE_AND_ADD (convert, parent, "videoconvert", out, NULL); + MAKE_AND_ADD (queue, parent, "queue", out, NULL); + g_object_set (queue, "max-size-buffers", 1, "max-size-bytes", 0, + "max-size-time", 0, NULL); + MAKE_AND_ADD (sink, parent, "autovideosink", out, NULL); + + if (!gst_element_link_many (scale, convert, queue, sink, NULL)) { + GST_ERROR ("failed linking video elements"); + goto out; + } + + sinkpad = gst_element_get_static_pad (scale, "sink"); + if (gst_pad_link (decode_pad, sinkpad) != GST_PAD_LINK_OK) { + GST_ERROR ("failed linking video pipeline"); + goto out; + } + + (void) gst_element_sync_state_with_parent (sink); + (void) gst_element_sync_state_with_parent (queue); + (void) gst_element_sync_state_with_parent (convert); + (void) gst_element_sync_state_with_parent (scale); + +out: + g_clear_object (&decode_pad); + g_clear_object (&sinkpad); + gst_object_unref (parent); +} + +static gboolean +setup_aac_pipeline (GstElement * element, GstPad * pad, GstPad ** decode_pad) +{ + gboolean ret = FALSE; + GstObject *parent = NULL; + GstElement *depay = NULL; + GstElement *decode = NULL; + GstPad *sinkpad = NULL; + + parent = gst_object_get_parent (GST_OBJECT (element)); + + MAKE_AND_ADD (depay, parent, "rtpmp4gdepay", out, NULL); + MAKE_AND_ADD (decode, parent, "avdec_aac", out, NULL); + + if (!gst_element_link (depay, decode)) { + GST_ERROR ("failed linking audio elements"); + goto out; + } + + sinkpad = gst_element_get_static_pad (depay, "sink"); + if (gst_pad_link (pad, sinkpad) != GST_PAD_LINK_OK) { + GST_ERROR ("linking sink failed"); + goto out; + } + + (void) gst_element_sync_state_with_parent (decode); + (void) gst_element_sync_state_with_parent (depay); + + *decode_pad = gst_element_get_static_pad (decode, "src"); + + ret = TRUE; + +out: + g_clear_object (&sinkpad); + gst_object_unref (parent); + + return ret; +} + +static void +setup_audio_sink (GstElement * element, GstPad * pad, GstStructure * st) +{ + GstObject *parent = NULL; + GstElement *convert = NULL; + GstElement *queue = NULL; + GstElement *sink = NULL; + GstPad *decode_pad = NULL; + GstPad *sinkpad = NULL; + const gchar *encoding, *mode; + + encoding = gst_structure_get_string (st, "encoding-name"); + mode = gst_structure_get_string (st, "mode"); + if (g_str_equal (encoding, "MPEG4-GENERIC") && g_str_has_prefix (mode, "AAC")) { + if (!setup_aac_pipeline (element, pad, &decode_pad)) { + GST_WARNING ("skipping aac stream"); + goto out; + } + } else { + GST_FIXME ("unhandled: encoding %s / mode: %s", encoding, mode); + goto out; + } + + parent = gst_object_get_parent (GST_OBJECT (element)); + + MAKE_AND_ADD (convert, parent, "audioconvert", out, NULL); + MAKE_AND_ADD (queue, parent, "queue", out, NULL); + g_object_set (queue, "max-size-buffers", 1, "max-size-bytes", 0, + "max-size-time", 0, NULL); + MAKE_AND_ADD (sink, parent, "autoaudiosink", out, NULL); + + if (!gst_element_link_many (convert, queue, sink, NULL)) { + GST_ERROR ("failed linking audio elements"); + goto out; + } + + sinkpad = gst_element_get_static_pad (convert, "sink"); + if (gst_pad_link (decode_pad, sinkpad) != GST_PAD_LINK_OK) { + GST_ERROR ("failed linking audio pipeline"); + goto out; + } + + (void) gst_element_sync_state_with_parent (sink); + (void) gst_element_sync_state_with_parent (queue); + (void) gst_element_sync_state_with_parent (convert); + +out: + g_clear_object (&decode_pad); + g_clear_object (&sinkpad); + gst_object_unref (parent); +} + +static void +pad_added (GstElement * element, GstPad * pad, G_GNUC_UNUSED gpointer user_data) +{ + GstCaps *caps; + GstStructure *st; + const gchar *name; + const gchar *media; + + caps = gst_pad_get_current_caps (pad); + + GST_DEBUG ("new pad %" GST_PTR_FORMAT " with caps %" GST_PTR_FORMAT, pad, + caps); + + st = gst_caps_get_structure (caps, 0); + name = gst_structure_get_name (st); + + if (!g_str_equal (name, "application/x-rtp")) { + GST_ERROR ("caps not understood"); + gst_caps_unref (caps); + return; + } + + media = gst_structure_get_string (st, "media"); + if (media == NULL) { + GST_ERROR ("no media in caps"); + gst_caps_unref (caps); + return; + } + + if (g_str_equal (media, "video")) { + setup_video_sink (element, pad, st); + } else if (g_str_equal (media, "audio")) { + setup_audio_sink (element, pad, st); + } else { + GST_WARNING ("media not understood"); + } + + gst_caps_unref (caps); +} + +static gboolean +select_stream (G_GNUC_UNUSED GstElement * rtspsrc, + guint stream_id, GstCaps * caps, G_GNUC_UNUSED gpointer user_data) +{ + GST_INFO ("Selecting stream with id: %u, %" GST_PTR_FORMAT, stream_id, caps); + streams = g_list_append (streams, GUINT_TO_POINTER (stream_id)); + return TRUE; +} + +static gboolean +build_pipeline (const gchar * location, KeyParam * key_param) +{ + GstElement *src = NULL; + gboolean ret = FALSE; + + GST_DEBUG ("building pipeline for: %s", location); + + pipeline = gst_pipeline_new ("srtp pipeline"); + + MAKE_AND_ADD (src, pipeline, "rtspsrc", out, NULL); + rtspsrc = gst_object_ref (src); + + g_object_set (src, "location", location, "tls-validation-flags", 0x20, + "client-managed-mikey", TRUE, NULL); + + g_signal_connect (src, "pad-added", G_CALLBACK (pad_added), NULL); + + if (key_param == NULL) { + GST_WARNING ("no key available"); + ret = TRUE; + goto out; + } + + g_signal_connect (src, "select-stream", G_CALLBACK (select_stream), NULL); + + g_signal_connect (src, "request-rtp-key", G_CALLBACK (request_key), + key_param); + g_signal_connect (src, "request-rtcp-key", G_CALLBACK (request_key), + key_param); + + g_signal_connect (src, "soft-limit", G_CALLBACK (on_soft_limit), NULL); + g_signal_connect (src, "hard-limit", G_CALLBACK (on_hard_limit), key_param); + + ret = TRUE; + +out: + if (!ret) { + gst_object_unref (pipeline); + pipeline = NULL; + } + + return ret; +} + +static gboolean +bus_message (G_GNUC_UNUSED GstBus * bus, GstMessage * message, + G_GNUC_UNUSED gpointer user_data) +{ + GST_TRACE ("got %" GST_PTR_FORMAT, message); + + switch (GST_MESSAGE_TYPE (message)) { + case GST_MESSAGE_ERROR:{ + GError *err = NULL; + gchar *name, *debug = NULL; + gst_message_parse_error (message, &err, &debug); + name = gst_object_get_path_string (message->src); + GST_ERROR ("ERROR from %s: %s", name, err->message); + if (debug != NULL) { + GST_ERROR ("debug; %s", debug); + } + g_error_free (err); + g_free (debug); + g_free (name); + } + /* fall through */ + case GST_MESSAGE_EOS: + GST_DEBUG ("stopping the main loop"); + g_main_loop_quit (loop); + break; + default: + break; + } + + return TRUE; +} + +gint +main (gint argc, gchar ** argv) +{ + gint res = EXIT_FAILURE; + GstBus *bus = NULL; + KeyParam *key_param = NULL; + gchar *location = NULL; + guint32 key_len, mki, rekey_int = 0; + if (argc != 5) { + g_printerr + ("Usage:\n\ttest-client-managed-mikey KEY_LEN MKI REKEY_INT LOCATION\n" + "\n\tWhere:\n" "\t\tKEY_LEN : len of the key (e.g. 30)\n" + "\t\tMKI : Master Key Index (e.g. 1200)\n" + "\t\tREKEY_INT: re-keying interval in seconds (e.g. 10). 0 to disable\n" + "\t\tLOCATION : rtsps://user:pass@host:port/resource (e.g. port " + "322)\n"); + goto out; + } + if (!sscanf (argv[1], "%u", &key_len)) { + g_printerr ("Expected an integer for KEY_LEN, got: %s", argv[1]); + goto out; + } + if (!sscanf (argv[2], "%u", &mki)) { + g_printerr ("Expected an integer for MKI, got: %s", argv[2]); + goto out; + } + if (!sscanf (argv[3], "%u", &rekey_int)) { + g_printerr ("Expected an integer for REKEY_INT, got: %s", argv[3]); + goto out; + } + location = argv[4]; + gst_init (&argc, &argv); + GST_DEBUG_CATEGORY_INIT (srtp_client_debug, "test-client-managed-mikey", 0, + "test-client-managed-mikey debug"); + loop = g_main_loop_new (NULL, TRUE); + key_param = key_param_new (key_len, mki); + if (!build_pipeline (location, key_param)) { + GST_ERROR ("Pipeline could not be built"); + goto out; + } + + bus = gst_element_get_bus (pipeline); + if (bus == NULL) { + GST_ERROR ("Could not get the pipeline bus"); + goto out; + } + + (void) gst_bus_add_watch (bus, bus_message, NULL); + if (gst_element_set_state (pipeline, GST_STATE_PLAYING) == + GST_STATE_CHANGE_FAILURE) { + GST_ERROR ("Could not set the pipeline in playing state"); + goto out; + } + + if (rekey_int) { + /* Automatically renew MKI */ + g_timeout_add_seconds (rekey_int, rekey_all, key_param); + } else { + GST_INFO ("Not using re-keying interval. Will wait for hard-limit"); + } + + g_main_loop_run (loop); + gst_element_set_state (pipeline, GST_STATE_NULL); + gst_bus_remove_watch (bus); + res = EXIT_SUCCESS; + +out: + g_clear_pointer (&key_param, key_param_free); + g_clear_pointer (&streams, g_list_free); + g_clear_object (&rtspsrc); + g_clear_object (&bus); + g_clear_object (&pipeline); + g_clear_pointer (&loop, g_main_loop_unref); + + return res; +}