Metadata-Version: 2.1
Name: sshkey-tools
Version: 0.9.1
Summary: A Python module for generating, parsing and handling OpenSSH keys and certificates
Home-page: https://github.com/scheiblingco/sshkey-tools
Author: Lars Scheibling
Author-email: lars@scheibling.se
License: GnuPG 3.0
Keywords: python openssh ssh keys sshkey ssh-keygen ssh-certificate certificate parser decoder
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: OS Independent
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE

# sshkey-tools

Python package for managing OpenSSH keypairs and certificates ([protocol.CERTKEYS](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys)). Supported functionality includes:

[TOC]

# Features
### SSH Keys
- Supports RSA, DSA, ECDSA and ED25519 keys
- Import existing keys from file, string, byte data or [pyca/cryptography](https://github.com/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, DSA, 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
- [x] Rewrite certificate field functionality for simpler usage
- [ ] Re-add functionality for changing RSA hash method
- [ ] Add CLI functionality
- [ ] Convert to/from putty format (keys only)


# Installation

## With pip

```bash
pip3 install sshkey-tools
# or
pip3 install -e git+https://github.com/scheiblingco/sshkey-tools.git
```

## From source

```bash
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/](https://scheiblingco.github.io/sshkey-tools/)

## SSH Keypairs (generating, loading, exporting)
```python
# Import the certificate classes
from sshkey_tools.keys import (
    RsaPrivateKey,
    DsaPrivateKey,
    EcdsaPrivateKey,
    Ed25519PrivateKey,
    EcdsaCurves
)
#
## Generating keys
#

# For all keys except ED25519, the key size/curve can be manually specified
# Generate RSA (default is 4096 bits)
rsa_priv = RsaPrivateKey.generate()
rsa_priv = RsaPrivateKey.generate(2048)

# Generate DSA keys (since SSH only supports 1024-bit keys, this is the default)
dsa_priv = DsaPrivateKey.generate()

# Generate ECDSA keys (The default curve is P521)
ecdsa_priv = EcdsaPrivateKey.generate()
ecdsa_priv = EcdsaPrivateKey.generate(EcdsaCurves.P256)

# Generate ED25519 keys (fixed key size)
ed25519_priv = Ed25519PrivateKey.generate()

#
## Loading keys
#

# Keys can be loaded either via the specific class:
rsa_priv = RsaPrivateKey.from_file("/path/to/key", "OptionalSecurePassword")

# or via the general class, in case the type is not known in advance
rsa_priv = PrivateKey.from_file("/path/to/key", "OptionalSecurePassword")

# The import functions are .from_file(), .from_string() and .from_class() and are valid for both PublicKey and PrivateKey-classes
rsa_priv = PrivateKey.from_string("-----BEGIN OPENSSH PRIVATE KEY...........END -----", "OptionalSecurePassword")
rsa_priv = PrivateKey.from_class(pyca_cryptography_class)

# The different keys can also be loaded from their numbers, e.g. RSA Pubkey:
rsa_priv = PublicKey.from_numbers(65537, 123123123....1)

#
## Key functionality
#

# The public key for any loaded or generated private key is available in the .public_key attribute
ed25519_pub = ed25519_priv.public_key

# The private keys can be exported using to_bytes, to_string or to_file
rsa_priv.to_bytes("OptionalSecurePassword")
rsa_priv.to_string("OptionalSecurePassword", "utf-8")
rsa_priv.to_file("/path/to/file", "OptionalSecurePassword", "utf-8")

# The public keys also have .to_string() and .to_file(), but .to_bytes() is divided into .serialize() and .raw_bytes()
# The comment can be set before export by changing the public_key.comment-attribute
rsa_priv.public_key.comment = "Comment@Comment"

# This will return the serialized public key as found in an OpenSSH keyfile
rsa_priv.public_key.serialize()
b"ssh-rsa AAAA......... Comment@Comment"

# This will return the raw bytes of the key (base64-decoded middle portion)
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
```python
from sshkey_tools.keys import RsaPrivateKey, RsaPublicKey

signable_data = b'This is a message that will be signed'

privkey = RsaPrivateKey.generate()
pubkey = RsaPrivateKey.public_key

# Sign the data
signature = privkey.sign(signable_data)

# Verify the signature (Throws exception if invalid)
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](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys).

### Certificate Header
|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|
|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
```python
# Every certificate needs two parts, the subject (user or host) public key and the CA Private key
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()

# There are multiple ways to create a certificate, either by creating the certificate body field object first and then creating the certificate, or creating the certificate and setting the fields one by one

# Create certificate body fields
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",
    ],
)

# Create certificate from existing fields
certificate = SSHCertificate(
    subject_pubkey=subject_pubkey,
    ca_privkey=ca_privkey,
    fields=cert_fields,
)

# Start with a blank certificate by calling the general class
certificate = SSHCertificate.create(
    subject_pubkey=subject_pubkey,
    ca_privkey=ca_privkey
)

# You can also call the specialized classes directly, for the general class the .create-function needs to be used
certificate = Ed25519Certificate(
    subject_pubkey=subject_pubkey,
    ca_privkey=ca_privkey
)

# Manually set the fields
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",
]

# Check if the certificate is ready to be signed
certificate.can_sign()

# Sign the certificate
certificate.sign()

# Verify the certificate against the included public key (insecure, but useful for testing)
certificate.verify()

# Verify the certificate against a public key that is not included in the certificate
certificate.verify(ca_privkey.public_key)

# Raise an exception if the certificate is invalid
certificate.verify(ca_privkey.public_key, True)

# Export the certificate to file/string
certificate.to_file('filename-cert.pub')
cert_str = certificate.to_string()

```
## Loading, re-creating and verifying existing certificates
```python
from sshkey_tools.cert import SSHCertificate, CertificateFields, Ed25519Certificate
from sshkey_tools.keys import PublicKey, PrivateKey
from datetime import datetime, timedelta

# Load a certificate from file or string
# This will return the correct certificate type based on the contents of the certificate
certificate = SSHCertificate.from_file('filename-cert.pub')
certificate = SSHCertificate.from_string(cert_str)

type(certificate) # sshkey_tools.cert.Ed25519Certificate

# Verify the certificate signature against the included public key (insecure, but useful for testing)
certificate.verify()

# Verify the certificate signature against a public key
pubkey = PublicKey.from_file('filename-pubkey.pub')
certificate.verify(pubkey)

# Raise an exception if the certificate is invalid
certificate.verify(pubkey, True)

# Use the loaded certificate as a template to create a new one
new_ca = PrivateKey.from_file('filename-ca')
certificate.replace_ca(new_ca)
certificate.sign()

```

## Changelog
### 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
- Initial public release
