From 9b4502cc5d04fc1634d63dfdaa5e864fa4a7e341 Mon Sep 17 00:00:00 2001 From: Vincent Valot Date: Mon, 3 May 2021 19:53:31 +0200 Subject: [PATCH] feat: add serverCertificateUri in DRM advanced config (#3358) Fixes #1906 --- demo/common/asset.js | 16 ++-- demo/common/assets.js | 4 +- demo/main.js | 1 + externs/shaka/manifest.js | 5 ++ externs/shaka/player.js | 5 ++ lib/media/drm_engine.js | 109 +++++++++++++++++++++----- lib/net/networking_engine.js | 1 + lib/util/error.js | 6 ++ lib/util/manifest_parser_utils.js | 1 + lib/util/player_configuration.js | 1 + test/media/drm_engine_unit.js | 69 ++++++++++++++++ test/offline/manifest_convert_unit.js | 1 + test/offline/storage_integration.js | 1 + test/test/util/manifest_generator.js | 2 + 14 files changed, 196 insertions(+), 26 deletions(-) diff --git a/demo/common/asset.js b/demo/common/asset.js index 6f6265d2b3..421091dc20 100644 --- a/demo/common/asset.js +++ b/demo/common/asset.js @@ -358,24 +358,26 @@ const ShakaDemoAssetInfo = class { getConfiguration() { const config = /** @type {shaka.extern.PlayerConfiguration} */( {drm: {advanced: {}}, manifest: {dash: {}}}); + + if (this.extraConfig) { + for (const key in this.extraConfig) { + config[key] = this.extraConfig[key]; + } + } + if (this.licenseServers.size) { - config.drm.servers = {}; + config.drm.servers = config.drm.servers || {}; this.licenseServers.forEach((value, key) => { config.drm.servers[key] = value; }); } if (this.clearKeys.size) { - config.drm.clearKeys = {}; + config.drm.clearKeys = config.drm.clearKeys || {}; this.clearKeys.forEach((value, key) => { config.drm.clearKeys[key] = value; }); } - if (this.extraConfig) { - for (const key in this.extraConfig) { - config[key] = this.extraConfig[key]; - } - } return config; } diff --git a/demo/common/assets.js b/demo/common/assets.js index 2e277be115..4872b6fc8f 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -370,7 +370,9 @@ shakaAssets.testAssets = [ .addFeature(shakaAssets.Feature.SUBTITLES) .addFeature(shakaAssets.Feature.WEBM) .addFeature(shakaAssets.Feature.OFFLINE) - .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth') + .setExtraConfig({drm: {advanced: {'com.widevine.alpha': {serverCertificateUri: + 'https://storage.googleapis.com/wvmedia/cert/cert_license_widevine_com_uat.bin'}}}}), new ShakaDemoAssetInfo( /* name= */ 'Sintel 4k (multicodec, Widevine, ads)', /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', diff --git a/demo/main.js b/demo/main.js index 1bca9194cf..e23cd6cb37 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1728,6 +1728,7 @@ shakaDemo.Main = class { audioRobustness: '', sessionType: '', serverCertificate: new Uint8Array(0), + serverCertificateUri: '', individualizationServer: '', }; } diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 13be37baba..92e1e5bbe4 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -111,6 +111,7 @@ shaka.extern.InitDataOverride; * audioRobustness: string, * videoRobustness: string, * serverCertificate: Uint8Array, + * serverCertificateUri: string, * sessionType: string, * initData: Array., * keyIds: Set. @@ -150,6 +151,10 @@ shaka.extern.InitDataOverride; * A key-system-specific server certificate used to encrypt license requests. * Its use is optional and is meant as an optimization to avoid a round-trip * to request a certificate. + * @property {string} serverCertificateUri + * Defaults to '', e.g., server certificate will be requested from the + * given URI if serverCertificate is not provided. Can be filled in by + * advanced DRM config. * @property {Array.} initData * Defaults to [], e.g., no override.
* A list of initialization data which override any initialization data found diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 1371753c65..0be58f147a 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -516,6 +516,7 @@ shaka.extern.EmsgInfo; * videoRobustness: string, * audioRobustness: string, * serverCertificate: Uint8Array, + * serverCertificateUri: string, * individualizationServer: string, * sessionType: string * }} @@ -545,6 +546,10 @@ shaka.extern.EmsgInfo; * A key-system-specific server certificate used to encrypt license requests. * Its use is optional and is meant as an optimization to avoid a round-trip * to request a certificate. + * @property {string} serverCertificateUri + * Defaults to ''.
+ * If given, will make a request to the given URI to get the server + * certificate. This is ignored if serverCertificate is set. * @property {string} individualizationServer * The server that handles an 'individualiation-request'. If the * server isn't given, it will default to the license server. diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index e2cc2f32a2..7f3424f708 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -274,6 +274,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', // Not required by queryMediaKeys_ videoRobustness: '', // Same serverCertificate: serverCertificate, + serverCertificateUri: '', initData: null, keyIds: null, }]; @@ -452,25 +453,62 @@ shaka.media.DrmEngine = class { goog.asserts.assert(this.initialized_, 'Must call init() before setServerCertificate'); - if (this.mediaKeys_ && - this.currentDrmInfo_ && - this.currentDrmInfo_.serverCertificate && - this.currentDrmInfo_.serverCertificate.length) { + if (!this.mediaKeys_ || !this.currentDrmInfo_) { + return; + } + + if (this.currentDrmInfo_.serverCertificateUri && + (!this.currentDrmInfo_.serverCertificate || + !this.currentDrmInfo_.serverCertificate.length)) { + const request = shaka.net.NetworkingEngine.makeRequest( + [this.currentDrmInfo_.serverCertificateUri], + this.config_.retryParameters); + try { - const supported = await this.mediaKeys_.setServerCertificate( - this.currentDrmInfo_.serverCertificate); - if (!supported) { - shaka.log.warning('Server certificates are not supported by the ' + - 'key system. The server certificate has been ' + - 'ignored.'); - } - } catch (exception) { + const operation = this.playerInterface_.netEngine.request( + shaka.net.NetworkingEngine.RequestType.SERVER_CERTIFICATE, + request); + const response = await operation.promise; + + this.currentDrmInfo_.serverCertificate = + shaka.util.BufferUtils.toUint8(response.data); + } catch (error) { + // Request failed! + goog.asserts.assert(error instanceof shaka.util.Error, + 'Wrong NetworkingEngine error type!'); + throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, - shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE, - exception.message); + shaka.util.Error.Code.SERVER_CERTIFICATE_REQUEST_FAILED, + error); + } + + if (this.destroyer_.destroyed()) { + return; + } + } + + if (!this.currentDrmInfo_.serverCertificate || + !this.currentDrmInfo_.serverCertificate.length) { + return; + } + + try { + const supported = await this.mediaKeys_.setServerCertificate( + this.currentDrmInfo_.serverCertificate); + + if (!supported) { + shaka.log.warning('Server certificates are not supported by the ' + + 'key system. The server certificate has been ' + + 'ignored.'); } + } catch (exception) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE, + exception.message); } } @@ -1118,6 +1156,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + serverCertificateUri: '', sessionType: '', initData: initDatas, keyIds: new Set(keyIds), @@ -1893,6 +1932,8 @@ shaka.media.DrmEngine = class { videoRobustness: drm1.videoRobustness || drm2.videoRobustness, audioRobustness: drm1.audioRobustness || drm2.audioRobustness, serverCertificate: drm1.serverCertificate || drm2.serverCertificate, + serverCertificateUri: drm1.serverCertificateUri || + drm2.serverCertificateUri, initData, keyIds, }; @@ -1967,6 +2008,7 @@ shaka.media.DrmEngine = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + serverCertificateUri: '', initData: [], keyIds: new Set(), }); @@ -1997,6 +2039,9 @@ shaka.media.DrmEngine = class { /** @type {!Array.} */ const licenseServers = []; + /** @type {!Array.} */ + const serverCertificateUris = []; + /** @type {!Array.} */ const serverCerts = []; @@ -2006,8 +2051,9 @@ shaka.media.DrmEngine = class { /** @type {!Set.} */ const keyIds = new Set(); - shaka.media.DrmEngine.processDrmInfos_(drmInfos, licenseServers, - serverCerts, initDatas, keyIds); + shaka.media.DrmEngine.processDrmInfos_( + drmInfos, licenseServers, serverCerts, + serverCertificateUris, initDatas, keyIds); if (serverCerts.length > 1) { shaka.log.warning('Multiple unique server certificates found! ' + @@ -2019,6 +2065,11 @@ shaka.media.DrmEngine = class { 'Only the first will be used.'); } + if (serverCertificateUris.length > 1) { + shaka.log.warning('Multiple unique server certificate URIs found! ' + + 'Only the first will be used.'); + } + const defaultSessionType = this.usePersistentLicenses_ ? 'persistent-license' : 'temporary'; @@ -2032,6 +2083,7 @@ shaka.media.DrmEngine = class { audioRobustness: drmInfos[0].audioRobustness || '', videoRobustness: drmInfos[0].videoRobustness || '', serverCertificate: serverCerts[0], + serverCertificateUri: serverCertificateUris[0], initData: initDatas, keyIds, }; @@ -2063,6 +2115,9 @@ shaka.media.DrmEngine = class { /** @type {!Array.} */ const licenseServers = []; + /** @type {!Array.} */ + const serverCertificateUris = []; + /** @type {!Array.} */ const serverCerts = []; @@ -2074,13 +2129,19 @@ shaka.media.DrmEngine = class { // TODO: refactor, don't stick drmInfos onto MediaKeySystemConfiguration shaka.media.DrmEngine.processDrmInfos_( - config['drmInfos'], licenseServers, serverCerts, initDatas, keyIds); + config['drmInfos'], licenseServers, serverCerts, + serverCertificateUris, initDatas, keyIds); if (serverCerts.length > 1) { shaka.log.warning('Multiple unique server certificates found! ' + 'Only the first will be used.'); } + if (serverCertificateUris.length > 1) { + shaka.log.warning('Multiple unique server certificate URIs found! ' + + 'Only the first will be used.'); + } + if (licenseServers.length > 1) { shaka.log.warning('Multiple unique license server URIs found! ' + 'Only the first will be used.'); @@ -2102,6 +2163,7 @@ shaka.media.DrmEngine = class { audioRobustness: audioRobustness || '', videoRobustness: videoRobustness || '', serverCertificate: serverCerts[0], + serverCertificateUri: serverCertificateUris[0], initData: initDatas, keyIds, }; @@ -2114,12 +2176,14 @@ shaka.media.DrmEngine = class { * @param {!Array.} drmInfos * @param {!Array.} licenseServers * @param {!Array.} serverCerts + * @param {!Array.} serverCertificateUris * @param {!Array.} initDatas * @param {!Set.} keyIds * @private */ static processDrmInfos_( - drmInfos, licenseServers, serverCerts, initDatas, keyIds) { + drmInfos, licenseServers, serverCerts, + serverCertificateUris, initDatas, keyIds) { /** @type {function(shaka.extern.InitDataOverride, * shaka.extern.InitDataOverride):boolean} */ const initDataOverrideEqual = (a, b) => { @@ -2138,6 +2202,11 @@ shaka.media.DrmEngine = class { licenseServers.push(drmInfo.licenseServerUri); } + // Build an array of unique license servers. + if (!serverCertificateUris.includes(drmInfo.serverCertificateUri)) { + serverCertificateUris.push(drmInfo.serverCertificateUri); + } + // Build an array of unique server certs. if (drmInfo.serverCertificate) { const found = serverCerts.some( @@ -2250,6 +2319,10 @@ shaka.media.DrmEngine = class { if (advancedConfig.sessionType) { drmInfo.sessionType = advancedConfig.sessionType; } + + if (!drmInfo.serverCertificateUri) { + drmInfo.serverCertificateUri = advancedConfig.serverCertificateUri; + } } // Chromecast has a variant of PlayReady that uses a different key diff --git a/lib/net/networking_engine.js b/lib/net/networking_engine.js index e03cc37484..9da4a8ffb3 100644 --- a/lib/net/networking_engine.js +++ b/lib/net/networking_engine.js @@ -706,6 +706,7 @@ shaka.net.NetworkingEngine.RequestType = { 'LICENSE': 2, 'APP': 3, 'TIMING': 4, + 'SERVER_CERTIFICATE': 5, }; diff --git a/lib/util/error.js b/lib/util/error.js index bedea2fcd5..a5bcfd5c81 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -817,6 +817,12 @@ shaka.util.Error.Code = { */ 'INIT_DATA_TRANSFORM_ERROR': 6016, + /** + * The server certificate request failed. + *
error.data[0] is a shaka.util.Error from the networking engine. + */ + 'SERVER_CERTIFICATE_REQUEST_FAILED': 6017, + /** * The call to Player.load() was interrupted by a call to Player.unload() diff --git a/lib/util/manifest_parser_utils.js b/lib/util/manifest_parser_utils.js index 4ea56282a1..4a7046edf2 100644 --- a/lib/util/manifest_parser_utils.js +++ b/lib/util/manifest_parser_utils.js @@ -55,6 +55,7 @@ shaka.util.ManifestParserUtils = class { audioRobustness: '', videoRobustness: '', serverCertificate: null, + serverCertificateUri: '', sessionType: '', initData: initData || [], keyIds: new Set(), diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 1c202358b0..5cd3ebad2e 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -293,6 +293,7 @@ shaka.util.PlayerConfiguration = class { audioRobustness: '', sessionType: '', serverCertificate: new Uint8Array(0), + serverCertificateUri: '', individualizationServer: '', }, }; diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index 9774af9c5c..8b276d450d 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -697,6 +697,7 @@ function testDrmEngine(useMediaCapabilities) { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: null, + serverCertificateUri: '', sessionType: 'persistent-license', individualizationServer: '', distinctiveIdentifierRequired: true, @@ -775,6 +776,7 @@ function testDrmEngine(useMediaCapabilities) { audioRobustness: 'bad', videoRobustness: 'so_bad_it_hurts', serverCertificate: null, + serverCertificateUri: '', sessionType: '', individualizationServer: '', distinctiveIdentifierRequired: false, @@ -972,16 +974,76 @@ function testDrmEngine(useMediaCapabilities) { it('sets server certificate if present in config', async () => { const cert = new Uint8Array(1); config.advanced['drm.abc'] = createAdvancedConfig(cert); + config.advanced['drm.abc'].serverCertificateUri = 'https://drm-service.com/certificate'; drmEngine.configure(config); const variants = manifest.variants; await drmEngine.initForPlayback(variants, manifest.offlineSessionIds, useMediaCapabilities); + expect(fakeNetEngine.request).not.toHaveBeenCalled(); // Should be set merely after init, without waiting for attach. expect(mockMediaKeys.setServerCertificate).toHaveBeenCalledWith(cert); }); + it('fetches and sets server certificate from uri', async () => { + const cert = new Uint8Array(0); + const serverCertificateUri = 'https://drm-service.com/certificate'; + config.advanced['drm.abc'] = createAdvancedConfig(cert); + config.advanced['drm.abc'].serverCertificateUri = serverCertificateUri; + + fakeNetEngine.setResponseValue( + serverCertificateUri, + shaka.util.BufferUtils.toArrayBuffer(new Uint8Array(1))); + + drmEngine.configure(config); + + const variants = manifest.variants; + await drmEngine.initForPlayback(variants, manifest.offlineSessionIds, + useMediaCapabilities); + + fakeNetEngine.expectRequest( + serverCertificateUri, + shaka.net.NetworkingEngine.RequestType.SERVER_CERTIFICATE); + + // Should be set merely after init, without waiting for attach. + expect(mockMediaKeys.setServerCertificate).toHaveBeenCalledWith( + new Uint8Array(1)); + }); + + it('fetches server certificate from uri and triggers error', async () => { + const cert = new Uint8Array(0); + const serverCertificateUri = 'https://drm-service.com/certificate'; + config.advanced['drm.abc'] = createAdvancedConfig(cert); + config.advanced['drm.abc'].serverCertificateUri = serverCertificateUri; + + // Simulate a permission error from the web server. + const netError = new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.BAD_HTTP_STATUS, + serverCertificateUri, 403); + const operation = shaka.util.AbortableOperation.failed(netError); + fakeNetEngine.request.and.returnValue(operation); + + drmEngine.configure(config); + + const expected = Util.jasmineError(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.DRM, + shaka.util.Error.Code.SERVER_CERTIFICATE_REQUEST_FAILED, + netError)); + + await expectAsync(initAndAttach()).toBeRejectedWith(expected); + + fakeNetEngine.expectRequest( + serverCertificateUri, + shaka.net.NetworkingEngine.RequestType.SERVER_CERTIFICATE); + + // Should be set merely after init, without waiting for attach. + expect(mockMediaKeys.setServerCertificate).not.toHaveBeenCalled(); + }); + it('prefers server certificate from DrmInfo', async () => { const cert1 = new Uint8Array(5); const cert2 = new Uint8Array(1); @@ -2191,6 +2253,7 @@ function testDrmEngine(useMediaCapabilities) { videoRobustness: 'really_really_ridiculously_good', distinctiveIdentifierRequired: true, serverCertificate: null, + serverCertificateUri: '', sessionType: '', individualizationServer: '', persistentStateRequired: true, @@ -2210,6 +2273,7 @@ function testDrmEngine(useMediaCapabilities) { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, + serverCertificateUri: '', sessionType: 'temporary', initData: [], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), @@ -2227,6 +2291,7 @@ function testDrmEngine(useMediaCapabilities) { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: undefined, + serverCertificateUri: '', initData: [], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }; @@ -2247,6 +2312,7 @@ function testDrmEngine(useMediaCapabilities) { persistentStateRequired: true, videoRobustness: 'really_really_ridiculously_good', serverCertificate: serverCert, + serverCertificateUri: '', initData: ['blah'], keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']), }; @@ -2257,6 +2323,7 @@ function testDrmEngine(useMediaCapabilities) { persistentStateRequired: false, audioRobustness: 'good', serverCertificate: undefined, + serverCertificateUri: '', initData: ['init data'], keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']), }; @@ -2268,6 +2335,7 @@ function testDrmEngine(useMediaCapabilities) { audioRobustness: 'good', videoRobustness: 'really_really_ridiculously_good', serverCertificate: serverCert, + serverCertificateUri: '', initData: ['blah', 'init data'], keyIds: new Set([ 'deadbeefdeadbeefdeadbeefdeadbeef', @@ -2575,6 +2643,7 @@ function testDrmEngine(useMediaCapabilities) { distinctiveIdentifierRequired: false, persistentStateRequired: false, serverCertificate: serverCert, + serverCertificateUri: '', individualizationServer: '', sessionType: '', videoRobustness: '', diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 1dd680a9fc..b926889b1f 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -101,6 +101,7 @@ describe('ManifestConverter', () => { audioRobustness: 'very', videoRobustness: 'kinda_sorta', serverCertificate: new Uint8Array([1, 2, 3]), + serverCertificateUri: '', sessionType: '', initData: [{ initData: new Uint8Array([4, 5, 6]), diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index cabde9d3d5..3dcb60acb5 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1598,6 +1598,7 @@ filterDescribe('Storage', storageSupport, () => { keyIds: null, sessionType: 'temporary', serverCertificate: null, + serverCertificateUri: '', audioRobustness: 'HARDY', videoRobustness: 'OTHER', }; diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 029fd84c3f..91fcd7f193 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -407,6 +407,8 @@ shaka.test.ManifestGenerator.DrmInfo = class { this.keyIds = new Set(); /** @type {string} */ this.sessionType = ''; + /** @type {string} */ + this.serverCertificateUri = ''; /** @type {shaka.extern.DrmInfo} */ const foo = this;