From 7576a1ebd03e1d7e68bd1701b8bff8159230fe19 Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Wed, 8 Dec 2021 09:55:37 +0100 Subject: add tooling for signing There's tools for key generation and conversion and there's tools for signing and verifying a tree leaf. Note that the leaf signing tools use the yet to be decided about SSH signing format, with message (ie signers checksum) being hashed with SHA-512 to match SSH tooling (ssh-keygen -Y). --- tools/libsigntools.py | 118 ++++++++++++++++++++++++++++++++++++++++++++ tools/sigsum-gensigkey.py | 21 ++++++++ tools/sigsum-sign-leaf.py | 61 +++++++++++++++++++++++ tools/sigsum-verify-leaf.py | 32 ++++++++++++ tools/sshkey2nacl.py | 20 ++++++++ 5 files changed, 252 insertions(+) create mode 100644 tools/libsigntools.py create mode 100755 tools/sigsum-gensigkey.py create mode 100755 tools/sigsum-sign-leaf.py create mode 100755 tools/sigsum-verify-leaf.py create mode 100755 tools/sshkey2nacl.py diff --git a/tools/libsigntools.py b/tools/libsigntools.py new file mode 100644 index 0000000..a3f0bcc --- /dev/null +++ b/tools/libsigntools.py @@ -0,0 +1,118 @@ +import sys +import struct +from hashlib import sha256, sha512 + +def checksum_stdin(hashalg='sha256'): + if hashalg == 'sha256': + d = sha256() + elif hashalg == 'sha512': + d = sha512() + else: + return None + + while True: + buf = sys.stdin.buffer.read() + if not buf: + break + d.update(buf) + + return d.digest() + +def ssh_to_sign(namespace, hashalg, checksum): + if hashalg == 'sha256': + hashlen = 32 + elif hashalg == 'sha512': + hashlen = 64 + else: + return None + s = struct.pack('!6sI{}sII6sI{}s'.format(len(namespace), hashlen), + b'SSHSIG', + len(namespace), bytes(namespace, 'ascii'), + 0, + 6, bytes(hashalg, 'ascii'), + hashlen, checksum) + return s + +# Adapted from https://stackoverflow.com/questions/65684414/how-to-use-ssh-keygen-ed25519-keys-for-encryption-in-python +# Author: LJHW +from base64 import b64decode +from nacl.encoding import RawEncoder +from nacl.signing import SigningKey, VerifyKey +class C25519: + # Adapted from https://gist.github.com/R-VdP/b7ac0106a4fd395ee1c37bfe6f552a36 sealing.py + # Author: Ramses https://github.com/R-VdP + __key_length = 32 + __private_key_signature = b'\x00\x00\x00\x40' + __public_key_signature = b'\x00\x00\x00\x20' + + @classmethod + def __bytes_after(cls, signature, length, bytestr): + start = bytestr.find(signature) + len(signature) + return bytestr[start:start+length] + + @classmethod + def __extract_signing_key(cls, private_data): + openssh_bytes = b64decode(private_data) + private_bytes = cls.__bytes_after( + cls.__private_key_signature, + cls.__key_length, + openssh_bytes + ) + signing_key = SigningKey(seed=private_bytes, encoder=RawEncoder) + return signing_key + + @classmethod + def __extract_verify_key(cls, public_data): + openssh_bytes = b64decode(public_data) + public_bytes = cls.__bytes_after( + cls.__public_key_signature, + cls.__key_length, + openssh_bytes + ) + verify_key = VerifyKey(key=public_bytes, encoder=RawEncoder) + return verify_key + + @classmethod + def __private_data_from_file(cls, file_name): + with open(file_name, 'r') as file: + contents = file.read() + contents = contents.split('\n') + private_data = '' + for line in contents: + if 'PRIVATE KEY' in line: + continue + if not line: + continue + private_data += line + return private_data + + @classmethod + def __public_data_from_file(cls, file_name): + with open(file_name, 'r') as file: + contents = file.read() + contents = contents.split(' ') + # assert contents[0] == 'ssh-ed25519' + public_data = contents[1].strip(' ') + return public_data + + @classmethod + def signingKey(cls, private_ed25519_file): + private_data = cls.__private_data_from_file(private_ed25519_file) + signing_key = cls.__extract_signing_key(private_data) + return signing_key + + @classmethod + def verifyKey(cls, public_ed25519_file): + public_data = cls.__public_data_from_file(public_ed25519_file) + verify_key = cls.__extract_verify_key(public_data) + return verify_key + + @classmethod + def privateKey(cls, private_ed25519_file): + signing_key = cls.signingKey(private_ed25519_file) + return signing_key.to_curve25519_private_key() + + @classmethod + def publicKey(cls, public_ed25519_file): + verify_key = cls.verifyKey(public_ed25519_file) + return verify_key.to_curve25519_public_key() diff --git a/tools/sigsum-gensigkey.py b/tools/sigsum-gensigkey.py new file mode 100755 index 0000000..3c74108 --- /dev/null +++ b/tools/sigsum-gensigkey.py @@ -0,0 +1,21 @@ +#! /usr/bin/env python3 + +import sys +import os +from stat import * +from nacl.encoding import HexEncoder +from nacl.signing import SigningKey + +def generate_and_store_sigkey(fn): + signing_key = SigningKey.generate() + verify_key = signing_key.verify_key + with open(fn, 'w') as f: + os.chmod(f.fileno(), S_IRUSR) + f.write(signing_key.encode(HexEncoder).decode('ascii') + '\n') + print(verify_key.encode(HexEncoder).decode('ascii')) + +def main(): + generate_and_store_sigkey(sys.argv[1]) + +if __name__ == '__main__': + main() diff --git a/tools/sigsum-sign-leaf.py b/tools/sigsum-sign-leaf.py new file mode 100755 index 0000000..19d7f5c --- /dev/null +++ b/tools/sigsum-sign-leaf.py @@ -0,0 +1,61 @@ +#! /usr/bin/env python3 + +# Input: skeyfile shard_hint [checksum] +# Output: tree_leaf signature +# Example: echo foo | ./sigsum-sign-leaf.py nacl.sk 1633039200 +# be70f92465c27bf412008f26fa953d06899c53fa9867f40d9c0a1d657b188c9631699954728c719cf6b3819c1343c6e9e454cd9d519a9bf96dad3cf4cd959c0a + +import struct, sys, binascii +from base64 import b64encode +from nacl.signing import VerifyKey, SigningKey +from nacl.encoding import HexEncoder +from libsigntools import checksum_stdin, ssh_to_sign + +alg = 'sha512' + +def ssh_blob(vk, sig, namespace): + vkdata = struct.pack('!I11sI32s', + 11, bytes('ssh-ed25519', 'ascii'), + 32, vk.encode()) + assert(len(vkdata) == 51) + + assert(len(sig) == 64) + sigdata = struct.pack('!I11sI64s', + 11, bytes('ssh-ed25519', 'ascii'), + 64, sig) + assert(len(sigdata) == 83) + + s = "-----BEGIN SSH SIGNATURE-----\n" + b = b64encode(struct.pack('!6sII51sI{}sII6sI83s'.format(len(namespace)), + b'SSHSIG', + 1, + 51, vkdata, + len(namespace), bytes(namespace, 'ascii'), + 0, + 6, bytes(alg, 'ascii'), + 83, sigdata)).decode('ascii') + while len(b) > 0: + s += b[:72] + '\n' + b = b[72:] + s += "-----END SSH SIGNATURE-----\n" + return s + +def main(): + keyfile = sys.argv[1] + shard_hint = int(sys.argv[2]) + if len(sys.argv) > 3: + checksum = bytes.fromhex(sys.argv[3]) + else: + checksum = checksum_stdin(hashalg=alg) + + with open(keyfile, 'r') as f: + signing_key = SigningKey(f.readline().strip(), encoder=HexEncoder) + namespace = 'tree_leaf:v0:{}@sigsum.org'.format(shard_hint) + signature = signing_key.sign(ssh_to_sign(namespace, alg, checksum)).signature + + print(binascii.hexlify(signature).decode('ascii')) + if False: + print(ssh_blob(signing_key.verify_key, signature, namespace)) + +if __name__ == '__main__': + main() diff --git a/tools/sigsum-verify-leaf.py b/tools/sigsum-verify-leaf.py new file mode 100755 index 0000000..d8a15fa --- /dev/null +++ b/tools/sigsum-verify-leaf.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python3 + +# Input: vkeyfile shard_hint signature [checksum] +# Example: echo foo | ./sigsum-verify-leaf.py nacl.vk 0 $(echo foo | ./sigsum-sign-leaf.py nacl.sk 0) +# OK + +import sys +from nacl.signing import VerifyKey +from nacl.encoding import HexEncoder +from libsigntools import checksum_stdin, ssh_to_sign + +alg = 'sha512' + +def main(): + keyfile = sys.argv[1] + shard_hint = int(sys.argv[2]) + sig = bytes.fromhex(sys.argv[3]) + + with open(keyfile, 'r') as f: + vkey = VerifyKey(f.readline().strip(), encoder=HexEncoder) + if len(sys.argv) > 4: + checksum = bytes.fromhex(sys.argv[4]) + else: + checksum = checksum_stdin(hashalg=alg) + + namespace = 'tree_leaf:v0:{}@sigsum.org'.format(shard_hint) + data = ssh_to_sign(namespace, alg, checksum) + vkey.verify(data, signature=sig) + print("OK") + +if __name__ == '__main__': + main() diff --git a/tools/sshkey2nacl.py b/tools/sshkey2nacl.py new file mode 100755 index 0000000..c109a3c --- /dev/null +++ b/tools/sshkey2nacl.py @@ -0,0 +1,20 @@ +#! /usr/bin/env python3 + +import sys +from libsigntools import C25519 +from nacl.encoding import HexEncoder + +def main(): + input_fn = sys.argv[1] + output_fn = sys.argv[2] + + sk = C25519.signingKey(input_fn) + with open('{}.sk'.format(output_fn), 'w') as f: + f.write(sk.encode(HexEncoder).decode('ascii')) + + vk = C25519.verifyKey('{}.pub'.format(input_fn)) + with open('{}.vk'.format(output_fn), 'w') as f: + f.write(vk.encode(HexEncoder).decode('ascii')) + +if __name__ == '__main__': + main() -- cgit v1.2.3