dotat ssh-box: use ssh keys to encrypt files

SSH-BOX(5) File Formats Manual SSH-BOX(5)

ssh-boxencrypted file format

A file encrypted by ssh-box is represented as PEM-encapsulated binary data. The binary consists of a mostly-cleartext header, followed by the ciphertext of the encrypted file.

The header can contain user-defined metatada about the file and the list of public keys that were used to encrypt the file. A recipient who owns a corresponding private key can decrypt a blob in the header that contains the symmetric key to decrypt the file itself.

An ssh-box encrypted file is self-contained. Anyone who has a private key that is able to decrypt the file can recover all the inputs that were used to encrypt the file.

See Data representation below for details of the types used in the following subsections.

The binary header and encrypted file are base64-encoded and delimited by prefix and suffix lines with the label ‘SSH-BOX ENCRYPTED FILE’, as described in RFC 7468. For example,

-----BEGIN SSH-BOX ENCRYPTED FILE-----
c3NoLWJveC12MQAAAAABAAAAC3NzaC1lZDI1NTE5AAAAIHRE3hd+N+jMlLuQsnB/IozFl/5O
4SBvM4uWlCN+Fs8PAAAAAmVnAAAAaKZcNtnpfC0VwHKA2EX/s7zNyuSraWc9xGVmpYJqeKMC
Py10Oi9sXUN/Q4Kk9aNvbSXVaXQz76Q94cGT89pPx/lD5QusSNxmc8F1PmaGlakDwinczXT7
JDoDtw/CJDXQ7qdnt/OVDnTRDakxZU+eGgRVMeiwAgkzphgDXFN0IXvW
-----END SSH-BOX ENCRYPTED FILE-----

An ssh-box file should be generated according to the ‘stricttextualmsg’ syntax, and it should be parsed according to the ‘laxtextualmsg’ syntax, as described in RFC 7468 section 3.

The encrypted file's ciphertext follows immediately after the ssh-box header. The file is encrypted using the libsodium function (), with the file's contents as the message, and the ssh-box header as the additional data.

The XChaCha20-Poly1305 AEAD construction also requires a nonce and key, which are laid out with the following fixed-size structure and encrypted using each recipient public key, before being included in the ssh-box header.

:
 
byte[24] nonce
byte[32] key

The header of an ssh-box encrypted file consists of a file format identifier followed by any number of header items.

The format identifier is a URL pointing to this specification, terminated by a zero byte.

Each item starts with a byte containing a count of the number of string fields in the item. The list of items, and the whole header, is terminated by an item whose count byte is zero.

 
byte[] https://dotat.at/prog/ssh-box/v1\0
item
item
...
byte equal to zero

Following its count byte, an item contains that many string fields. The first field is a name indicating the type of the item. The number of fields that are required in an item, and the contents of those fields, depends on the first type field.

:
 
byte count count is at least one
name type
string
... item has count strings including the type

In general, a recipient in an ssh-box header is represented by their ssh public key; and a comment, like those in an authorized_keys file; and a blob, encrypted using the public key, containing the secrets used for AEAD Encryption.

:
 
byte count
name type public key format identifier
string ... public key fields
utf8 comment
string blob encrypted secrets

The comment and blob fields must be ignored when comparing a recipient item to a public key. The type and all public key fields should be equal for the keys to match.

When an ssh-box header contains multiple recipient items matching the user's public key, a decryption utility should try to decrypt with all of them, and not give up at the first failure.

The ‘ssh-rsa’ public key format is described in RFC 4253 section 6.6.

The AEAD secrets are encrypted using RSAES-OAEP with MFG1 and SHA-256, and the label ‘ssh-box-v1-rsa-oaep’. RSAES-OAEP is described in RFC 8017.

byte 5
name ssh-rsa
mpint e public exponent
mpint n public modulus
utf8 comment
string blob encrypted secrets

The ‘ssh-ed25519’ public key format is described in RFC 8709 section 4. Ed25519 keys are for signatures and authentication; for encryption and decryption the key must be converted to curve25519, but note that ssh-box always stores keys in ‘ssh-ed25519’ form.

Each ‘ssh-ed25519’ recipient public key is converted using the libsodium function () and the resulting key is used to encrypt the AEAD secrets using the libsodium function ().

Decryption uses the libsodium functions () and ().

byte 4
name ssh-ed25519
string[32] key
utf8 comment
string blob encrypted secrets

The ‘label’ on an ssh-box is arbitrary application-defined public metadata describing the encrypted contents of the file. For example, if the box contains a password, the label might be a JSON object containing the corresponding username and the URL of the login form.

