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 |
chasej
2016/04/07 15:52:30
Nit: There are a couple usages of "API" instead of
| |
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 origin trial_name | 11 origin trial_name |
12 | 12 |
13 Run "generate_token.py -h" for more help on usage. | 13 Run "generate_token.py -h" for more help on usage. |
14 """ | 14 """ |
15 import argparse | 15 import argparse |
16 import base64 | 16 import base64 |
17 import json | |
17 import re | 18 import re |
18 import os | 19 import os |
20 import struct | |
19 import sys | 21 import sys |
20 import time | 22 import time |
21 import urlparse | 23 import urlparse |
22 | 24 |
23 script_dir = os.path.dirname(os.path.realpath(__file__)) | 25 script_dir = os.path.dirname(os.path.realpath(__file__)) |
24 sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) | 26 sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) |
25 import ed25519 | 27 import ed25519 |
26 | 28 |
27 | 29 |
28 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, | 30 # Matches a valid DNS name label (alphanumeric plus hyphens, except at the ends, |
29 # no longer than 63 ASCII characters) | 31 # no longer than 63 ASCII characters) |
30 DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE) | 32 DNS_LABEL_REGEX = re.compile(r"^(?!-)[a-z\d-]{1,63}(?<!-)$", re.IGNORECASE) |
31 | 33 |
34 # This script generates Version 1 tokens | |
35 VERSION = "\x01" | |
36 | |
32 def HostnameFromArg(arg): | 37 def HostnameFromArg(arg): |
33 """Determines whether a string represents a valid hostname. | 38 """Determines whether a string represents a valid hostname. |
34 | 39 |
35 Returns the canonical hostname if its argument is valid, or None otherwise. | 40 Returns the canonical hostname if its argument is valid, or None otherwise. |
36 """ | 41 """ |
37 if not arg or len(arg) > 255: | 42 if not arg or len(arg) > 255: |
38 return None | 43 return None |
39 if arg[-1] == ".": | 44 if arg[-1] == ".": |
40 arg = arg[:-1] | 45 arg = arg[:-1] |
41 if all(DNS_LABEL_REGEX.match(label) for label in arg.split(".")): | 46 if all(DNS_LABEL_REGEX.match(label) for label in arg.split(".")): |
(...skipping 25 matching lines...) Expand all Loading... | |
67 if not port: | 72 if not port: |
68 port = {"https": 443, "http": 80}[origin.scheme] | 73 port = {"https": 443, "http": 80}[origin.scheme] |
69 # Strip any extra components and return the origin URL: | 74 # Strip any extra components and return the origin URL: |
70 return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) | 75 return "{0}://{1}:{2}".format(origin.scheme, origin.hostname, port) |
71 | 76 |
72 def ExpiryFromArgs(args): | 77 def ExpiryFromArgs(args): |
73 if args.expire_timestamp: | 78 if args.expire_timestamp: |
74 return int(args.expire_timestamp) | 79 return int(args.expire_timestamp) |
75 return (int(time.time()) + (int(args.expire_days) * 86400)) | 80 return (int(time.time()) + (int(args.expire_days) * 86400)) |
76 | 81 |
77 def GenerateTokenData(origin, api_name, expiry): | 82 def GenerateTokenData(origin, api_name, expiry): |
chasej
2016/04/07 15:52:30
Ditto for "API" vs "feature".
| |
78 return "{0}|{1}|{2}".format(origin, api_name, expiry) | 83 return json.dumps({"origin": origin, |
84 "feature": api_name, | |
85 "expiry": expiry}).encode('utf-8') | |
86 | |
87 def GenerateDataToSign(version, data): | |
88 return version + struct.pack(">I",len(data)) + data | |
79 | 89 |
80 def Sign(private_key, data): | 90 def Sign(private_key, data): |
81 return ed25519.signature(data, private_key[:32], private_key[32:]) | 91 return ed25519.signature(data, private_key[:32], private_key[32:]) |
82 | 92 |
83 def FormatToken(version, signature, data): | 93 def FormatToken(version, signature, data): |
84 return version + "|" + base64.b64encode(signature) + "|" + data | 94 return base64.b64encode(version + signature + |
95 struct.pack(">I",len(data)) + data) | |
chasej
2016/04/07 15:52:30
Nit: Could pull out the logic to pack the length +
| |
85 | 96 |
86 def main(): | 97 def main(): |
87 parser = argparse.ArgumentParser( | 98 parser = argparse.ArgumentParser( |
88 description="Generate tokens for enabling experimental APIs") | 99 description="Generate tokens for enabling experimental APIs") |
chasej
2016/04/07 15:52:30
Ditto for "API" vs "feature".
| |
89 parser.add_argument("origin", | 100 parser.add_argument("origin", |
90 help="Origin for which to enable the API. This can be " | 101 help="Origin for which to enable the API. This can be " |
chasej
2016/04/07 15:52:30
Ditto for "API" vs "feature".
| |
91 "either a hostname (default scheme HTTPS, default " | 102 "either a hostname (default scheme HTTPS, default " |
92 "port 443) or a URL.", | 103 "port 443) or a URL.", |
93 type=OriginFromArg) | 104 type=OriginFromArg) |
94 parser.add_argument("trial_name", | 105 parser.add_argument("trial_name", |
95 help="Feature to enable. The current list of " | 106 help="Feature to enable. The current list of " |
96 "experimental feature trials can be found in " | 107 "experimental feature trials can be found in " |
97 "RuntimeFeatures.in") | 108 "RuntimeFeatures.in") |
98 parser.add_argument("--key-file", | 109 parser.add_argument("--key-file", |
99 help="Ed25519 private key file to sign the token with", | 110 help="Ed25519 private key file to sign the token with", |
100 default="eftest.key") | 111 default="eftest.key") |
(...skipping 15 matching lines...) Expand all Loading... | |
116 | 127 |
117 # Validate that the key file read was a proper Ed25519 key -- running the | 128 # 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 | 129 # publickey method on the first half of the key should return the second |
119 # half. | 130 # half. |
120 if (len(private_key) < 64 or | 131 if (len(private_key) < 64 or |
121 ed25519.publickey(private_key[:32]) != private_key[32:]): | 132 ed25519.publickey(private_key[:32]) != private_key[32:]): |
122 print("Unable to use the specified private key file.") | 133 print("Unable to use the specified private key file.") |
123 sys.exit(1) | 134 sys.exit(1) |
124 | 135 |
125 token_data = GenerateTokenData(args.origin, args.trial_name, expiry) | 136 token_data = GenerateTokenData(args.origin, args.trial_name, expiry) |
126 signature = Sign(private_key, token_data) | 137 data_to_sign = GenerateDataToSign(VERSION, token_data) |
138 signature = Sign(private_key, data_to_sign) | |
chasej
2016/04/07 15:52:30
Nit: Add a comment linking to the design doc somew
| |
127 | 139 |
128 # Verify that that the signature is correct before printing it. | 140 # Verify that that the signature is correct before printing it. |
129 try: | 141 try: |
130 ed25519.checkvalid(signature, token_data, private_key[32:]) | 142 ed25519.checkvalid(signature, data_to_sign, private_key[32:]) |
131 except Exception, exc: | 143 except Exception, exc: |
132 print "There was an error generating the signature." | 144 print "There was an error generating the signature." |
133 print "(The original error was: %s)" % exc | 145 print "(The original error was: %s)" % exc |
134 sys.exit(1) | 146 sys.exit(1) |
135 | 147 |
136 # Output a properly-formatted token. Version 1 is hard-coded, as it is | 148 # Output a properly-formatted token. Version 1 is hard-coded, as it is |
137 # the only defined token version. | 149 # the only defined token version. |
138 print FormatToken("1", signature, token_data) | 150 print FormatToken(VERSION, signature, token_data) |
139 | 151 |
140 if __name__ == "__main__": | 152 if __name__ == "__main__": |
141 main() | 153 main() |
OLD | NEW |