 Issue 29751598:
  Issue 6291 - Use client certificate for Windows Store uploads  (Closed) 
  Base URL: https://hg.adblockplus.org/abpssembly/file/a67d8f0e66b2
    
  
    Issue 29751598:
  Issue 6291 - Use client certificate for Windows Store uploads  (Closed) 
  Base URL: https://hg.adblockplus.org/abpssembly/file/a67d8f0e66b2| Index: sitescripts/extensions/bin/createNightlies.py | 
| diff --git a/sitescripts/extensions/bin/createNightlies.py b/sitescripts/extensions/bin/createNightlies.py | 
| index 87447617bff44c881e7986aa0a4ce2cfbba426d0..1a84427b4481056db1b16b44b2651f5458d5f932 100644 | 
| --- a/sitescripts/extensions/bin/createNightlies.py | 
| +++ b/sitescripts/extensions/bin/createNightlies.py | 
| @@ -25,6 +25,7 @@ Nightly builds generation script | 
| import argparse | 
| import ConfigParser | 
| +import binascii | 
| import base64 | 
| import hashlib | 
| import hmac | 
| @@ -32,19 +33,23 @@ import json | 
| import logging | 
| import os | 
| import pipes | 
| -import random | 
| import shutil | 
| import struct | 
| import subprocess | 
| import sys | 
| import tempfile | 
| import time | 
| +import uuid | 
| from urllib import urlencode | 
| import urllib2 | 
| import urlparse | 
| import zipfile | 
| import contextlib | 
| +from Crypto.PublicKey import RSA | 
| +from Crypto.Signature import PKCS1_v1_5 | 
| +import Crypto.Hash.SHA256 | 
| + | 
| from xml.dom.minidom import parse as parseXml | 
| 
Vasily Kuznetsov
2018/04/17 17:43:54
Nit: this should be above together with other stdl
 
tlucas
2018/04/18 12:58:30
Done.
 | 
