diff --git a/examples/andytoshi b/examples/andytoshi new file mode 100644 index 0000000..1d17ac9 --- /dev/null +++ b/examples/andytoshi @@ -0,0 +1 @@ +This is andytoshi on 2017-05-16 21:30 UTC \ No newline at end of file diff --git a/examples/andytoshi.ots b/examples/andytoshi.ots new file mode 100644 index 0000000..15b9c5c Binary files /dev/null and b/examples/andytoshi.ots differ diff --git a/opentimestamps/core/op.py b/opentimestamps/core/op.py index b67a2b7..d351d25 100644 --- a/opentimestamps/core/op.py +++ b/opentimestamps/core/op.py @@ -347,3 +347,6 @@ def _do_op_call(self, msg): r = sha3.keccak_256(bytes(msg)).digest() assert len(r) == self.DIGEST_LENGTH return r + +from opentimestamps.core.secp256k1 import OpSecp256k1Commitment + diff --git a/opentimestamps/core/secp256k1.py b/opentimestamps/core/secp256k1.py new file mode 100644 index 0000000..58cfcc5 --- /dev/null +++ b/opentimestamps/core/secp256k1.py @@ -0,0 +1,146 @@ +# Copyright (C) 2017 The OpenTimestamps developers +# +# This file is part of python-opentimestamps. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-opentimestamps including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +import hashlib + +from opentimestamps.core.op import UnaryOp, MsgValueError + +@UnaryOp._register_op +class OpSecp256k1Commitment(UnaryOp): + """Map (P || commit) -> [P + sha256(P||commit)G]_x for a given secp256k1 point P + + This is a unary op rather than a binary op to allow timestamps to also + timestamp the point itself; in the event of an ECC break this might be + relevant. Such a break would not affect the integrity of the commitment, + but knowledge of the underlying key may be interesting in its own right. + """ + TAG = b'\x09' + TAG_NAME = 'secp256k1commitment' + + def _do_op_call(self, msg): + if len(msg) < 33: + raise MsgValueError("Missing secp256k1 point") + + pt = Point.decode(msg[0:33]) + assert(pt.encode() == msg[0:33]) + + hasher = hashlib.sha256() + hasher.update(msg[0:33]) + hasher.update(msg[33:]) + tweak = int.from_bytes(hasher.digest(), 'big') + tweak_pt = SECP256K1_GEN.scalar_mul(tweak) + final_pt = pt.add(tweak_pt) + return final_pt.x.to_bytes(32, 'big') + + +## What follows is a lot of inefficient but explicit secp256k1 math +class Point(object): + inf = True + x = 0 + y = 0 + + def __init__(self, x=0, y=0): + self.x = x + self.y = y + if x == 0 and y == 0: + self.inf = True + else: + self.inf = False + + def __repr__(self): + if self.inf: + return "Point(infinity)" + else: + return "Point(%x, %x)" % (self.x, self.y) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.inf == True and other.inf == True) or\ + (self.inf == False and other.inf == False and self.x == other.x and self.y == other.y) + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + @staticmethod + def decode(data): + if len(data) != 33 or (data[0] != 2 and data[0] != 3): + raise MsgValueError("Incorrectly formatted public key") + + x = int.from_bytes(data[1:], 'big') + if x >= SECP256K1_P: + raise MsgValueError("out of range x coordinate for secp256k1 point") + + ysqr = (x ** 3 + 7) % SECP256K1_P + y = psqrt(ysqr) + if pow(y, 2, SECP256K1_P) != ysqr: + raise MsgValueError("invalid x coordinate for secp256k1 point") + + if y % 2 == 1 and data[0] == 2: + y = SECP256K1_P - y + if y % 2 == 0 and data[0] == 3: + y = SECP256K1_P - y + + return Point(x, y) + + def encode(self): + ret = bytearray(self.x.to_bytes(33, 'big')) + assert(ret[0] == 0) + if self.y % 2 == 1: + ret[0] = 3 + else: + ret[0] = 2 + return ret + + def add(self, pt): + if self.inf: + return pt + if pt.inf: + return self + + if self.x == pt.x: + if self.y == SECP256K1_P - pt.y: + return Point() + else: + assert(self.y == pt.y) + lam = (3 * self.x ** 2 * pinv(2 * self.y)) % SECP256K1_P + else: + lam = ((pt.y - self.y) * pinv(pt.x - self.x)) % SECP256K1_P + + x3 = (lam ** 2 - self.x - pt.x) % SECP256K1_P + y3 = (self.y + lam * (x3 - self.x)) % SECP256K1_P + + return Point(x3, SECP256K1_P - y3) + + def scalar_mul(self, s): + ret = Point() + add = self + s = s % SECP256K1_N + while s > 0: + if s % 2 == 1: + ret = ret.add(add) + add = add.add(add) # add + s >>= 1 + return ret + +SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 +SECP256K1_GEN = Point(0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, + 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8) + +def pinv(x): + return pow(x, SECP256K1_P - 2, SECP256K1_P) + +def psqrt(x): + # using `>> 2` in place of `/ 4` keeps everything as an int rather than float + return pow(x, (SECP256K1_P + 1) >> 2, SECP256K1_P) + diff --git a/opentimestamps/tests/core/test_secp256k1.py b/opentimestamps/tests/core/test_secp256k1.py new file mode 100644 index 0000000..1b3de11 --- /dev/null +++ b/opentimestamps/tests/core/test_secp256k1.py @@ -0,0 +1,108 @@ +# Copyright (C) 2017 The OpenTimestamps developers +# +# This file is part of python-opentimestamps. +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-opentimestamps including this file, may be copied, +# modified, propagated, or distributed except according to the terms contained +# in the LICENSE file. + +import hashlib +import binascii +import unittest + +from opentimestamps.core.secp256k1 import * + +class Test_Secp256k1(unittest.TestCase): + def test_point_rt(self): + """Point encoding round trip""" + gen = SECP256K1_GEN + encode = gen.encode() + self.assertEqual(encode, binascii.unhexlify("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")) + gen2 = Point.decode(encode) + self.assertEqual(gen, gen2) + + def test_pinv(self): + """Field inversion mod p""" + self.assertEqual(pinv(1), 1) + self.assertEqual(pinv(2), 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18) + self.assertEqual(pinv(3), 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75) + self.assertEqual(2, pinv(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffffe18)) + self.assertEqual(3, pinv(0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9fffffd75)) + + def test_psqrt(self): + """Field square root mod p""" + self.assertEqual(psqrt(1), 1) + self.assertEqual(psqrt(2), 0x210c790573632359b1edb4302c117d8a132654692c3feeb7de3a86ac3f3b53f7) + self.assertEqual(psqrt(4), 2) + # may return the sqrt or its negative + self.assertEqual(psqrt(9), SECP256K1_P - 3) + self.assertEqual(psqrt(49), SECP256K1_P - 7) + + def test_point_add(self): + """Point adding and doubling""" + + inf = Point() + # P random chosen by dice roll + p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67, + 0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63) + # 2P + p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4) + # -2P + np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b) + # 3P + p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043, + 0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f) + + self.assertEqual(inf.add(inf), inf) + self.assertEqual(p1.add(inf), p1) + self.assertEqual(inf.add(p1), p1) + self.assertEqual(p1.add(p1), p2) + self.assertEqual(p1.add(p2), p3) + self.assertEqual(p2.add(p1), p3) + self.assertEqual(p3.add(np2), p1) + self.assertEqual(np2.add(p3), p1) + self.assertEqual(p2.add(np2), inf) + self.assertEqual(np2.add(p2), inf) + + def test_scalar_mul(self): + inf = Point() + # P random chosen by dice roll + p1 = Point(0x394867ad93f5c9612e8d8b7600443334026e648e365337d799190e845d649e67, + 0x0b84af9a00c1a55a7ac03917e59b21c68d1ffdf18720c3ad279077049cfaaf63) + # 2P + p2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0x2b3c07687787ff07ae312305f30481c451ae3b78d4f479a3b729615fedc040e4) + # -2P + np2 = Point(0x8e6575f6c759aea04a8ec65f61f71eba237a0af54292d41e3a4bac2efa922dea, + 0xd4c3f897887800f851cedcfa0cfb7e3bae51c4872b0b865c48d69e9f123fbb4b) + # 3P + p3 = Point(0x53dd5e495c7404790f9347470cc9c38ee239809c758f02ec04ba641ab3d0e043, + 0xd7a4f5e5bdf21000b1fe7216adbea92cb9917d8fea7b37628c1eddb409a5cd3f) + + # nP + n = 0xa91ce154dcab9adabe08cc1ee84ec3cd0f426bbc08a54a1c41bd25f2587caedd + pn = Point(0x9dc4b057a857ad2ef3535b4a207a7bfc9264e8fcacf718c895db7ead8d445b26, + 0x5af110ecb68636e5c352b69fc6348173932b83ca64587a91fd88af1446e33979) + + self.assertEqual(inf.scalar_mul(0), inf) + self.assertEqual(inf.scalar_mul(1000), inf) + self.assertEqual(inf.scalar_mul(-1), inf) + + self.assertEqual(p1.scalar_mul(0), inf) + self.assertEqual(p1.scalar_mul(1), p1) + self.assertEqual(p1.scalar_mul(2), p2) + self.assertEqual(p1.scalar_mul(-2), np2) + self.assertEqual(p2.scalar_mul(-1), np2) + self.assertEqual(p1.scalar_mul(3), p3) + + def test_op_signtocontract(self): + pt_encode = binascii.unhexlify("0308aec434612f56df3f02c4e678260424415882ebd3efc16d52e3f9c1e39afdb0") + msg = hashlib.sha256("This is andytoshi on 2017-05-16 21:30 UTC".encode()).digest() + result = binascii.unhexlify("d386ef692770fcecad43362cf541858662e4ebe31d3ad04d196f94168897947a") + self.assertEqual(OpSecp256k1Commitment()(pt_encode + msg), result) +