summaryrefslogtreecommitdiff
path: root/pkgsign
blob: c908ab2f7a0e30aee3c7ac58bb3baa52f88f8b2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#!/usr/bin/env python3.7
# -*- mode:python; coding:utf-8 -*-
#
# NAME
#     pkgsign -- tool for use with FreeBSD pkg-repo(8) that utilizes
#                ssh-agent for private key management when signing
#                repositories
#
# LICENSE
#     Copyright (c) 2011 lars  at oddbit   dot com
#     Copyright (c) 2021 anrxc at sysphere dot org
#
#     Permission is hereby granted, free of charge, to any person obtaining a copy
#     of this software and associated documentation files (the "Software"), to deal
#     in the Software without restriction, including without limitation the rights
#     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#     copies of the Software, and to permit persons to whom the Software is
#     furnished to do so, subject to the following conditions:
#
#     The above copyright notice and this permission notice shall be included in all
#     copies or substantial portions of the Software.
#
#     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#     SOFTWARE.
#
# SYNOPSIS
#     /usr/sbin/pkg repo /path/to/repository signing_command: pkgsign [FINGERPRINT]
#     /usr/sbin/pkg repo /path/to/repository signing_command: ssh signing-server pkgsign [FINGERPRINT]
#
# FILES
#     $HOME/.ssh/ssh-agent.info
#     $HOME/.gnupg/gpg-agent.info
#     /usr/local/etc/ssl/public/[FINGERPRINT].pub
#

import paramiko.agent
from sys import stdin
from sys import stdout
from os  import environ
from binascii import hexlify
from sys import argv as sysargv
from struct import unpack as strunpack


try:
    # For obtaining fingerprints from an agent
    _KDUMP = sysargv[1] == "--dump"  and 1
    # For experimenting with keys and signatures
    _DEBUG = sysargv[1] == "--debug" and 1
except IndexError:
    _KDUMP = 0
    _DEBUG = 0

try:
    if _KDUMP < 1:
        if _DEBUG > 0:
            _PKGID = stdin.readline().strip()
            _KEYID = sysargv[2]
        else:
            _PKGID = stdin.readline().strip()
            _KEYID = sysargv[1]
except IndexError:
    raise SystemExit("ERROR: key fingerprint missing from command line, aborting")


if "SSH_AUTH_SOCK" not in environ:
    import re
    ## gpg-agent untested, at some point in 2013 SSH support was broken
    ## for months and if such a thing is allowed to happen then best to
    ## rely on ssh-agent
    #agent_info  = open("%s/.gnupg/gpg-agent.info" % environ["HOME"], "r")
    agent_info   = open("%s/.ssh/ssh-agent.info" % environ["HOME"], "r")
    agent_format = re.compile("SSH_AUTH_SOCK=(?P<SOCK>[^;]+).*SSH_AGENT_PID=(?P<PID>\d+)", re.MULTILINE | re.DOTALL)

    match = agent_format.search(agent_info.read())
    agent_info.close()

    localenv = match.groupdict()
    environ["SSH_AGENT_PID"] = localenv["PID"]
    environ["SSH_AUTH_SOCK"] = localenv["SOCK"]


if _DEBUG > 0:
    print("INFO: SSH_AGENT_PID is %s " % environ["SSH_AGENT_PID"])
    print("INFO: SSH_AUTH_SOCK is %s " % environ["SSH_AUTH_SOCK"])


agent = paramiko.agent.Agent()
agent_keys = agent.get_keys()

if len(agent_keys) == 0:
    if _DEBUG > 0:
        print("ERROR: no key(s) found in ssh-agent, aborting")
    raise SystemExit(1)


if _KDUMP > 0:
    for key in agent_keys:
        print("INFO: found ssh-agent key %s" % hexlify(key.get_fingerprint()).decode())
    raise SystemExit(0)


for key in agent_keys:
    if _DEBUG > 0:
        print("INFO: found ssh-agent key %s" % hexlify(key.get_fingerprint()).decode())

    if _KEYID.encode() == hexlify(key.get_fingerprint()):
        if _DEBUG > 0:
            print("INFO: key match found, signing with %s" % _KEYID)

        # RSA sign flags: 0 (sha1), 2 (sha256), 4 (sha512)
        # - for widespread use upstream must accept the flags support patch
        try:
            raw_sig = key.sign_ssh_data(_PKGID, 2)
        except TypeError:
            raise SystemExit("ERROR: agent.py missing flags support, see patch at the bottom")

        # Strip fields with the algorithm name and length of the signature
        sig_parts = []
        while raw_sig:
            len = strunpack('>I', raw_sig[:4])[0]
            bits = raw_sig[4:len+4]
            sig_parts.append(bits)
            raw_sig = raw_sig[len+4:]
        sig = sig_parts[1]

        # To convert key.get_base64() to pkcs8 would be more code than this
        # entire thing. To use ssh-keygen instead we need a temporary file as
        # it can't read it from stdin when performing a conversion. In the end
        # it is much simpler to just read it from a pregenerated file.
        pub_key = open("/usr/local/etc/ssl/public/%s.pub" % _KEYID, "r")

        # Print data in order that pkg-repo(8) is expecting
        print("SIGNATURE")
        # - flush to ensure order and prevent pkg-repo(8) segfault
        stdout.flush()
        # - write the signature raw bytes that pkg-repo(8) is expecting
        stdout.buffer.write(sig)
        stdout.flush()
        print()
        stdout.flush()
        print("CERT")
        stdout.flush()
        print(pub_key.read().strip())
        stdout.flush()
        print("END")
        stdout.flush()
        pub_key.close()

        # For validating signatures against/with openssl pkeyutl
        # - generate: echo -n "[HASH]" | openssl dgst -sign [PRIVATEKEY] -sha256 -binary >signature-cmp
        if _DEBUG > 0:
            # - validate: echo -n "[HASH]" | openssl sha256 -binary | \
            #             openssl pkeyutl -verify -sigfile signature-[HASH] -pubin \
            #             -inkey [PUBLICKEY] -pkeyopt digest:sha256
            open("signature-%s" % _PKGID, "wb").write(sig)

        raise SystemExit(0)
    else:
        if _DEBUG > 0:
            print("WARN: key mismatch, continuing search...")
        continue


if _DEBUG > 0:
    raise SystemExit("ERROR: no matching key(s) found for signing, aborting")
else:
    raise SystemExit(1)


# Subject: [PATCH] paramiko agent RSA sign flags support
#
#--- paramiko/agent.py  2021-01-15 23:03:50.387801224 +0100
#+++ paramiko/agent.py  2021-01-15 23:04:34.667800388 +0100
#@@ -407,12 +407,12 @@
#     def get_name(self):
#         return self.name
#
#-    def sign_ssh_data(self, data):
#+    def sign_ssh_data(self, data, flags=0):
#         msg = Message()
#         msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
#         msg.add_string(self.blob)
#         msg.add_string(data)
#-        msg.add_int(0)
#+        msg.add_int(flags)
#         ptype, result = self.agent._send_message(msg)
#         if ptype != SSH2_AGENT_SIGN_RESPONSE:
#             raise SSHException("key cannot be used for signing")