| #!/usr/bin/env python |
| # Copyright (c) 2017 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 validating and inspecting origin trial tokens |
| |
| usage: check_token.py [-h] [--use-chrome-key | |
| --use-test-key | |
| --private-key-file KEY_FILE] |
| "base64-encoded token" |
| |
| Run "check_token.py -h" for more help on usage. |
| """ |
| import argparse |
| import base64 |
| from datetime import datetime |
| import json |
| import os |
| import struct |
| import sys |
| import time |
| |
| script_dir = os.path.dirname(os.path.realpath(__file__)) |
| sys.path.insert(0, os.path.join(script_dir, 'third_party', 'ed25519')) |
| import ed25519 |
| |
| # Version is a 1-byte field at offset 0. |
| # - To support version-dependent formats, the version number must be the first |
| # first part of the token. |
| VERSION_OFFSET = 0 |
| VERSION_SIZE = 1 |
| |
| # These constants define the Version 2 field sizes and offsets. |
| # Contents are: version|signature|payload length|payload |
| SIGNATURE_OFFSET = VERSION_OFFSET + VERSION_SIZE |
| SIGNATURE_SIZE = 64 |
| PAYLOAD_LENGTH_OFFSET = SIGNATURE_OFFSET + SIGNATURE_SIZE |
| PAYLOAD_LENGTH_SIZE = 4 |
| PAYLOAD_OFFSET = PAYLOAD_LENGTH_OFFSET + PAYLOAD_LENGTH_SIZE |
| |
| # This script only supports Version 2 tokens. |
| VERSION2 = "\x02" |
| |
| # Chrome public key, used by default to validate signatures |
| # - Copied from chrome/common/origin_trials/chrome_origin_trial_policy.cc |
| CHROME_PUBLIC_KEY = [ |
| 0x7c, 0xc4, 0xb8, 0x9a, 0x93, 0xba, 0x6e, 0xe2, 0xd0, 0xfd, 0x03, |
| 0x1d, 0xfb, 0x32, 0x66, 0xc7, 0x3b, 0x72, 0xfd, 0x54, 0x3a, 0x07, |
| 0x51, 0x14, 0x66, 0xaa, 0x02, 0x53, 0x4e, 0x33, 0xa1, 0x15, |
| ] |
| |
| # Default key file, relative to script_dir. |
| DEFAULT_KEY_FILE = 'eftest.key' |
| |
| |
| class OverrideKeyFileAction(argparse.Action): |
| def __init__(self, option_strings, dest, **kwargs): |
| super(OverrideKeyFileAction, self).__init__( |
| option_strings, dest, **kwargs) |
| def __call__(self, parser, namespace, values, option_string=None): |
| setattr(namespace, "use_chrome_key", None) |
| setattr(namespace, self.dest, values) |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Inspect origin trial tokens") |
| parser.add_argument("token", |
| help="Token to be checked (must be Base64 encoded)") |
| |
| key_group = parser.add_mutually_exclusive_group() |
| key_group.add_argument("--use-chrome-key", |
| help="Validate token using the real Chrome public key", |
| dest="use_chrome_key", |
| action="store_true") |
| key_group.add_argument("--use-test-key", |
| help="Validate token using the eftest.key", |
| dest="use_chrome_key", |
| action="store_false") |
| key_group.add_argument("--key-file", |
| help="Ed25519 private key file to validate the token", |
| dest="key_file", |
| action=OverrideKeyFileAction) |
| parser.set_defaults(use_chrome_key=False) |
| |
| args = parser.parse_args() |
| |
| # Figure out which public key to use: Chrome, test key (default option), or |
| # key file provided on command line. |
| public_key = None |
| private_key_file = None |
| if (args.use_chrome_key is None): |
| private_key_file = args.key_file |
| else: |
| if (args.use_chrome_key): |
| public_key = "".join(chr(x) for x in CHROME_PUBLIC_KEY) |
| else: |
| # Use the test key, relative to this script. |
| private_key_file = os.path.join(script_dir, DEFAULT_KEY_FILE) |
| |
| # If not using the Chrome public key, extract the public key from either the |
| # test key file, or the private key file provided on the command line. |
| if public_key is None: |
| try: |
| key_file = open(os.path.expanduser(private_key_file), mode="rb") |
| except IOError as exc: |
| print "Unable to open key file: %s" % private_key_file |
| print "(%s)" % exc |
| sys.exit(1) |
| |
| 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) |
| |
| public_key = private_key[32:] |
| |
| try: |
| token_contents = base64.b64decode(args.token) |
| except TypeError as exc: |
| print "Error decoding the token (%s)" % exc |
| sys.exit(1) |
| |
| |
| # Only version 2 currently supported. |
| if (len(token_contents) < (VERSION_OFFSET + VERSION_SIZE)): |
| print "Token is malformed - too short." |
| sys.exit(1) |
| |
| version = token_contents[VERSION_OFFSET:(VERSION_OFFSET + VERSION_SIZE)] |
| if (version != VERSION2): |
| # Convert the version string to a number |
| version_number = 0 |
| for x in version: |
| version_number <<= 8 |
| version_number += ord(x) |
| print "Token has wrong version: %d" % version_number |
| sys.exit(1) |
| |
| # Token must be large enough to contain a version, signature, and payload |
| # length. |
| minimum_token_length = PAYLOAD_LENGTH_OFFSET + PAYLOAD_LENGTH_SIZE |
| if (len(token_contents) < minimum_token_length): |
| print "Token is malformed - too short: %d bytes, minimum is %d" % \ |
| (len(token_contents), minimum_token_length) |
| sys.exit(1) |
| |
| # Extract the length of the signed data (Big-endian). |
| # (unpack returns a tuple). |
| payload_length = struct.unpack_from(">I", token_contents, |
| PAYLOAD_LENGTH_OFFSET)[0] |
| |
| # Validate that the stated length matches the actual payload length. |
| actual_payload_length = len(token_contents) - PAYLOAD_OFFSET |
| if (payload_length != actual_payload_length): |
| print "Token is %d bytes, expected %d" % (actual_payload_length, |
| payload_length) |
| sys.exit(1) |
| |
| # Extract the version-specific contents of the token. |
| # Contents are: version|signature|payload length|payload |
| signature = token_contents[SIGNATURE_OFFSET:PAYLOAD_LENGTH_OFFSET] |
| |
| # The data which is covered by the signature is (version + length + payload). |
| signed_data = version + token_contents[PAYLOAD_LENGTH_OFFSET:] |
| |
| # Validate the signature on the data. |
| try: |
| ed25519.checkvalid(signature, signed_data, public_key) |
| except Exception as exc: |
| print "Signature invalid (%s)" % exc |
| sys.exit(1) |
| |
| try: |
| payload = token_contents[PAYLOAD_OFFSET:].decode('utf-8') |
| except UnicodeError as exc: |
| print "Unable to decode token contents (%s)" % exc |
| sys.exit(1) |
| |
| try: |
| token_data = json.loads(payload) |
| except Exception as exc: |
| print "Unable to parse payload (%s)" % exc |
| print "Payload: %s" % payload |
| sys.exit(1) |
| |
| print |
| print "Token data: %s" % token_data |
| print |
| |
| # Extract the required fields |
| for field in ["origin", "feature", "expiry"]: |
| if not token_data.has_key(field): |
| print "Token is missing required field: %s" % field |
| sys.exit(1) |
| |
| origin = token_data["origin"] |
| trial_name = token_data["feature"] |
| expiry = token_data["expiry"] |
| |
| # Extract the optional fields |
| is_subdomain = token_data.get("isSubdomain") |
| |
| # Output the token details |
| print "Token details:" |
| print " Origin: %s" % origin |
| print " Is Subdomain: %s" % is_subdomain |
| print " Feature: %s" % 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 |
| |
| if __name__ == "__main__": |
| main() |