The label is unencrypted cleartext, so that you can find out what a file is for even without a decryption key. The label (and the rest of the header) is authenticated, so if you do have a decryption key, you can be sure the label has not been tampered with.

byte 2
name label
string contents

When an ssh-box header contains multiple ‘label’ items, the complete contents of the label should be constructed by concatenating the contents of every ‘label’ item, in the same order as they appear in the ssh-box header, without any framing.

A program must not fail to read an ssh-box encrypted file because its header contains an item with an unknown type.

A program that is generating or manipulating an ssh-box encrypted file must not include any header item that it does not understand. (In particular, it must not carelessly copy items from one ssh-box header to another.)

Any file with a different PEM encapsulation label or a different format identifier is not covered by this spec.

The ssh-box header is based on data types and structures used by SSH, as described in RFC 4251 section 5, and as follows:

byte
An arbitrary 8-bit value (octet).
byte[n]
Fixed length data represented as an array of bytes, where n is the number of bytes in the array. The length n can be omitted when it is implied by the contents of the array.
uint32
A 32-bit unsigned integer, represented as four bytes in network byte order (big-endian, decreasing order of significance).
string
An arbitrary length binary string, represented as a uint32 containing the string's length (the number of bytes that follow), then zero (= empty string) or more bytes that are the contents of the string.

There are a few kinds of field that have the same representation as a string but whose contents have a particular purpose, or a restricted syntax.

mpint
A string containing a multiple precision integer in two's complement big-endian format.
utf8
A string containing human-readable text encoded in UTF-8.
string[n]
A string containing fixed-length binary data. The difference between a string[n] and byte[n] is that a string[n] has a uint32 length prefix and a byte[n] does not.
name
A string containing a keyword that follows the “Conventions for Names” in RFC 4250 section 4.6.1.

Standard names consist of ASCII characters with codes between 33 and 126 (inclusive), excluding ‘,’ (ASCII 44) and ‘@’ (ASCII 64).

Non-standard extensions can use names of the form ‘name@domain’.

It is generally considered to be a bad idea to use the same key pair for signing and encryption. SSH key pairs are normally used for signing (i.e for authentication), but ssh-box repurposes them as encryption keys.

The risk with this kind of reuse is that it opens you up to cross-protocol attacks, where one protocol is used to gain access to a signing or encryption oracle that allows you to break the other protocol.

Another tool that re-uses ssh keys for encryption is age. The ‘age-encryption’ format specification argues that key reuse is safe with age's tweaked curve25519 scheme. In ssh-box, there is no additional tweak of the curve25519 keys, because of ssh-box's goal to use off-the-shelf cryptographic constructions.

You can also use RSA keys with age, but its spec doesn't explain why this use of RSA is safe. Both age and ssh-box use RSAES-OAEP, whereas ssh uses PKCS #1 v1.5. This difference may reduce the risk of cross-protocol attacks.

To reduce risks, you can:

  • Generate ssh keys exclusively for use with ssh-box, separate from the ssh keys you use for authentication.
  • Never use ssh-box with ssh host keys, because ssh host authentication allows an attacker to provoke private key operations much more easily than user authentication.

Given an unencrypted cleartext file,

EXAMPLE
its ssh-box encrypted version is conventionally called
EXAMPLE.box
and its detached label is called
EXAMPLE.label

See ssh-box(1) for more details of the files it uses.

ssh-box(1), ssh-keygen(1)

The ssh-box web page

Libsodium documentation
specifically the sections on:

age-encryption format specification

The Secure Shell (SSH) Protocol Assigned Numbers, RFC 4250, https://www.rfc-editor.org/rfc/rfc4250.

The Secure Shell (SSH) Protocol Architecture, RFC 4251, https://www.rfc-editor.org/rfc/rfc4251.

The Secure Shell (SSH) Transport Layer Protocol, RFC 4253, https://www.rfc-editor.org/rfc/rfc4253.

Textual Encodings of PKIX, PKCS, and CMS Structures, RFC 7468, https://www.rfc-editor.org/rfc/rfc7468.

PKCS #1: RSA Cryptography Specifications Version 2.2, RFC 8017, https://www.rfc-editor.org/rfc/rfc8017.

Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol, RFC 8709, https://www.rfc-editor.org/rfc/rfc4251.

This specification is written in terms of several libsodium functions. The aim is to use the best available misuse-resistant high-level cryptographic functions, and avoid being too clever. It would be better to have a description of what these functions do, in enough detail that an expert would be able to write an alternative implementation. However the libsodium documentation and source code do not cite any specifications.

Tony Finch ⟨dot@dotat.at⟩

November 14, 2021 SSH-BOX Pq 5