Skip to content

Commit fc7684d

Browse files
author
Senjuu
committed
Fix Support for ssh-certificates
1 parent dace3eb commit fc7684d

File tree

3 files changed

+107
-11
lines changed

3 files changed

+107
-11
lines changed

libagent/ssh/certificate.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Utilities for signing ssh-certificates."""
2+
import io
3+
4+
from . import formats, util
5+
6+
7+
def _parse_stringlist(i):
8+
res = []
9+
length, = util.recv(i, '>L')
10+
while length >= 4:
11+
size, = util.recv(i, '>L')
12+
length -= 4
13+
size = min(size, length)
14+
if size == 0:
15+
continue
16+
res.append(util.recv(i, size).decode('utf8'))
17+
length -= size
18+
return res
19+
20+
21+
def parse(blob):
22+
"""Parses a data blob to a to-be-signed ssh-certificate."""
23+
# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
24+
res = {}
25+
i = io.BytesIO(blob)
26+
firstString = util.read_frame(i)
27+
if firstString.endswith(b'[email protected]'):
28+
res['isCertificate'] = True
29+
_certificate_key_type = firstString
30+
_nonce = util.read_frame(i)
31+
if (_certificate_key_type.startswith(b'ssh-rsa')):
32+
_pub_key = {}
33+
_pub_key['e'] = util.read_frame(i)
34+
_pub_key['n'] = util.read_frame(i)
35+
elif (_certificate_key_type.startswith(b'ssh-dsa')):
36+
_pub_key = {}
37+
_pub_key['p'] = util.read_frame(i)
38+
_pub_key['q'] = util.read_frame(i)
39+
_pub_key['g'] = util.read_frame(i)
40+
_pub_key['y'] = util.read_frame(i)
41+
elif (_certificate_key_type.startswith(b'ecdsa-sha2-nistp')):
42+
_curve = util.read_frame(i)
43+
_pub_key = util.read_frame(i)
44+
elif (_certificate_key_type.startswith(b'ssh-ed25519')):
45+
_pub_key = util.read_frame(i)
46+
else:
47+
raise ValueError('unknown certificate key type: '+_certificate_key_type.decode('utf8'))
48+
_serial_number, = util.recv(i, '>Q')
49+
res['certificate_type'], = util.recv(i, '>L')
50+
_key_id_ = util.read_frame(i)
51+
res['principals'] = _parse_stringlist(i)
52+
res['principals'] = ', '.join(res['principals'])
53+
_valid_after, = util.recv(i, '>Q')
54+
_valid_before, = util.recv(i, '>Q')
55+
_critical_options = _parse_stringlist(i)
56+
_extensions = _parse_stringlist(i)
57+
_reserved = util.read_frame(i)
58+
_signature_key = util.read_frame(i)
59+
assert not i.read()
60+
return res
61+
res['isCertificate'] = False
62+
i.close()
63+
return res
64+
65+
66+
def format(certificate):
67+
"""
68+
Makes certificate better human readable.
69+
70+
Formats list properties to comma seperated strings and
71+
the signature key to human readable string.
72+
"""
73+
certificate['principals'] = ', '.join(certificate['principals'])
74+
certificate['critical_options'] = ', '.join(certificate['critical_options'])
75+
certificate['extensions'] = ', '.join(certificate['extensions'])
76+
certificate['signature_key'] = formats.parse_pubkey(certificate['signature_key'])

libagent/ssh/client.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import io
77
import logging
88

9-
from . import formats, util
9+
from . import certificate, formats, util
1010

1111
log = logging.getLogger(__name__)
1212

@@ -36,6 +36,15 @@ def sign_ssh_challenge(self, blob, identity):
3636
if msg['sshsig']:
3737
log.info('please confirm "%s" signature for "%s" using %s...',
3838
msg['namespace'], identity.to_string(), self.device)
39+
elif msg['sshcertsign']:
40+
entity = 'unknown'
41+
if msg['certificate_type'] == 1:
42+
entity = 'user'
43+
elif msg['certificate_type'] == 2:
44+
entity = 'host'
45+
log.info('please confirm signing public key for %s "%s" with "%s" using %s...',
46+
entity, msg['principals'], identity.to_string(),
47+
self.device)
3948
else:
4049
log.debug('%s: user %r via %r (%r)',
4150
msg['conn'], msg['user'], msg['auth'], msg['key_type'])
@@ -59,22 +68,31 @@ def parse_ssh_blob(data):
5968
i = io.BytesIO(data[6:])
6069
# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
6170
res['sshsig'] = True
71+
res['sshcertsign'] = False
6272
res['namespace'] = util.read_frame(i)
6373
res['reserved'] = util.read_frame(i)
6474
res['hashalg'] = util.read_frame(i)
6575
res['message'] = util.read_frame(i)
6676
else:
67-
i = io.BytesIO(data)
6877
res['sshsig'] = False
69-
res['nonce'] = util.read_frame(i)
70-
i.read(1) # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108)
71-
res['user'] = util.read_frame(i)
72-
res['conn'] = util.read_frame(i)
73-
res['auth'] = util.read_frame(i)
74-
i.read(1) # have_sig == 1 (from sshconnect2.c, line 1056)
75-
res['key_type'] = util.read_frame(i)
76-
public_key = util.read_frame(i)
77-
res['public_key'] = formats.parse_pubkey(public_key)
78+
_certificate = certificate.parse(data)
79+
if _certificate['isCertificate']:
80+
certificate.format(_certificate)
81+
_certificate['sshsig'] = res['sshsig']
82+
_certificate['sshcertsign'] = True
83+
return _certificate
84+
else:
85+
res['sshcertsign'] = False
86+
i = io.BytesIO(data)
87+
res['nonce'] = util.read_frame(i)
88+
i.read(1) # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108)
89+
res['user'] = util.read_frame(i)
90+
res['conn'] = util.read_frame(i)
91+
res['auth'] = util.read_frame(i)
92+
i.read(1) # have_sig == 1 (from sshconnect2.c, line 1056)
93+
res['key_type'] = util.read_frame(i)
94+
public_key = util.read_frame(i)
95+
res['public_key'] = formats.parse_pubkey(public_key)
7896

7997
unparsed = i.read()
8098
if unparsed:

libagent/ssh/tests/test_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def test_parse_ssh_challenge():
101101
'fingerprint': '47:a3:26:af:0b:5d:a2:c3:91:ed:26:36:94:be:3a:d5',
102102
'type': b'ssh-ed25519'},
103103
'sshsig': False,
104+
'sshcertsign': False,
104105
'user': b'git',
105106
}
106107

@@ -122,4 +123,5 @@ def test_parse_ssh_signature():
122123
'namespace': b'file',
123124
'reserved': b'',
124125
'sshsig': True,
126+
'sshcertsign': False,
125127
}

0 commit comments

Comments
 (0)