#!/usr/bin/env python # Copyright (c) 2016 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Utility for generating experimental API tokens usage: generate_token.py [-h] [--key-file KEY_FILE] [--expire-days EXPIRE_DAYS | --expire-timestamp EXPIRE_TIMESTAMP] origin trial_name Run "generate_token.py -h" for more help on usage. """ import argparse import base64 import re import os import sys import time import urlparse script_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) import ed25519 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, # no longer than 63 ASCII characters) DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(? 255: return None if arg[-1] == ".": arg = arg[:-1] if all(DNS_LABEL_REGEX.match(label) for label in arg.split(".")): return arg.lower() def OriginFromArg(arg): """Constructs the origin for the token from a command line argument. Returns None if this is not possible (neither a valid hostname nor a valid origin URL was provided.) """ # Does it look like a hostname? hostname = HostnameFromArg(arg) if hostname: return "https://" + hostname + ":443" # If not, try to construct an origin URL from the argument origin = urlparse.urlparse(arg) if not origin or not origin.scheme or not origin.netloc: raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) # HTTPS or HTTP only if origin.scheme not in ('https','http'): raise argparse.ArgumentTypeError("%s does not use a recognized URL scheme" % arg) # Add default port if it is not specified try: port = origin.port except ValueError: raise argparse.ArgumentTypeError("%s is not a hostname or a URL" % arg) if not port: port = {"https": 443, "http": 80}[origin.scheme] # Strip any extra components and return the origin URL: return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) def ExpiryFromArgs(args): if args.expire_timestamp: return int(args.expire_timestamp) return (int(time.time()) + (int(args.expire_days) * 86400)) def GenerateTokenData(origin, api_name, expiry): return "{0}|{1}|{2}".format(origin, api_name, expiry) def Sign(private_key, data): return ed25519.signature(data, private_key[:32], private_key[32:]) def FormatToken(version, signature, data): return version + "|" + base64.b64encode(signature) + "|" + data def main(): parser = argparse.ArgumentParser( description="Generate tokens for enabling experimental APIs") parser.add_argument("origin", help="Origin for which to enable the API. This can be " "either a hostname (default scheme HTTPS, default " "port 443) or a URL.", type=OriginFromArg) parser.add_argument("trial_name", help="Feature to enable. The current list of " "experimental feature trials can be found in " "RuntimeFeatures.in") parser.add_argument("--key-file", help="Ed25519 private key file to sign the token with", default="eftest.key") expiry_group = parser.add_mutually_exclusive_group() expiry_group.add_argument("--expire-days", help="Days from now when the token should exipire", type=int, default=42) expiry_group.add_argument("--expire-timestamp", help="Exact time (seconds since 1970-01-01 " "00:00:00 UTC) when the token should exipire", type=int) args = parser.parse_args() expiry = ExpiryFromArgs(args) key_file = open(os.path.expanduser(args.key_file), mode="rb") private_key = key_file.read(64) # Validate that the key file read was a proper Ed25519 key -- running the # publickey method on the first half of the key should return the second # half. if (len(private_key) < 64 or ed25519.publickey(private_key[:32]) != private_key[32:]): print("Unable to use the specified private key file.") sys.exit(1) token_data = GenerateTokenData(args.origin, args.trial_name, expiry) signature = Sign(private_key, token_data) # Verify that that the signature is correct before printing it. try: ed25519.checkvalid(signature, token_data, private_key[32:]) except Exception, exc: print "There was an error generating the signature." print "(The original error was: %s)" % exc sys.exit(1) # Output a properly-formatted token. Version 1 is hard-coded, as it is # the only defined token version. print FormatToken("1", signature, token_data) if __name__ == "__main__": main()