Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for signing ED25519 public keys #71

Merged
merged 2 commits into from
Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions bless/ssh/certificates/ed25519_certificate_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
.. module: bless.ssh.certificates.ed25519_certificate_builder
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from bless.ssh.certificates.ssh_certificate_builder import \
SSHCertificateBuilder, SSHCertifiedKeyType
from bless.ssh.protocol.ssh_protocol import pack_ssh_string


class ED25519CertificateBuilder(SSHCertificateBuilder):
def __init__(self, ca, cert_type, ssh_public_key_ed25519):
"""
Produces an SSH certificate for ED25519 public keys.
:param ca: The SSHCertificateAuthority that will sign the certificate. The
SSHCertificateAuthority type does not need to be the same type as the
SSHCertificateBuilder.
:param cert_type: The SSHCertificateType. Is this a User or Host certificate? Some of
the SSH Certificate fields do not apply or have a slightly different meaning depending on
the certificate type.
See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys
:param ssh_public_key_ed25519: The ED25519PublicKey to issue a certificate for.
"""
super(ED25519CertificateBuilder, self).__init__(ca, cert_type)
self.cert_key_type = SSHCertifiedKeyType.ED25519
self.ssh_public_key = ssh_public_key_ed25519
self.public_key_comment = ssh_public_key_ed25519.key_comment
self.a = ssh_public_key_ed25519.a

def _serialize_ssh_public_key(self):
"""
Serialize the Public Key into a string. This is not specified in
http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys
but https://tools.ietf.org/id/draft-ietf-curdle-ssh-ed25519-02.html
:return: The bytes that belong in the SSH Certificate between the nonce and the
certificate serial number.
"""
public_key = pack_ssh_string(self.a)
return public_key
4 changes: 4 additions & 0 deletions bless/ssh/certificates/ssh_certificate_builder_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""
from bless.ssh.certificates.rsa_certificate_builder \
import RSACertificateBuilder
from bless.ssh.certificates.ed25519_certificate_builder \
import ED25519CertificateBuilder
from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType
from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key

Expand All @@ -23,5 +25,7 @@ def get_ssh_certificate_builder(ca, cert_type, public_key_to_sign):

if ssh_public_key.type is SSHPublicKeyType.RSA:
return RSACertificateBuilder(ca, cert_type, ssh_public_key)
elif ssh_public_key.type is SSHPublicKeyType.ED25519:
return ED25519CertificateBuilder(ca, cert_type, ssh_public_key)
else:
raise TypeError("Unsupported Public Key Type")
63 changes: 63 additions & 0 deletions bless/ssh/public_keys/ed25519_public_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
.. module: bless.ssh.public_keys.ed25519_public_key
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
import base64
import hashlib

from bless.ssh.public_keys.ssh_public_key import SSHPublicKey, SSHPublicKeyType
from cryptography.hazmat.primitives import serialization


class ED25519PublicKey(SSHPublicKey):
def __init__(self, ssh_public_key):
"""
Extracts the useful ED25519 Public Key information from an SSH Public Key file.
:param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-ed25519 AAAAB3NzaC1yc2E..').
"""
super(ED25519PublicKey, self).__init__()

self.type = SSHPublicKeyType.ED25519

split_ssh_public_key = ssh_public_key.split(' ')
split_key_len = len(split_ssh_public_key)

# is there a key comment at the end?
if split_key_len > 2:
self.key_comment = ' '.join(split_ssh_public_key[2:])
else:
self.key_comment = ''

# hazmat does not support ed25519 so we have out own loader based on serialization.load_ssh_public_key

if split_key_len < 2:
raise ValueError(
'Key is not in the proper format or contains extra data.')

key_type = split_ssh_public_key[0]
key_body = split_ssh_public_key[1]

if key_type != SSHPublicKeyType.ED25519:
raise TypeError("Public Key is not the correct type or format")

try:
decoded_data = base64.b64decode(key_body)
except TypeError:
raise ValueError('Key is not in the proper format.')

inner_key_type, rest = serialization._ssh_read_next_string(decoded_data)

if inner_key_type != key_type:
raise ValueError(
'Key header and key body contain different key type values.'
)

# ed25519 public key is a single string https://tools.ietf.org/html/rfc8032#section-5.1.5
self.a, rest = serialization._ssh_read_next_string(rest)

key_bytes = base64.b64decode(split_ssh_public_key[1])
fingerprint = hashlib.md5(key_bytes).hexdigest()

