| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2016 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Utility for generating experimental API tokens | 6 """Utility for generating experimental API tokens |
| 7 | 7 |
| 8 usage: generate_token.py [-h] [--key-file KEY_FILE] | 8 usage: generate_token.py [-h] [--key-file KEY_FILE] |
| 9 [--expire-days EXPIRE_DAYS | | 9 [--expire-days EXPIRE_DAYS | |
| 10 --expire-timestamp EXPIRE_TIMESTAMP] | 10 --expire-timestamp EXPIRE_TIMESTAMP] |
| 11 [--is_subdomain | --no-subdomain] | 11 [--is_subdomain | --no-subdomain] |
| 12 origin trial_name | 12 origin trial_name |
| 13 | 13 |
| 14 Run "generate_token.py -h" for more help on usage. | 14 Run "generate_token.py -h" for more help on usage. |
| 15 """ | 15 """ |
| 16 import argparse | 16 import argparse |
| 17 import base64 | 17 import base64 |
| 18 from datetime import datetime | 18 from datetime import datetime |
| 19 import json | 19 import json |
| 20 import re | 20 import re |
| 21 import os | 21 import os |
| 22 import struct | 22 import struct |
| 23 import subprocess |
| 23 import sys | 24 import sys |
| 24 import time | 25 import time |
| 25 import urlparse | 26 import urlparse |
| 26 | 27 |
| 27 script_dir = os.path.dirname(os.path.realpath(__file__)) | 28 script_dir = os.path.dirname(os.path.realpath(__file__)) |
| 28 sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) | 29 sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) |
| 29 import ed25519 | 30 import ed25519 |
| 30 | 31 |
| 31 | 32 |
| 32 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, | 33 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, |
| 33 # no longer than 63 ASCII characters) | 34 # no longer than 63 ASCII characters) |
| 34 DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE) | 35 DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE) |
| 35 | 36 |
| 36 # This script generates Version 2 tokens. | 37 # This script generates Version 2 tokens. |
| 37 VERSION = "\x02" | 38 VERSION = "\x02" |
| 38 | 39 |
| 39 # Default key file, relative to script_dir. | 40 # Default key file, relative to script_dir. |
| 40 DEFAULT_KEY_FILE = 'eftest.key' | 41 DEFAULT_KEY_FILE = 'eftest.key' |
| 41 | 42 |
| 43 # Default location of validate subdomain utility, relative to script_dir. |
| 44 DEFAULT_TARGET_PATH = '../../out/Default/' |
| 45 |
| 42 def HostnameFromArg(arg): | 46 def HostnameFromArg(arg): |
| 43 """Determines whether a string represents a valid hostname. | 47 """Determines whether a string represents a valid hostname. |
| 44 | 48 |
| 45 Returns the canonical hostname if its argument is valid, or None otherwise. | 49 Returns the canonical hostname if its argument is valid, or None otherwise. |
| 46 """ | 50 """ |
| 47 if not arg or len(arg) > 255: | 51 if not arg or len(arg) > 255: |
| 48 return None | 52 return None |
| 49 if arg[-1] == ".": | 53 if arg[-1] == ".": |
| 50 arg = arg[:-1] | 54 arg = arg[:-1] |
| 51 if "." not in arg and arg != "localhost": | 55 if "." not in arg and arg != "localhost": |
| (...skipping 27 matching lines...) Expand all Loading... |
| 79 if not port: | 83 if not port: |
| 80 port = {"https": 443, "http": 80}[origin.scheme] | 84 port = {"https": 443, "http": 80}[origin.scheme] |
| 81 # Strip any extra components and return the origin URL: | 85 # Strip any extra components and return the origin URL: |
| 82 return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) | 86 return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) |
| 83 | 87 |
| 84 def ExpiryFromArgs(args): | 88 def ExpiryFromArgs(args): |
| 85 if args.expire_timestamp: | 89 if args.expire_timestamp: |
| 86 return int(args.expire_timestamp) | 90 return int(args.expire_timestamp) |
| 87 return (int(time.time()) + (int(args.expire_days) * 86400)) | 91 return (int(time.time()) + (int(args.expire_days) * 86400)) |
| 88 | 92 |
| 93 def ValidateSubdomainTokenOrigin(origin, target_path): |
| 94 """ Calls validate_subdomain_origin utility to check the origin |
| 95 |
| 96 If the utility is not found, prints a warning for manual validation, and |
| 97 returns True |
| 98 """ |
| 99 utility_path = "%s/validate_subdomain_origin" % target_path |
| 100 if not os.path.exists(utility_path): |
| 101 print "WARNING!" |
| 102 print "Origin not validated for use in subdomain token" |
| 103 print " (missing '%s' utility)" % utility_path |
| 104 print "Must manually check origin against the Public Suffix List" |
| 105 print |
| 106 return True |
| 107 |
| 108 rc = subprocess.call([utility_path, "--quiet", origin]) |
| 109 if (rc < 0 or rc > 4): |
| 110 print("Unexpected return code from %s: %d" % (utility_path, rc)) |
| 111 sys.exit(1) |
| 112 |
| 113 return rc == 0 |
| 114 |
| 89 def GenerateTokenData(origin, is_subdomain, feature_name, expiry): | 115 def GenerateTokenData(origin, is_subdomain, feature_name, expiry): |
| 90 data = {"origin": origin, | 116 data = {"origin": origin, |
| 91 "feature": feature_name, | 117 "feature": feature_name, |
| 92 "expiry": expiry} | 118 "expiry": expiry} |
| 93 if is_subdomain is not None: | 119 if is_subdomain is not None: |
| 94 data["isSubdomain"] = is_subdomain | 120 data["isSubdomain"] = is_subdomain |
| 95 return json.dumps(data).encode('utf-8') | 121 return json.dumps(data).encode('utf-8') |
| 96 | 122 |
| 97 def GenerateDataToSign(version, data): | 123 def GenerateDataToSign(version, data): |
| 98 return version + struct.pack(">I",len(data)) + data | 124 return version + struct.pack(">I",len(data)) + data |
| 99 | 125 |
| 100 def Sign(private_key, data): | 126 def Sign(private_key, data): |
| 101 return ed25519.signature(data, private_key[:32], private_key[32:]) | 127 return ed25519.signature(data, private_key[:32], private_key[32:]) |
| 102 | 128 |
| 103 def FormatToken(version, signature, data): | 129 def FormatToken(version, signature, data): |
| 104 return base64.b64encode(version + signature + | 130 return base64.b64encode(version + signature + |
| 105 struct.pack(">I",len(data)) + data) | 131 struct.pack(">I",len(data)) + data) |
| 106 | 132 |
| 107 def main(): | 133 def main(): |
| 108 default_key_file_absolute = os.path.join(script_dir, DEFAULT_KEY_FILE) | 134 default_key_file_absolute = os.path.join(script_dir, DEFAULT_KEY_FILE) |
| 135 default_target_path_absolute = os.path.join(script_dir, DEFAULT_TARGET_PATH) |
| 109 | 136 |
| 110 parser = argparse.ArgumentParser( | 137 parser = argparse.ArgumentParser( |
| 111 description="Generate tokens for enabling experimental features") | 138 description="Generate tokens for enabling experimental features") |
| 112 parser.add_argument("origin", | 139 parser.add_argument("origin", |
| 113 help="Origin for which to enable the feature. This can " | 140 help="Origin for which to enable the feature. This can " |
| 114 "be either a hostname (default scheme HTTPS, " | 141 "be either a hostname (default scheme HTTPS, " |
| 115 "default port 443) or a URL.", | 142 "default port 443) or a URL.", |
| 116 type=OriginFromArg) | 143 type=OriginFromArg) |
| 117 parser.add_argument("trial_name", | 144 parser.add_argument("trial_name", |
| 118 help="Feature to enable. The current list of " | 145 help="Feature to enable. The current list of " |
| (...skipping 19 matching lines...) Expand all Loading... |
| 138 expiry_group = parser.add_mutually_exclusive_group() | 165 expiry_group = parser.add_mutually_exclusive_group() |
| 139 expiry_group.add_argument("--expire-days", | 166 expiry_group.add_argument("--expire-days", |
| 140 help="Days from now when the token should expire", | 167 help="Days from now when the token should expire", |
| 141 type=int, | 168 type=int, |
| 142 default=42) | 169 default=42) |
| 143 expiry_group.add_argument("--expire-timestamp", | 170 expiry_group.add_argument("--expire-timestamp", |
| 144 help="Exact time (seconds since 1970-01-01 " | 171 help="Exact time (seconds since 1970-01-01 " |
| 145 "00:00:00 UTC) when the token should expire", | 172 "00:00:00 UTC) when the token should expire", |
| 146 type=int) | 173 type=int) |
| 147 | 174 |
| 175 parser.add_argument("--target", |
| 176 help="Path to the output directory for compiled " |
| 177 "resources", |
| 178 default=default_target_path_absolute) |
| 179 |
| 148 args = parser.parse_args() | 180 args = parser.parse_args() |
| 149 expiry = ExpiryFromArgs(args) | 181 expiry = ExpiryFromArgs(args) |
| 150 | 182 |
| 151 key_file = open(os.path.expanduser(args.key_file), mode="rb") | 183 key_file = open(os.path.expanduser(args.key_file), mode="rb") |
| 152 private_key = key_file.read(64) | 184 private_key = key_file.read(64) |
| 153 | 185 |
| 154 # Validate that the key file read was a proper Ed25519 key -- running the | 186 # Validate that the key file read was a proper Ed25519 key -- running the |
| 155 # publickey method on the first half of the key should return the second | 187 # publickey method on the first half of the key should return the second |
| 156 # half. | 188 # half. |
| 157 if (len(private_key) < 64 or | 189 if (len(private_key) < 64 or |
| 158 ed25519.publickey(private_key[:32]) != private_key[32:]): | 190 ed25519.publickey(private_key[:32]) != private_key[32:]): |
| 159 print("Unable to use the specified private key file.") | 191 print("Unable to use the specified private key file.") |
| 160 sys.exit(1) | 192 sys.exit(1) |
| 161 | 193 |
| 194 # For subdomain tokens, validate that the origin is allowed |
| 195 if args.is_subdomain: |
| 196 target_path = os.path.expanduser(args.target) |
| 197 if not ValidateSubdomainTokenOrigin(args.origin, target_path): |
| 198 print "The specified origin is not valid for use in a subdomain token." |
| 199 sys.exit(1) |
| 200 |
| 162 token_data = GenerateTokenData(args.origin, args.is_subdomain, | 201 token_data = GenerateTokenData(args.origin, args.is_subdomain, |
| 163 args.trial_name, expiry) | 202 args.trial_name, expiry) |
| 164 data_to_sign = GenerateDataToSign(VERSION, token_data) | 203 data_to_sign = GenerateDataToSign(VERSION, token_data) |
| 165 signature = Sign(private_key, data_to_sign) | 204 signature = Sign(private_key, data_to_sign) |
| 166 | 205 |
| 167 # Verify that that the signature is correct before printing it. | 206 # Verify that that the signature is correct before printing it. |
| 168 try: | 207 try: |
| 169 ed25519.checkvalid(signature, data_to_sign, private_key[32:]) | 208 ed25519.checkvalid(signature, data_to_sign, private_key[32:]) |
| 170 except Exception, exc: | 209 except Exception, exc: |
| 171 print "There was an error generating the signature." | 210 print "There was an error generating the signature." |
| 172 print "(The original error was: %s)" % exc | 211 print "(The original error was: %s)" % exc |
| 173 sys.exit(1) | 212 sys.exit(1) |
| 174 | 213 |
| 175 | 214 |
| 176 # Output the token details | 215 # Output the token details |
| 177 print "Token details:" | 216 print "Token details:" |
| 178 print " Origin: %s" % args.origin | 217 print " Origin: %s" % args.origin |
| 179 print " Is Subdomain: %s" % args.is_subdomain | 218 print " Is Subdomain: %s" % args.is_subdomain |
| 180 print " Feature: %s" % args.trial_name | 219 print " Feature: %s" % args.trial_name |
| 181 print " Expiry: %d (%s UTC)" % (expiry, datetime.utcfromtimestamp(expiry)) | 220 print " Expiry: %d (%s UTC)" % (expiry, datetime.utcfromtimestamp(expiry)) |
| 182 print | 221 print |
| 183 | 222 |
| 184 # Output the properly-formatted token. | 223 # Output the properly-formatted token. |
| 185 print FormatToken(VERSION, signature, token_data) | 224 print FormatToken(VERSION, signature, token_data) |
| 186 | 225 |
| 187 if __name__ == "__main__": | 226 if __name__ == "__main__": |
| 188 main() | 227 main() |
| OLD | NEW |