| from sitescripts.extensions.utils import ( | 
| @@ -454,32 +459,52 @@ class NightlyBuild(object): | 
| pass | 
| self.write_downloads_lockfile(current) | 
| - def generate_jwt_request(self, issuer, secret, url, method, data=None, | 
| - add_headers=[]): | 
| - header = { | 
| - 'alg': 'HS256', # HMAC-SHA256 | 
| - 'typ': 'JWT', | 
| - } | 
| + def azure_jwt_signature_fnc(self): | 
| + return ( | 
| + 'RS256', | 
| + lambda s, m: PKCS1_v1_5.new(s).sign(Crypto.Hash.SHA256.new(m)) | 
| 
Vasily Kuznetsov
2018/04/17 17:43:54
Nit: According to the recently landed coding style
 
tlucas
2018/04/18 12:58:30
Done.
 | 
| + ) | 
| + | 
| + def mozilla_jwt_signature_fnc(self): | 
| + return ( | 
| + 'HS256', | 
| + lambda s, m: hmac.new(s, msg=m, digestmod=hashlib.sha256).digest() | 
| + ) | 
| + | 
| + def sign_jwt(self, issuer, secret, url, signature_fnc, jwt_headers={}): | 
| + alg, fnc = signature_fnc() | 
| + | 
| + header = {'typ': 'JWT'} | 
| + header.update(jwt_headers) | 
| + header.update({'alg': alg}) | 
| issued = int(time.time()) | 
| + expires = issued + 60 | 
| + | 
| payload = { | 
| + 'aud': url, | 
| 'iss': issuer, | 
| - 'jti': random.random(), | 
| + 'sub': issuer, | 
| + 'jti': str(uuid.uuid4()), | 
| 'iat': issued, | 
| - 'exp': issued + 60, | 
| + 'nbf': issued, | 
| + 'exp': expires, | 
| } | 
| - hmac_data = '{}.{}'.format( | 
| - base64.b64encode(json.dumps(header)), | 
| - base64.b64encode(json.dumps(payload)) | 
| - ) | 
| + segments = [base64.urlsafe_b64encode(json.dumps(header)), | 
| + base64.urlsafe_b64encode(json.dumps(payload))] | 
| - signature = hmac.new(secret, msg=hmac_data, | 
| - digestmod=hashlib.sha256).digest() | 
| - token = '{}.{}'.format(hmac_data, base64.b64encode(signature)) | 
| + signature = fnc(secret, b'.'.join(segments)) | 
| + segments.append(base64.urlsafe_b64encode(signature)) | 
| + return b'.'.join(segments) | 
| + | 
| + def generate_mozilla_jwt_request(self, issuer, secret, url, method, | 
| + data=None, add_headers=[]): | 
| + signed = self.sign_jwt(issuer, secret, url, | 
| + self.mozilla_jwt_signature_fnc) | 
| request = urllib2.Request(url, data) | 
| - request.add_header('Authorization', 'JWT ' + token) | 
| + request.add_header('Authorization', 'JWT ' + signed) | 
| for header in add_headers: | 
| request.add_header(*header) | 
| request.get_method = lambda: method | 
| @@ -503,13 +528,13 @@ class NightlyBuild(object): | 
| ) | 
| }) | 
| - request = self.generate_jwt_request( | 
| + request = self.generate_mozilla_jwt_request( | 
| config.get('extensions', 'amo_key'), | 
| config.get('extensions', 'amo_secret'), | 
| upload_url, | 
| 'PUT', | 
| data, | 
| - [('Content-Type', content_type)] | 
| + [('Content-Type', content_type)], | 
| ) | 
| try: | 
| @@ -539,7 +564,9 @@ class NightlyBuild(object): | 
| url = ('https://addons.mozilla.org/api/v3/addons/{}/' | 
| 'versions/{}/').format(app_id, version) | 
| - request = self.generate_jwt_request(iss, secret, url, 'GET') | 
| + request = self.generate_mozilla_jwt_request( | 
| + iss, secret, url, 'GET', | 
| + ) | 
| response = json.load(urllib2.urlopen(request)) | 
| filename = '{}-{}.xpi'.format(self.basename, version) | 
| @@ -554,8 +581,9 @@ class NightlyBuild(object): | 
| download_url = response['files'][0]['download_url'] | 
| checksum = response['files'][0]['hash'] | 
| - request = self.generate_jwt_request(iss, secret, download_url, | 
| - 'GET') | 
| + request = self.generate_mozilla_jwt_request( | 
| + iss, secret, download_url, 'GET', | 
| + ) | 
| try: | 
| response = urllib2.urlopen(request) | 
| except urllib2.HTTPError as e: | 
| @@ -646,21 +674,45 @@ class NightlyBuild(object): | 
| if any(status not in ('OK', 'ITEM_PENDING_REVIEW') for status in response['status']): | 
| raise Exception({'status': response['status'], 'statusDetail': response['statusDetail']}) | 
| + def generate_certificate_token_request(self, url, private_key): | 
| + # Construct the token request according to | 
| + # https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials | 
| + hex_val = binascii.a2b_hex(self.config.thumbprint) | 
| + x5t = base64.urlsafe_b64encode(hex_val).decode() | 
| + | 
| + key = RSA.importKey(private_key) | 
| + | 
| + signed = self.sign_jwt(self.config.clientID, key, url, | 
| + self.azure_jwt_signature_fnc, | 
| + jwt_headers={'x5t': x5t}) | 
| + | 
| + # generate oauth parameters for login.microsoft.com | 
| + oauth_params = { | 
| + 'grant_type': 'client_credentials', | 
| + 'client_id': self.config.clientID, | 
| + 'resource': 'https://graph.windows.net', | 
| + 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-' | 
| + 'type:jwt-bearer', | 
| + 'client_assertion': signed, | 
| + } | 
| + | 
| + request = urllib2.Request(url, urlencode(oauth_params)) | 
| + request.get_method = lambda: 'POST' | 
| + | 
| + return request | 
| + | 
| def get_windows_store_access_token(self): | 
| - # use refresh token to obtain a valid access token | 
| - # https://docs.microsoft.com/en-us/azure/active-directory/active-directory-protocols-oauth-code#refreshing-the-access-tokens | 
| - server = 'https://login.microsoftonline.com' | 
| - token_path = '{}/{}/oauth2/token'.format(server, self.config.tenantID) | 
| + # use client certificate to obtain a valid access token | 
| + url = 'https://login.microsoftonline.com/{}/oauth2/token'.format( | 
| 
Vasily Kuznetsov
2018/04/17 17:43:54
Preexisting version of this was kind of a hack to
 
tlucas
2018/04/18 12:58:30
Done.
 | 
| + self.config.tenantID | 
| + ) | 
| + | 
| + with open(self.config.privateKey, 'r') as fp: | 
| + private_key = fp.read() | 
| opener = urllib2.build_opener(HTTPErrorBodyHandler) | 
| - post_data = urlencode([ | 
| - ('refresh_token', self.config.refreshToken), | 
| - ('client_id', self.config.clientID), | 
| - ('client_secret', self.config.clientSecret), | 
| - ('grant_type', 'refresh_token'), | 
| - ('resource', 'https://graph.windows.net') | 
| - ]) | 
| - request = urllib2.Request(token_path, post_data) | 
| + request = self.generate_certificate_token_request(url, private_key) | 
| + | 
| with contextlib.closing(opener.open(request)) as response: | 
| data = json.load(response) | 
| auth_token = '{0[token_type]} {0[access_token]}'.format(data) | 
| @@ -811,7 +863,7 @@ class NightlyBuild(object): | 
| self.uploadToMozillaAddons() | 
| elif self.config.type == 'chrome' and self.config.clientID and self.config.clientSecret and self.config.refreshToken: | 
| self.uploadToChromeWebStore() | 
| - elif self.config.type == 'edge' and self.config.clientID and self.config.clientSecret and self.config.refreshToken and self.config.tenantID: | 
| + elif self.config.type == 'edge' and self.config.clientID and self.config.tenantID and self.config.privateKey and self.config.thumbprint: | 
| self.upload_to_windows_store() | 
| finally: |