self.fingerprint = 'ED25519 ' + ':'.join(
fingerprint[i:i + 2] for i in range(0, len(fingerprint), 2))
4 changes: 4 additions & 0 deletions bless/ssh/public_keys/ssh_public_key_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:license: Apache, see LICENSE for more details.
"""
from bless.ssh.public_keys.rsa_public_key import RSAPublicKey
from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey
from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType


Expand All @@ -17,5 +18,8 @@ def get_ssh_public_key(ssh_public_key):
rsa_public_key = RSAPublicKey(ssh_public_key)
rsa_public_key.validate_for_signing()
return rsa_public_key
elif ssh_public_key.startswith(SSHPublicKeyType.ED25519):
ed25519_public_key = ED25519PublicKey(ssh_public_key)
return ed25519_public_key
else:
raise TypeError("Unsupported Public Key Type")
11 changes: 7 additions & 4 deletions tests/ssh/test_ssh_certificate_builder_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
get_ssh_certificate_authority
from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder, \
SSHCertifiedKeyType
from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder
from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType
from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder
from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \
Expand All @@ -18,10 +19,12 @@ def test_valid_rsa_request():
assert cert.startswith(SSHCertifiedKeyType.RSA)


def test_invalid_ed25519_request():
with pytest.raises(TypeError):
ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD)
get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY)
def test_valid_ed25519_request():
ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD)
cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY)
cert = cert_builder.get_cert_file()
assert isinstance(cert_builder, ED25519CertificateBuilder)
assert cert.startswith(SSHCertifiedKeyType.ED25519)


def test_invalid_key_request():
Expand Down
17 changes: 16 additions & 1 deletion tests/ssh/test_ssh_certificate_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@

from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority
from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder
from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder
from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType
from bless.ssh.public_keys.rsa_public_key import RSAPublicKey
from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey
from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \
EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION, RSA_USER_CERT_MINIMAL, \
RSA_USER_CERT_DEFAULTS, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT, \
RSA_USER_CERT_MANY_PRINCIPALS, RSA_HOST_CERT_MANY_PRINCIPALS, \
RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS, \
RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID, RSA_HOST_CERT_MANY_PRINCIPALS_KEY_ID, \
RSA_USER_CERT_MANY_PRINCIPALS_KEY_ID, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID, \
RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS
RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS, \
EXAMPLE_ED25519_PUBLIC_KEY, ED25519_USER_CERT_DEFAULTS, ED25519_USER_CERT_DEFAULTS_KEY_ID

USER1 = 'user1'

Expand Down Expand Up @@ -219,3 +222,15 @@ def test_nonce():
cert_builder2.set_nonce()

assert cert_builder.nonce != cert_builder2.nonce


def test_ed25519_user_cert_defaults():
ca = get_basic_rsa_ca()
pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY)
cert_builder = ED25519CertificateBuilder(ca, SSHCertificateType.USER, pub_key)
cert_builder.set_nonce(
nonce=extract_nonce_from_cert(ED25519_USER_CERT_DEFAULTS))
cert_builder.set_key_id(ED25519_USER_CERT_DEFAULTS_KEY_ID)

cert = cert_builder.get_cert_file()
assert ED25519_USER_CERT_DEFAULTS == cert
29 changes: 29 additions & 0 deletions tests/ssh/test_ssh_public_key_ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey
from tests.ssh.vectors import EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY_A, \
EXAMPLE_ECDSA_PUBLIC_KEY, \
EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION


def test_valid_key():
pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY)
assert 'Test ED25519 User Key' == pub_key.key_comment
assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a
assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint


def test_valid_key_no_description():
pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION)
assert '' == pub_key.key_comment
assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a
assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint


def test_invalid_keys():
with pytest.raises(TypeError):
ED25519PublicKey(EXAMPLE_ECDSA_PUBLIC_KEY)

with pytest.raises(ValueError):
ED25519PublicKey('bogus')

11 changes: 7 additions & 4 deletions tests/ssh/test_ssh_public_key_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key
from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY, \
EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E
EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E, \
EXAMPLE_ED25519_PUBLIC_KEY_A


def test_valid_rsa():
Expand All @@ -13,9 +14,11 @@ def test_valid_rsa():
assert 'RSA 57:3d:48:4c:65:90:30:8e:39:ba:d8:fa:d0:20:2e:6c' == pub_key.fingerprint


def test_unsupported_ed_25519():
with pytest.raises(TypeError):
get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY)
def test_valid_ed25519():
pub_key = get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY)
assert 'Test ED25519 User Key' == pub_key.key_comment
assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a
assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint


def test_invalid_key():
Expand Down
11 changes: 10 additions & 1 deletion tests/ssh/vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

EXAMPLE_ED25519_PUBLIC_KEY = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key'

EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp'

EXAMPLE_ED25519_PUBLIC_KEY_A = base64.b64decode(
'bv5wBtPgQVI+zzuhLiQNhK1DQovTHAdapVJnZtgcgCk=')

EXAMPLE_ECDSA_PUBLIC_KEY = u'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key'

EXAMPLE_RSA_PUBLIC_KEY_2048 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33'
Expand Down Expand Up @@ -45,4 +50,8 @@
'AAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==')

SSH_CERT_CUSTOM_EXTENSIONS = base64.b64decode(
'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==')
'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==')

# ssh-keygen -s test-rsa-ca -I "ssh-keygen -s test-rsa-ca -I '' test-ed25519-user-all-defaults.pub" test-ed25519-user-all-defaults.pub
ED25519_USER_CERT_DEFAULTS = '[email protected] AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINa9ZmyR9YBRNfC464IJE2AlDa0xU02tVbY37AlRr79/AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIApAAAAAAAAAAAAAAABAAAAQnNzaC1rZXlnZW4gLXMgdGVzdC1yc2EtY2EgLUkgJycgdGVzdC1lZDI1NTE5LXVzZXItYWxsLWRlZmF1bHRzLnB1YgAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAOI0JnxeJSdtUXtHu7x3MVVLdDN2iFCr8B8xCSGsdmJ+VdI2sBF0xc7l5/HZVyATgCBTt9dT7eG6zFCnQhPJUUB+uhUu9EeKwu3Pqw9mWnTXy/sa/R++yswFjU1Syxws+iBbiG9IG1BIqXY+eOuXaf+8kEFi877bjU4Nrl0U/KL2qpCJwhFh3vu5XBQ26ih/TcZVRaPyOTBVqOD0MNeVXs0JkVc/gd4mLHWgFvcIrTNmmKwR0HNSDPpYbLxol+3DbfaY9CE/sUetLA6OxO4EF38PQKPY+Ud2/yypc4uC/GT1VfEc4ALZVCZYC8Cut3hYb5ef3xdab7W4ikXIyU2vtPR/n1Ju/5nXVFX1Y0F5u0ShZkz8SI7/BLF2i4SIMiZhNTgqVj8mr2LpwSHB04m68d9GsPnD1mQQxlcfx7pbfOAjXmnTV3mQmn4TnW1KxKH0n7NJE1jvRy31n3Crs3aMJ6cuq6+gxbK4cg/X48Q16PnpuYzOiTtD5hEuu4RL4cg86Mi57DmchNwb1CsvxFJrueJB65J4efIDTqmuppDQVjkZOzH2URzRzGE2azdcQPgcV5aQQiZuiqeZ5xG9oEwB/2kNVu3hsM49ugJ9OBGHtlDoemC4YkQkJe0ejFJVc8uKOzGmXfjGcXKi21Aq5jGvEsDRb3DcuLEceOfXRpU/59OpAAACDwAAAAdzc2gtcnNhAAACACnir9y/PhoKvLNvs4fMkS24mrHyGxc24nTxaKJO9OBGr4PY3Glg90hKaZvC/cLN0wSAeIYzigp/PGm1PNIM342NqxMkIx7yUKrEbcIQaommn8kfThMIcLJo9QOpDoMAxOsfAtZrtWQKMMYM6s8hZKrb3OT8k0le/x4S2GttHMb9z006nZgyTUI4cniq+ZxwcL4l5/wzaXamQUsod/JRysEBZdLF88A50iCiSC+4NQUqquPBs76UmTpU+Lv3OEvVBwMil7Hxkzv8kqit2Xal0Ou9n/+CC1G9l/dpTO1TgaNtNERQT3rQOhkjqwmgN3wbh1sRkc1+zSenWsNrHkFBnVTJPxLwXbFeAvstDWDxTvNCpygsTfz/ejUnKfqZ1rAWfNRhzrSIz1D1+/GQbOdtM3xq3r6CVjVxE0KTLsdR1hyOd5SmszubE1UAkKWF7NRyHcgma/9hkOXc6a/4ylBcOj0yUFnjVq7Jb6C33ba0Ra6LnopZWUS7lr02dt7aYG/Qhd8OtJx7R+XiRYRnfsuJH+L18UxM34xqj9qlMPA5p1nUB2ZnklKyueLhrp0/thuWsdKCv4w66A8rOthbDtLip6TYtKwDDMBupq5ROoRHYXb6nYthHJYCX1QuDIzmoBjTlkWa7aVohggQWezYhGGo0owulURxkFNZBNi1Lc/aRuDs Test ED25519 User Key'
ED25519_USER_CERT_DEFAULTS_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-ed25519-user-all-defaults.pub'