gnuk/tool/add_openpgp_authkey_from_gpgssh.py
Bertrand Jacquin 81d6945081 tool: switch to python3
python2 is no longer maintained upstream
2020-01-14 14:07:55 +09:00

190 lines
6.8 KiB
Python

"""
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()