| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 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 |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Utility for generating experimental API tokens |
| 7 |
| 8 usage: generate_token.py [-h] [--key-file KEY_FILE] |
| 9 [--expire-days EXPIRE_DAYS | |
| 10 --expire-timestamp EXPIRE_TIMESTAMP] |
| 11 origin trial_name |
| 12 |
| 13 Run "generate_token.py -h" for more help on usage. |
| 14 """ |
| 15 import argparse |
| 16 import base64 |
| 17 import re |
| 18 import os |
| 19 import sys |
| 20 import time |
| 21 import urlparse |
| 22 |
| 23 script_dir = os.path.dirname(os.path.realpath(__file__)) |
| 24 sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) |
| 25 import ed25519 |
| 26 |
| 27 |
| 28 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, |
| 29 # no longer than 63 ASCII characters) |
| 30 DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE) |
| 31 |
| 32 def HostnameFromArg(arg): |
| 33 """Determines whether a string represents a valid hostname. |
| 34 |
| 35 Returns the canonical hostname if its argument is valid, or None otherwise. |
| 36 """ |
| 37 if not arg or len(arg) > 255: |
| 38 return None |
| 39 if arg[-1] == ".": |
| 40 arg = arg[:-1] |
| 41 if all(DNS_LABEL_REGEX.match(label) for label in arg.split(".")): |
| 42 return arg.lower() |
| 43 |
| 44 def OriginFromArg(arg): |
| 45 """Constructs the origin for the token from a command line argument. |
| 46 |
| 47 Returns None if this is not possible (neither a valid hostname nor a |
| 48 valid origin URL was provided.) |
| 49 """ |
| 50 # Does it look like a hostname? |
| 51 hostname = HostnameFromArg(arg) |
| 52 if hostname: |
| 53 return "https://" + hostname + ":443" |
| 54 # If not, try to construct an origin URL from the argument |
| 55 origin = urlparse.urlparse(arg) |
| 56 if not origin or not origin.scheme or not origin.netloc: |
| 57 raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) |
| 58 # HTTPS or HTTP only |
| 59 if origin.scheme not in ('https','http'): |
| 60 raise argparse.ArgumentTypeError("%s does not use a recognized URL scheme" % |
| 61 arg) |
| 62 # Add default port if it is not specified |
| 63 try: |
| 64 port = origin.port |
| 65 except ValueError: |
| 66 raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) |
| 67 if not port: |
| 68 port = {"https": 443, "http": 80}[origin.scheme] |
| 69 # Strip any extra components and return the origin URL: |
| 70 return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) |
| 71 |
| 72 def ExpiryFromArgs(args): |
| 73 if args.expire_timestamp: |
| 74 return int(args.expire_timestamp) |
| 75 return (int(time.time()) + (int(args.expire_days) * 86400)) |
| 76 |
| 77 def GenerateTokenData(origin, api_name, expiry): |
| 78 return "{0}|{1}|{2}".format(origin, api_name, expiry) |
| 79 |
| 80 def Sign(private_key, data): |
| 81 return ed25519.signature(data, private_key[:32], private_key[32:]) |
| 82 |
| 83 def FormatToken(version, signature, data): |
| 84 return version + "|" + base64.b64encode(signature) + "|" + data |
| 85 |
| 86 def main(): |
| 87 parser = argparse.ArgumentParser( |
| 88 description="Generate tokens for enabling experimental APIs") |
| 89 parser.add_argument("origin", |
| 90 help="Origin for which to enable the API. This can be " |
| 91 "either a hostname (default scheme HTTPS, default " |
| 92 "port 443) or a URL.", |
| 93 type=OriginFromArg) |
| 94 parser.add_argument("trial_name", |
| 95 help="Feature to enable. The current list of " |
| 96 "experimental feature trials can be found in " |
| 97 "RuntimeFeatures.in") |
| 98 parser.add_argument("--key-file", |
| 99 help="Ed25519 private key file to sign the token with", |
| 100 default="eftest.key") |
| 101 expiry_group = parser.add_mutually_exclusive_group() |
| 102 expiry_group.add_argument("--expire-days", |
| 103 help="Days from now when the token should exipire", |
| 104 type=int, |
| 105 default=42) |
| 106 expiry_group.add_argument("--expire-timestamp", |
| 107 help="Exact time (seconds since 1970-01-01 " |
| 108 "00:00:00 UTC) when the token should exipire", |
| 109 type=int) |
| 110 |
| 111 args = parser.parse_args() |
| 112 expiry = ExpiryFromArgs(args) |
| 113 |
| 114 key_file = open(os.path.expanduser(args.key_file), mode="rb") |
| 115 private_key = key_file.read(64) |
| 116 |
| 117 # Validate that the key file read was a proper Ed25519 key -- running the |
| 118 # publickey method on the first half of the key should return the second |
| 119 # half. |
| 120 if (len(private_key) < 64 or |
| 121 ed25519.publickey(private_key[:32]) != private_key[32:]): |
| 122 print("Unable to use the specified private key file.") |
| 123 sys.exit(1) |
| 124 |
| 125 token_data = GenerateTokenData(args.origin, args.trial_name, expiry) |
| 126 signature = Sign(private_key, token_data) |
| 127 |
| 128 # Verify that that the signature is correct before printing it. |
| 129 try: |
| 130 ed25519.checkvalid(signature, token_data, private_key[32:]) |
| 131 except Exception, exc: |
| 132 print "There was an error generating the signature." |
| 133 print "(The original error was: %s)" % exc |
| 134 sys.exit(1) |
| 135 |
| 136 # Output a properly-formatted token. Version 1 is hard-coded, as it is |
| 137 # the only defined token version. |
| 138 print FormatToken("1", signature, token_data) |
| 139 |
| 140 if __name__ == "__main__": |
| 141 main() |
| OLD | NEW |