Index: sitescripts/extensions/bin/createNightlies.py |
diff --git a/sitescripts/extensions/bin/createNightlies.py b/sitescripts/extensions/bin/createNightlies.py |
index ed2cabfbe1977fb84e53b3be74c116d2f88d9912..913b83f273d194e7180c26705f7771a182d1f2e3 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,21 +33,24 @@ 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 xml.dom.minidom import parse as parseXml |
+from Crypto.PublicKey import RSA |
+from Crypto.Signature import PKCS1_v1_5 |
+import Crypto.Hash.SHA256 |
+ |
from sitescripts.extensions.utils import ( |
compareVersions, Configuration, |
writeAndroidUpdateManifest, |
@@ -454,32 +458,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)), |
+ ) |
+ |
+ 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 = fnc(secret, b'.'.join(segments)) |
+ segments.append(base64.urlsafe_b64encode(signature)) |
+ return b'.'.join(segments) |
- signature = hmac.new(secret, msg=hmac_data, |
- digestmod=hashlib.sha256).digest() |
- token = '{}.{}'.format(hmac_data, base64.b64encode(signature)) |
+ 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,7 +527,7 @@ 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, |
@@ -539,7 +563,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 +580,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 +673,44 @@ 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_template = 'https://login.microsoftonline.com/{}/oauth2/token' |
+ url = url_template.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) |
@@ -811,7 +861,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: |