Source release 17.1.1

This commit is contained in:
John "Juce" Bruce
2022-11-29 12:54:04 -08:00
parent 694cf6fb25
commit f11df1e144
139 changed files with 11266 additions and 771 deletions

View File

@@ -0,0 +1,10 @@
Instructions:
1. Write the main function or modify the example main function in example_main.cpp
2. To compile the example_main:
clang++ *.cpp ../common/src/*.cpp ../../util/src/string_conversions.cpp -ldl -rdynamic -I../../util/include -I../../oemcrypto/include -I../common/include -o example_main.out
3. Specify the location of liboemcrypto.so:
export LIBOEMCRYPTO_PATH=/path/to/liboemcrypto.so
4. Run the main program to extract the BCC and save it to a file.
./example_main.out > csr.bin
5. Upload the csr.bin with the following command. cred.json is the OAuth 2.0 client credentials obtained via Google cloud platform.
python3 upload.py --credentials=cred.json --org-name={your organization name} --json-csr=csr.bin

View File

@@ -0,0 +1,33 @@
// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#include <iostream>
#include "log.h"
#include "wv_factory_extractor.h"
int main() {
widevine::ClientInfo client_info;
client_info.company_name = "";
client_info.arch_name = "";
client_info.device_name = "";
client_info.model_name = "";
client_info.product_name = "";
client_info.build_info = "";
auto extractor = widevine::WidevineFactoryExtractor::Create(client_info);
if (extractor == nullptr) {
LOGE("Failed to create WidevineFactoryExtractor");
return 1;
}
std::string request;
widevine::Status status = extractor->GenerateUploadRequest(request);
if (status != widevine::Status::kSuccess) {
LOGE("Fail to generate upload request: %d", status);
return 2;
}
std::cout << request << std::endl;
return 0;
}

View File

@@ -0,0 +1,42 @@
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
//
// Log - implemented using stderr.
#include "log.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
namespace wvutil {
LogPriority g_cutoff = CDM_LOG_INFO;
void InitLogging() {}
void Log(const char* file, const char* function, int line, LogPriority level,
const char* fmt, ...) {
const char* severities[] = {"ERROR", "WARN", "INFO", "DEBUG", "VERBOSE"};
if (level >=
static_cast<LogPriority>(sizeof(severities) / sizeof(*severities))) {
fprintf(stderr, "[FATAL:%s(%d)] Invalid log priority level: %d\n", file,
line, level);
return;
}
if (level > g_cutoff) return;
fprintf(stderr, "[%s:%s(%d):%s] ", severities[level], file, line, function);
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
fflush(stderr);
}
} // namespace wvutil

View File

@@ -0,0 +1,87 @@
// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#include "properties_ce.h"
#include "log.h"
#include "properties.h"
#include "wv_factory_extractor.h"
// This anonymous namespace is shared between both the widevine namespace and
// wvcdm namespace objects below.
namespace {
widevine::ClientInfo client_info_;
bool GetValue(const std::string& source, std::string* output) {
if (!output) {
LOGE("Null output");
return false;
}
*output = source;
return source.size() != 0;
}
} // namespace
namespace widevine {
// static
bool PropertiesCE::SetClientInfo(const ClientInfo& client_info) {
if (client_info.product_name.empty() || client_info.company_name.empty() ||
client_info.device_name.empty() || client_info.model_name.empty() ||
client_info.arch_name.empty() || client_info.build_info.empty()) {
return false;
}
client_info_ = client_info;
return true;
}
// static
ClientInfo PropertiesCE::GetClientInfo() { return client_info_; }
} // namespace widevine
namespace wvcdm {
// static
bool Properties::GetCompanyName(std::string* company_name) {
return GetValue(client_info_.company_name, company_name);
}
// static
bool Properties::GetModelName(std::string* model_name) {
return GetValue(client_info_.model_name, model_name);
}
// static
bool Properties::GetArchitectureName(std::string* arch_name) {
return GetValue(client_info_.arch_name, arch_name);
}
// static
bool Properties::GetDeviceName(std::string* device_name) {
return GetValue(client_info_.device_name, device_name);
}
// static
bool Properties::GetProductName(std::string* product_name) {
return GetValue(client_info_.product_name, product_name);
}
// static
bool Properties::GetBuildInfo(std::string* build_info) {
return GetValue(client_info_.build_info, build_info);
}
// static
bool Properties::GetOEMCryptoPath(std::string* path) {
if (path == nullptr) return false;
// Using an environment variable is useful for testing.
const char* env_path = getenv("LIBOEMCRYPTO_PATH");
if (env_path) {
*path = std::string(env_path);
} else {
*path = "liboemcrypto.so";
}
return true;
}
} // namespace wvcdm

View File

@@ -0,0 +1,19 @@
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#ifndef WVCDM_CDM_PROPERTIES_CE_H_
#define WVCDM_CDM_PROPERTIES_CE_H_
#include "wv_factory_extractor.h"
namespace widevine {
class PropertiesCE {
public:
static bool SetClientInfo(const ClientInfo& client_info);
static ClientInfo GetClientInfo();
};
} // namespace widevine
#endif // WVCDM_CDM_PROPERTIES_CE_H_

View File

@@ -0,0 +1,83 @@
// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#include "wv_factory_extractor.h"
#include "log.h"
#include "properties.h"
#include "properties_ce.h"
#include "string_conversions.h"
namespace widevine {
namespace {
std::string StringMapToJson(
const std::map<std::string, std::string>& string_map) {
std::string json = "{";
for (const auto& value_pair : string_map) {
json.append("\"" + value_pair.first + "\": " + "\"" + value_pair.second +
"\",");
}
json.resize(json.size() - 1); // Remove the last comma.
json.append("}");
return json;
}
} // namespace
std::unique_ptr<WidevineFactoryExtractor> WidevineFactoryExtractor::Create(
const ClientInfo& client_info) {
if (!PropertiesCE::SetClientInfo(client_info)) {
LOGE("Invalid client info.");
return nullptr;
}
return std::unique_ptr<WidevineFactoryExtractor>(
new WidevineFactoryExtractor());
}
Status WidevineFactoryExtractor::GenerateUploadRequest(std::string& request) {
if (crypto_interface_ == nullptr) {
std::string oemcrypto_path;
if (!wvcdm::Properties::GetOEMCryptoPath(&oemcrypto_path)) {
LOGE("Failed to get OEMCrypto path.");
return kOEMCryptoError;
}
LOGI("OEMCrypto path is %s", oemcrypto_path.c_str());
crypto_interface_ = std::make_unique<OEMCryptoInterface>();
if (!crypto_interface_->Init(oemcrypto_path)) {
LOGE("Failed to initialize OEMCrypto interface.");
crypto_interface_.reset(nullptr);
return kOEMCryptoError;
}
}
std::vector<uint8_t> bcc;
OEMCryptoResult result = crypto_interface_->GetBcc(bcc);
if (result != OEMCrypto_SUCCESS) {
LOGE("Failed to get BCC.");
return kOEMCryptoError;
}
std::string oemcrypto_build_info;
result = crypto_interface_->GetOEMCryptoBuildInfo(oemcrypto_build_info);
if (result != OEMCrypto_SUCCESS) {
LOGE("Failed to get oemcrypto build info.");
return kOEMCryptoError;
}
std::map<std::string, std::string> request_map;
request_map["company"] = PropertiesCE::GetClientInfo().company_name;
request_map["architecture"] = PropertiesCE::GetClientInfo().arch_name;
request_map["name"] = PropertiesCE::GetClientInfo().device_name;
request_map["model"] = PropertiesCE::GetClientInfo().model_name;
request_map["product"] = PropertiesCE::GetClientInfo().product_name;
request_map["build_info"] = PropertiesCE::GetClientInfo().build_info;
request_map["oemcrypto_build_info"] = oemcrypto_build_info;
request_map["bcc"] = wvutil::Base64Encode(bcc);
std::string request_json = StringMapToJson(request_map);
request.assign(request_json.begin(), request_json.end());
return kSuccess;
}
} // namespace widevine

View File

@@ -0,0 +1,70 @@
// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#ifndef WV_FACTORY_UPLOADER_H_
#define WV_FACTORY_UPLOADER_H_
#include <cstdint>
#include <memory>
#include <string>
#include "WidevineOemcryptoInterface.h"
namespace widevine {
enum Status : int32_t {
kSuccess = 0,
kUnknownError = 1,
kOEMCryptoError = 2,
};
// Client information, provided by the application, independent of CDM
// instances.
// See Cdm::initialize().
// These parameters end up as client identification in license requests.
// All fields may be used by a license server proxy to drive business logic.
// Some fields are required (indicated below), but please fill out as many
// as make sense for your application.
// No user-identifying information may be put in these fields!
struct ClientInfo {
// The name of the product or application, e.g. "TurtleTube"
// Required.
std::string product_name;
// The name of the company who makes the device, e.g. "Kubrick, Inc."
// Required.
std::string company_name;
// The name of the device, e.g. "HAL"
std::string device_name;
// The device model, e.g. "HAL 9000"
// Required.
std::string model_name;
// The architecture of the device, e.g. "x86-64"
std::string arch_name;
// Information about the build of the browser, application, or platform into
// which the CDM is integrated, e.g. "v2.71828, 2038-01-19-03:14:07"
std::string build_info;
};
class WidevineFactoryExtractor {
public:
~WidevineFactoryExtractor() = default;
static std::unique_ptr<WidevineFactoryExtractor> Create(
const ClientInfo& client_info);
Status GenerateUploadRequest(std::string& request);
private:
WidevineFactoryExtractor() = default;
std::unique_ptr<OEMCryptoInterface> crypto_interface_;
};
} // namespace widevine
#endif // WV_FACTORY_UPLOADER_H_

View File

@@ -0,0 +1,347 @@
#!/usr/bin/env python3
# Copyright 2022 Google LLC. All rights reserved.
"""Uploader tool for sending device keys to Widevine remote provisioning server.
This tool consumes an input file containing device info, which includes the
device's public key, and batch uploads it to Google. Once uploaded, the device
may use Widevine provisioning 4 to request OEM certificates from Google.
This tool is designed to be used with the widevine factory extraction tool.
Therefore, the JSON output from widevine factory extraction tool is the expected
input, with one JSON string per line of input.
"""
import argparse
import http.server
import json
import os
import sys
import urllib.parse
import urllib.request
import uuid
import webbrowser
DEFAULT_BASE = 'https://widevine.googleapis.com/v1/orgs/'
UPLOAD_PATH = '/uniqueDeviceInfo:batchUpload'
TOKEN_CACHE_FILE = os.path.join(
os.path.expanduser('~'), '.device_info_uploader.token')
OAUTH_SERVICE_BASE = 'https://accounts.google.com/o/oauth2'
OAUTH_AUTHN_URL = OAUTH_SERVICE_BASE + '/auth'
OAUTH_TOKEN_URL = OAUTH_SERVICE_BASE + '/token'
class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
"""HTTP Handler used to accept the oauth response when the user logs in."""
def do_GET(self): # pylint: disable=invalid-name
"""Handles GET, extracting the authorization code in the query params."""
print(f'GET path: {self.path}')
parsed_path = urllib.parse.urlparse(self.path)
params = dict(urllib.parse.parse_qsl(parsed_path.query))
if 'error' in params:
error = params['error']
self.respond(400, error,
f'Error received from the OAuth server: {error}.')
sys.exit(-1)
elif 'code' not in params:
self.respond(400, 'ERROR',
('Response from OAuth server is missing the authorization '
f'code. Full response: "{self.path}"'))
sys.exit(-1)
else:
self.respond(200, 'Success!',
'Success! You may close this browser window.')
self.server.code = params['code']
def do_POST(self): # pylint: disable=invalid-name
print(f'POST path: {self.path}')
def respond(self, code, title, message):
"""Send a response to the HTTP client.
Args:
code: The HTTP status code to send
title: The page title to display
message: The message to display to the user on the page
"""
if code != 200:
eprint(message)
self.send_response(code)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(('<html>'
f' <title>{title}</title>'
f' <body>'
f' <p style="font-size:24px;">{message}</p>'
f' </body>'
'</html>').encode('utf-8'))
class LocalOAuthReceiver(http.server.HTTPServer):
"""HTTP server that will wait for an OAuth authorization code."""
def __init__(self):
super(LocalOAuthReceiver, self).__init__(('127.0.0.1', 0),
OAuthHTTPRequestHandler)
self.code = None
def port(self):
return self.socket.getsockname()[1]
def wait_for_code(self):
print('Waiting for a response from the Google OAuth service.')
print('If you receive an error in your browser, interrupt this script.')
self.handle_request()
return self.code
def eprint(message):
print(message, file=sys.stderr)
def die(message):
eprint(message)
sys.exit(-1)
def parse_args():
"""Parse and return the command line args.
Returns:
An argparse.Namespace object populated with the arguments.
"""
parser = argparse.ArgumentParser(description='Upload device info')
parser.add_argument(
'--json-csr',
nargs='+',
required=True,
help='list of files containing JSON output from rkp_factory_extraction_tool'
)
parser.add_argument(
'--credentials', required=True, help='JSON credentials file')
parser.add_argument(
'--endpoint', default=DEFAULT_BASE, help='destination server URL')
parser.add_argument('--org-name', required=True, help='orgnization name')
parser.add_argument(
'--cache-token',
action='store_true',
help='Use a locally cached a refresh token')
return parser.parse_args()
def parse_json_csrs(filename, batches):
"""Parse the given file and insert it into batches.
If the input is not a valid JSON CSR blob, exit the program.
Args:
filename: The file that contains a JSON-formatted build and CSR
batches: Output dict containing a mapping from json dumped device metadata
to BCCs.
"""
line_count = 0
for line in open(filename):
line_count = line_count + 1
try:
obj = json.loads(line)
except json.JSONDecodeError as e:
die(f'{e.msg} {filename}:{line_count}, char {e.pos}')
try:
bcc = {'boot_certificate_chain': obj['bcc']}
device_metadata = json.dumps({
'company': obj['company'],
'architecture': obj['architecture'],
'name': obj['name'],
'model': obj['model'],
'product': obj['product'],
'build_info': obj['build_info']
})
except KeyError as e:
die(f'Invalid object at {filename}:{line_count}, missing {e}')
if device_metadata not in batches:
batches[device_metadata] = []
batches[device_metadata].append(bcc)
def format_request_body(args, device_metadata, bccs):
"""Generate a formatted request buffer for the given build and CSRs."""
request = {
'parent': 'orgs/' + args.org_name,
'request_id': uuid.uuid4().hex,
'metadata': json.loads(device_metadata),
'device_info': bccs,
}
return json.dumps(request).encode('utf-8')
def load_refresh_token():
if not os.path.exists(TOKEN_CACHE_FILE):
return None
with open(TOKEN_CACHE_FILE) as f:
return f.readline()
def store_refresh_token(refresh_token):
with open(TOKEN_CACHE_FILE, 'w') as f:
f.write(refresh_token)
def fetch_access_token(creds, cache_token=False, code=None, redirect_uri=None):
"""Fetch an oauth2 access token.
If a code is passed, then it is used to get the token. If code
is None, then look for a persisted refresh token and use that to
get the access token instead.
Args:
creds: The OAuth client credentials, including client secret and id.
cache_token: If True, then the refresh token is cached on disk so that the
user does not have to reauthenticate when the script is used again.
code: The OAuth authorization code, returned by Google's OAuth service.
redirect_uri: If an authorization code is supplied, then the redirect_uri
used to fetch the code must be passed here.
Returns:
A base64-encode OAuth access token, suitable for including in a request.
"""
request = urllib.request.Request(OAUTH_TOKEN_URL)
request.add_header('Content-Type', 'application/x-www-form-urlencoded')
body = 'client_id=' + creds['client_id']
body += '&client_secret=' + creds['client_secret']
if code is not None:
if redirect_uri is None:
raise ValueError('"code" was supplied, but "redirect_uri" is None')
body += '&grant_type=authorization_code'
body += '&code=' + code
body += '&redirect_uri=' + redirect_uri
else:
refresh_token = load_refresh_token()
if refresh_token is None:
return None
body += '&grant_type=refresh_token'
body += '&refresh_token=' + refresh_token
try:
response = urllib.request.urlopen(request, body.encode('utf-8'))
parsed_response = json.load(response)
if cache_token:
store_refresh_token(parsed_response['refresh_token'])
return parsed_response['access_token']
except urllib.error.HTTPError as e:
# Catch bogus/expired refresh tokens, but bubble up errors when
# an authorization code is used.
if code is None:
return None
die(f'Failed to receive access token: {e.code} {e.reason}')
def load_and_validate_creds(credfile):
"""Loads the credentials from the given file and validates them.
Args:
credfile: the name of the file containing the client credentials
Returns:
A map containing the credentials for connecting to the APE backend.
"""
credmap = json.load(open(credfile))
not_local_app_creds_error = (
'ERROR: Invalid credential file.\n'
' The given credentials do not appear to be for a locally installed\n'
' application. Please navigate to the credentials dashboard and\n'
' ensure that the "Type" of your client is "Desktop":\n'
' https://console.cloud.google.com/apis/credentials')
if 'installed' not in credmap:
die(not_local_app_creds_error)
creds = credmap['installed']
expected_keys = set(['client_id', 'client_secret', 'redirect_uris'])
if not expected_keys.issubset(creds.keys()):
die(('ERROR: Invalid credential file.\n'
' The given credentials do not appear to be valid. Please\n'
' re-download the client credentials file from the dashboard:\n'
' https://console.cloud.google.com/apis/credentials'))
if 'http://localhost' not in creds['redirect_uris']:
die(not_local_app_creds_error)
return creds
def authenticate_and_fetch_token(args):
"""Authenticate the user and fetch an OAUTH2 access token."""
creds = load_and_validate_creds(args.credentials)
access_type = 'online'
if args.cache_token:
token = fetch_access_token(creds)
if token is not None:
return token
access_type = 'offline'
httpd = LocalOAuthReceiver()
redirect_uri = f'http://127.0.0.1:{httpd.port()}'
url = (
OAUTH_AUTHN_URL + '?response_type=code' + '&client_id=' +
creds['client_id'] + '&redirect_uri=' + redirect_uri +
'&scope=https://www.googleapis.com/auth/widevine/frontend' +
'&access_type=' + access_type + '&prompt=select_account')
print('Opening your web browser to authenticate...')
if not webbrowser.open(url, new=1, autoraise=True):
print('Error opening the browser. Please open this link in a browser')
print(f'that is running on this same system:\n {url}\n')
code = httpd.wait_for_code()
return fetch_access_token(creds, args.cache_token, code, redirect_uri)
def upload_batch(args, device_metadata, bccs):
"""Batch upload all the CSRs associated build device_metadata.
Args:
args: The parsed command-line arguments
device_metadata: The build for which we're uploading CSRs
bccs: a list of BCCs to be uploaded for the given build
"""
print("Uploading {} bcc(s) for build '{}'".format(len(bccs), device_metadata))
body = format_request_body(args, device_metadata, bccs)
print(body)
print(args.endpoint + args.org_name + UPLOAD_PATH)
request = urllib.request.Request(args.endpoint + args.org_name + UPLOAD_PATH)
request.add_header('Content-Type', 'application/json')
request.add_header('X-GFE-SSL', 'yes')
request.add_header('Authorization',
'Bearer ' + authenticate_and_fetch_token(args))
try:
response = urllib.request.urlopen(request, body)
except urllib.error.HTTPError as e:
eprint(f'Error uploading bccs. {e}')
for line in e:
eprint(line.decode('utf-8').rstrip())
sys.exit(1)
while chunk := response.read(1024):
print(chunk.decode('utf-8'))
def main():
args = parse_args()
batches = {}
for filename in args.json_csr:
parse_json_csrs(filename, batches)
for device_metadata, bccs in batches.items():
upload_batch(args, device_metadata, bccs)
if __name__ == '__main__':
main()