 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 4bd0f7fe3792361c1211c7049bf6f09ac0acb264..3a1d3e2b91e219dae19693d46bf7b470eb680725 100644 | 
| --- a/sitescripts/extensions/bin/createNightlies.py | 
| +++ b/sitescripts/extensions/bin/createNightlies.py | 
| @@ -25,7 +25,9 @@ Nightly builds generation script | 
| import argparse | 
| import ConfigParser | 
| +import binascii | 
| import base64 | 
| +import datetime | 
| import hashlib | 
| import hmac | 
| import json | 
| @@ -39,12 +41,17 @@ 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 | 
| from sitescripts.extensions.utils import ( | 
| @@ -350,7 +357,7 @@ class NightlyBuild(object): | 
| env = dict(env, SPIDERMONKEY_BINARY=spiderMonkeyBinary) | 
| command = [os.path.join(self.tempdir, 'build.py')] | 
| - if self.config.type == 'safari': | 
| + if self.config.type in {'safari', 'edge'}: | 
| 
tlucas
2018/04/13 13:06:04
The branch 'edge' is built from still uses the old
 
Sebastian Noack
2018/04/14 02:47:30
I would rather see a dependency update landing in
 
tlucas
2018/04/14 08:55:46
Yeah, i agree.
@Ollie - what do you think about th
 
Sebastian Noack
2018/04/14 09:40:54
Alternatively, we could add a hack to build.py:
 
Oleksandr
2018/04/16 04:37:07
I would rather merge `master` into `edge` first. T
 
Sebastian Noack
2018/04/16 05:45:50
Wouldn't this rather be a reason to go with my sug
 
Sebastian Noack
2018/04/16 05:47:19
Sorry, I meant "edge" (not "next").
 
tlucas
2018/04/16 10:06:07
All of the above does IMHO encourage a workaround
 
tlucas
2018/04/16 10:25:14
https://codereview.adblockplus.org/29753557/
https
 
tlucas
2018/04/16 14:50:09
Done.
 | 
| command.extend(['-t', self.config.type, 'build']) | 
| else: | 
| command.extend(['build', '-t', self.config.type]) | 
| @@ -649,21 +656,72 @@ 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 | 
| + def base64url_encode(data): | 
| + return base64.urlsafe_b64encode(data).replace(b'=', b'') | 
| 
Sebastian Noack
2018/04/14 02:47:30
How about .rstrip(b'=')?
 
tlucas
2018/04/14 08:55:46
see below
 
tlucas
2018/04/16 14:50:09
FWIW, there's no stripping / replacing done anymor
 | 
| + | 
| + segments = [] | 
| + | 
| + hex_val = binascii.a2b_hex(self.config.thumbprint) | 
| + x5t = base64.urlsafe_b64encode(hex_val).decode() | 
| + | 
| + now = datetime.datetime.now() | 
| 
Sebastian Noack
2018/04/14 02:47:30
It seems you are relying on the fact that our serv
 
tlucas
2018/04/14 08:55:46
I'm actually thinking about refactoring parts of t
 
tlucas
2018/04/16 14:50:09
Done, almost: mktime() expects a time_struct in lo
 
Sebastian Noack
2018/04/16 16:13:29
You are right. But time.time() gives the same resu
 
tlucas
2018/04/16 16:50:24
Done.
 | 
| + minutes = datetime.timedelta(0, 0, 0, 0, 10) | 
| + expires = now + minutes | 
| + | 
| + # generate the full jwt body | 
| + jwt_payload = { | 
| + 'aud': url, | 
| + 'iss': self.config.clientID, | 
| + 'sub': self.config.clientID, | 
| + 'nbf': int(time.mktime(now.timetuple())), | 
| + 'exp': int(time.mktime(expires.timetuple())), | 
| + 'jti': str(uuid.uuid4()), | 
| + } | 
| + | 
| + jwt_headers = {'typ': 'JWT', 'alg': 'RS256', 'x5t': x5t} | 
| + | 
| + # sign the jwt body with the given private key | 
| + key = RSA.importKey(private_key) | 
| + | 
| + segments.append(base64url_encode(json.dumps(jwt_headers))) | 
| + segments.append(base64url_encode(json.dumps(jwt_payload))) | 
| 
Sebastian Noack
2018/04/14 02:47:30
Since we don't append to segments above, how about
 
tlucas
2018/04/14 08:55:46
Acknowledged.
 
tlucas
2018/04/16 14:50:09
Done.
 | 
| + | 
| + body = b'.'.join(segments) | 
| + signature = PKCS1_v1_5.new(key).sign(Crypto.Hash.SHA256.new(body)) | 
| + | 
| + segments.append(base64url_encode(signature)) | 
| + signed_jwt = b'.'.join(segments) | 
| + | 
| + # 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_jwt, | 
| + } | 
| + | 
| + 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( | 
| + 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) | 
| @@ -814,7 +872,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: |