Add new tool/add_openpgp_authkey_from_gpgssh.py
authorNIIBE Yutaka <gniibe@fsij.org>
Thu, 5 Jun 2014 04:02:05 +0000 (13:02 +0900)
committerNIIBE Yutaka <gniibe@fsij.org>
Thu, 5 Jun 2014 04:02:05 +0000 (13:02 +0900)
ChangeLog
tool/add_openpgp_authkey_from_gpgssh.py [new file with mode: 0644]

index f4c57b3..b99e6d0 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2014-06-05  Niibe Yutaka  <gniibe@fsij.org>
+
+       * tool/add_openpgp_authkey_from_gpgssh.py: New.
+
 2014-04-17  Niibe Yutaka  <gniibe@fsij.org>
 
        * src/muladd_256.h (MULADD_256_ASM): New.
diff --git a/tool/add_openpgp_authkey_from_gpgssh.py b/tool/add_openpgp_authkey_from_gpgssh.py
new file mode 100644 (file)
index 0000000..1eb80de
--- /dev/null
@@ -0,0 +1,189 @@
+"""
+add_openpgp_authkey_from_gpgssh.py
+
+Copyright (C) 2014 Free Software Initiative of Japan
+Author: NIIBE Yutaka <gniibe@fsij.org>
+
+This file is a part of Gnuk, a GnuPG USB Token implementation.
+
+Gnuk is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Gnuk is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
+License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from gpg_agent import gpg_agent
+from binascii import hexlify, unhexlify
+from sexp import sexp
+from time import time
+from struct import pack, unpack
+from hashlib import sha1, sha256
+import re
+
+ALGO_RSA=1
+DIGEST_SHA256=8
+OPENPGP_VERSION=4
+
+def count_bits(mpi_bytes):
+    return ord(mpi_bytes[0]).bit_length()+(len(mpi_bytes)-1)*8
+
+class rsa_key(object):
+    def __init__(self, timestamp, n, e):
+        self.__timestamp = timestamp
+        self.__n = n
+        self.__e = e
+
+    def hash_pubkey_key(self, md):
+        hl = 6 + len(self.__n) + 2 + len(self.__e) + 2
+        md.update(pack('>BHBLB', 0x99, hl, 4, self.__timestamp, ALGO_RSA))
+        md.update(pack('>H', count_bits(self.__n)) + self.__n)
+        md.update(pack('>H', count_bits(self.__e)) + self.__e)
+
+    def compose_public_subkey_packet(self):
+        psp = pack('>BLB', OPENPGP_VERSION, self.__timestamp, ALGO_RSA)
+        psp += pack('>H', count_bits(self.__n)) + self.__n
+        psp += pack('>H', count_bits(self.__e)) + self.__e
+        return '\xB9' + pack('>H', len(psp)) + psp
+
+    def compute_keygrip(self):
+        md = sha1("\x00" + self.__n)
+        return md.digest()
+
+    def compute_fpr(self):
+        md = sha1()
+        self.hash_pubkey_key(md)
+        return md.digest()
+
+def compose_binding_signature_packet(g, primary_key, subkey, sig_timestamp):
+    # Binding signature packet consists of: subpackets of hashed and unhashed
+    # (1) hashed subpacket: this subpacket is the target to calculate digest
+    sig_subp_hashed = pack('>B', 5) + '\x02' + pack('>L', sig_timestamp)
+    sig_subp_hashed += pack('>B', 2) + '\x1b' + '\x20'     # Usage AUTH
+    # (2) unhashed subpacket: this subpacket is _not_ the target for digest
+    sig_subp_unhashed = pack('>B', 9) + '\x10' + primary_key.compute_fpr()[-8:]
+    #
+    md = sha256()
+    primary_key.hash_pubkey_key(md)
+    subkey.hash_pubkey_key(md)
+    # Start building binding signature packet, starting OPENPGP_VERSION...
+    sigp = pack('>BBBB', OPENPGP_VERSION, 0x18, ALGO_RSA, DIGEST_SHA256)
+    sigp += pack('>H', len(sig_subp_hashed)) + sig_subp_hashed
+    # And feed it to digest calculator
+    md.update(sigp)
+    md.update(pack('>BBL', OPENPGP_VERSION, 0xff, len(sig_subp_hashed)+6))
+    digest = md.digest()
+    # Then, add unhashed subpacket and first two bytes of digest
+    sigp += pack('>H', len(sig_subp_unhashed)) + sig_subp_unhashed
+    sigp += digest[0:2]
+    print("Digest 2-byte: %s" % hexlify(digest[0:2]))
+    # Ask signing to this digest by the corresponding secret key to PRIMARY_KEY
+    signature = do_sign(g, primary_key, DIGEST_SHA256, digest)
+    # Then, add the signature to the binding signature packet
+    sigp += pack('>H', count_bits(signature)) + signature
+    # Prepending header, it's the binding signature packet
+    return '\x89' + pack('>H', len(sigp)) + sigp
+
+def build_rsakey_from_ssh_key_under_gpg_agent(g, timestamp=None):
+    # (1) Get the list of available key specifying '--with-ssh'
+    g.send_command("KEYINFO --list --with-ssh --data\n")
+    kl_str = g.get_response()
+    kl_str = kl_str[0:-1]
+    kl = kl_str.split('\n')
+    # (2) Select SSH key(s)
+    kl_ssh = [kg for kg in kl if re.search("S$", kg)] # Select SSH key
+    # (3) Use the first entry of the list (in future, use all???)
+    print("KEYINFO: %s" % kl_ssh[0])
+    # KG: The keygrip of key in question
+    kg = kl_ssh[0].split(' ')[0]
+    # By READKEY command, get the public key information of KG
+    g.send_command("READKEY %s\n" % kg)
+    pubkey_info_str = g.get_response()
+    # The information is in SEXP format, extract N and E
+    s = sexp(pubkey_info_str)
+    if s[0] != 'public-key':
+        print s
+        exit(1)
+    rsa = s[1]
+    if rsa[0] != 'rsa':
+        print rsa
+        exit(1)
+    n_x = rsa[1]
+    if n_x[0] != 'n':
+        print n_x
+        exit(1)
+    n_byte_str = n_x[1]
+    while n_byte_str[0] == '\x00':
+        n_byte_str = n_byte_str[1:]
+    n = n_byte_str
+    e_x = rsa[2]
+    if e_x[0] != 'e':
+        print e_x
+        exit(1)
+    e = e_x[1]
+    if not timestamp:
+        timestamp = int(time()) 
+    # Compose our RSA_KEY by TIMESTAMP, N, and E
+    return rsa_key(timestamp,n,e)
+
+BUFSIZE=1024
+def build_rsakey_from_openpgp_file(filename):
+    f = open(filename, "rb")
+    openpgp_bytes = f.read(BUFSIZE)
+    f.close()
+    header_tag, packet_len, version, timestamp, algo, n_bitlen = \
+        unpack('>BHBLBH', openpgp_bytes[:11])
+    if header_tag != 0x99:
+        print ("openpgp: 0x99 expected (0x%02x)" % header_tag)
+        exit(1)
+    n_len = (n_bitlen + 7) / 8
+    n = openpgp_bytes[11:11+n_len]
+    e_bitlen = unpack('>H', openpgp_bytes[11+n_len:11+n_len+2])[0]
+    e_len = (e_bitlen + 7) / 8
+    e = openpgp_bytes[11+n_len+2:11+n_len+2+e_len]
+    return rsa_key(timestamp,n,e)
+
+def do_sign(g, pubkey, digest_algo, digest):
+    g.send_command('SIGKEY %s\n' % hexlify(pubkey.compute_keygrip()))
+    if digest_algo == DIGEST_SHA256:
+        g.send_command('SETHASH --hash=sha256 %s\n' % hexlify(digest))
+    else:
+        raise('Unknown digest algorithm', digest_algo)
+    g.send_command('PKSIGN\n')
+    sig_result_str = g.get_response()
+    sig_sexp = sexp(sig_result_str)  # [ "sig-val" [ "rsa" [ "s" "xxx" ] ] ]
+    return sig_sexp[1][1][1]
+
+import sys
+
+if __name__ == '__main__':
+    #
+    filename = sys.argv[1]
+    # Connect to GPG-agent:
+    g = gpg_agent()
+    print("GPG-agent says: %s" % g.read_line())
+    #
+    primary_key = build_rsakey_from_openpgp_file(filename)
+    print("Primary key fingerprint: %s" % hexlify(primary_key.compute_fpr()))
+    print("Primary keygrip: %s" % hexlify(primary_key.compute_keygrip()))
+    #
+    subkey = build_rsakey_from_ssh_key_under_gpg_agent(g)
+    print("Subkey fingerprint: %s" % hexlify(subkey.compute_fpr()))
+    print("Subkey keygrip: %s" % hexlify(subkey.compute_keygrip()))
+    #
+    openpgp_subkey_packet = subkey.compose_public_subkey_packet()
+    openpgp_sig_packet = compose_binding_signature_packet(g, primary_key, subkey, int(time()))
+    # Query to GPG-agent finished
+    g.close()
+    # Append OpenPGP packets to file
+    f = open(filename, "ab")
+    f.write(openpgp_subkey_packet)
+    f.write(openpgp_sig_packet)
+    f.close()