Posted by Amir Mazzarella on October 07, 2018
Welcome back to the Widevine series of blog posts. In this post, I'll discuss the structure of Widevine's messages. I touched on them in the previous blog post (Part 2), but I didn't go in depth. So, let's start from the top.
Widevine's messages are serialized using Google's Protocol Buffers. Protocol Buffers are a method of serializing structured data to a string for easy transmission. Let's demo this on Widevine initialization data:
CAESED8BB4cTPEOinHSTAIZ7hRYaBmFtYXpvbiI1Y2lkOk9DRzNKMTRTUzhHV2RYTHVSYnlvWUE9PSxQd0VIaHhNOFE2S2NkSk1BaG51RkZnPT0qAlNEMgA=
Obviously, this looks like some arbitrary base64 decoded bytes. But, they are far from arbitrary. Decode the base64 and write the bytes to a binary file, like so:
>>> import base64
>>> with open('pssh.bin', 'wb') as f:
... f.write(base64.b64decode('CAESED8BB4cTPEOinHSTAIZ7hRYaBmFtYXpvbiI1Y2lkOk9DRzNKMTRTUzhHV2RYTHVSYnlvWUE9PSxQd0VIaHhNOFE2S2NkSk1BaG51RkZnPT0qAlNEMgA='))
Now, we can use Google's Protocol Buffer compiler, also known as protoc
, which you can find here: https://github.com/google/protobuf/releases/latest.
Run this command:
protoc --decode_raw < pssh.bin
And out will print the structured protobuf:
1: 1
2: "?\001\007\207\023<C\242\234t\223\000\206{\205\026"
3: "amazon"
4: "cid:OCG3J14SS8GWdXLuRbyoYA==,PwEHhxM8Q6KcdJMAhnuFFg=="
5: "SD"
6: ""
But wait, what do all these numbers mean? What are those bytes? This is where the "security" of protocol buffers comes in. When we were decoding the serialized protocol buffer, we were using the --decode_raw
flag. This means that we're decoding the protocol buffer without an external .proto
file. The .proto
file acts as a sort of "dictionary" for protoc
, as in, it provides names for those tag indices above. Luckily, Widevine provides the protocol buffer message for initialization data in the Widevine documentation: https://storage.googleapis.com/wvdocs/Widevine_DRM_Encryption_API.pdf.
syntax = "proto2";
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
optional Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
optional string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
optional string policy = 6;
// Crypto period index, for media using key rotation.
optional uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
optional bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
optional uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
optional uint32 crypto_period_seconds = 10;
}
This is what's called a protocol buffer "message." It has tag names for the protocol buffer indices. Save that block to a file ending in .proto
, and now we'll decode the initialization data properly:
protoc --decode=WidevineCencHeader pssh.proto < pssh.bin
algorithm: AESCTR
key_id: "?\001\007\207\023<C\242\234t\223\000\206{\205\026"
provider: "amazon"
content_id: "cid:OCG3J14SS8GWdXLuRbyoYA==,PwEHhxM8Q6KcdJMAhnuFFg=="
track_type_deprecated: "SD"
policy: ""
We can see what the tags are! Now let's move on from initialization data to a fully fledged license request. Here's what the license request from Widevine CDM version 1.4.8.903 looks like with --decode_raw
:
1: 1
2 {
1 {
1: 1
2 {
1 {
1: 2
2: "ChromeCDM-Windows-x86"
3: 1371772510
4: "0\202\001\n\002\202\001\001\000\241\247d\2627\372\036\335\263\006\333D\213\274\247\302 \362\3723\013G\365\304\313\324\313\303\006T\250\205y\312\324\327\320\251\222\332\000\257\202\345\037\336\257E-\301<|A\n]\343\265\344,\320\371\367\205#\306\023\310x\341=-\2455WL^\334\355\n\233\234\356\251\255\371h\246\007\366d\367\226\235\2031\307|<]J\321\312\225\303\002$\373:u\225\313\243\rG\330\n\352\267L\002f\267\266\350Z\251\022\220d\207\001[\303v\331\263\241.\341\203m\\\226\3140\361\006\271L\302|4^\250\356\307\315\236\332\262\314u\276\002U\223\3629\212S\021 f\234\244\014~\351\306\207F\tI\177\026\r[\270B+]\022\274\317\007\315J\265\216\035\340\222\023\230R<;56R\331\212\340\227\tvz3N\004\323IY\006l${y\033\005\254\254@\3328\224\241\235\307C\332\325_\372\217\220\222\312\010\204kK\263\214\377\t\002\003\001\000\001"
5: 4350
}
2: "8\322\246\035\340\005\377\002\260\356\"\273H W\201lr\006\352\246\374^\307\010\303Q\313\326\367J\331\370\204\274\342\036\005\255\240\276\331\'\257I\331\233[\017U\320\301\347\243f\330\376\033\316\304\344g!R\001\355x|t@}\022\311[\252\3766X\024C\360\004\027\235\300:\345h6\271\013b\330\242\240\\\032\331\252\2316B\204Y\032w\243\205\340y/\277\020\014\0339w-\034\375h\331\240\2667|\321\010\\\005\373\316\250\266\361{\266\370\033\264\332JF\375\027t\217\273\001J\310\255\337\007i\006G_4\377\246\265\324s\210\351s\243t\345S\200\032\271s\260I\247Rqq\003ND\000\276\221\224\232\215\216\272+\266}a\3706\340\355\201N7;F\201xhd\202M\312^\0254r(\3133\010o4i\225\354\344\246\032\200\3447f\345\322\224\321\r\013\326\222wt\326\250\255g<\273\201\363\306ll*\355p"
3 {
1 {
1: 1
2: "\200\326\212\322\357\344\341X\220=E\373\313.\212\207"
3: 1371771566
4: "0\202\001\n\002\202\001\001\000\353\331\007-\344\rv\016;\251N+O\n]\234{h\243\236<\375\261\200\330\2034\330\341\350\355\256\236\226\014\274Sa<\030?\226\236\220Q\022U\341i\031\010\221\330\031\206\014\000\340q\267\255i\336\214\371\304s\317j\357\334\207\034M&\306ii\334\025\007\367\231\022\347\321\3354\372\360\022\016\301\275\030\343XGK\311\210\345\251\230\n\341\240\273\363\331\302+\017\030\305^$\273\323\342{\035\344\316\031t\235s\013\357\225\272\023\346i\245G\203\036\266\010\0255\001\306p\235\271\013\312\036\001\305\356\263\330\037\233\247\344.\230\257n(\2634\270\332E\r\034>^\023X\020\250\240pn\272k\214\321\274\272\315g\261\354\245>\374+\375\022\300`/-\365\0242\367\006\336\301\344\351,\001\267~%~\216\344\270[_>\274\"(\367\221\363V<;P\037{f\250x\245\275\234\350N\342Fi\313\215yf\200)\020\0134\312\347\002\003\001\000\001"
5: 4350
}
2: "\000\262\240o\242\367\247\263\252\241\356\276\265{\357\033\256\370\317W3\017\306\366\211n\337h\242F\362`/\026A|-\374\270\221\315\307\340\241\324\253sBlN\270\242\021R\210\236f\372\342\265\310\030\274\316\324\023\377\277\356\270\253\236S\337\371t\250\371b\t\013y\255\023\020z\342\333d6\356\331W\005\302\373o8\253$\300\t\034S\304\243^\361\343\027\346h1\375\274C\215\312W#\230\177\315\017\001\257\016\030_xv\235\036\241k\241\026\241>I\300\004\n%\254<\007\037\353\226\025G\2222\026o@\017k/\000F\033\212\266\331N\225\276h\201)\231\225B\261\222R\221\263k\257\331Rg\027X1\255\031\311[\252\314\016\257\374\n\275\262\267\355R\326E-j\\+\350\212D\306\326|\370J\240\221\034\371G\366\275)\014u\3519\340B\325\251m\016\321\301\244\024W\373(\275\n\222=\215x\206\016>OdUjL\262\226\362\217\266?\310\226a\035V\327<\211\354\375\244\242\031\032\236Kx\223\020\017\252\367T\342\327\253p\n\244]\225\374\374\206e\345\031\276T\271\037\"cn\251R]\367\352\250\270\342\314\030}\267O\007\256\031\313\320\314~(w\362,\342\330KqZo\274\207@\343\206te\027\260\247\200\362\350\316\324\254U\035\017\272\"\200\327\341\2726\376\254`%\r3\353#4:#\0051\032&+\375\004*|\316\t"
}
}
3 {
1: "architecture_name"
2: "x86-32"
}
3 {
1: "company_name"
2: "Google"
}
3 {
1: "model_name"
2: "ChromeCDM"
}
3 {
1: "platform_name"
2: "Windows"
}
3 {
1: "widevine_cdm_version"
2 {
6: 0x3330392e382e342e
}
}
6 {
1: 0
2: 0
3: 1
4: 1
}
}
2 {
1 {
1 {
1: 1
2: "\353gj\273\3134^\226\273\317af0\361\243\332"
3: "widevine_test"
4: "fkj3ljaSdfalkr3j"
5 {
9: 68
}
6: ""
}
2: 1
3: "\262\251\221\340K\332\026q\301\036|uVy8\363"
}
}
3: 1
4: 1531095027
6: 20
}
3: "-C$b\301\365s\227\003\203\247\213\354\363TJ\217>\033\351\324cS\252\240\266\032|\254\314X\301\357\n\367\262\200\324zqP1\255\022\277\267\233\370r~EDWx\266\270\246\202I\240`9\314\n\343 \244\210L\202i\371EwMk\347\373\\\266\0062\327\n\033\213\004`\315\235l!\024q6=F,\2630\236]\314\004\206\310}\364\304\356\311*\354\204\275\307\245\000\351\213\353\031\340\353T\333\304\303#\257\247\340\212\231J0 \325:\266\355\2076s@hz\343\323\001\242\253\236P\273\215\317\353r~\326\223K\025qD\2616\345c\354\371\006#\3722\245\306P\305TMZ\211\217\372\330\274mK\263\256\263l\235\333,\300\t1\336py\270jDT[0\352\204*\347\"\220\311\205\364\357k\244\230W\201\361\212\250\357\225\221\311\244\364\252*\305\325\223\277\'\035,\242\242\253[\272\351\261:\241/\022\337f\212"
Just as a note, this license request is not generated from the same initialization data that we deserialized earlier. The initialization data it is built from is:
algorithm: AESCTR
key_id: "\353gj\273\3134^\226\273\317af0\361\243\332"
provider: "widevine_test"
content_id: "fkj3ljaSdfalkr3j"
track_type_deprecated: "HD"
policy: ""
You can see this initialization data in the license request. So, how do we get the tag names for a license request? Or even a license? Well, it's not so easy. Google keeps the .proto
a secret for a reason. However, there are protobuf symbols left over in the Widevine Android library, specifically libwvdrmengine.so
. You could reverse the tag names and tag indices from that, or you could do something a lot easier. You could get the .proto
yourself. Yup, it's lying on the Internet, and I'll detail how I found it.
Disassembling the Windows 1.4.8.903 Widevine CDM yields some exciting strings:
Specifically, license_protocol.pb.cc
. That's a compiled C++ .proto
file. Let's try searching for it: https://www.google.com/search?q="license_protocol"+"widevine"
Using quotes in a Google search only makes exact results appear, which is what we want. The first result is a GitHub repo for Chromecast Widevine tools: https://github.com/EiNSTeiN-/chromecast-widevine-tools. Reading through the README, you'll find some intriguing text. Most notably, "The data contained in the base64 blob is a SignedProvisioningMessage, the format of which is defined in the license_protocol protocol definition file at chromium/src/out_arm_eureka/Release/pyproto/license_protocol/ in the chromium source tree."
Bingo! It looks like it's in Chromium source code. If you read through the Makefile, it downloads it. This is one of the URLs it clones: https://code.google.com/p/chromecast-mirrored-source.prebuilt, and that links to this Google Drive: https://drive.google.com/folderview?id=0B3j4zj2IQp7MZkplRzRvcERtaU0&usp=sharing, which has a myriad of Chromecast versions. Let's download the latest Chromecast source code, which at the time of this post is 1.33. Navigate to 1.33/Chromium, and you'll find two gzipped tarballs. I'm not sure what the difference is between anchovy and salami, but I downloaded anchovy. Untar the archive and do a recursive search for license_protocol
and you'll find a license_protocol_pb2.py
and a license_protocol.pb.cc
. These are both compiled .proto
s for Python and C++ respectively. I'm going to use the Python one as I'm more comfortable with the language.
So, how do we decompile them back to .proto
files? Luckily, the C++ protobuf library has a method to decompile a serialize file_descriptor
protocol buffer back to a .proto
: desc->DebugString();
. Using that, I wrote a C++ program to decode a file_descriptor
protobuf back to a .proto:
#include <fstream>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <google/protobuf/text_format.h>
#include <iostream>
int main(int argc, char* argv[]) {
google::protobuf::FileDescriptorProto fileProto;
std::fstream input(argv[1], std::ios::in | std::ios::binary);
if (!input) {
std::cerr << argv[1] << ": File not found." << std::endl;
} else if (!fileProto.ParseFromIstream(&input)) {
std::cerr << argv[1] << ": Failed to parse file." << std::endl;
return -1;
}
google::protobuf::DescriptorPool pool;
const google::protobuf::FileDescriptor* desc = pool.BuildFile(fileProto);
std::fstream output(argv[2], std::ios::out);
if (!output) {
std::cerr << argv[2] << ": Unable to create output file." << std::endl;
}
std::string proto = desc->DebugString();
proto.pop_back();
output << proto;
output.close();
return 0;
}
You can get the file_descriptor
proto by importing license_protocol_pb2.py
within a Python shell and writing the contents of the variable license_protocol_pb2.DESCRIPTOR.serialized_pb
to a file.
Before we start decoding, let's define a new type in our new license_protocol.proto
:
message SignedLicenseRequest {
optional .video_widevine.SignedMessage.MessageType type = 1;
optional .video_widevine.LicenseRequest msg = 2;
optional bytes signature = 3;
optional bytes session_key = 4;
optional .video_widevine.RemoteAttestation remote_attestation = 5;
repeated .video_widevine.MetricData metric_data = 6;
}
We define this type because according to the Widevine library, a license request is of the message SignedMessage
, but that message has the main license request defined as bytes
:
message SignedMessage {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional .video_widevine.SignedMessage.MessageType type = 1;
optional bytes msg = 2;
optional bytes signature = 3;
optional bytes session_key = 4;
optional .video_widevine.RemoteAttestation remote_attestation = 5;
repeated .video_widevine.MetricData metric_data = 6;
}
So, we define a new SignedLicenseRequest
message that treats the msg
as an actual LicenseRequest
message instead of a blob. After saving that file, we can decode it the same way we did the initialization data:
protoc --decode=video_widevine.SignedLicenseRequest license_protocol.proto < license_request.bin
type: LICENSE_REQUEST
msg {
client_id {
type: DRM_DEVICE_CERTIFICATE
token: "\n\263\002\010\002\022\025ChromeCDM-Windows-x86\030\336\254\216\216\005\"\216\0020\202\001\n\002\202\001\001\000\241\247d\2627\372\036\335\263\006\333D\213\274\247\302 \362\3723\013G\365\304\313\324\313\303\006T\250\205y\312\324\327\320\251\222\332\000\257\202\345\037\336\257E-\301<|A\n]\343\265\344,\320\371\367\205#\306\023\310x\341=-\2455WL^\334\355\n\233\234\356\251\255\371h\246\007\366d\367\226\235\2031\307|<]J\321\312\225\303\002$\373:u\225\313\243\rG\330\n\352\267L\002f\267\266\350Z\251\022\220d\207\001[\303v\331\263\241.\341\203m\\\226\3140\361\006\271L\302|4^\250\356\307\315\236\332\262\314u\276\002U\223\3629\212S\021 f\234\244\014~\351\306\207F\tI\177\026\r[\270B+]\022\274\317\007\315J\265\216\035\340\222\023\230R<;56R\331\212\340\227\tvz3N\004\323IY\006l${y\033\005\254\254@\3328\224\241\235\307C\332\325_\372\217\220\222\312\010\204kK\263\214\377\t\002\003\001\000\001(\376!\022\200\0028\322\246\035\340\005\377\002\260\356\"\273H W\201lr\006\352\246\374^\307\010\303Q\313\326\367J\331\370\204\274\342\036\005\255\240\276\331\'\257I\331\233[\017U\320\301\347\243f\330\376\033\316\304\344g!R\001\355x|t@}\022\311[\252\3766X\024C\360\004\027\235\300:\345h6\271\013b\330\242\240\\\032\331\252\2316B\204Y\032w\243\205\340y/\277\020\014\0339w-\034\375h\331\240\2667|\321\010\\\005\373\316\250\266\361{\266\370\033\264\332JF\375\027t\217\273\001J\310\255\337\007i\006G_4\377\246\265\324s\210\351s\243t\345S\200\032\271s\260I\247Rqq\003ND\000\276\221\224\232\215\216\272+\266}a\3706\340\355\201N7;F\201xhd\202M\312^\0254r(\3133\010o4i\225\354\344\246\032\200\3447f\345\322\224\321\r\013\326\222wt\326\250\255g<\273\201\363\306ll*\355p\032\264\005\n\256\002\010\001\022\020\200\326\212\322\357\344\341X\220=E\373\313.\212\207\030\256\245\216\216\005\"\216\0020\202\001\n\002\202\001\001\000\353\331\007-\344\rv\016;\251N+O\n]\234{h\243\236<\375\261\200\330\2034\330\341\350\355\256\236\226\014\274Sa<\030?\226\236\220Q\022U\341i\031\010\221\330\031\206\014\000\340q\267\255i\336\214\371\304s\317j\357\334\207\034M&\306ii\334\025\007\367\231\022\347\321\3354\372\360\022\016\301\275\030\343XGK\311\210\345\251\230\n\341\240\273\363\331\302+\017\030\305^$\273\323\342{\035\344\316\031t\235s\013\357\225\272\023\346i\245G\203\036\266\010\0255\001\306p\235\271\013\312\036\001\305\356\263\330\037\233\247\344.\230\257n(\2634\270\332E\r\034>^\023X\020\250\240pn\272k\214\321\274\272\315g\261\354\245>\374+\375\022\300`/-\365\0242\367\006\336\301\344\351,\001\267~%~\216\344\270[_>\274\"(\367\221\363V<;P\037{f\250x\245\275\234\350N\342Fi\313\215yf\200)\020\0134\312\347\002\003\001\000\001(\376!\022\200\003\000\262\240o\242\367\247\263\252\241\356\276\265{\357\033\256\370\317W3\017\306\366\211n\337h\242F\362`/\026A|-\374\270\221\315\307\340\241\324\253sBlN\270\242\021R\210\236f\372\342\265\310\030\274\316\324\023\377\277\356\270\253\236S\337\371t\250\371b\t\013y\255\023\020z\342\333d6\356\331W\005\302\373o8\253$\300\t\034S\304\243^\361\343\027\346h1\375\274C\215\312W#\230\177\315\017\001\257\016\030_xv\235\036\241k\241\026\241>I\300\004\n%\254<\007\037\353\226\025G\2222\026o@\017k/\000F\033\212\266\331N\225\276h\201)\231\225B\261\222R\221\263k\257\331Rg\027X1\255\031\311[\252\314\016\257\374\n\275\262\267\355R\326E-j\\+\350\212D\306\326|\370J\240\221\034\371G\366\275)\014u\3519\340B\325\251m\016\321\301\244\024W\373(\275\n\222=\215x\206\016>OdUjL\262\226\362\217\266?\310\226a\035V\327<\211\354\375\244\242\031\032\236Kx\223\020\017\252\367T\342\327\253p\n\244]\225\374\374\206e\345\031\276T\271\037\"cn\251R]\367\352\250\270\342\314\030}\267O\007\256\031\313\320\314~(w\362,\342\330KqZo\274\207@\343\206te\027\260\247\200\362\350\316\324\254U\035\017\272\"\200\327\341\2726\376\254`%\r3\353#4:#\0051\032&+\375\004*|\316\t"
client_info {
name: "architecture_name"
value: "x86-32"
}
client_info {
name: "company_name"
value: "Google"
}
client_info {
name: "model_name"
value: "ChromeCDM"
}
client_info {
name: "platform_name"
value: "Windows"
}
client_info {
name: "widevine_cdm_version"
value: "1.4.8.903"
}
client_capabilities {
client_token: false
session_token: false
video_resolution_constraints: true
max_hdcp_version: HDCP_V1
}
}
content_id {
cenc_id_deprecated {
pssh: "\010\001\022\020\353gj\273\3134^\226\273\317af0\361\243\332\032\rwidevine_test\"\020fkj3ljaSdfalkr3j*\002HD2\000"
license_type: STREAMING
request_id: "\262\251\221\340K\332\026q\301\036|uVy8\363"
}
}
type: NEW
request_time: 1531095027
protocol_version: VERSION_2_0
}
signature: "-C$b\301\365s\227\003\203\247\213\354\363TJ\217>\033\351\324cS\252\240\266\032|\254\314X\301\357\n\367\262\200\324zqP1\255\022\277\267\233\370r~EDWx\266\270\246\202I\240`9\314\n\343 \244\210L\202i\371EwMk\347\373\\\266\0062\327\n\033\213\004`\315\235l!\024q6=F,\2630\236]\314\004\206\310}\364\304\356\311*\354\204\275\307\245\000\351\213\353\031\340\353T\333\304\303#\257\247\340\212\231J0 \325:\266\355\2076s@hz\343\323\001\242\253\236P\273\215\317\353r~\326\223K\025qD\2616\345c\354\371\006#\3722\245\306P\305TMZ\211\217\372\330\274mK\263\256\263l\235\333,\300\t1\336py\270jDT[0\352\204*\347\"\220\311\205\364\357k\244\230W\201\361\212\250\357\225\221\311\244\364\252*\305\325\223\277\'\035,\242\242\253[\272\351\261:\241/\022\337f\212"
We use video_widevine.SignedLicenseRequest
because at the top of the .proto
there's a package declaration: package video_widevine;
.
You'll notice in the deserialized protobuf that the token
field is not the same, which is because you have to do more type changing to treat the device certificate as a protobuf instead of a blob. Change optional bytes token = 2;
in the ClientIdentification
message to optional SignedDrmDeviceCertificate token = 2;
. Then change optional bytes drm_certificate = 1;
in the SignedDrmDeviceCertificate
message to optional DrmDeviceCertificate drm_certificate = 1;
. Now if you decode the license request, it'll look like this:
type: LICENSE_REQUEST
msg {
client_id {
type: DRM_DEVICE_CERTIFICATE
token {
drm_certificate {
type: DRM_USER_DEVICE
serial_number: "ChromeCDM-Windows-x86"
creation_time_seconds: 1371772510
public_key: "0\202\001\n\002\202\001\001\000\241\247d\2627\372\036\335\263\006\333D\213\274\247\302 \362\3723\013G\365\304\313\324\313\303\006T\250\205y\312\324\327\320\251\222\332\000\257\202\345\037\336\257E-\301<|A\n]\343\265\344,\320\371\367\205#\306\023\310x\341=-\2455WL^\334\355\n\233\234\356\251\255\371h\246\007\366d\367\226\235\2031\307|<]J\321\312\225\303\002$\373:u\225\313\243\rG\330\n\352\267L\002f\267\266\350Z\251\022\220d\207\001[\303v\331\263\241.\341\203m\\\226\3140\361\006\271L\302|4^\250\356\307\315\236\332\262\314u\276\002U\223\3629\212S\021 f\234\244\014~\351\306\207F\tI\177\026\r[\270B+]\022\274\317\007\315J\265\216\035\340\222\023\230R<;56R\331\212\340\227\tvz3N\004\323IY\006l${y\033\005\254\254@\3328\224\241\235\307C\332\325_\372\217\220\222\312\010\204kK\263\214\377\t\002\003\001\000\001"
system_id: 4350
}
signature: "8\322\246\035\340\005\377\002\260\356\"\273H W\201lr\006\352\246\374^\307\010\303Q\313\326\367J\331\370\204\274\342\036\005\255\240\276\331\'\257I\331\233[\017U\320\301\347\243f\330\376\033\316\304\344g!R\001\355x|t@}\022\311[\252\3766X\024C\360\004\027\235\300:\345h6\271\013b\330\242\240\\\032\331\252\2316B\204Y\032w\243\205\340y/\277\020\014\0339w-\034\375h\331\240\2667|\321\010\\\005\373\316\250\266\361{\266\370\033\264\332JF\375\027t\217\273\001J\310\255\337\007i\006G_4\377\246\265\324s\210\351s\243t\345S\200\032\271s\260I\247Rqq\003ND\000\276\221\224\232\215\216\272+\266}a\3706\340\355\201N7;F\201xhd\202M\312^\0254r(\3133\010o4i\225\354\344\246\032\200\3447f\345\322\224\321\r\013\326\222wt\326\250\255g<\273\201\363\306ll*\355p"
signer {
drm_certificate {
type: DRM_INTERMEDIATE
serial_number: "\200\326\212\322\357\344\341X\220=E\373\313.\212\207"
creation_time_seconds: 1371771566
public_key: "0\202\001\n\002\202\001\001\000\353\331\007-\344\rv\016;\251N+O\n]\234{h\243\236<\375\261\200\330\2034\330\341\350\355\256\236\226\014\274Sa<\030?\226\236\220Q\022U\341i\031\010\221\330\031\206\014\000\340q\267\255i\336\214\371\304s\317j\357\334\207\034M&\306ii\334\025\007\367\231\022\347\321\3354\372\360\022\016\301\275\030\343XGK\311\210\345\251\230\n\341\240\273\363\331\302+\017\030\305^$\273\323\342{\035\344\316\031t\235s\013\357\225\272\023\346i\245G\203\036\266\010\0255\001\306p\235\271\013\312\036\001\305\356\263\330\037\233\247\344.\230\257n(\2634\270\332E\r\034>^\023X\020\250\240pn\272k\214\321\274\272\315g\261\354\245>\374+\375\022\300`/-\365\0242\367\006\336\301\344\351,\001\267~%~\216\344\270[_>\274\"(\367\221\363V<;P\037{f\250x\245\275\234\350N\342Fi\313\215yf\200)\020\0134\312\347\002\003\001\000\001"
system_id: 4350
}
signature: "\000\262\240o\242\367\247\263\252\241\356\276\265{\357\033\256\370\317W3\017\306\366\211n\337h\242F\362`/\026A|-\374\270\221\315\307\340\241\324\253sBlN\270\242\021R\210\236f\372\342\265\310\030\274\316\324\023\377\277\356\270\253\236S\337\371t\250\371b\t\013y\255\023\020z\342\333d6\356\331W\005\302\373o8\253$\300\t\034S\304\243^\361\343\027\346h1\375\274C\215\312W#\230\177\315\017\001\257\016\030_xv\235\036\241k\241\026\241>I\300\004\n%\254<\007\037\353\226\025G\2222\026o@\017k/\000F\033\212\266\331N\225\276h\201)\231\225B\261\222R\221\263k\257\331Rg\027X1\255\031\311[\252\314\016\257\374\n\275\262\267\355R\326E-j\\+\350\212D\306\326|\370J\240\221\034\371G\366\275)\014u\3519\340B\325\251m\016\321\301\244\024W\373(\275\n\222=\215x\206\016>OdUjL\262\226\362\217\266?\310\226a\035V\327<\211\354\375\244\242\031\032\236Kx\223\020\017\252\367T\342\327\253p\n\244]\225\374\374\206e\345\031\276T\271\037\"cn\251R]\367\352\250\270\342\314\030}\267O\007\256\031\313\320\314~(w\362,\342\330KqZo\274\207@\343\206te\027\260\247\200\362\350\316\324\254U\035\017\272\"\200\327\341\2726\376\254`%\r3\353#4:#\0051\032&+\375\004*|\316\t"
}
}
client_info {
name: "architecture_name"
value: "x86-32"
}
client_info {
name: "company_name"
value: "Google"
}
client_info {
name: "model_name"
value: "ChromeCDM"
}
client_info {
name: "platform_name"
value: "Windows"
}
client_info {
name: "widevine_cdm_version"
value: "1.4.8.903"
}
client_capabilities {
client_token: false
session_token: false
video_resolution_constraints: true
max_hdcp_version: HDCP_V1
}
}
content_id {
cenc_id_deprecated {
pssh: "\010\001\022\020\353gj\273\3134^\226\273\317af0\361\243\332\032\rwidevine_test\"\020fkj3ljaSdfalkr3j*\002HD2\000"
license_type: STREAMING
request_id: "\262\251\221\340K\332\026q\301\036|uVy8\363"
}
}
type: NEW
request_time: 1531095027
protocol_version: VERSION_2_0
}
signature: "-C$b\301\365s\227\003\203\247\213\354\363TJ\217>\033\351\324cS\252\240\266\032|\254\314X\301\357\n\367\262\200\324zqP1\255\022\277\267\233\370r~EDWx\266\270\246\202I\240`9\314\n\343 \244\210L\202i\371EwMk\347\373\\\266\0062\327\n\033\213\004`\315\235l!\024q6=F,\2630\236]\314\004\206\310}\364\304\356\311*\354\204\275\307\245\000\351\213\353\031\340\353T\333\304\303#\257\247\340\212\231J0 \325:\266\355\2076s@hz\343\323\001\242\253\236P\273\215\317\353r~\326\223K\025qD\2616\345c\354\371\006#\3722\245\306P\305TMZ\211\217\372\330\274mK\263\256\263l\235\333,\300\t1\336py\270jDT[0\352\204*\347\"\220\311\205\364\357k\244\230W\201\361\212\250\357\225\221\311\244\364\252*\305\325\223\277\'\035,\242\242\253[\272\351\261:\241/\022\337f\212"
While we're at it, add these messages to help with decoding licenses and service certificates:
message SignedLicense {
optional .video_widevine.SignedMessage.MessageType type = 1;
optional .video_widevine.License msg = 2;
optional bytes signature = 3;
optional bytes session_key = 4;
optional .video_widevine.RemoteAttestation remote_attestation = 5;
repeated .video_widevine.MetricData metric_data = 6;
}
message SignedServiceCertificate {
optional .video_widevine.SignedMessage.MessageType type = 1;
optional .video_widevine.SignedDrmDeviceCertificate msg = 2;
optional bytes signature = 3;
optional bytes session_key = 4;
optional .video_widevine.RemoteAttestation remote_attestation = 5;
repeated .video_widevine.MetricData metric_data = 6;
}
That concludes this blog post! Protocol buffers are very powerful, and there's tons of documentation on how to use them in Python or C++ and how to construct messages. With license_protocol.proto
, you could even construct Widevine messages yourself. Unfortunately, you wouldn't be able to sign them, however, as that requires the private component of the device RSA key used to create the request. That might be a topic of a future post though. In the next post, I'll go over Widevine security levels and how to get a Widevine Level 1 Keybox. Thanks for reading!