#!/usr/bin/python3 # Copyright (C) 2012 Peter Todd # # This file is part of the OpenTimestamps Client. # # It is subject to the license terms in the LICENSE file found in the top-level # directory of this distribution and at http://opentimestamps.org # # No part of the OpenTimestamps Client, including this file, may be copied, # modified, propagated, or distributed except according to the terms contained # in the LICENSE file. import argparse import binascii import datetime import hashlib import io import json import logging import os import sys import opentimestamps from opentimestamps._internal import FileManager from opentimestamps.crypto import random_bytes from opentimestamps.dag import * from opentimestamps.io import * from opentimestamps.notary import * from opentimestamps.rpc import * import opentimestamps.client context = None parser = argparse.ArgumentParser(description="OpenTimestamps client.") def submit_command(args): if args.timestamp is None: if args.file is not '-' and not args.digest: args.timestamp = args.file + '.ots' else: parser.error("Must specify timestamp file (or stdout) if input is stdin or raw digest") if args.digest: import binascii try: digest = unhexlify(args.file) except binascii.Error as err: parser.error("Digest must be specified in hex notation: {}".format(str(err))) args.file = '-' else: # Don't let the user overwrite the input if os.path.realpath(args.file) == os.path.realpath(args.timestamp): parser.error("timestamp would overwrite file") with FileManager(args.file,'-') as file_fm: # FIXME: handle extending timestamps, IE, timestamp file already exists # look in v0.1 git history for how assert not os.path.exists(args.timestamp) with FileManager('-',args.timestamp) as stamp_fm: timestamp = None timestamp = TimestampFile(data_fd=file_fm.in_fd, out_fd=stamp_fm.out_fd) logging.info("Creating a new timestamp file") if args.digest: timestamp.digests['none'] = digest else: timestamp.add_algorithms('sha256d') digest = timestamp.digests['sha256d'] new_ops = [] if not args.no_nonce: nonce = random_bytes(32) hash_op = Hash(digest,nonce,parents=(digest,)) new_ops.append(hash_op) digest = hash_op server = OtsServer(args.server) reply_ops = server.post_digest(digest=hexlify(digest)) reply_ops = [Op.from_primitives(op) for op in reply_ops] new_ops.extend(reply_ops) timestamp.dag.update(new_ops) timestamp.write() stamp_fm.commit() logging.debug("New ops:\n" + json.dumps([op.to_primitives() for op in new_ops],indent=4)) def sign_command(args): raise NotImplementedError server = OtsServer(args.server) notary = PGPNotary(identity=args.fingerprint) notary.canonicalize_identity() merkle_tip_ops = server.get_merkle_tip() logging.debug(json.dumps(json_serialize(merkle_tip_ops),indent=4)) sig = notary.sign(merkle_tip_ops[-1].digest,int(time.time()*1000000)) verify_op = Verify(inputs=(merkle_tip_ops[-1],),signature=sig) merkle_tip_ops.append(verify_op) r = server.post_verification(ops=merkle_tip_ops) logging.debug(json.dumps(json_serialize(r),indent=4)) logging.info('Signed digest %s for server %s' % (binascii.hexlify(merkle_tip_ops[-2].digest),server.url)) def complete_command(args): with FileManager(args.timestamp,args.output) as fm: timestamp_file = TimestampFile(in_fd=fm.in_fd,out_fd=fm.out_fd) new_sigs = [] new_ops = [] old_ops = set(timestamp_file.dag) server = OtsServer(args.server) for op in timestamp_file.dag: if args.server in op.metadata: (ops,sigs) = server.get_path(hexlify(op), args.notary) ops = [Op.from_primitives(op) for op in ops] sigs = [Signature.from_primitives(sig) for sig in sigs] new_sigs.extend(sigs) new_ops.extend(ops) timestamp_file.dag.update(new_ops) timestamp_file.signatures.update(new_sigs) if new_ops or new_sigs: timestamp_file.write() fm.commit() new_ops = set(new_ops).difference(old_ops) logging.debug("Got new ops from server:\n %s", json.dumps([Op.to_primitives(op) for op in new_ops], indent=4)) new_sigs = set(new_sigs).difference(timestamp_file.signatures) for sig in new_sigs: timestamp = datetime.datetime.fromtimestamp(sig.timestamp) logging.info('New signature from {}:{} with timestamp {}'.format( sig.method, sig.identity, timestamp.isoformat(' '))) else: logging.info('No new signatures found.') def jsondump_command(args): with FileManager(args.timestamp,'-') as fm: timestamp_file = TimestampFile(in_fd=fm.in_fd,out_fd=None) print(json.dumps(timestamp_file.to_primitives(), indent=4)) def verify_command(args): data_filename = args.data if args.data is None: data_filename = '-' with FileManager(data_filename,'-') as fm_data,\ FileManager(args.timestamp,'-') as fm_timestamp: data_fd = None if args.data is not None: data_fd = fm_data.in_fd timestamp = TimestampFile(data_fd=data_fd, in_fd=fm_timestamp.in_fd, out_fd=None) if args.data is not None: timestamp.verify_data() print('Data in {} matches timestamp'.format(args.data)) else: print('Data NOT checked') timestamp.verify_consistency() for sig in timestamp.signatures: try: sig.verify(context=context) except SignatureVerificationError: # FIXME: need more detail here... pass else: # FIXME: need better time formatting print('GOOD signature from {}:{} with timestamp {}'.format( sig.method, sig.identity, datetime.datetime.fromtimestamp(sig.timestamp))) def getsourcecode_command(args): server = OtsServer(args.server) sourcecode_url = server.get_sourcecode() print("%s - %s" % (args.server,sourcecode_url)) parser.add_argument("--version",action="version",version=opentimestamps.implementation_identifier) parser.add_argument("-q","--quiet",action="count",default=0, help="Be more quiet.") parser.add_argument("-v","--verbose",action="count",default=0, help="Be more verbose. Both -v and -q may be used multiple times.") parser.add_argument("-c","--config",action="store",default="~/.opentimestamps/config", help="Location of config file. Defaults to: ~/.opentimestamps/config") parser.add_argument("-s","--server",action="store",default="http://pool.opentimestamps.org", help="Specify the server. Defaults to: http://pool.opentimestamps.org") subparsers = parser.add_subparsers(title='Subcommands', description='All operations are done through subcommands:') # ----- sign ----- parser_sign = subparsers.add_parser('sign',#aliases=['s'], help='Sign a calendar.') parser_sign.set_defaults(cmd_func=sign_command) parser_sign.add_argument("fingerprint",action="store", help="PGP fingerprint of the signing key") # ----- submit ----- parser_submit = subparsers.add_parser('submit',#aliases=['s'], help='Submit a file to be timestamped.') parser_submit.set_defaults(cmd_func=submit_command) parser_submit.add_argument("--no-nonce",action="store_true",default=False, help="Don't use a nonce in the hash calculation. Note: this means that"\ " the server knows what data you have submitted.") parser_submit.add_argument("-d", "--digest", action="store_true", default=False, help="Interpret file as a raw hex digest of hashing a file to generate the digest.") parser_submit.add_argument("file",action="store", help="The file to timestamp. (or - for standard input)") parser_submit.add_argument("timestamp",action="store",nargs='?', help="Optionally specify filename (or - for standard output) to write timestamp to. Required if"\ " input is standard input. Defaults to .ots") # ----- complete ----- parser_complete = subparsers.add_parser('complete',#aliases=['x'], help='Complete a timestamp.') parser_complete.set_defaults(cmd_func=complete_command) parser_complete.add_argument("--notary",action="store",default="*:*", help="Notary to complete to") parser_complete.add_argument("timestamp",action="store", help="Filename of timestamp to complete. (or - for standard input)") parser_complete.add_argument("output",action="store",nargs='?', help="Write to this filename (or - for standard output) instead of modifying existing timestamp.") # ----- jsondump ----- parser_jsondump = subparsers.add_parser('jsondump',#aliases=['x'], help='JSON dump a timestamp') parser_jsondump.set_defaults(cmd_func=jsondump_command) parser_jsondump.add_argument("timestamp",action="store", help="Filename (or - for standard input)") # ----- verify ----- parser_verify = subparsers.add_parser('verify',#aliases=['v'], aliases not supported in 2.7! help="Verify a timestamp") parser_verify.set_defaults(cmd_func=verify_command) parser_verify.add_argument("timestamp",action="store", help="Filename of timestamp") parser_verify.add_argument("data",action="store",nargs='?', help="Optional data to verify (or - for standard input)") # ----- getsourcecode ----- parser_getsourcecode = subparsers.add_parser('getsourcecode', help='Ask the specified server(s) for where to obtain source code. (AGPL license compliance)') parser_getsourcecode.set_defaults(cmd_func=getsourcecode_command) args = parser.parse_args() args.verbosity = args.verbose - args.quiet if args.verbosity == 0: logging.root.setLevel(logging.INFO) elif args.verbosity > 0: logging.root.setLevel(logging.DEBUG) elif args.verbosity == -1: logging.root.setLevel(logging.WARNING) elif args.verbosity < -1: logging.root.setLevel(logging.ERROR) context = opentimestamps.client.Context(args.config) args.cmd_func(args)