key.c: Add hashing and comparison for `Key' objects.
[catacomb-python] / t / t-key.py
1 ### -*-python-*-
2 ###
3 ### Testing key-management functionality
4 ###
5 ### (c) 2019 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Python interface to Catacomb.
11 ###
12 ### Catacomb/Python is free software: you can redistribute it and/or
13 ### modify it under the terms of the GNU General Public License as
14 ### published by the Free Software Foundation; either version 2 of the
15 ### License, or (at your option) any later version.
16 ###
17 ### Catacomb/Python is distributed in the hope that it will be useful, but
18 ### WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 ### General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with Catacomb/Python. If not, write to the Free Software
24 ### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
25 ### USA.
26
27 ###--------------------------------------------------------------------------
28 ### Imported modules.
29
30 import catacomb as C
31 import sys as SYS
32 import unittest as U
33 import testutils as T
34 import time as TM
35
36 ###--------------------------------------------------------------------------
37 class TestKeyError (U.TestCase):
38
39 def test_keyerror(me):
40
41 try: C.KeyFile("notexist", C.KOPEN_NOFILE).newkey(1, "foo")
42 except C.KeyError: e = SYS.exc_info()[1]
43 else: me.fail("expected `catacomb.KeyError'")
44 me.assertEqual(e.err, C.KERR_READONLY)
45 me.assertEqual(e.errstring, "Key file is read-only")
46 me.assertEqual(e.args, (C.KERR_READONLY,))
47 me.assertEqual(str(e),
48 "KERR_READONLY (%d): Key file is read-only" %
49 C.KERR_READONLY)
50
51 me.assertRaises(TypeError, C.KeyError)
52 token = ["TOKEN"]
53 e = C.KeyError(C.KERR_DUPID, token)
54 me.assertEqual(e.err, C.KERR_DUPID)
55 me.assertEqual(e.errstring, "Key id already exists")
56 me.assertEqual(e.args, (C.KERR_DUPID, token))
57
58 ###--------------------------------------------------------------------------
59 class TestKeyFile (U.TestCase):
60
61 def test_keyring(me):
62
63 kf = C.KeyFile("t/keyring")
64
65 ## Check basic attributes.
66 me.assertEqual(kf.name, "t/keyring")
67 me.assertEqual(kf.modifiedp, False)
68 me.assertEqual(kf.writep, False)
69 me.assertEqual(kf.filep, False)
70
71 ## Check enumeration.
72 me.assertEqual(set(k.type for k in T.itervalues(kf)),
73 set(["rsa", "ec", "ec-param", "twofish"]))
74 me.assertEqual(len(kf), 4)
75
76 ## Start with `rsa'.
77 k = kf.bytag("ron")
78 me.assertEqual(k.type, "rsa")
79 me.assertEqual(k.id, 0x8599dbab)
80 me.assertEqual(type(k.data), C.KeyDataStructured)
81 me.assertEqual(set(k.data), set(["e", "n", "private"]))
82 priv = k.data["private"]
83 me.assertEqual(type(priv), C.KeyDataEncrypted)
84 me.assertRaises(C.KeyError, priv.unlock, T.bin("wrong secret"))
85 priv = priv.unlock(T.bin("very secret"))
86 me.assertEqual(type(priv), C.KeyDataStructured)
87 me.assertEqual(set(priv),
88 set(["p", "q", "d", "d-mod-p", "d-mod-q", "q-inv"]))
89 me.assertEqual(k.data["n"].mp, priv["p"].mp*priv["q"].mp)
90
91 ## This key has an attribute. Poke about at them.
92 a = k.attr
93 me.assertEqual(len(a), 1)
94 me.assertEqual(set(a), set(["attr"]))
95 me.assertEqual(a["attr"], "value")
96 me.assertRaises(KeyError, lambda: a["notexist"])
97 me.assertEqual(a.get("attr"), "value")
98 me.assertEqual(a.get("notexist"), None)
99
100 ## Check fingerprinting while we're here.
101 for filter in ["-secret", "none"]:
102 h = C.sha256(); me.assertTrue(k.fingerprint(h, filter)); fp0 = h.done()
103 h = C.sha256()
104 h.hash(T.bin("catacomb-key-fingerprint:")) \
105 .hashu32(k.id) \
106 .hashbuf8(T.bin(k.type))
107 h.hash(k.data.encode(filter))
108 for a in sorted(T.iterkeys(k.attr)):
109 h.hashbuf8(T.bin(a)).hashbuf16(T.bin(k.attr[a]))
110 fp1 = h.done()
111 me.assertEqual(fp0, fp1)
112
113 ## Try `ec-param'. This should be fairly easy.
114 k = kf["ec-param"]
115 me.assertEqual(k.tag, None)
116 me.assertEqual(k.id, 0x4a4e1ee7)
117 me.assertEqual(type(k.data), C.KeyDataStructured)
118 me.assertEqual(set(k.data), set(["curve"]))
119 curve = k.data["curve"]
120 me.assertEqual(type(curve), C.KeyDataString)
121 me.assertEqual(curve.str, "nist-p256")
122
123 ## Check qualified-tag lookups.
124 me.assertRaises(C.KeyError, kf.qtag, "notexist.curve")
125 me.assertRaises(C.KeyError, kf.qtag, "ec-param.notexist")
126 t, k, kd = kf.qtag("ec-param.curve")
127 me.assertEqual(t, "4a4e1ee7:ec-param.curve")
128 me.assertEqual(k.type, "ec-param")
129 me.assertEqual(type(kd), C.KeyDataString)
130 me.assertEqual(kd.str, "nist-p256")
131
132 ## Try `ec'. A little trickier.
133 k = kf.bytype("ec")
134 me.assertEqual(k.tag, None)
135 me.assertEqual(k.id, 0xbd761d35)
136 me.assertEqual(type(k.data), C.KeyDataStructured)
137 me.assertEqual(set(k.data), set(["curve", "p", "private"]))
138 curve = k.data["curve"]
139 me.assertEqual(type(curve), C.KeyDataString)
140 me.assertEqual(curve.str, "nist-p256")
141 einfo = C.eccurves[curve.str]
142 me.assertEqual(type(k.data["p"]), C.KeyDataECPt)
143 X = k.data["p"].ecpt
144 priv = k.data["private"]
145 me.assertEqual(type(priv), C.KeyDataEncrypted)
146 me.assertRaises(C.KeyError, priv.unlock, T.bin("wrong secret"))
147 priv = priv.unlock(T.bin("super secret"))
148 me.assertEqual(type(priv), C.KeyDataStructured)
149 me.assertEqual(set(priv), set(["x"]))
150 x = priv["x"].mp
151 me.assertEqual(x*einfo.G, X)
152
153 ## Finish with `twofish'.
154 k = kf.byid(0x60090be2)
155 me.assertEqual(k.tag, None)
156 me.assertEqual(k.type, "twofish")
157 me.assertEqual(type(k.data), C.KeyDataEncrypted)
158 me.assertRaises(C.KeyError, k.data.unlock, T.bin("wrong secret"))
159 kd = k.data.unlock(T.bin("not secret"))
160 me.assertEqual(type(kd), C.KeyDataBinary)
161 me.assertEqual(kd.bin, C.bytes("d337b98eea24425826df202a6a3d1ef8"
162 "377b71923fe1179451564776da29bb84"))
163
164 ## Check unsuccessful searches.
165 me.assertRaises(KeyError, lambda: kf["notexist"])
166 me.assertEqual(kf.bytag("notexist"), None)
167 me.assertEqual(kf.bytag(12345), None)
168 me.assertEqual(kf.bytype("notexist"), None)
169 me.assertRaises(TypeError, kf.bytype, 12345)
170 me.assertRaises(C.KeyError, kf.byid, 0x12345678)
171
172 me.assertRaises(C.KeyError, kf.mergeline, "nowhere", 2, "")
173
174 ## The keyring should be readonly.
175 me.assertRaises(C.KeyError, kf.newkey, 0x12345678, "fail")
176 me.assertRaises(C.KeyError, setattr, k, "tag", "foo")
177 me.assertRaises(C.KeyError, delattr, k, "tag")
178 me.assertRaises(C.KeyError, kf.mergeline, "notexist", 1,
179 "22222222:test integer,public:32519164 forever forever -")
180 me.assertRaises(C.KeyError, setattr, k, "data", C.KeyDataString("foo"))
181
182 def test_keywrite(me):
183 kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
184 me.assertEqual(kf.modifiedp, False)
185 now = int(TM.time())
186 exp = now + 86400
187
188 k = kf.newkey(0x11111111, "first", exp)
189 me.assertEqual(kf.modifiedp, True)
190
191 me.assertEqual(kf[0x11111111].id, 0x11111111)
192 me.assertEqual(k.exptime, exp)
193 me.assertEqual(k.deltime, exp)
194 me.assertRaises(ValueError, setattr, k, "deltime", C.KEXP_FOREVER)
195 k.exptime = exp + 5
196 me.assertEqual(k.data.str, "<unset>")
197 n = 9876543210
198 k.data = C.KeyDataMP(n)
199 me.assertEqual(k.data.mp, n)
200 me.assertEqual(k.comment, None)
201 c = ";; just a test"
202 k.comment = c
203 me.assertEqual(k.comment, c)
204 k.comment = None
205 me.assertEqual(k.comment, None)
206 k.comment = c
207 me.assertEqual(k.comment, c)
208 del k.comment
209 me.assertEqual(k.comment, None)
210
211 kf.mergeline("notexist", 1,
212 "22222222:test integer,public:32519164 forever forever -")
213
214 ###--------------------------------------------------------------------------
215
216 def keydata_equalp(kd0, kd1):
217 if type(kd0) is not type(kd1): return False
218 elif type(kd0) is C.KeyDataBinary: return kd0.bin == kd1.bin
219 elif type(kd0) is C.KeyDataMP: return kd0.mp == kd1.mp
220 elif type(kd0) is C.KeyDataEncrypted: return kd0.ct == kd1.ct
221 elif type(kd0) is C.KeyDataECPt: return kd0.ecpt == kd1.ecpt
222 elif type(kd0) is C.KeyDataString: return kd0.str == kd1.str
223 elif type(kd0) is C.KeyDataStructured:
224 if len(kd0) != len(kd1): return False
225 for t, v0 in T.iteritems(kd0):
226 try: v1 = kd1[t]
227 except KeyError: return False
228 if not keydata_equalp(v0, v1): return False
229 return True
230 else:
231 raise SystemError("unexpected keydata type")
232
233 class TestKeyData (U.TestCase):
234
235 def test_flags(me):
236 me.assertEqual(C.KeyData.readflags("none"), (0, 0, ""))
237 me.assertEqual(C.KeyData.readflags("ec,public:..."),
238 (C.KENC_EC | C.KCAT_PUB,
239 C.KF_ENCMASK | C.KF_CATMASK,
240 ":..."))
241 me.assertEqual(C.KeyData.readflags("int,burn"),
242 (C.KENC_MP | C.KF_BURN, C.KF_ENCMASK | C.KF_BURN, ""))
243 me.assertRaises(C.KeyError, C.KeyData.readflags, "int,burn?")
244 me.assertRaises(C.KeyError, C.KeyData.readflags, "int,ec")
245 me.assertRaises(C.KeyError, C.KeyData.readflags, "snork")
246 me.assertEqual(C.KeyData.writeflags(0), "binary,symmetric")
247 me.assertEqual(C.KeyData.writeflags(C.KENC_EC | C.KCAT_PUB), "ec,public")
248
249 def test_misc(me):
250 kd = C.KeyDataStructured({ "a": C.KeyDataString("foo", "public"),
251 "b": C.KeyDataMP(12345, "private"),
252 "c": C.KeyDataString("bar", "public") })
253
254 kd2 = kd.copy()
255 me.assertEqual(type(kd2), C.KeyDataStructured)
256 me.assertEqual(set(T.iterkeys(kd2)), set(["a", "b", "c"]))
257
258 kd2 = C.KeyDataMP(12345, C.KCAT_PRIV).copy("private")
259
260 kd2 = kd.copy("-secret")
261 me.assertEqual(type(kd2), C.KeyDataStructured)
262 me.assertEqual(set(T.iterkeys(kd2)), set(["a", "c"]))
263
264 kd2 = kd.copy((0, C.KF_NONSECRET))
265 me.assertEqual(type(kd2), C.KeyDataStructured)
266 me.assertEqual(set(T.iterkeys(kd2)), set(["b"]))
267
268 def check_encode(me, kd):
269 me.assertTrue(keydata_equalp(C.KeyData.decode(kd.encode()), kd))
270 kd1, tail = C.KeyData.read(kd.write())
271 me.assertEqual(tail, "")
272 me.assertTrue(keydata_equalp(kd, kd1))
273
274 def test_bin(me):
275 rng = T.detrand("kd-bin")
276 by = rng.block(16)
277 kd = C.KeyDataBinary(by, "symm,burn")
278 me.assertEqual(kd.bin, by)
279 me.check_encode(kd)
280
281 def test_mp(me):
282 rng = T.detrand("kd-mp")
283 x = rng.mp(128)
284 kd = C.KeyDataMP(x, "symm,burn")
285 me.assertEqual(kd.mp, x)
286 me.check_encode(kd)
287
288 def test_string(me):
289 s = "some random string"
290 kd = C.KeyDataString(s, "symm,burn")
291 me.assertEqual(kd.str, s)
292 me.check_encode(kd)
293
294 def test_enc(me):
295 rng = T.detrand("kd-enc")
296 ct = rng.block(16)
297 kd = C.KeyDataEncrypted(ct, "symm")
298 me.assertEqual(kd.ct, ct)
299 me.check_encode(kd)
300
301 def test_ecpt(me):
302 rng = T.detrand("kd-ec")
303 Q = C.ECPt(rng.mp(128), rng.mp(128))
304 kd = C.KeyDataECPt(Q, "symm,burn")
305 me.assertEqual(kd.ecpt, Q)
306 me.check_encode(kd)
307
308 def test_struct(me):
309 rng = T.detrand("kd-struct")
310 kd = C.KeyDataStructured({ "a": C.KeyDataString("a"),
311 "b": C.KeyDataString("b"),
312 "c": C.KeyDataString("c"),
313 "d": C.KeyDataString("d") })
314 for i in ["a", "b", "c", "d"]: me.assertEqual(kd[i].str, i)
315 me.assertEqual(len(kd), 4)
316 me.check_encode(kd)
317 me.assertRaises(TypeError, C.KeyDataStructured, { "a": "a" })
318
319 ###--------------------------------------------------------------------------
320 ### Mappings.
321
322 class TestKeyFileMapping (T.ImmutableMappingTextMixin):
323 def _mkkey(me, i): return i
324 def _getkey(me, k): return k
325 def _getvalue(me, v): return v.data.mp
326
327 def test_keyfile(me):
328 kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
329 model = {}
330 for i in [1, 2, 3]:
331 model[i] = 100 + i
332 kf.newkey(i, "k#%d" % i).data = C.KeyDataMP(100 + i)
333
334 me.assertEqual(kf[1], kf[1])
335
336 me.check_immutable_mapping(kf, model)
337
338 class TestKeyAttrMapping (T.MutableMappingTestMixin):
339
340 def test_attrmap(me):
341 def mkmap():
342 kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
343 k = kf.newkey(0x12345678, "test-key")
344 return k.attr
345 me.check_mapping(mkmap)
346
347 a = mkmap()
348 me.assertRaises(TypeError, a.update, { 3: 3, 4: 5 })
349
350 ###----- That's all, folks --------------------------------------------------
351
352 if __name__ == "__main__": U.main()