2018-10-18 17:10:29 +09:00
|
|
|
[](https://godoc.org/github.com/opencoff/go-sign)
|
|
|
|
|
|
|
|
# README for sigtool
|
|
|
|
|
|
|
|
|
|
|
|
## What is this?
|
2019-10-17 14:29:01 -07:00
|
|
|
`sigtool` is an opinionated tool to generate keys, sign, verify, encrypt &
|
|
|
|
decrypt files using Ed25519 signature scheme. In many ways, it is like
|
|
|
|
like OpenBSD's [signify][1] -- except written in Golang and definitely
|
2021-05-15 19:35:54 -07:00
|
|
|
easier to use. It can use SSH ed25519 public and private keys.
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
It can sign and verify very large files - it prehashes the files
|
2019-10-17 14:29:01 -07:00
|
|
|
with SHA-512 and then signs the SHA-512 checksum. The keys and signatures
|
2021-05-15 19:35:54 -07:00
|
|
|
are human readable YAML files.
|
2019-10-17 14:29:01 -07:00
|
|
|
|
2019-10-18 15:42:08 -07:00
|
|
|
It can encrypt files for multiple recipients - each of whom is identified
|
2021-05-15 19:35:54 -07:00
|
|
|
by their Ed25519 public key. The encryption generates ephmeral
|
2019-10-18 15:42:08 -07:00
|
|
|
Curve25519 keys and creates pair-wise shared secret for each recipient of
|
2021-05-15 19:35:54 -07:00
|
|
|
the encrypted file. The caller can optionally use a specific private key
|
2019-10-18 15:42:08 -07:00
|
|
|
during the encryption process - this has the benefit of also authenticating
|
|
|
|
the sender (and the receiver can verify the sender if they possess the
|
2021-05-15 19:35:54 -07:00
|
|
|
corresponding sender's public key).
|
2019-10-17 14:29:01 -07:00
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys
|
|
|
|
*or* the keys generated by sigtool. This means, you can send encrypted
|
|
|
|
files to any recipient identified by their comment in `~/.ssh/authorized_keys`.
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
## How do I build it?
|
2022-04-27 08:59:18 +05:30
|
|
|
You need two things:
|
|
|
|
|
|
|
|
1. Protobuf compiler:
|
|
|
|
|
|
|
|
On Debian based systems: `apt install protobuf-compiler`
|
|
|
|
|
|
|
|
Consult your OS's package manager to install protobuf tools;
|
|
|
|
these are typically named 'protobuf' or 'protoc'.
|
|
|
|
|
|
|
|
2. go 1.13+ toolchain
|
|
|
|
|
|
|
|
|
|
|
|
Next, build sigtool:
|
2018-10-18 17:10:29 +09:00
|
|
|
|
2025-02-25 11:08:20 -08:00
|
|
|
git clone https://git.rgst.io/homelab/sigtool/v3
|
2018-10-18 17:10:29 +09:00
|
|
|
cd sigtool
|
|
|
|
make
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
The binary will be in `./bin/$HOSTOS-$ARCH/sigtool`.
|
|
|
|
where `$HOSTOS` is the host OS where you are building (e.g., openbsd)
|
|
|
|
and `$ARCH` is the CPU architecture (e.g., amd64).
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
## How do I use it?
|
|
|
|
Broadly, the tool can:
|
|
|
|
|
|
|
|
- generate new key pairs (public key and private key)
|
|
|
|
- sign a file
|
|
|
|
- verify a file against its signature
|
2019-10-17 14:29:01 -07:00
|
|
|
- encrypt a file
|
|
|
|
- decrypt a file
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
### Generate Key pair
|
|
|
|
To start with, you generate a new key pair (a public key used for
|
|
|
|
verification and a private key used for signing). e.g.,
|
|
|
|
|
|
|
|
sigtool gen /tmp/testkey
|
|
|
|
|
|
|
|
The tool then generates */tmp/testkey.pub* and */tmp/testkey.key*. The secret
|
|
|
|
key (".key") can optionally be encrypted with a user supplied pass
|
|
|
|
phrase - which the user has to enter via interactive prompt:
|
|
|
|
|
|
|
|
sigtool gen -p /tmp/testkey
|
|
|
|
|
|
|
|
### Sign a file
|
|
|
|
Signing a file requires the user to provide a previously generated
|
|
|
|
Ed25519 private key. The signature (YAML) is written to STDOUT.
|
|
|
|
e.g., to sign `archive.tar.gz` with private key `/tmp/testkey.key`:
|
|
|
|
|
|
|
|
sigtool sign /tmp/testkey.key archive.tar.gz
|
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
If *testkey.key* was encrypted without a user pass phrase:
|
2018-10-18 17:10:29 +09:00
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
sigtool sign --no-password /tmp/testkey.key archive.tar.gz
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
|
|
|
|
The signature can also be written directly to a user supplied output
|
|
|
|
file.
|
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
sigtool sign -o archive.sig /tmp/testkey.key archive.tar.gz
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
|
|
|
|
### Verify a signature against a file
|
|
|
|
Verifying a signature of a file requires the user to supply three
|
|
|
|
pieces of information:
|
|
|
|
|
|
|
|
- the Ed25519 public key to be used for verification
|
|
|
|
- the Ed25519 signature
|
|
|
|
- the file whose signature must be verified
|
|
|
|
|
|
|
|
e.g., to verify the signature of *archive.tar.gz* against
|
|
|
|
*testkey.pub* using the signature *archive.sig*
|
|
|
|
|
|
|
|
sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz
|
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
|
2024-01-13 10:34:24 -08:00
|
|
|
You can also pass a public key as a string (instead of a file):
|
|
|
|
|
|
|
|
sigtool verify iF84Dymq/bAEnUMK6DRIHWAQDRD8FwDDDfsgFfzdjWM= archive.sig archive.tar.gz
|
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
Note that signing and verifying can also work with OpenSSH ed25519
|
|
|
|
keys.
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
### Encrypt a file by authenticating the sender
|
|
|
|
If the sender wishes to prove to the recipient that they encrypted
|
|
|
|
a file:
|
|
|
|
|
2019-10-22 10:06:49 -07:00
|
|
|
sigtool encrypt -s sender.key to.pub -o archive.tar.gz.enc archive.tar.gz
|
2019-10-17 14:29:01 -07:00
|
|
|
|
|
|
|
|
|
|
|
This will create an encrypted file *archive.tar.gz.enc* such that the
|
2019-10-18 15:42:08 -07:00
|
|
|
recipient in possession of *to.key* can decrypt it. Furthermore, if
|
|
|
|
the recipient has *sender.pub*, they can verify that the sender is indeed
|
2019-10-17 14:29:01 -07:00
|
|
|
who they expect.
|
|
|
|
|
2019-10-18 15:42:08 -07:00
|
|
|
### Decrypt a file and verify the sender
|
|
|
|
If the receiver has the public key of the sender, they can verify that
|
|
|
|
they indeed sent the file by cryptographically checking the output:
|
|
|
|
|
2019-10-22 10:06:49 -07:00
|
|
|
sigtool decrypt -o archive.tar.gz -v sender.pub to.key archive.tar.gz.enc
|
2019-10-18 15:42:08 -07:00
|
|
|
|
|
|
|
Note that the verification is optional and if the `-v` option is not
|
|
|
|
used, then decryption will proceed without verifying the sender.
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
### Encrypt a file *without* authenticating the sender
|
2019-10-18 15:42:08 -07:00
|
|
|
`sigtool` can generate ephemeral keys for encrypting a file such that
|
|
|
|
the receiver doesn't need to authenticate the sender:
|
|
|
|
|
2019-10-22 10:06:49 -07:00
|
|
|
sigtool encrypt to.pub -o archive.tar.gz.enc archive.tar.gz
|
2019-10-18 15:42:08 -07:00
|
|
|
|
|
|
|
This will create an encrypted file *archive.tar.gz.enc* such that the
|
|
|
|
recipient in possession of *to.key* can decrypt it.
|
2019-10-17 14:29:01 -07:00
|
|
|
|
2019-11-05 21:42:25 +01:00
|
|
|
### Encrypt a file to an OpenSSH recipient *without* authenticating the sender
|
|
|
|
Suppose you want to send an encrypted file where the recipient's
|
|
|
|
public key is in `~/.ssh/authorized_keys`. Such a recipient is identified
|
|
|
|
by their OpenSSH key comment (typically `name@domain`):
|
|
|
|
|
|
|
|
sigtool encrypt user@domain -o archive.tar.gz.enc archive.tar.gz
|
|
|
|
|
|
|
|
If you have their public key in file "name-domain.pub", you can do:
|
|
|
|
|
|
|
|
sigtool encrypt name-domain.pub -o archive.tar.gz.enc archive.tar.gz
|
|
|
|
|
|
|
|
This will create an encrypted file *archive.tar.gz.enc* such that the
|
|
|
|
recipient can decrypt using their private key.
|
|
|
|
|
2019-10-18 15:42:08 -07:00
|
|
|
## Technical Details
|
2019-10-17 14:29:01 -07:00
|
|
|
|
2020-03-23 10:44:40 -07:00
|
|
|
### How is the file encryption done?
|
2019-10-18 15:42:08 -07:00
|
|
|
The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses
|
2022-11-15 09:16:06 -08:00
|
|
|
a random 32-byte AES-256 key. This root key is expanded via
|
|
|
|
HKDF-SHA256 into:
|
2019-10-18 15:42:08 -07:00
|
|
|
|
2022-11-15 09:16:06 -08:00
|
|
|
- AES-GCM-256 key (32 bytes)
|
|
|
|
- AES Nonce (12 bytes)
|
|
|
|
- HMAC-SHA-256 key (32 bytes)
|
2021-05-15 19:35:54 -07:00
|
|
|
|
2022-11-15 09:16:06 -08:00
|
|
|
The input to the HKDF is the root-key, header-checksum ("salt") and
|
|
|
|
a context string.
|
|
|
|
|
|
|
|
The input is broken into chunks and each chunk is individually AEAD encrypted.
|
|
|
|
The default chunk size is 4MB (4 * 1048576 bytes). Each chunk generates
|
|
|
|
its own nonce: the top-4 bytes of the nonce is the chunk-number. The
|
|
|
|
actual chunk-length and EOF marker is used as additional data (the
|
|
|
|
"AD" of "AEAD").
|
2021-05-15 19:35:54 -07:00
|
|
|
The last block has its most-signficant-bit set to 1 to denote EOF. Thus, the
|
|
|
|
maximum chunk size is set to 1GB.
|
|
|
|
|
2022-11-15 09:16:06 -08:00
|
|
|
We calculate a running hmac of the plaintext blocks; when sender
|
|
|
|
identity is present, the final HMAC is signed via the sender's
|
|
|
|
Ed25519 key. This signature is appended as the "trailer" (last 64
|
|
|
|
bytes of the encrypted file are the Ed25519 signature).
|
|
|
|
|
|
|
|
When sender identity is not present, the last bytes are random
|
|
|
|
bytes.
|
|
|
|
|
2021-05-15 19:35:54 -07:00
|
|
|
### What is the public-key cryptography in sigtool?
|
2020-03-23 10:44:40 -07:00
|
|
|
`sigtool` uses ephemeral Curve25519 keys to generate shared secrets
|
|
|
|
between pairs of sender & one or more recipients. This pairwise shared
|
2021-05-15 19:35:54 -07:00
|
|
|
secret is used as a key-encryption-key (KEK) to wrap the
|
2020-03-23 10:44:40 -07:00
|
|
|
data-encryption key in AEAD mode. Thus, each recipient has their own
|
2021-05-15 19:35:54 -07:00
|
|
|
individual encrypted key blob - that **only** they can decrypt.
|
2020-03-23 10:44:40 -07:00
|
|
|
|
|
|
|
If the sender authenticates the encryption by providing their secret
|
2022-11-15 09:16:06 -08:00
|
|
|
key, the encryption key material is signed via Ed25519 and the signature
|
2020-03-23 10:44:40 -07:00
|
|
|
is encrypted (using the data-encryption key) and stored in the
|
|
|
|
header. If the sender opts to not authenticate, a "signature" of all
|
|
|
|
zeroes is encrypted instead.
|
|
|
|
|
|
|
|
The Ed25519 keys generated by `sigtool` or OpenSSH are transformed to their
|
2021-05-15 19:35:54 -07:00
|
|
|
corresponding Curve25519 points in order to generate the pair-wise shared secret.
|
2019-10-18 15:42:08 -07:00
|
|
|
This elliptic co-ordinate transform follows [FiloSottile's writeup][2].
|
|
|
|
|
|
|
|
### Format of the Encrypted File
|
2020-01-08 09:17:54 -08:00
|
|
|
Every encrypted file starts with a header and the header-checksum:
|
|
|
|
|
|
|
|
* Fixed-size header
|
|
|
|
* Variable-length header
|
|
|
|
* SHA256 sum of both of the above
|
|
|
|
|
|
|
|
The fixed length header is:
|
2019-10-18 15:42:08 -07:00
|
|
|
|
|
|
|
7 byte magic ("SigTool")
|
|
|
|
1 byte version number
|
|
|
|
4 byte header length (big endian encoding)
|
|
|
|
|
2020-01-08 09:17:54 -08:00
|
|
|
The variable length header has the per-recipient wrapped keys. This is
|
|
|
|
described as a protobuf file (sign/hdr.proto):
|
2019-10-18 15:42:08 -07:00
|
|
|
|
|
|
|
```protobuf
|
|
|
|
message header {
|
2020-03-23 10:44:40 -07:00
|
|
|
uint32 chunk_size = 1;
|
|
|
|
bytes salt = 2;
|
|
|
|
bytes pk = 3; // sender's ephemeral curve PK
|
2022-11-15 09:16:06 -08:00
|
|
|
bytes sender = 4; // ed25519 signature of key material
|
2020-03-23 10:44:40 -07:00
|
|
|
repeated wrapped_key keys = 5;
|
2020-03-20 17:40:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* A file encryption key is wrapped by a recipient specific public
|
|
|
|
* key. WrappedKey describes such a wrapped key.
|
|
|
|
*/
|
2019-10-18 15:42:08 -07:00
|
|
|
message wrapped_key {
|
2020-03-23 10:44:40 -07:00
|
|
|
bytes d_key = 1;
|
2022-11-15 09:16:06 -08:00
|
|
|
bytes nonce = 2;
|
2019-10-18 15:42:08 -07:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2020-01-08 09:17:54 -08:00
|
|
|
The SHA256 sum covers the fixed-length and variable-length headers.
|
|
|
|
|
2019-10-18 15:42:08 -07:00
|
|
|
The encrypted data immediately follows the headers above. Each encrypted
|
|
|
|
chunk is encoded the same way:
|
|
|
|
|
|
|
|
```C
|
|
|
|
4 byte chunk length (big endian encoding)
|
2021-05-15 19:35:54 -07:00
|
|
|
AEAD encrypted chunk data
|
2019-10-18 15:42:08 -07:00
|
|
|
AEAD tag
|
|
|
|
```
|
|
|
|
|
2020-03-20 17:40:52 -07:00
|
|
|
The chunk length does _not_ include the AEAD tag length; it is implicitly
|
2021-05-15 19:35:54 -07:00
|
|
|
computed. The chunk data and AEAD tag are treated as an atomic unit for AEAD
|
2019-10-18 15:42:08 -07:00
|
|
|
decryption.
|
|
|
|
|
2020-03-23 10:44:40 -07:00
|
|
|
### How is the private key protected?
|
|
|
|
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key
|
2022-04-29 21:36:39 +05:30
|
|
|
derived from the user's pass-phrase. The user pass phrase is expanded via
|
|
|
|
SHA256; this expanded pass phrase is fed to `scrypt()` to
|
2021-05-15 19:35:54 -07:00
|
|
|
generate a key-encryption-key. In pseudo code, this operation looks
|
|
|
|
like below:
|
|
|
|
|
|
|
|
passphrase = get_user_passphrase()
|
|
|
|
expanded = SHA512(passphrase)
|
|
|
|
salt = randombytes(32)
|
|
|
|
key = Scrypt(expanded, salt, N, r, p)
|
|
|
|
esk = AES256_GCM(ed25519_private_key, key)
|
|
|
|
|
|
|
|
Where, ```N```, ```r```, ```p``` are Scrypt parameters. In our
|
|
|
|
implementation:
|
|
|
|
|
|
|
|
N = 2^19 (1 << 19)
|
|
|
|
r = 8
|
|
|
|
p = 1
|
2020-03-23 10:44:40 -07:00
|
|
|
|
|
|
|
|
2018-10-18 17:10:29 +09:00
|
|
|
## Understanding the Code
|
2020-03-20 17:40:52 -07:00
|
|
|
The core logic is in `src/sign`: it is a library that exposes all the
|
|
|
|
functionality: key generation, key parsing, signing, encryption, decryption
|
|
|
|
etc.
|
|
|
|
|
|
|
|
* `src/encrypt.go` contains the core encryption, decryption code
|
|
|
|
* `src/sign.go` contains the Ed25519 signing, verification code
|
|
|
|
* `src/keys.go` contains key generation, serialization, de-serialization
|
|
|
|
* `src/ssh.go` contains code to parse SSH Ed25519 key files
|
|
|
|
* `src/stream.go` contains code that provides an `io.Reader` and `io.WriteCloser` interface
|
2021-05-15 19:35:54 -07:00
|
|
|
for encryption and decryption.
|
2022-04-29 21:36:39 +05:30
|
|
|
* `tests.sh` simple round trip test using the tool; this is in addition to the tests in
|
|
|
|
`sign/`.
|
|
|
|
|
2019-10-18 15:42:08 -07:00
|
|
|
|
2018-10-18 17:10:29 +09:00
|
|
|
The generated keys and signatures are proper YAML files and human
|
|
|
|
readable.
|
|
|
|
|
|
|
|
The signature file contains a hash of the public key - so that at
|
|
|
|
verification time, the right private key may be used (in situations
|
|
|
|
where there are lots of keys).
|
|
|
|
|
|
|
|
Signatures on large files are calculated efficiently by reading them
|
|
|
|
in memory mapped mode (```mmap(2)```) and hashing the file contents
|
|
|
|
using SHA-512. The Ed25519 signature is calculated on the file-hash.
|
|
|
|
|
2022-04-29 21:36:39 +05:30
|
|
|
### Tests
|
|
|
|
The core library in `sign/` has extensive tests to verify signing and encryption.
|
|
|
|
Additionally, a simple shell script `tests.sh` does a full roundtrip of tests
|
|
|
|
using `sigtool`.
|
|
|
|
|
2018-10-18 17:10:29 +09:00
|
|
|
## Example of Keys, Signature
|
|
|
|
|
|
|
|
### Ed25519 Public Key
|
|
|
|
A serialized Ed25519 public key looks like so:
|
|
|
|
|
|
|
|
pk: uxpDh+gqXojAmxA/6vxZHzA+Uk+8wogUwvEhPBlWgvo=
|
|
|
|
|
|
|
|
### Ed25519 Private Key
|
|
|
|
And, a serialized Ed25519 private key looks like so:
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
```yaml
|
|
|
|
|
2018-10-18 17:10:29 +09:00
|
|
|
esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ==
|
|
|
|
salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s=
|
|
|
|
algo: scrypt-sha256
|
|
|
|
Z: 131072
|
|
|
|
r: 16
|
|
|
|
p: 1
|
2019-10-17 14:29:01 -07:00
|
|
|
```
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
|
|
|
|
### Ed25519 Signature
|
|
|
|
A generated signature looks like below after serialization:
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
```yaml
|
|
|
|
|
2018-10-18 17:10:29 +09:00
|
|
|
comment: inpfile=/tmp/file.txt
|
|
|
|
pkhash: 36z9tCwTIVNwwDlExrB0SQ==
|
|
|
|
signature: ow2oBP+buDbEvlNakOrsxgB5Yc/7PYyPVZCkfyu7oahw8BakF4Qf32uswPaKGZ8RVz4uXboYHdZtfrEjCgP/Cg==
|
2019-10-17 14:29:01 -07:00
|
|
|
```
|
2018-10-18 17:10:29 +09:00
|
|
|
|
|
|
|
Here, ```pkhash`` is a SHA256 of the public key needed to verify
|
|
|
|
this signature.
|
|
|
|
|
|
|
|
## Licensing Terms
|
|
|
|
The tool and code is licensed under the terms of the
|
|
|
|
GNU Public License v2.0 (strictly v2.0). If you need a commercial
|
|
|
|
license or a different license, please get in touch with me.
|
|
|
|
|
|
|
|
See the file ``LICENSE.md`` for the full terms of the license.
|
|
|
|
|
|
|
|
## Author
|
|
|
|
Sudhi Herle <sw@herle.net>
|
|
|
|
|
2019-10-17 14:29:01 -07:00
|
|
|
[1]: https://www.openbsd.org/papers/bsdcan-signify.html
|
|
|
|
[2]: https://blog.filippo.io/using-ed25519-keys-for-encryption/
|