summaryrefslogtreecommitdiffstats
path: root/tools/origin_trials/generate_token.py
blob: 54e61c6448254458b3f3ce28824791bc6defac48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/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}(?<!-)$", re.IGNORECASE)

def HostnameFromArg(arg):
  """Determines whether a string represents a valid hostname.

  Returns the canonical hostname if its argument is valid, or None otherwise.
  """
  if not arg or len(arg) > 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()