1#!/usr/bin/env python
2#
3# Copyright 2017 John-Mark Gurney.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27#
28
29'''This is a wrapper around Ed448-Goldilocks.
30
31This module does not follow the standard Crypto modular method
32of signing due to the complexity of integration w/ the library, and
33that things should be more simple to use.'''
34
35__author__ = 'John-Mark Gurney'
36__copyright__ = 'Copyright 2017 John-Mark Gurney'''
37__license__ = 'BSD'
38__version__ = '0.1'
39__status__ = 'alpha'
40
41import array
42import os
43import os.path
44import sys
45import unittest
46import warnings
47
48from ctypes import *
49
50try:
51	_dname = os.path.dirname(__file__)
52	if not _dname:
53		_dname = '.'
54	_path = os.path.join(_dname, 'libdecaf.so')
55	decaf = CDLL(_path)
56except OSError as e: # pragma: no cover
57	import warnings
58	warnings.warn('libdecaf.so not installed.')
59	raise ImportError(str(e))
60
61DECAF_EDDSA_448_PUBLIC_BYTES = 57
62DECAF_EDDSA_448_PRIVATE_BYTES = DECAF_EDDSA_448_PUBLIC_BYTES
63DECAF_EDDSA_448_SIGNATURE_BYTES = DECAF_EDDSA_448_PUBLIC_BYTES + DECAF_EDDSA_448_PRIVATE_BYTES
64
65# Types
66
67ed448_pubkey_t = c_uint8 * DECAF_EDDSA_448_PUBLIC_BYTES
68ed448_privkey_t = c_uint8 * DECAF_EDDSA_448_PRIVATE_BYTES
69ed448_sig_t = c_uint8 * DECAF_EDDSA_448_SIGNATURE_BYTES
70
71c_uint8_p = POINTER(c_uint8)
72
73decaf_error_t = c_int
74
75# Data
76try:
77	DECAF_ED448_NO_CONTEXT = POINTER(c_uint8).in_dll(decaf, 'DECAF_ED448_NO_CONTEXT')
78except ValueError:
79	DECAF_ED448_NO_CONTEXT = None
80
81funs = {
82	'decaf_ed448_derive_public_key': (None, [ ed448_pubkey_t, ed448_privkey_t]),
83	'decaf_ed448_sign': (None, [ ed448_sig_t, ed448_privkey_t, ed448_pubkey_t, c_uint8_p, c_size_t, c_uint8, c_uint8_p, c_uint8 ]),
84	'decaf_ed448_verify': (decaf_error_t, [ ed448_sig_t, ed448_pubkey_t, c_uint8_p, c_size_t, c_uint8, c_uint8_p, c_uint8 ]),
85}
86
87for i in funs:
88	f = getattr(decaf, i)
89	f.restype, f.argtypes = funs[i]
90
91def _makeba(s):
92	r = (c_ubyte * len(s))()
93	r[:] = array.array('B', s)
94	return r
95
96def _makestr(a):
97	# XXX - because python3 sucks, and unittest doesn't offer
98	# ability to silence stupid warnings, hide the tostring
99	# DeprecationWarning.
100	with warnings.catch_warnings():
101		warnings.simplefilter('ignore')
102		return array.array('B', a).tostring()
103
104
105def _ed448_privkey():
106	return _makeba(os.urandom(DECAF_EDDSA_448_PRIVATE_BYTES))
107
108class EDDSA448(object):
109	_PUBLIC_SIZE = DECAF_EDDSA_448_PUBLIC_BYTES
110	_PRIVATE_SIZE = DECAF_EDDSA_448_PRIVATE_BYTES
111	_SIG_SIZE = DECAF_EDDSA_448_SIGNATURE_BYTES
112
113	def __init__(self, priv=None, pub=None):
114		'''Generate a new sign or verify object.  At least one
115		of priv or pub MUST be specified.
116
117		If pub is not specified, it will be generated from priv.
118		If both are specified, there is no verification that pub
119		is the public key for priv.
120
121		It is recommended that you use the generate method to
122		generate a new key.'''
123
124		if priv is None and pub is None:
125			raise ValueError('at least one of priv or pub must be specified.')
126
127		if priv is not None:
128			try:
129				priv = _makeba(priv)
130			except Exception as e:
131				raise ValueError('priv must be a byte string', e)
132
133		self._priv = priv
134
135		if self._priv is not None and pub is None:
136			self._pub = ed448_pubkey_t()
137			decaf.decaf_ed448_derive_public_key(self._pub, self._priv)
138		else:
139			self._pub = _makeba(pub)
140
141	@classmethod
142	def generate(cls):
143		'''Generate a signing object w/ a newly generated key.'''
144
145		return cls(priv=_ed448_privkey())
146
147	def has_private(self):
148		'''Returns True if object has private key.'''
149
150		return self._priv is not None
151
152	def public_key(self):
153		'''Returns a new object w/o the private key.  This new
154		object will have the public part and can be used for
155		verifying messages'''
156
157		return self.__class__(pub=self._pub)
158
159	def export_key(self, format):
160		'''Export the key.  The only format supported is 'raw'.
161
162		If has_private is True, then the private part will be
163		exported.  If it is False, then the public part will be.
164		There is no indication on the output if the key is
165		public or private.  It must be tracked independantly
166		of the data.'''
167
168		if format == 'raw':
169			if self._priv is None:
170				return _makestr(self._pub)
171			else:
172				return _makestr(self._priv)
173		else:
174			raise ValueError('unsupported format: %s' % repr(format))
175
176	@staticmethod
177	def _makectxargs(ctx):
178		if ctx is None:
179			ctxargs = (DECAF_ED448_NO_CONTEXT, 0)
180		else:
181			ctxargs = (_makeba(ctx), len(ctx))
182
183		return ctxargs
184
185	def sign(self, msg, ctx=None):
186		'''Returns a signature over the message.  Requires that has_private returns True.'''
187
188		sig = ed448_sig_t()
189		ctxargs = self._makectxargs(ctx)
190		decaf.decaf_ed448_sign(sig, self._priv, self._pub, _makeba(msg), len(msg), 0, *ctxargs)
191
192		return _makestr(sig)
193
194	def verify(self, sig, msg, ctx=None):
195		'''Raises an error if sig is not valid for msg.'''
196
197		_sig = ed448_sig_t()
198		_sig[:] = array.array('B', sig)
199		ctxargs = self._makectxargs(ctx)
200		if not decaf.decaf_ed448_verify(_sig, self._pub, _makeba(msg), len(msg), 0, *ctxargs):
201			raise ValueError('signature is not valid')
202
203def generate(curve='ed448'):
204	return EDDSA448.generate()
205
206class TestEd448(unittest.TestCase):
207	def test_init(self):
208		self.assertRaises(ValueError, EDDSA448)
209
210	def test_gen(self):
211		key = generate(curve='ed448')
212		self.assertIsInstance(key, EDDSA448)
213
214		self.assertTrue(key.has_private())
215
216		pubkey = key.public_key()
217		self.assertFalse(pubkey.has_private())
218
219	def test_keyexport(self):
220		# Generate key and export
221		key = generate(curve='ed448')
222		privkey = key.export_key('raw')
223
224		# Generate signature
225		message = b'sdlkfjsdf'
226		sig = key.sign(message)
227
228		# Verify that the key can be imported and verifies
229		key2 = EDDSA448(privkey)
230		key2.verify(sig, message)
231
232		# Export the public key
233		keypub = key.public_key()
234		pubkey = keypub.export_key('raw')
235
236		# Verify that the public key can be imported and verifies
237		key3 = EDDSA448(pub=pubkey)
238		key3.verify(sig, message)
239
240		self.assertRaises(ValueError, key.export_key, 'PEM')
241
242	def test_keyimportexport(self):
243		privkey = b'1' * DECAF_EDDSA_448_PRIVATE_BYTES
244		key = EDDSA448(privkey)
245
246		self.assertEqual(key.export_key(format='raw'), privkey)
247
248		key = EDDSA448(pub=b'1' * DECAF_EDDSA_448_PUBLIC_BYTES)
249
250		self.assertRaises(ValueError, EDDSA448, priv=u'1' * DECAF_EDDSA_448_PRIVATE_BYTES)
251
252	def test_sig(self):
253		key = generate()
254
255		message = b'this is a test message for signing'
256		sig = key.sign(message)
257
258		# Make sure sig is a string of bytes
259		self.assertIsInstance(sig, bytes)
260		self.assertEqual(len(sig), EDDSA448._SIG_SIZE)
261
262		# Make sure sig is valid
263		key.verify(sig, message)
264
265		# Make sure sig is valid for public only version
266		pubkey = key.public_key()
267		pubkey.verify(sig, message)
268
269		# Ensure that the wrong message fails
270		message = b'this is the wrong message'
271		self.assertRaises(ValueError, pubkey.verify, sig, message)
272
273	def test_ctx(self):
274		key = generate()
275
276		message = b'foobar'
277		ctx = b'contexta'
278		sig = key.sign(message, ctx)
279
280		# Make sure it verifies correctly
281		key.verify(sig, message, ctx)
282
283		# Make sure it fails w/o context
284		self.assertRaises(ValueError, key.verify, sig, message)
285
286		# Make sure it fails w/ invalid/different context
287		self.assertRaises(ValueError, key.verify, sig, message, ctx + b'a')
288
289class TestBasicLib(unittest.TestCase):
290	def test_basic(self):
291		priv = _ed448_privkey()
292		pub = ed448_pubkey_t()
293
294		decaf.decaf_ed448_derive_public_key(pub, priv)
295
296		message = b'this is a test message'
297
298		sig = ed448_sig_t()
299		decaf.decaf_ed448_sign(sig, priv, pub, _makeba(message), len(message), 0, None, 0)
300
301		r = decaf.decaf_ed448_verify(sig, pub, _makeba(message), len(message), 0, None, 0)
302		self.assertTrue(r)
303
304		message = b'aofeijseflj'
305		r = decaf.decaf_ed448_verify(sig, pub, _makeba(message), len(message), 0, None, 0)
306		self.assertFalse(r)
307