
sshkey-tools

Python package for managing OpenSSH keypairs and certificates (protocol.CERTKEYS). Supported functionality includes:
Notice
The DSA algorithm has been deprecated and is removed in pyca/cryptography 41.x, meaning version 0.9. of this package will be the last to support DSA keys and certificates* for SSH. If there is any demand to reintroduce DSA support, please open an issue regarding this and we'll look into it.
For now, 0.9. will be restricted to version <41.1 of the cryptography package* and 0.10 will have its DSA support removed. We've introduced a deprecation notice in version 0.9.3.
Background
The DSA algorithm is considered deprecated and will be removed in a future version. If possible, use RSA, (ECDSA) or ED25519 as a first-hand choice.
Notice from OpenSSH:
OpenSSH 7.0 and greater similarly disable the ssh-dss (DSA) public key algorithm. It too is weak and we recommend against its use. It can be re-enabled using the HostKeyAlgorithms configuration option: sshd_config(5) HostKeyAlgorithms
ECDSA has some flaws, especially when using short nonces or re-using nonces, it can still be used but exercise some caution in regards to nonces/re-signing identical data multiple times.
Features
SSH Keys
- Supports RSA, ECDSA and ED25519 keys
- Import existing keys from file, string, byte data or pyca/cryptography class
- Generate new keys
- Get public key from private keys
- Sign bytestrings with private keys
- Export to file, string or bytes
- Generate fingerprint
OpenSSH Certificates
- Supports RSA, ECDSA and ED25519 certificates
- Import existing certificates from file, string or bytes
- Verify certificate signature against internal or separate public key
- Create new certificates from CA private key and subject public key
- Create new certificates using old certificate as template
- Sign certificates
- Export certificates to file, string or bytes
Roadmap
See issues for planned features and fixes
Installation
With pip
pip3 install sshkey-tools
pip3 install -e git+https://github.com/scheiblingco/sshkey-tools.git
From source
git clone https://github.com/scheiblingco/sshkey-tools
cd sshkey-tools
pip3 install ./
Documentation
You can find the full documentation at scheiblingco.github.io/sshkey-tools/
Building the documentation
pdoc3 src/sshkey_tools/ -o docs --html
cp -rf docs/sshkey_tools/* docs/
rm -rf docs/sshkey_tools
SSH Keypairs (generating, loading, exporting)
from sshkey_tools.keys import (
RsaPrivateKey,
EcdsaPrivateKey,
Ed25519PrivateKey,
EcdsaCurves
)
rsa_priv = RsaPrivateKey.generate()
rsa_priv = RsaPrivateKey.generate(2048)
ecdsa_priv = EcdsaPrivateKey.generate()
ecdsa_priv = EcdsaPrivateKey.generate(EcdsaCurves.P256)
ed25519_priv = Ed25519PrivateKey.generate()
rsa_priv = RsaPrivateKey.from_file("/path/to/key", "OptionalSecurePassword")
rsa_priv = PrivateKey.from_file("/path/to/key", "OptionalSecurePassword")
rsa_priv = PrivateKey.from_string("-----BEGIN OPENSSH PRIVATE KEY...........END -----", "OptionalSecurePassword")
rsa_priv = PrivateKey.from_class(pyca_cryptography_class)
rsa_priv = PublicKey.from_numbers(65537, 123123123....1)
ed25519_pub = ed25519_priv.public_key
rsa_priv.to_bytes("OptionalSecurePassword")
rsa_priv.to_string("OptionalSecurePassword", "utf-8")
rsa_priv.to_file("/path/to/file", "OptionalSecurePassword", "utf-8")
rsa_priv.public_key.comment = "Comment@Comment"
rsa_priv.public_key.serialize()
b"ssh-rsa AAAA......... Comment@Comment"
rsa_priv.public_key.raw_bytes()
b"\0xc\0a\........"
SSH Key Signatures
The loaded private key objects can be used to sign bytestrings, and the public keys can be used to verify signatures on those
from sshkey_tools.keys import RsaPrivateKey, RsaPublicKey
from sshkey_tools.fields import RsaAlgs
signable_data = b'This is a message that will be signed'
privkey = RsaPrivateKey.generate()
pubkey = RsaPrivateKey.public_key
signature = privkey.sign(signable_data)
signature = privkey.sign(signable_data, RsaAlgs.SHA512)
pubkey.verify(signable_data, signature)
OpenSSH Certificates
Introduction
Certificates are a way to handle access management/PAM for OpenSSH with the ability to dynamically grant access during a specific time, to specific servers and/or with specific attributes. There are a couple of upsides to using certificates instead of public/private keys, mainly:
- Additional Security: Certificate authentication for OpenSSH is built as an extension of public key authentication, enabling additional features on top of key-based access control.
- Short-term access: The user has to request a certificate for their keypair, which together with the private key grants access to the server. Without the certificate the user can't connect to the server - giving you control over how, when and from where the user can connect.
- Hostkey Verification: Certificiates can be issued for the OpenSSH Server, adding the CA public key to the clients enables you to establish servers as trusted without the hostkey warning.
- RBAC: Control which servers or users (principals) a keypair has access to, and specify the required principals for access to certain functionality on the server side.
- Logging: Key ID and Serial fields for tracking of issued certificates
- CRL: Revoke certificates prematurely if they are compromised
Structure
The original OpenSSH certificate format is a block of parameters, encoded and packed to a bytestring. In this package, the fields have been divided into three parts. For a more detailed information about the format, see PROTOCOL.certkeys.
Attribute | Type(Length) | Key | Example Value | Description |
---|
Public Key/Certificate type | string(fixed) | pubkey_type | ssh-rsa-sha2-512-cert-v01@openssh.com | The private key (and certificate) type, derived from the public key for which the certificate is created (Automatically set upon creation) |
Subject public key | bytestring(variable) | public_key | \x00\x00\x00.......... | The public key for which the certificate is created (Automatically set upon creation) |
Nonce | string | nonce(variable, typically 16 or 32 bytes) | abcdefghijklmnopqrstuvwxyz | A random string included to make attacks that depend on inducing collisions in the signature hash infeasible. (Default is automatically set, can be changed with Certificate.header.nonce = "abcdefg..." |
Certificate Fields
Attribute | Type(Length) | Key | Example Value | Description |
---|
Serial | Integer(64-bit) | serial | 1234567890 | An optional certificate serial number set by the CA to provide an abbreviated way to refer to certificates from that CA. If a CA does not wish to number its certificates, it must set this field to zero. |
Certificate type | Integer(1 or 2) | cert_type | 1 | The type of the certificate, 1 for user certificates, 2 for host certificates |
Key ID | string(variable) | key_id | someuser@somehost | Free-form text field that is filled in by the CA at the time of signing; the intention is that the contents of this field are used to identify the identity principal in log messages. |
Valid Principals | List(string(variable)) | principals | ['some-user', 'some-group', production-webservers'] | These principals list the names for which this certificate is valid hostnames for SSH_CERT_TYPE_HOST certificates and usernames for SH_CERT_TYPE_USER certificates. As a special case, a zero-length "valid principals" field means the certificate is valid for any principal of the specified type. |
Valid After | Timestamp | valid_after | datetime.now() | Timestamp for the start of the validity period for the certificate |
Valid Before | Timestamp | valid_before | datetime.now()+timedelta(hours=8) or 1658322031 | Timestamp for the end of the validity period for the certificate. Needs to be larger than valid_after, can be a string (ex. 2d, 2w, 1h4m, 99d) or forever (MAX_INT64) |
Critical Options | Dict(string, string) | critical_options | [] | Zero or more of the available critical options (see below) |
Extensions | Dict(string, string)/List/Tuple/Set | extensions | [] | Zero or more of the available extensions (see below) |
Critical Options
Name | Format | Description |
---|
force-command | string | Specifies a command that is executed (replacing any the user specified on the ssh command-line) whenever this key is used for authentication. |
source-address | string | Comma-separated list of source addresses from which this certificate is accepted for authentication. Addresses are specified in CIDR format (nn.nn.nn.nn/nn or hhhh::hhhh/nn). If this option is not present, then certificates may be presented from any source address. |
verify-required | empty | Flag indicating that signatures made with this certificate must assert FIDO user verification (e.g. PIN or biometric). This option only makes sense for the U2F/FIDO security key types that support this feature in their signature formats. |
Extensions
Name | Format | Description |
---|
no-touch-required | empty | Flag indicating that signatures made with this certificate need not assert FIDO user presence. This option only makes sense for the U2F/FIDO security key types that support this feature in their signature formats. |
permit-X11-forwarding | empty | Flag indicating that X11 forwarding should be permitted. X11 forwarding will be refused if this option is absent. |
permit-agent-forwarding | empty | Flag indicating that agent forwarding should be allowed. Agent forwarding must not be permitted unless this option is present. |
permit-port-forwarding | empty | Flag indicating that port-forwarding should be allowed. If this option is not present, then no port forwarding will be allowed. |
permit-pty | empty | Flag indicating that PTY allocation should be permitted. In the absence of this option PTY allocation will be disabled. |
permit-user-rc | empty | Flag indicating that execution of ~/.ssh/rc should be permitted. Execution of this script will not be permitted if this option is not present. |
Certificate Body
Attribute | Type(Length) | Key | Example Value | Description |
---|
Reserved | string(0) | reserved | "" | Reserved for future use, must be empty (automatically set upon signing) |
CA Public Key | bytestring(variable) | ca_pubkey | \x00\x00\x00.......... | The public key of the CA that issued this certificate (automatically set upon signing) |
Signature | bytestring(variable) | signature | \x00\x00\x00.......... | The signature of the certificate, created by the CA (automatically set upon signing) |
Creating, signing and verifying certificates
from sshkey_tools.cert import SSHCertificate, CertificateFields, Ed25519Certificate
from sshkey_tools.keys import Ed25519PrivateKey
from datetime import datetime, timedelta
subject_pubkey = Ed25519PrivateKey.generate().public_key
ca_privkey = Ed25519PrivateKey.generate()
cert_fields = CertificateFields(
serial=1234567890,
cert_type=1,
key_id="someuser@somehost",
principals=["some-user", "some-group", "production-webservers"],
valid_after=datetime.now(),
valid_before=datetime.now() + timedelta(hours=8),
critical_options=[],
extensions=[
"permit-pty",
"permit-X11-forwarding",
"permit-agent-forwarding",
],
)
certificate = SSHCertificate(
subject_pubkey=subject_pubkey,
ca_privkey=ca_privkey,
fields=cert_fields,
)
certificate = SSHCertificate.create(
subject_pubkey=subject_pubkey,
ca_privkey=ca_privkey
)
certificate = Ed25519Certificate(
subject_pubkey=subject_pubkey,
ca_privkey=ca_privkey
)
certificate.fields.serial = 1234567890
certificate.fields.cert_type = 1
certificate.fields.key_id = "someuser@somehost"
certificate.fields.principals = ["some-user", "some-group", "production-webservers"]
certificate.fields.valid_after = datetime.now()
certificate.fields.valid_before = datetime.now() + timedelta(hours=8)
certificate.fields.critical_options = []
certificate.fields.extensions = [
"allow-pty",
"permit-X11-forwarding",
"permit-agent-forwarding",
]
certificate.can_sign()
certificate.sign()
certificate.verify()
certificate.verify(ca_privkey.public_key)
certificate.verify(ca_privkey.public_key, True)
certificate.to_file('filename-cert.pub')
cert_str = certificate.to_string()
Loading, re-creating and verifying existing certificates
from sshkey_tools.cert import SSHCertificate, CertificateFields, Ed25519Certificate
from sshkey_tools.keys import PublicKey, PrivateKey
from datetime import datetime, timedelta
certificate = SSHCertificate.from_file('filename-cert.pub')
certificate = SSHCertificate.from_string(cert_str)
type(certificate)
certificate.verify()
pubkey = PublicKey.from_file('filename-pubkey.pub')
certificate.verify(pubkey)
certificate.verify(pubkey, True)
new_ca = PrivateKey.from_file('filename-ca')
certificate.replace_ca(new_ca)
certificate.sign()
Changelog
0.9.1
- Updated documentation
- Fix for bug where exception would occur when trying to export a key without a comment set
0.9
- Adjustments to certificate field handling for easier usage/syntax autocompletion
- Updated testing
- Removed method for changing RSA hash method (now default SHA512)
0.8.2
- Fixed bug where an RSA certificate would send the RSA alg to the sign() function of another key type
0.8.1
- Changed versioning for out-of-github installation/packaging
- Moved documentation to HTML (PDOC3)
- Added verification of certificate signature
- Added option to choose RSA hashing algorithm for signing
- Removed test files
- Added documentation deployment CD for GH pages
0.8