forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerate_token.py
executable file
·190 lines (160 loc) · 6.94 KB
/
generate_token.py
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/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]
[--is_subdomain | --no-subdomain]
origin trial_name
Run "generate_token.py -h" for more help on usage.
"""
import argparse
import base64
from datetime import datetime
import json
import re
import os
import struct
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)
# This script generates Version 2 tokens.
VERSION = "\x02"
# Default key file, relative to script_dir.
DEFAULT_KEY_FILE = 'eftest.key'
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 "." not in arg and arg != "localhost":
return None
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, is_subdomain, feature_name, expiry):
data = {"origin": origin,
"feature": feature_name,
"expiry": expiry}
if is_subdomain is not None:
data["isSubdomain"] = is_subdomain
return json.dumps(data).encode('utf-8')
def GenerateDataToSign(version, data):
return version + struct.pack(">I",len(data)) + data
def Sign(private_key, data):
return ed25519.signature(data, private_key[:32], private_key[32:])
def FormatToken(version, signature, data):
return base64.b64encode(version + signature +
struct.pack(">I",len(data)) + data)
def main():
default_key_file_absolute = os.path.join(script_dir, DEFAULT_KEY_FILE)
parser = argparse.ArgumentParser(
description="Generate tokens for enabling experimental features")
parser.add_argument("origin",
help="Origin for which to enable the feature. 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=default_key_file_absolute)
subdomain_group = parser.add_mutually_exclusive_group()
subdomain_group.add_argument("--is-subdomain",
help="Token will enable the feature for all "
"subdomains that match the origin",
dest="is_subdomain",
action="store_true")
subdomain_group.add_argument("--no-subdomain",
help="Token will only match the specified "
"origin (default behavior)",
dest="is_subdomain",
action="store_false")
parser.set_defaults(is_subdomain=None)
expiry_group = parser.add_mutually_exclusive_group()
expiry_group.add_argument("--expire-days",
help="Days from now when the token should expire",
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 expire",
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.is_subdomain,
args.trial_name, expiry)
data_to_sign = GenerateDataToSign(VERSION, token_data)
signature = Sign(private_key, data_to_sign)
# Verify that that the signature is correct before printing it.
try:
ed25519.checkvalid(signature, data_to_sign, 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 the token details
print "Token details:"
print " Origin: %s" % args.origin
print " Is Subdomain: %s" % args.is_subdomain
print " Feature: %s" % args.trial_name
print " Expiry: %d (%s UTC)" % (expiry, datetime.utcfromtimestamp(expiry))
print " Signature: %s" % ", ".join('0x%02x' % ord(x) for x in signature)
print " Signature (Base64): %s" % base64.b64encode(signature)
print
# Output the properly-formatted token.
print FormatToken(VERSION, signature, token_data)
if __name__ == "__main__":
main()