Compare commits
65 commits
Author | SHA1 | Date | |
---|---|---|---|
aa96303d43 | |||
8d3686eb14 | |||
6b2e5526b2 | |||
d128cfa7ce | |||
3f6b9cd2af | |||
6a16789886 | |||
1823aaa5e0 | |||
810aa02bdd | |||
|
1786734c0a | ||
|
d47a4596ef | ||
|
fc94d7cd7d | ||
|
2e6d92c753 | ||
|
5c6152b4ed | ||
|
e3053142f5 | ||
|
d49f732c71 | ||
|
15053202a1 | ||
|
c5400a6b18 | ||
|
eae20abd24 | ||
|
c4f79962c9 | ||
|
a538ac8e5c | ||
|
c95515af0e | ||
|
bbd7afd496 | ||
|
f343d45a8e | ||
|
a428db8feb | ||
|
0ddf48c92f | ||
|
42bbe5ddeb | ||
|
f180079586 | ||
|
445c13ca6f | ||
|
bce89dacb0 | ||
|
460f1cf703 | ||
|
43a9f38592 | ||
|
81a6522ee7 | ||
|
85ed0d06dd | ||
|
0658fb75d4 | ||
|
945046a815 | ||
|
3fa0ce0c9c | ||
|
e22fae05f7 | ||
|
00542dec02 | ||
|
36410626dd | ||
|
1cd3a94180 | ||
|
088f1e9ca2 | ||
|
8ed3bff6db | ||
|
d18b7a05bc | ||
|
0ba5c8b599 | ||
|
fbfcd37679 | ||
|
f32525a864 | ||
|
262a554356 | ||
|
374daebb8d | ||
|
48142c5577 | ||
|
0abbfd37ec | ||
|
a9c17988c4 | ||
|
f82c1336ac | ||
|
b14f9d1e53 | ||
|
817aa7fd6a | ||
|
a347fdca79 | ||
|
387c75e791 | ||
|
d9755bc793 | ||
|
3c3c51b5ac | ||
|
a1bbcbd5a8 | ||
|
0d61498db9 | ||
|
a27044154a | ||
|
21445ba1a1 | ||
|
9473c10bfd | ||
|
1cc55e1a55 | ||
|
f0302e3a7d |
34 changed files with 5493 additions and 930 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -24,12 +24,11 @@ _testmain.go
|
||||||
*.prof
|
*.prof
|
||||||
vendor/*
|
vendor/*
|
||||||
|
|
||||||
# vendor management
|
|
||||||
vendor/src/*
|
|
||||||
vendor/pkg/*
|
|
||||||
bin/*
|
bin/*
|
||||||
|
sigtool
|
||||||
|
|
||||||
.??*.sw?
|
.??*.sw?
|
||||||
*.pub
|
*.pub
|
||||||
*.key
|
*.key
|
||||||
*.sig
|
*.sig
|
||||||
|
releases/*
|
||||||
|
|
2
.mise.toml
Normal file
2
.mise.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[tools]
|
||||||
|
golang = "1.24"
|
5
Makefile
5
Makefile
|
@ -4,11 +4,10 @@ pwd = $(shell pwd)
|
||||||
.PHONY: all test clean realclean
|
.PHONY: all test clean realclean
|
||||||
|
|
||||||
all:
|
all:
|
||||||
mkdir -p bin
|
./build -s
|
||||||
go build -o bin/sigtool .
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./sign
|
go test ./sign
|
||||||
|
|
||||||
clean realclean:
|
clean realclean:
|
||||||
rm -f bin/sigtool
|
rm -rf bin
|
||||||
|
|
283
README.md
283
README.md
|
@ -4,24 +4,49 @@
|
||||||
|
|
||||||
|
|
||||||
## What is this?
|
## What is this?
|
||||||
`sigtool` is an opinionated tool to generate, sign and verify Ed25519
|
`sigtool` is an opinionated tool to generate keys, sign, verify, encrypt &
|
||||||
signatures on files. In many ways, it is like like OpenBSD's signify_
|
decrypt files using Ed25519 signature scheme. In many ways, it is like
|
||||||
-- except written in Golang and definitely easier to use.
|
like OpenBSD's [signify][1] -- except written in Golang and definitely
|
||||||
|
easier to use. It can use SSH ed25519 public and private keys.
|
||||||
|
|
||||||
It can sign and verify very large files - it prehashes the files
|
It can sign and verify very large files - it prehashes the files
|
||||||
with SHA-512 and then signs the SHA-512 checksum.
|
with SHA-512 and then signs the SHA-512 checksum. The keys and signatures
|
||||||
|
are human readable YAML files.
|
||||||
|
|
||||||
All the artifacts produced by this tool are standard YAML files -
|
It can encrypt files for multiple recipients - each of whom is identified
|
||||||
thus, human readable.
|
by their Ed25519 public key. The encryption generates ephmeral
|
||||||
|
Curve25519 keys and creates pair-wise shared secret for each recipient of
|
||||||
|
the encrypted file. The caller can optionally use a specific private key
|
||||||
|
during the encryption process - this has the benefit of also authenticating
|
||||||
|
the sender (and the receiver can verify the sender if they possess the
|
||||||
|
corresponding sender's public key).
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
## How do I build it?
|
## How do I build it?
|
||||||
With Go 1.5 and later:
|
You need two things:
|
||||||
|
|
||||||
git clone https://github.com/opencoff/sigtool
|
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:
|
||||||
|
|
||||||
|
git clone https://git.rgst.io/homelab/sigtool/v3
|
||||||
cd sigtool
|
cd sigtool
|
||||||
make
|
make
|
||||||
|
|
||||||
The binary will be in `./sigtool`.
|
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).
|
||||||
|
|
||||||
## How do I use it?
|
## How do I use it?
|
||||||
Broadly, the tool can:
|
Broadly, the tool can:
|
||||||
|
@ -29,6 +54,8 @@ Broadly, the tool can:
|
||||||
- generate new key pairs (public key and private key)
|
- generate new key pairs (public key and private key)
|
||||||
- sign a file
|
- sign a file
|
||||||
- verify a file against its signature
|
- verify a file against its signature
|
||||||
|
- encrypt a file
|
||||||
|
- decrypt a file
|
||||||
|
|
||||||
### Generate Key pair
|
### Generate Key pair
|
||||||
To start with, you generate a new key pair (a public key used for
|
To start with, you generate a new key pair (a public key used for
|
||||||
|
@ -49,15 +76,15 @@ e.g., to sign `archive.tar.gz` with private key `/tmp/testkey.key`:
|
||||||
|
|
||||||
sigtool sign /tmp/testkey.key archive.tar.gz
|
sigtool sign /tmp/testkey.key archive.tar.gz
|
||||||
|
|
||||||
If *testkey.key* was encrypted with a user pass phrase:
|
If *testkey.key* was encrypted without a user pass phrase:
|
||||||
|
|
||||||
sigtool sign -p /tmp/testkey.key archive.tar.gz
|
sigtool sign --no-password /tmp/testkey.key archive.tar.gz
|
||||||
|
|
||||||
|
|
||||||
The signature can also be written directly to a user supplied output
|
The signature can also be written directly to a user supplied output
|
||||||
file.
|
file.
|
||||||
|
|
||||||
sigtool sign -p -o archive.sig /tmp/testkey.key archive.tar.gz
|
sigtool sign -o archive.sig /tmp/testkey.key archive.tar.gz
|
||||||
|
|
||||||
|
|
||||||
### Verify a signature against a file
|
### Verify a signature against a file
|
||||||
|
@ -73,22 +100,190 @@ e.g., to verify the signature of *archive.tar.gz* against
|
||||||
|
|
||||||
sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz
|
sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz
|
||||||
|
|
||||||
## How is the private key protected?
|
|
||||||
The Ed25519 private key is encrypted using a key derived from the
|
|
||||||
user supplied pass phrase. This pass phrase is used to derive an
|
|
||||||
encryption key using the Scrypt key derivation algorithm. The
|
|
||||||
resulting derived key is XOR'd with the Ed25519 private key before
|
|
||||||
being committed to disk. To protect the integrity of the process,
|
|
||||||
the essential parameters used for deriving the key, and the derived
|
|
||||||
key are hashed via SHA256 and stored along with the encrypted key.
|
|
||||||
|
|
||||||
As an additional security measure, the user supplied pass phrase is
|
You can also pass a public key as a string (instead of a file):
|
||||||
hashed with SHA512.
|
|
||||||
|
sigtool verify iF84Dymq/bAEnUMK6DRIHWAQDRD8FwDDDfsgFfzdjWM= archive.sig archive.tar.gz
|
||||||
|
|
||||||
|
Note that signing and verifying can also work with OpenSSH ed25519
|
||||||
|
keys.
|
||||||
|
|
||||||
|
### Encrypt a file by authenticating the sender
|
||||||
|
If the sender wishes to prove to the recipient that they encrypted
|
||||||
|
a file:
|
||||||
|
|
||||||
|
sigtool encrypt -s sender.key to.pub -o archive.tar.gz.enc archive.tar.gz
|
||||||
|
|
||||||
|
|
||||||
|
This will create an encrypted file *archive.tar.gz.enc* such that the
|
||||||
|
recipient in possession of *to.key* can decrypt it. Furthermore, if
|
||||||
|
the recipient has *sender.pub*, they can verify that the sender is indeed
|
||||||
|
who they expect.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
sigtool decrypt -o archive.tar.gz -v sender.pub to.key archive.tar.gz.enc
|
||||||
|
|
||||||
|
Note that the verification is optional and if the `-v` option is not
|
||||||
|
used, then decryption will proceed without verifying the sender.
|
||||||
|
|
||||||
|
### Encrypt a file *without* authenticating the sender
|
||||||
|
`sigtool` can generate ephemeral keys for encrypting a file such that
|
||||||
|
the receiver doesn't need to authenticate the sender:
|
||||||
|
|
||||||
|
sigtool encrypt to.pub -o archive.tar.gz.enc archive.tar.gz
|
||||||
|
|
||||||
|
This will create an encrypted file *archive.tar.gz.enc* such that the
|
||||||
|
recipient in possession of *to.key* can decrypt it.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### How is the file encryption done?
|
||||||
|
The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses
|
||||||
|
a random 32-byte AES-256 key. This root key is expanded via
|
||||||
|
HKDF-SHA256 into:
|
||||||
|
|
||||||
|
- AES-GCM-256 key (32 bytes)
|
||||||
|
- AES Nonce (12 bytes)
|
||||||
|
- HMAC-SHA-256 key (32 bytes)
|
||||||
|
|
||||||
|
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").
|
||||||
|
The last block has its most-signficant-bit set to 1 to denote EOF. Thus, the
|
||||||
|
maximum chunk size is set to 1GB.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### What is the public-key cryptography in sigtool?
|
||||||
|
`sigtool` uses ephemeral Curve25519 keys to generate shared secrets
|
||||||
|
between pairs of sender & one or more recipients. This pairwise shared
|
||||||
|
secret is used as a key-encryption-key (KEK) to wrap the
|
||||||
|
data-encryption key in AEAD mode. Thus, each recipient has their own
|
||||||
|
individual encrypted key blob - that **only** they can decrypt.
|
||||||
|
|
||||||
|
If the sender authenticates the encryption by providing their secret
|
||||||
|
key, the encryption key material is signed via Ed25519 and the signature
|
||||||
|
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
|
||||||
|
corresponding Curve25519 points in order to generate the pair-wise shared secret.
|
||||||
|
This elliptic co-ordinate transform follows [FiloSottile's writeup][2].
|
||||||
|
|
||||||
|
### Format of the Encrypted File
|
||||||
|
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:
|
||||||
|
|
||||||
|
7 byte magic ("SigTool")
|
||||||
|
1 byte version number
|
||||||
|
4 byte header length (big endian encoding)
|
||||||
|
|
||||||
|
The variable length header has the per-recipient wrapped keys. This is
|
||||||
|
described as a protobuf file (sign/hdr.proto):
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
message header {
|
||||||
|
uint32 chunk_size = 1;
|
||||||
|
bytes salt = 2;
|
||||||
|
bytes pk = 3; // sender's ephemeral curve PK
|
||||||
|
bytes sender = 4; // ed25519 signature of key material
|
||||||
|
repeated wrapped_key keys = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A file encryption key is wrapped by a recipient specific public
|
||||||
|
* key. WrappedKey describes such a wrapped key.
|
||||||
|
*/
|
||||||
|
message wrapped_key {
|
||||||
|
bytes d_key = 1;
|
||||||
|
bytes nonce = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The SHA256 sum covers the fixed-length and variable-length headers.
|
||||||
|
|
||||||
|
The encrypted data immediately follows the headers above. Each encrypted
|
||||||
|
chunk is encoded the same way:
|
||||||
|
|
||||||
|
```C
|
||||||
|
4 byte chunk length (big endian encoding)
|
||||||
|
AEAD encrypted chunk data
|
||||||
|
AEAD tag
|
||||||
|
```
|
||||||
|
|
||||||
|
The chunk length does _not_ include the AEAD tag length; it is implicitly
|
||||||
|
computed. The chunk data and AEAD tag are treated as an atomic unit for AEAD
|
||||||
|
decryption.
|
||||||
|
|
||||||
|
### How is the private key protected?
|
||||||
|
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key
|
||||||
|
derived from the user's pass-phrase. The user pass phrase is expanded via
|
||||||
|
SHA256; this expanded pass phrase is fed to `scrypt()` to
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
## Understanding the Code
|
## Understanding the Code
|
||||||
`src/sign` is a library to generate, verify and store Ed25519 keys
|
The core logic is in `src/sign`: it is a library that exposes all the
|
||||||
and signatures. It uses the extended library (golang.org/x/crypto)
|
functionality: key generation, key parsing, signing, encryption, decryption
|
||||||
for the underlying operations.
|
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
|
||||||
|
for encryption and decryption.
|
||||||
|
* `tests.sh` simple round trip test using the tool; this is in addition to the tests in
|
||||||
|
`sign/`.
|
||||||
|
|
||||||
|
|
||||||
The generated keys and signatures are proper YAML files and human
|
The generated keys and signatures are proper YAML files and human
|
||||||
readable.
|
readable.
|
||||||
|
@ -101,6 +296,11 @@ Signatures on large files are calculated efficiently by reading them
|
||||||
in memory mapped mode (```mmap(2)```) and hashing the file contents
|
in memory mapped mode (```mmap(2)```) and hashing the file contents
|
||||||
using SHA-512. The Ed25519 signature is calculated on the file-hash.
|
using SHA-512. The Ed25519 signature is calculated on the file-hash.
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
## Example of Keys, Signature
|
## Example of Keys, Signature
|
||||||
|
|
||||||
### Ed25519 Public Key
|
### Ed25519 Public Key
|
||||||
|
@ -111,44 +311,26 @@ A serialized Ed25519 public key looks like so:
|
||||||
### Ed25519 Private Key
|
### Ed25519 Private Key
|
||||||
And, a serialized Ed25519 private key looks like so:
|
And, a serialized Ed25519 private key looks like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ==
|
esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ==
|
||||||
salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s=
|
salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s=
|
||||||
algo: scrypt-sha256
|
algo: scrypt-sha256
|
||||||
verify: JvjRjJMKhJhBmZngC3Pvq7x3KCLKt7gar1AAz7HB4qM=
|
|
||||||
Z: 131072
|
Z: 131072
|
||||||
r: 16
|
r: 16
|
||||||
p: 1
|
p: 1
|
||||||
|
```
|
||||||
|
|
||||||
The Ed25519 private key is encrypted using Scrypt password hashing
|
|
||||||
mechanism. A user supplied passphrase to protect the private key
|
|
||||||
is first pre-hashed using SHA-512 before being used in
|
|
||||||
```scrypt()```. In pseudo code, this operation looks like below:
|
|
||||||
|
|
||||||
passphrase = get_user_passphrase()
|
|
||||||
hpass = SHA512(passphrase)
|
|
||||||
salt = randombytes(32)
|
|
||||||
xorkey = Scrypt(hpass, salt, N, r, p)
|
|
||||||
verify = SHA256(salt, xorkey)
|
|
||||||
esk = ed25519_private_key ^ xorkey
|
|
||||||
|
|
||||||
Where, ```N```, ```r```, ```p``` are Scrypt parameters. In our
|
|
||||||
implementation:
|
|
||||||
|
|
||||||
N = 131072
|
|
||||||
r = 16
|
|
||||||
p = 1
|
|
||||||
|
|
||||||
```verify``` is used during the decryption of the Ed25519 private
|
|
||||||
key - *before* actually doing the "xor" operation. This check
|
|
||||||
ensures that the supplied passphrase yields the same value as
|
|
||||||
```verify```.
|
|
||||||
|
|
||||||
### Ed25519 Signature
|
### Ed25519 Signature
|
||||||
A generated signature looks like below after serialization:
|
A generated signature looks like below after serialization:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
comment: inpfile=/tmp/file.txt
|
comment: inpfile=/tmp/file.txt
|
||||||
pkhash: 36z9tCwTIVNwwDlExrB0SQ==
|
pkhash: 36z9tCwTIVNwwDlExrB0SQ==
|
||||||
signature: ow2oBP+buDbEvlNakOrsxgB5Yc/7PYyPVZCkfyu7oahw8BakF4Qf32uswPaKGZ8RVz4uXboYHdZtfrEjCgP/Cg==
|
signature: ow2oBP+buDbEvlNakOrsxgB5Yc/7PYyPVZCkfyu7oahw8BakF4Qf32uswPaKGZ8RVz4uXboYHdZtfrEjCgP/Cg==
|
||||||
|
```
|
||||||
|
|
||||||
Here, ```pkhash`` is a SHA256 of the public key needed to verify
|
Here, ```pkhash`` is a SHA256 of the public key needed to verify
|
||||||
this signature.
|
this signature.
|
||||||
|
@ -163,4 +345,5 @@ See the file ``LICENSE.md`` for the full terms of the license.
|
||||||
## Author
|
## Author
|
||||||
Sudhi Herle <sw@herle.net>
|
Sudhi Herle <sw@herle.net>
|
||||||
|
|
||||||
.. _signify: https://www.openbsd.org/papers/bsdcan-signify.html
|
[1]: https://www.openbsd.org/papers/bsdcan-signify.html
|
||||||
|
[2]: https://blog.filippo.io/using-ed25519-keys-for-encryption/
|
||||||
|
|
462
build
Executable file
462
build
Executable file
|
@ -0,0 +1,462 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
# Tool to build go programs in this repo
|
||||||
|
#
|
||||||
|
# - it tacks on a version number for use by the individual tools
|
||||||
|
# - it supports git and mercurial version#
|
||||||
|
#
|
||||||
|
# NB:
|
||||||
|
# o the attempt at decoding dirty repo state for mercurial is
|
||||||
|
# borked. It doesn't know about untracked files
|
||||||
|
#
|
||||||
|
# (c) 2016 Sudhi Herle
|
||||||
|
#
|
||||||
|
# License: GPLv2
|
||||||
|
#
|
||||||
|
Progs="src:sigtool"
|
||||||
|
|
||||||
|
# Relative path to protobuf sources
|
||||||
|
# e.g. src/foo/a.proto
|
||||||
|
Protobufs="internal/pb/hdr.proto"
|
||||||
|
|
||||||
|
#set -x
|
||||||
|
|
||||||
|
# -- DO NOT CHANGE ANYTHING AFTER THIS --
|
||||||
|
|
||||||
|
Z=`basename $0`
|
||||||
|
PWD=`pwd`
|
||||||
|
|
||||||
|
Static=0
|
||||||
|
Dryrun=0
|
||||||
|
Prodver=""
|
||||||
|
Repover=""
|
||||||
|
Verbose=0
|
||||||
|
Go=`which go`
|
||||||
|
Bindir=$PWD/bin
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "$Z: $@" 1>&2
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "$Z: $@" 1>&2
|
||||||
|
}
|
||||||
|
|
||||||
|
case $BASH_VERSION in
|
||||||
|
4.*|5.*) ;;
|
||||||
|
|
||||||
|
*) die "I need bash 4.x to run!"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
getvcs_version() {
|
||||||
|
local rev=
|
||||||
|
local prodv=
|
||||||
|
local git=`which git`
|
||||||
|
local hg=`which hg`
|
||||||
|
|
||||||
|
if [ -n "$git" ]; then
|
||||||
|
local xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1
|
||||||
|
rev="git:$xrev"
|
||||||
|
prodv=$(git tag --list | sort -V | tail -1)
|
||||||
|
elif [ -n "$hg" ]; then
|
||||||
|
local xrev=$(hg id --id) || exit 1
|
||||||
|
local brev=${xrev%+}
|
||||||
|
if [ "$brev" != "$xrev" ]; then
|
||||||
|
rev="hg:${brev}-dirty"
|
||||||
|
else
|
||||||
|
rev="hg:${brev}"
|
||||||
|
fi
|
||||||
|
prodv=$(hg log -r "branch(stable) and tag()" -T "{tags}\n" | sort -V | tail -1)
|
||||||
|
else
|
||||||
|
warn "no git or hg found; can't get VCS info"
|
||||||
|
rev="UNKNOWN-VER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$Prodver" ] && prodv=$Prodver
|
||||||
|
|
||||||
|
echo "$rev $prodv"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
read -r Repover Prodver <<< $(getvcs_version)
|
||||||
|
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
declare -a progv=($Progs)
|
||||||
|
declare n=${#progv[@]}
|
||||||
|
declare pstr=
|
||||||
|
|
||||||
|
for ((i=0; i < n; i++)); do
|
||||||
|
local ent=${progv[$i]}
|
||||||
|
local dir=${ent%%:*}
|
||||||
|
local tool=${ent##*:}
|
||||||
|
pstr=$(printf "$pstr\n\t%s $Prodver $Repover (from ./%s)" $tool $dir)
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
$0 - A Go production build tool that adds git-repository information,
|
||||||
|
product version, build-timestamp etc. It supports cross-compilation,
|
||||||
|
static linking and generating protobuf output.
|
||||||
|
|
||||||
|
Build output is in bin/\$OS-\$CPU for a given OS, CPU combination.
|
||||||
|
|
||||||
|
Usage: $0
|
||||||
|
$0 [options] [PROGS]
|
||||||
|
|
||||||
|
Where OS-ARCH denotes one of the valid OS, ARCH combinations supported by 'go'.
|
||||||
|
And, PROGS is one or more go programs.
|
||||||
|
|
||||||
|
With no arguments, $0 builds: $pstr
|
||||||
|
|
||||||
|
The repository's latest tag is used as the default version of the software being
|
||||||
|
built. The current repository version is $Repover.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message and quit
|
||||||
|
-b D, --bindir=D Put the binaries in the directory 'D' [$Bindir]
|
||||||
|
-s, --static Build a statically linked binary [False]
|
||||||
|
-V N, --version=N Use 'N' as the product version string [$Prodver]
|
||||||
|
-a X, --arch=X Cross compile for OS-CPU 'X' [$hostos-$hostcpu]
|
||||||
|
-n, --dry-run Dry-run, don't actually build anything [False]
|
||||||
|
-t, --test Run "go test" on modules named on the command line [False]
|
||||||
|
-v, --verbose Build verbosely (adds "-v" to go tooling) [False]
|
||||||
|
--vet Run "go vet" on modules named on the command line [False]
|
||||||
|
--mod Run "go mod ..." [False]
|
||||||
|
--go=G Use Go in 'G' [$Go]
|
||||||
|
-x Run in debug/trace mode [False]
|
||||||
|
--print-arch Print the target architecture and exit
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
host=`uname|tr '[A-Z]' '[a-z]'`
|
||||||
|
|
||||||
|
declare -A oses
|
||||||
|
declare -A cpus
|
||||||
|
declare -A cgo
|
||||||
|
|
||||||
|
# Supported & Verified OS/CPU combos for this script
|
||||||
|
oslist="linux android openbsd freebsd darwin dragonfly netbsd windows"
|
||||||
|
needcgo="android"
|
||||||
|
cpulist="i386 amd64 arm arm64"
|
||||||
|
cpualias_i386="i486 i586 i686"
|
||||||
|
cpualias_amd64="x86_64"
|
||||||
|
cpualias_arm64="aarch64"
|
||||||
|
|
||||||
|
# CGO Cross-Compilers for various CPU+OS combinations of Android
|
||||||
|
android_i386=i686-linux-android-gcc
|
||||||
|
android_arm64=aarch64-linux-android-gcc
|
||||||
|
android_arm=arm-linux-androideabi-gcc
|
||||||
|
|
||||||
|
# initialize the various hash tables
|
||||||
|
for o in $oslist; do oses[$o]=$o; done
|
||||||
|
for o in $needcgo; do cgo[$o]=$o; done
|
||||||
|
for c in $cpulist; do
|
||||||
|
cpus[$c]=$c
|
||||||
|
a="cpualias_$c"
|
||||||
|
a=${!a}
|
||||||
|
for x in $a; do cpus[$x]=$c; done
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
Tool=
|
||||||
|
doinit=0
|
||||||
|
args=
|
||||||
|
Printarch=0
|
||||||
|
|
||||||
|
#set -x
|
||||||
|
ac_prev=
|
||||||
|
for ac_option
|
||||||
|
do
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [ -n "$ac_prev" ]; then
|
||||||
|
eval "$ac_prev=\$ac_option"
|
||||||
|
ac_prev=
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ac_option" in
|
||||||
|
-*=*) ac_optarg=`echo "$ac_option" | sed 's/[-_a-zA-Z0-9]*=//'` ;;
|
||||||
|
*) ac_optarg= ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
case "$ac_option" in
|
||||||
|
--help|-h|--hel|--he|--h)
|
||||||
|
usage;
|
||||||
|
;;
|
||||||
|
|
||||||
|
--arch=*)
|
||||||
|
Arch=$ac_optarg
|
||||||
|
;;
|
||||||
|
|
||||||
|
-a|--arch)
|
||||||
|
ac_prev=Arch
|
||||||
|
;;
|
||||||
|
|
||||||
|
-b|--bindir)
|
||||||
|
ac_prev=Bindir
|
||||||
|
;;
|
||||||
|
|
||||||
|
--bindir=*)
|
||||||
|
Bindir=$ac_optarg
|
||||||
|
;;
|
||||||
|
|
||||||
|
--version=*)
|
||||||
|
Prodver=$ac_optarg
|
||||||
|
;;
|
||||||
|
|
||||||
|
--test|-t)
|
||||||
|
Tool=test
|
||||||
|
;;
|
||||||
|
|
||||||
|
--vet)
|
||||||
|
Tool=vet
|
||||||
|
;;
|
||||||
|
|
||||||
|
--mod)
|
||||||
|
Tool=mod
|
||||||
|
;;
|
||||||
|
|
||||||
|
-V|--version)
|
||||||
|
ac_prev=Prodver
|
||||||
|
;;
|
||||||
|
|
||||||
|
-v|--verbose)
|
||||||
|
Verbose=1
|
||||||
|
;;
|
||||||
|
|
||||||
|
-s|--static)
|
||||||
|
Static=1
|
||||||
|
;;
|
||||||
|
|
||||||
|
--dry-run|-n)
|
||||||
|
Dryrun=1
|
||||||
|
;;
|
||||||
|
|
||||||
|
--debug|-x)
|
||||||
|
set -x
|
||||||
|
;;
|
||||||
|
|
||||||
|
--go-root=*)
|
||||||
|
GoRoot=$ac_optarg
|
||||||
|
;;
|
||||||
|
|
||||||
|
--print-arch)
|
||||||
|
Printarch=1
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) # first non option terminates option processing.
|
||||||
|
# we gather all remaining args and bundle them up.
|
||||||
|
args="$args $ac_option"
|
||||||
|
for xx
|
||||||
|
do
|
||||||
|
args="$args $xx"
|
||||||
|
done
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ $Dryrun -gt 0 ] && e=echo
|
||||||
|
|
||||||
|
# let every error abort
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# build a tool that runs on the host - if needed.
|
||||||
|
hosttool() {
|
||||||
|
local tool=$1
|
||||||
|
local bindir=$2
|
||||||
|
local src=$3
|
||||||
|
|
||||||
|
p=$bindir/$tool
|
||||||
|
if [ -x $p ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmpdir=/tmp/$tool.$$
|
||||||
|
mkdir $tmpdir || die "can't make $tmpdir"
|
||||||
|
|
||||||
|
# since go1.20 - install uses env vars to decide where to put
|
||||||
|
# build artifacts. Why are all the google tooling so bloody dev
|
||||||
|
# hostile! WTF is wrong with command line args?!
|
||||||
|
export GOBIN=$bindir
|
||||||
|
|
||||||
|
# build it and stash it in the hostdir
|
||||||
|
echo "Building tool $tool from $src .."
|
||||||
|
(
|
||||||
|
cd $tmpdir
|
||||||
|
$e $Go install $src@latest || die "can't install $tool"
|
||||||
|
)
|
||||||
|
$e rm -rf $tmpdir
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# protobuf gen
|
||||||
|
buildproto() {
|
||||||
|
local pbgo=protoc-gen-go
|
||||||
|
local vtgo=protoc-gen-go-vtproto
|
||||||
|
local vtgo_src=github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto
|
||||||
|
local pc
|
||||||
|
local args="$*"
|
||||||
|
|
||||||
|
local pgen=$(type -p protoc)
|
||||||
|
local gogen=$(type -p $pbgo)
|
||||||
|
local vt=$Hostbindir/$vtgo
|
||||||
|
|
||||||
|
[ -z $pgen ] && die "install protoc tools"
|
||||||
|
[ -z $gogen ] && die "install protoc-gen-go"
|
||||||
|
|
||||||
|
# now install the vtproto generator
|
||||||
|
hosttool $vtgo $Hostbindir $vtgo_src
|
||||||
|
|
||||||
|
for f in $args; do
|
||||||
|
local dn=$(dirname $f)
|
||||||
|
local bn=$(basename $f .proto)
|
||||||
|
|
||||||
|
|
||||||
|
$e $pgen \
|
||||||
|
--go_out=. --plugin protoc-gen-go=$gogen \
|
||||||
|
--go-vtproto_out=. --plugin protoc-gen-go-vtproto="$vt" \
|
||||||
|
--go-vtproto_opt=features=marshal+unmarshal+size \
|
||||||
|
$f || die "can't generate protobuf output for $f .."
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# the rest has to execute in the context of main shell (not funcs)
|
||||||
|
|
||||||
|
hostos=$($Go env GOHOSTOS) || exit 1
|
||||||
|
hostcpu=$($Go env GOHOSTARCH) || exit 1
|
||||||
|
|
||||||
|
# This fragment can't be in a function - since it exports several vars
|
||||||
|
if [ -n "$Arch" ]; then
|
||||||
|
ox=${Arch%%-*}
|
||||||
|
cx=${Arch##*-}
|
||||||
|
[ "$ox" = "$cx" ] && cx=$hostcpu
|
||||||
|
|
||||||
|
os=${oses[$ox]}
|
||||||
|
cpu=${cpus[$cx]}
|
||||||
|
[ -z "$os" ] && die "Don't know anything about OS $ox"
|
||||||
|
[ -z "$cpu" ] && die "Don't know anything about CPU $cx"
|
||||||
|
|
||||||
|
export GOOS=$os GOARCH=$cpu
|
||||||
|
cross=$os-$cpu
|
||||||
|
|
||||||
|
else
|
||||||
|
os=$hostos
|
||||||
|
cpu=$hostcpu
|
||||||
|
cross=$os-$cpu
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we don't need CGO, then we can attempt a static link
|
||||||
|
if [ -n "${cgo[$os]}" ]; then
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
|
||||||
|
# See if we have a specific cross-compiler for this CPU+OS combo
|
||||||
|
xcc="${GOOS}_${GOARCH}"
|
||||||
|
xcc=${!xcc}
|
||||||
|
if [ -n "$xcc" ]; then
|
||||||
|
p=`type -p $xcc`
|
||||||
|
[ -n "$p" ] || die "Can't find $xcc! Do you have compilers for $GOARCH available in PATH?"
|
||||||
|
export CC=$xcc
|
||||||
|
else
|
||||||
|
echo "$Z: No Cross compiler defined for $GOOS-$GOARCH. Build may fail.." 1>&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ $Static -gt 0 ]; then
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
|
||||||
|
isuffix="--installsuffix cgo"
|
||||||
|
ldflags="-s"
|
||||||
|
msg="statically linked"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $Printarch -gt 0 ]; then
|
||||||
|
echo "$hostos-$hostcpu"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# This is where build outputs go
|
||||||
|
Outdir=$Bindir/$cross
|
||||||
|
Hostbindir=$Bindir/$hostos-$hostcpu
|
||||||
|
export PATH=$Hostbindir:$PATH
|
||||||
|
|
||||||
|
[ -d $Outdir ] || mkdir -p $Outdir
|
||||||
|
[ -d $Hostbindir ] || mkdir -p $Hostbindir
|
||||||
|
|
||||||
|
|
||||||
|
# Do Protobufs if needed
|
||||||
|
if [ -n "$Protobufs" ]; then
|
||||||
|
set +e
|
||||||
|
buildproto $Protobufs
|
||||||
|
set -e
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get git/hg version info for the build
|
||||||
|
repover="main.RepoVersion=$Repover"
|
||||||
|
prodver="main.ProductVersion=$Prodver"
|
||||||
|
ldflags="-ldflags \"-X $repover -X $prodver $ldflags -buildid=\""
|
||||||
|
vflag=""
|
||||||
|
|
||||||
|
[ $Verbose -gt 0 ] && vflag="-v"
|
||||||
|
|
||||||
|
case $Tool in
|
||||||
|
test)
|
||||||
|
set -- $args
|
||||||
|
$e $Go test $vflag "$@"
|
||||||
|
;;
|
||||||
|
|
||||||
|
vet)
|
||||||
|
set -- $args
|
||||||
|
$e $Go vet $vflag "$@"
|
||||||
|
;;
|
||||||
|
|
||||||
|
mod)
|
||||||
|
set -- $args
|
||||||
|
$e $Go mod $vflag "$@"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) # Default is to build programs
|
||||||
|
set -- $args
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
all=$Progs
|
||||||
|
else
|
||||||
|
all="$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$all" ] && die "No programs specified. Try '$Z --help'"
|
||||||
|
|
||||||
|
echo "Building $Prodver ($Repover), $cross $msg .."
|
||||||
|
|
||||||
|
for p in $all; do
|
||||||
|
if echo $p | grep -q ':' ; then
|
||||||
|
out=${p##*:}
|
||||||
|
dir=${p%%:*}
|
||||||
|
else
|
||||||
|
out=$p
|
||||||
|
dir=$p
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add .exe suffix to out if needed
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
base=${out%%.exe}
|
||||||
|
out="${base}.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " $dir: $out .. "
|
||||||
|
$e eval $Go build $vflag -trimpath -o $Outdir/$out $isuffix "$ldflags" ./$dir || exit 1
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# vim: ft=sh:expandtab:ts=4:sw=4:tw=84:
|
27
go.mod
27
go.mod
|
@ -1,10 +1,25 @@
|
||||||
module github.com/opencoff/sigtool
|
module git.rgst.io/homelab/sigtool/v3
|
||||||
|
|
||||||
go 1.12
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/opencoff/go-utils v0.0.0-20180228221344-c48e36f0cfc3
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a
|
||||||
github.com/opencoff/pflag v0.2.0
|
github.com/opencoff/go-fio v0.5.14
|
||||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
|
github.com/opencoff/go-mmap v0.1.5
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
github.com/opencoff/go-utils v1.0.2
|
||||||
|
github.com/opencoff/pflag v1.0.7
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.0
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
|
google.golang.org/protobuf v1.36.5
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pkg/xattr v0.4.10 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/term v0.30.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
//replace github.com/opencoff/go-mmap => ../go-mmap
|
||||||
|
//replace github.com/opencoff/go-utils => ../go-utils
|
||||||
|
|
52
go.sum
52
go.sum
|
@ -1,15 +1,39 @@
|
||||||
github.com/opencoff/go-utils v0.0.0-20180228221344-c48e36f0cfc3 h1:IFg/fPDg7NFEGTxTcFcxkdvPd2RQG0u3K8MfWMEamM8=
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||||
github.com/opencoff/go-utils v0.0.0-20180228221344-c48e36f0cfc3/go.mod h1:W/XsuvsMfXD7lUVR2TC408iKFfydI0RkNgkQ33s48gs=
|
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||||
github.com/opencoff/pflag v0.2.0 h1:bSI5Qz5W15aCs+4YRDYX6LK5qaUD9GpjZKYP4fli/EY=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/opencoff/pflag v0.2.0/go.mod h1:mTLzGGUGda1Av3d34iAJlh0JIlRxmFZtmc6qoWPspK0=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/opencoff/go-fio v0.5.14 h1:PGi4XLLO4RSuc3m5exY0G2vweov6w3UThhScehBfM8c=
|
||||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU=
|
github.com/opencoff/go-fio v0.5.14/go.mod h1:hoSySYpavRnfQUsxzUgadk31kYiNQhMDvA2MObsXKf8=
|
||||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
github.com/opencoff/go-mmap v0.1.5 h1:RKPtevC4mOW5bi9skBPPo4nFTIH4lVWAL20Tff+FjLg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
github.com/opencoff/go-mmap v0.1.5/go.mod h1:y/6Jk/tDUc00k3oSQpiJX++20Nw7xFSlc5kLkhGnRXw=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
github.com/opencoff/go-utils v1.0.2 h1:BANRL8ZxgHpuo8gQBAzT3M9Im3aNFhaWW28jhc86LNs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
github.com/opencoff/go-utils v1.0.2/go.mod h1:eZkEVQVzNfuE8uGepyhscMsqcXq7liGbBHYYwgYaoy8=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
github.com/opencoff/pflag v1.0.7 h1:o5cQIuX75bDcdJ6AXl68gzpA72a3CJ2MPStaMnEuwi4=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
github.com/opencoff/pflag v1.0.7/go.mod h1:2bXtpAD/5h/2LarkbsRwiUxqnvB1nZBzn9Xjad1P41A=
|
||||||
|
github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=
|
||||||
|
github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
258
internal/pb/hdr.pb.go
Normal file
258
internal/pb/hdr.pb.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.32.0
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: internal/pb/hdr.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Every encrypted file starts with a header describing the
|
||||||
|
// Block Size, Salt, Recipient keys etc. Header represents a
|
||||||
|
// decoded version of this information. It is encoded in
|
||||||
|
// protobuf format before writing to disk.
|
||||||
|
type Header struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
ChunkSize uint32 `protobuf:"varint,1,opt,name=chunk_size,json=chunkSize,proto3" json:"chunk_size,omitempty"` // encryption block size
|
||||||
|
Salt []byte `protobuf:"bytes,2,opt,name=salt,proto3" json:"salt,omitempty"` // master salt (nonces are derived from this)
|
||||||
|
Pk []byte `protobuf:"bytes,3,opt,name=pk,proto3" json:"pk,omitempty"` // ephemeral curve PK
|
||||||
|
Sender []byte `protobuf:"bytes,4,opt,name=sender,proto3" json:"sender,omitempty"` // sender signed artifacts
|
||||||
|
Keys []*WrappedKey `protobuf:"bytes,5,rep,name=keys,proto3" json:"keys,omitempty"` // list of wrapped receiver blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) Reset() {
|
||||||
|
*x = Header{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_pb_hdr_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Header) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Header) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_pb_hdr_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Header.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Header) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_pb_hdr_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) GetChunkSize() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChunkSize
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) GetSalt() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Salt
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) GetPk() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Pk
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) GetSender() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Sender
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Header) GetKeys() []*WrappedKey {
|
||||||
|
if x != nil {
|
||||||
|
return x.Keys
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A file encryption key is wrapped by a recipient specific public
|
||||||
|
// key. WrappedKey describes such a wrapped key.
|
||||||
|
type WrappedKey struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
DKey []byte `protobuf:"bytes,1,opt,name=d_key,json=dKey,proto3" json:"d_key,omitempty"` // encrypted data key
|
||||||
|
Nonce []byte `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` // nonce used for encryption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WrappedKey) Reset() {
|
||||||
|
*x = WrappedKey{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_internal_pb_hdr_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WrappedKey) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*WrappedKey) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *WrappedKey) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_internal_pb_hdr_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use WrappedKey.ProtoReflect.Descriptor instead.
|
||||||
|
func (*WrappedKey) Descriptor() ([]byte, []int) {
|
||||||
|
return file_internal_pb_hdr_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WrappedKey) GetDKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.DKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *WrappedKey) GetNonce() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Nonce
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_internal_pb_hdr_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_internal_pb_hdr_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x15, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x62, 0x2f, 0x68, 0x64,
|
||||||
|
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x85, 0x01, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64,
|
||||||
|
0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65,
|
||||||
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a,
|
||||||
|
0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
|
0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x70, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
|
0x0c, 0x52, 0x02, 0x70, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18,
|
||||||
|
0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x20, 0x0a,
|
||||||
|
0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x77, 0x72,
|
||||||
|
0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22,
|
||||||
|
0x38, 0x0a, 0x0b, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x13,
|
||||||
|
0x0a, 0x05, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64,
|
||||||
|
0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||||
|
0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x42, 0x0d, 0x5a, 0x0b, 0x69, 0x6e, 0x74,
|
||||||
|
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_internal_pb_hdr_proto_rawDescOnce sync.Once
|
||||||
|
file_internal_pb_hdr_proto_rawDescData = file_internal_pb_hdr_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_internal_pb_hdr_proto_rawDescGZIP() []byte {
|
||||||
|
file_internal_pb_hdr_proto_rawDescOnce.Do(func() {
|
||||||
|
file_internal_pb_hdr_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_pb_hdr_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_internal_pb_hdr_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_internal_pb_hdr_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_internal_pb_hdr_proto_goTypes = []interface{}{
|
||||||
|
(*Header)(nil), // 0: header
|
||||||
|
(*WrappedKey)(nil), // 1: wrapped_key
|
||||||
|
}
|
||||||
|
var file_internal_pb_hdr_proto_depIdxs = []int32{
|
||||||
|
1, // 0: header.keys:type_name -> wrapped_key
|
||||||
|
1, // [1:1] is the sub-list for method output_type
|
||||||
|
1, // [1:1] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_internal_pb_hdr_proto_init() }
|
||||||
|
func file_internal_pb_hdr_proto_init() {
|
||||||
|
if File_internal_pb_hdr_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_internal_pb_hdr_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Header); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_internal_pb_hdr_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*WrappedKey); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_internal_pb_hdr_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 0,
|
||||||
|
},
|
||||||
|
GoTypes: file_internal_pb_hdr_proto_goTypes,
|
||||||
|
DependencyIndexes: file_internal_pb_hdr_proto_depIdxs,
|
||||||
|
MessageInfos: file_internal_pb_hdr_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_internal_pb_hdr_proto = out.File
|
||||||
|
file_internal_pb_hdr_proto_rawDesc = nil
|
||||||
|
file_internal_pb_hdr_proto_goTypes = nil
|
||||||
|
file_internal_pb_hdr_proto_depIdxs = nil
|
||||||
|
}
|
29
internal/pb/hdr.proto
Normal file
29
internal/pb/hdr.proto
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
syntax="proto3";
|
||||||
|
|
||||||
|
|
||||||
|
option go_package = "internal/pb";
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every encrypted file starts with a header describing the
|
||||||
|
* Block Size, Salt, Recipient keys etc. Header represents a
|
||||||
|
* decoded version of this information. It is encoded in
|
||||||
|
* protobuf format before writing to disk.
|
||||||
|
*/
|
||||||
|
message header {
|
||||||
|
uint32 chunk_size = 1; // encryption block size
|
||||||
|
bytes salt = 2; // master salt (nonces are derived from this)
|
||||||
|
bytes pk = 3; // ephemeral curve PK
|
||||||
|
bytes sender = 4; // sender signed artifacts
|
||||||
|
repeated wrapped_key keys = 5; // list of wrapped receiver blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A file encryption key is wrapped by a recipient specific public
|
||||||
|
* key. WrappedKey describes such a wrapped key.
|
||||||
|
*/
|
||||||
|
message wrapped_key {
|
||||||
|
bytes d_key = 1; // encrypted data key
|
||||||
|
bytes nonce = 2; // nonce used for encryption
|
||||||
|
}
|
||||||
|
|
512
internal/pb/hdr_vtproto.pb.go
Normal file
512
internal/pb/hdr_vtproto.pb.go
Normal file
|
@ -0,0 +1,512 @@
|
||||||
|
// Code generated by protoc-gen-go-vtproto. DO NOT EDIT.
|
||||||
|
// protoc-gen-go-vtproto version: v0.6.0
|
||||||
|
// source: internal/pb/hdr.proto
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
import (
|
||||||
|
fmt "fmt"
|
||||||
|
protohelpers "github.com/planetscale/vtprotobuf/protohelpers"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
io "io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Header) MarshalVT() (dAtA []byte, err error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
size := m.SizeVT()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) MarshalToVT(dAtA []byte) (int, error) {
|
||||||
|
size := m.SizeVT()
|
||||||
|
return m.MarshalToSizedBufferVT(dAtA[:size])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||||
|
if m == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
i := len(dAtA)
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.unknownFields != nil {
|
||||||
|
i -= len(m.unknownFields)
|
||||||
|
copy(dAtA[i:], m.unknownFields)
|
||||||
|
}
|
||||||
|
if len(m.Keys) > 0 {
|
||||||
|
for iNdEx := len(m.Keys) - 1; iNdEx >= 0; iNdEx-- {
|
||||||
|
size, err := m.Keys[iNdEx].MarshalToSizedBufferVT(dAtA[:i])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i -= size
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x2a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m.Sender) > 0 {
|
||||||
|
i -= len(m.Sender)
|
||||||
|
copy(dAtA[i:], m.Sender)
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Sender)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x22
|
||||||
|
}
|
||||||
|
if len(m.Pk) > 0 {
|
||||||
|
i -= len(m.Pk)
|
||||||
|
copy(dAtA[i:], m.Pk)
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Pk)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x1a
|
||||||
|
}
|
||||||
|
if len(m.Salt) > 0 {
|
||||||
|
i -= len(m.Salt)
|
||||||
|
copy(dAtA[i:], m.Salt)
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Salt)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
}
|
||||||
|
if m.ChunkSize != 0 {
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ChunkSize))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x8
|
||||||
|
}
|
||||||
|
return len(dAtA) - i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedKey) MarshalVT() (dAtA []byte, err error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
size := m.SizeVT()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedKey) MarshalToVT(dAtA []byte) (int, error) {
|
||||||
|
size := m.SizeVT()
|
||||||
|
return m.MarshalToSizedBufferVT(dAtA[:size])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedKey) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||||
|
if m == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
i := len(dAtA)
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.unknownFields != nil {
|
||||||
|
i -= len(m.unknownFields)
|
||||||
|
copy(dAtA[i:], m.unknownFields)
|
||||||
|
}
|
||||||
|
if len(m.Nonce) > 0 {
|
||||||
|
i -= len(m.Nonce)
|
||||||
|
copy(dAtA[i:], m.Nonce)
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Nonce)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
}
|
||||||
|
if len(m.DKey) > 0 {
|
||||||
|
i -= len(m.DKey)
|
||||||
|
copy(dAtA[i:], m.DKey)
|
||||||
|
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.DKey)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
}
|
||||||
|
return len(dAtA) - i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) SizeVT() (n int) {
|
||||||
|
if m == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.ChunkSize != 0 {
|
||||||
|
n += 1 + protohelpers.SizeOfVarint(uint64(m.ChunkSize))
|
||||||
|
}
|
||||||
|
l = len(m.Salt)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
l = len(m.Pk)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
l = len(m.Sender)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
if len(m.Keys) > 0 {
|
||||||
|
for _, e := range m.Keys {
|
||||||
|
l = e.SizeVT()
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n += len(m.unknownFields)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedKey) SizeVT() (n int) {
|
||||||
|
if m == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
l = len(m.DKey)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
l = len(m.Nonce)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
|
||||||
|
}
|
||||||
|
n += len(m.unknownFields)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) UnmarshalVT(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: Header: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: Header: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field ChunkSize", wireType)
|
||||||
|
}
|
||||||
|
m.ChunkSize = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.ChunkSize |= uint32(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Salt", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Salt = append(m.Salt[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Salt == nil {
|
||||||
|
m.Salt = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 3:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Pk", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Pk = append(m.Pk[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Pk == nil {
|
||||||
|
m.Pk = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 4:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Sender = append(m.Sender[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Sender == nil {
|
||||||
|
m.Sender = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 5:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Keys", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msglen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Keys = append(m.Keys, &WrappedKey{})
|
||||||
|
if err := m.Keys[len(m.Keys)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *WrappedKey) UnmarshalVT(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: WrappedKey: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: WrappedKey: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field DKey", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.DKey = append(m.DKey[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.DKey == nil {
|
||||||
|
m.DKey = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return protohelpers.ErrIntOverflow
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= int(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Nonce = append(m.Nonce[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Nonce == nil {
|
||||||
|
m.Nonce = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
|
return protohelpers.ErrInvalidLength
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
16
internal/pb/helpers.go
Normal file
16
internal/pb/helpers.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// helper routines to maintain backwards compat with gogo
|
||||||
|
|
||||||
|
package pb
|
||||||
|
|
||||||
|
|
||||||
|
func (m *Header) Size() int {
|
||||||
|
return m.SizeVT()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) MarshalTo(buf []byte) (int, error) {
|
||||||
|
return m.MarshalToVT(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Header) Unmarshal(buf []byte) error {
|
||||||
|
return m.UnmarshalVT(buf)
|
||||||
|
}
|
54
mk-rel.sh
Executable file
54
mk-rel.sh
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
Z=`basename $0`
|
||||||
|
die() {
|
||||||
|
echo "$Z: $@" 1>&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "$Z: $@" 1>&2
|
||||||
|
}
|
||||||
|
|
||||||
|
case $BASH_VERSION in
|
||||||
|
4.*|5.*) ;;
|
||||||
|
|
||||||
|
*) die "I need bash 4.x to run!"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
Rel=$PWD/releases
|
||||||
|
Bindir=$Rel/bin
|
||||||
|
mkdir -p $Bindir || die "can't make $Bindir"
|
||||||
|
|
||||||
|
pkgit() {
|
||||||
|
local os=$1
|
||||||
|
local cpu=$2
|
||||||
|
local rev=$3
|
||||||
|
local arch="$os-$cpu"
|
||||||
|
local tgz="$Rel/sigtool-${rev}_${arch}.tar.gz"
|
||||||
|
local bindir=$Bindir/$arch
|
||||||
|
local bin=sigtool
|
||||||
|
|
||||||
|
if [ "$os" = "windows" ]; then
|
||||||
|
bin=${bin}.exe
|
||||||
|
fi
|
||||||
|
|
||||||
|
./build -V $rev -b $Bindir -s -a $arch || die "can't build $arch"
|
||||||
|
(cd $bindir && tar cf - $bin) | gzip -9 > $tgz || die "can't tar $tgz"
|
||||||
|
}
|
||||||
|
|
||||||
|
xrev=$(git describe --always --dirty --abbrev=12) || exit 1
|
||||||
|
if echo $xrev | grep -q dirty; then
|
||||||
|
die "won't build releases; repo dirty!"
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
|
||||||
|
os="linux windows openbsd darwin"
|
||||||
|
arch="amd64 arm64"
|
||||||
|
|
||||||
|
for xx in $os; do
|
||||||
|
for yy in $arch; do
|
||||||
|
pkgit $xx $yy $xrev
|
||||||
|
done
|
||||||
|
done
|
3
renovate.json
Normal file
3
renovate.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
[](https://godoc.org/github.com/opencoff/sigtool/sign)
|
[](https://godoc.org/git.rgst.io/homelab/sigtool/v3/sign)
|
||||||
|
|
||||||
# sigtool/sign - Ed25519 signature calculation and verification
|
# sigtool/sign - Ed25519 signature calculation and verification
|
||||||
|
|
||||||
This is a small library that makes it easier to create and serialize Ed25519 keys, and sign,
|
This is a small library that makes it easier to create and serialize Ed25519 keys, and sign,
|
||||||
verify files using those keys. The library uses mmap(2) to read and process very large files.
|
verify files using those keys. The library uses mmap(2) to read and process very large files.
|
||||||
|
|
||||||
The companion program [sigtool](https://github.com/opencoff/sigtool) uses this library.
|
The companion program [sigtool](https://git.rgst.io/homelab/sigtool/v3) uses this library.
|
||||||
## License
|
## License
|
||||||
GPL v2.0
|
GPL v2.0
|
||||||
|
|
33
sign/doc.go
Normal file
33
sign/doc.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// doc.go -- Documentation for sign & encrypt
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
// Package sign implements Ed25519 signing, verification on files.
|
||||||
|
// It builds upon golang.org/x/crypto/ed25519 by adding methods
|
||||||
|
// for serializing and deserializing Ed25519 private & public keys.
|
||||||
|
//
|
||||||
|
// It can sign and verify very large files - it prehashes the files
|
||||||
|
// with SHA-512 and then signs the SHA-512 checksum. The keys and signatures
|
||||||
|
// are YAML files and so, human readable.
|
||||||
|
//
|
||||||
|
// It can encrypt files for multiple recipients - each of whom is identified
|
||||||
|
// by their Ed25519 public key. The encryption by default generates ephmeral
|
||||||
|
// Curve25519 keys and creates pair-wise shared secret for each recipient of
|
||||||
|
// the encrypted file. The caller can optionally use a specific secret key
|
||||||
|
// during the encryption process - this has the benefit of also authenticating
|
||||||
|
// the sender (and the receiver can verify the sender if they possess the
|
||||||
|
// corresponding public key).
|
||||||
|
//
|
||||||
|
// 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`.
|
||||||
|
package sign
|
914
sign/encrypt.go
Normal file
914
sign/encrypt.go
Normal file
|
@ -0,0 +1,914 @@
|
||||||
|
// encrypt.go -- Ed25519 based encrypt/decrypt
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Implementation Notes for Encryption/Decryption:
|
||||||
|
//
|
||||||
|
// Header: has 3 parts:
|
||||||
|
// - Fixed sized header
|
||||||
|
// - Variable sized protobuf encoded header
|
||||||
|
// - SHA256 sum of both above.
|
||||||
|
//
|
||||||
|
// Fixed size header:
|
||||||
|
// - Magic: 7 bytes
|
||||||
|
// - Version: 1 byte
|
||||||
|
// - VLen: 4 byte
|
||||||
|
//
|
||||||
|
// Variable Length Segment:
|
||||||
|
// - Protobuf encoded, per-recipient wrapped keys
|
||||||
|
//
|
||||||
|
// The variable length segment consists of one or more
|
||||||
|
// recipients, each with their individually wrapped keys.
|
||||||
|
//
|
||||||
|
// The input data is encrypted with an expanded random 32-byte key:
|
||||||
|
// - hkdf-sha512 of random key, salt, context
|
||||||
|
// - the hkdf process yields a data-encryption key, nonce and hmac key.
|
||||||
|
// - we use the header checksum as the 'salt' for HKDF; this ensures that
|
||||||
|
// any modification of the header yields different keys
|
||||||
|
//
|
||||||
|
// We also calculate the cumulative hmac-sha256 of the plaintext blocks.
|
||||||
|
// - When sender identity is present, we sign the final hmac and append
|
||||||
|
// the signature as the "trailer".
|
||||||
|
// - When sender identity is NOT present, we put random bytes as the
|
||||||
|
// "signature". ie in either case, there is a trailer.
|
||||||
|
//
|
||||||
|
// Note: If the trailer is missing from a sigtool encrypted file - the
|
||||||
|
// recipient has no guarantees of content immutability (ie tampering
|
||||||
|
// from one of the _other_ recipients).
|
||||||
|
//
|
||||||
|
// The input data is broken up into "chunks"; each no larger than
|
||||||
|
// maxChunkSize. The default block size is "chunkSize". Each block
|
||||||
|
// is AEAD encrypted:
|
||||||
|
// AEAD nonce = header.nonce || block#
|
||||||
|
// AD of AEAD = chunk length+eof marker
|
||||||
|
//
|
||||||
|
// The encrypted block (includes the AEAD tag) length is written
|
||||||
|
// as a big-endian 4-byte prefix. The high-order bit of this length
|
||||||
|
// field is set for the last-block (denoting EOF).
|
||||||
|
//
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/internal/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encryption chunk size = 4MB
|
||||||
|
const (
|
||||||
|
// The latest version of the tool's output file format
|
||||||
|
_SigtoolVersion = 3
|
||||||
|
|
||||||
|
chunkSize uint32 = 4 * 1048576 // 4 MB
|
||||||
|
maxChunkSize uint32 = 1 << 30
|
||||||
|
_EOF uint32 = 1 << 31
|
||||||
|
|
||||||
|
_Magic = "SigTool"
|
||||||
|
_MagicLen = len(_Magic)
|
||||||
|
_FixedHdrLen = _MagicLen + 1 + 4 // 1: version, 4: len of variable segment
|
||||||
|
|
||||||
|
_AesKeySize = 32
|
||||||
|
_AEADNonceSize = 12
|
||||||
|
_SaltSize = 32
|
||||||
|
_RxNonceSize = 12 // nonce size of per-recipient encrypted blocks
|
||||||
|
|
||||||
|
_WrapReceiver = "Receiver Key"
|
||||||
|
_WrapSender = "Sender Sig"
|
||||||
|
_DataKeyExpansion = "Data Key Expansion"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encryptor holds the encryption context
|
||||||
|
type Encryptor struct {
|
||||||
|
pb.Header
|
||||||
|
key []byte // root key
|
||||||
|
|
||||||
|
nonce []byte // nonce for the data encrypting cipher
|
||||||
|
buf []byte // I/O buf (chunk-sized)
|
||||||
|
|
||||||
|
ae cipher.AEAD
|
||||||
|
hmac hash.Hash
|
||||||
|
|
||||||
|
// ephemeral key
|
||||||
|
encSK []byte
|
||||||
|
|
||||||
|
// sender identity
|
||||||
|
sender *PrivateKey
|
||||||
|
|
||||||
|
auth bool // set if the sender idetity is sent
|
||||||
|
started bool
|
||||||
|
stream bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Encryption context for encrypting blocks of size 'blksize'.
|
||||||
|
// If 'sk' is not nil, authenticate the sender to each receiver.
|
||||||
|
func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||||
|
var blksz uint32
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case blksize == 0:
|
||||||
|
blksz = chunkSize
|
||||||
|
case blksize > uint64(maxChunkSize):
|
||||||
|
blksz = maxChunkSize
|
||||||
|
default:
|
||||||
|
blksz = uint32(blksize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate ephemeral Curve25519 keys
|
||||||
|
esk, epk, err := newSender()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := randBuf(_AesKeySize)
|
||||||
|
salt := randBuf(_SaltSize)
|
||||||
|
|
||||||
|
e := &Encryptor{
|
||||||
|
Header: pb.Header{
|
||||||
|
ChunkSize: blksz,
|
||||||
|
Salt: salt,
|
||||||
|
Pk: epk,
|
||||||
|
},
|
||||||
|
|
||||||
|
key: key,
|
||||||
|
encSK: esk,
|
||||||
|
sender: sk,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = e.addSenderSig(sk); err != nil {
|
||||||
|
return nil, fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new recipient to this encryption context.
|
||||||
|
func (e *Encryptor) AddRecipient(pk *PublicKey) error {
|
||||||
|
if e.started {
|
||||||
|
return ErrEncStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := e.wrapKey(pk)
|
||||||
|
if err == nil {
|
||||||
|
e.Keys = append(e.Keys, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the input stream 'rd' and write encrypted stream to 'wr'
|
||||||
|
func (e *Encryptor) Encrypt(rd io.Reader, wr io.WriteCloser) error {
|
||||||
|
if e.stream {
|
||||||
|
return ErrEncIsStream
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.started {
|
||||||
|
err := e.start(wr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, e.ChunkSize)
|
||||||
|
|
||||||
|
var i uint32
|
||||||
|
var eof bool
|
||||||
|
for !eof {
|
||||||
|
n, err := io.ReadAtLeast(rd, buf, int(e.ChunkSize))
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case io.EOF, io.ErrClosedPipe, io.ErrUnexpectedEOF:
|
||||||
|
eof = true
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("encrypt: I/O read error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n >= 0 {
|
||||||
|
err = e.encrypt(buf[:n], wr, i, eof)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the encryption process by writing the header
|
||||||
|
func (e *Encryptor) start(wr io.Writer) error {
|
||||||
|
varSize := e.Size()
|
||||||
|
|
||||||
|
buffer := make([]byte, _FixedHdrLen+varSize+sha256.Size)
|
||||||
|
fixHdr := buffer[:_FixedHdrLen]
|
||||||
|
varHdr := buffer[_FixedHdrLen : _FixedHdrLen+varSize]
|
||||||
|
sumHdr := buffer[_FixedHdrLen+varSize:]
|
||||||
|
|
||||||
|
// Now assemble the fixed header
|
||||||
|
copy(fixHdr[:], []byte(_Magic))
|
||||||
|
fixHdr[_MagicLen] = _SigtoolVersion
|
||||||
|
binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize))
|
||||||
|
|
||||||
|
// Now marshal the variable portion
|
||||||
|
_, err := e.MarshalTo(varHdr[:varSize])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt: can't marshal header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(buffer[:_FixedHdrLen+varSize])
|
||||||
|
cksum := h.Sum(sumHdr[:0])
|
||||||
|
|
||||||
|
// now make the data encryption keys, nonces etc.
|
||||||
|
outbuf := make([]byte, sha256.Size+_AesKeySize+_AEADNonceSize)
|
||||||
|
|
||||||
|
// we mix the header checksum (and it captures the sigtool version, sender
|
||||||
|
// identity, etc.)
|
||||||
|
buf := expand(outbuf, e.key, cksum, []byte(_DataKeyExpansion))
|
||||||
|
|
||||||
|
var dkey, hmackey []byte
|
||||||
|
|
||||||
|
e.nonce, buf = buf[:_AEADNonceSize], buf[_AEADNonceSize:]
|
||||||
|
dkey, buf = buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
hmackey = buf
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(dkey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ae, err = cipher.NewGCM(aes); err != nil {
|
||||||
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally write out the header
|
||||||
|
err = fullwrite(buffer, wr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.hmac = hmac.New(sha256.New, hmackey)
|
||||||
|
e.buf = make([]byte, e.ChunkSize+4+uint32(e.ae.Overhead()))
|
||||||
|
e.started = true
|
||||||
|
|
||||||
|
debug("encrypt:\n\thdr-cksum: %x\n\taes-key: %x\n\tnonce: %x\n\thmac-key: %x\n",
|
||||||
|
cksum, dkey, e.nonce, hmackey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt exactly _one_ block of data
|
||||||
|
// The nonce is constructed from the salt, block# and block-size.
|
||||||
|
// This protects the output stream from re-ordering attacks and length
|
||||||
|
// modification attacks. The encoded length & block number is used as
|
||||||
|
// additional data in the AEAD construction.
|
||||||
|
func (e *Encryptor) encrypt(pt []byte, wr io.Writer, i uint32, eof bool) error {
|
||||||
|
var z uint32 = uint32(len(pt))
|
||||||
|
var nonce [_AEADNonceSize]byte
|
||||||
|
|
||||||
|
// mark last block
|
||||||
|
if eof {
|
||||||
|
z |= _EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(nonce[:], e.nonce)
|
||||||
|
|
||||||
|
// now change the upper bytes to track the block#; we use the len+eof as AD
|
||||||
|
binary.BigEndian.PutUint32(nonce[:4], i)
|
||||||
|
|
||||||
|
// put the encoded length+eof at the start of the output buf
|
||||||
|
b := e.buf[:4]
|
||||||
|
ctbuf := e.buf[4:]
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(b, z)
|
||||||
|
ct := e.ae.Seal(ctbuf[:0], nonce[:], pt, b)
|
||||||
|
|
||||||
|
// total number of bytes written
|
||||||
|
n := len(ct) + 4
|
||||||
|
err := fullwrite(e.buf[:n], wr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.hmac.Write(b)
|
||||||
|
e.hmac.Write(pt)
|
||||||
|
|
||||||
|
if eof {
|
||||||
|
return e.writeTrailer(wr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a trailer:
|
||||||
|
// - if authenticating sender, sign the hmac and put the signature in the trailer
|
||||||
|
// - if not authenticating sender, write random bytes to the trailer
|
||||||
|
func (e *Encryptor) writeTrailer(wr io.Writer) error {
|
||||||
|
var tr []byte
|
||||||
|
|
||||||
|
switch e.auth {
|
||||||
|
case true:
|
||||||
|
var hmac [sha256.Size]byte
|
||||||
|
|
||||||
|
e.hmac.Sum(hmac[:0])
|
||||||
|
|
||||||
|
// We know sender is non null.
|
||||||
|
sig, err := e.sender.SignMessage(hmac[:], "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt: trailer: %w", err)
|
||||||
|
}
|
||||||
|
tr = sig.Sig
|
||||||
|
|
||||||
|
case false:
|
||||||
|
tr = randBuf(ed25519.SignatureSize)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fullwrite(tr, wr); err != nil {
|
||||||
|
return fmt.Errorf("encrypt: trailer %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decryptor holds the decryption context
|
||||||
|
type Decryptor struct {
|
||||||
|
pb.Header
|
||||||
|
|
||||||
|
ae cipher.AEAD
|
||||||
|
hmac hash.Hash
|
||||||
|
|
||||||
|
sender *PublicKey
|
||||||
|
|
||||||
|
rd io.Reader
|
||||||
|
buf []byte
|
||||||
|
nonce []byte // nonce for the data decrypting cipher
|
||||||
|
|
||||||
|
key []byte // Decrypted root key
|
||||||
|
hdrsum []byte // cached header checksum
|
||||||
|
auth bool // flag set to true if sender signed the key
|
||||||
|
eof bool
|
||||||
|
stream bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new decryption context and if 'pk' is given, check that it matches
|
||||||
|
// the sender
|
||||||
|
func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
||||||
|
var b [_FixedHdrLen]byte
|
||||||
|
|
||||||
|
_, err := io.ReadFull(rd, b[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt: err while reading header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Compare(b[:_MagicLen], []byte(_Magic)) != 0 {
|
||||||
|
return nil, ErrNotSigTool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version check
|
||||||
|
if b[_MagicLen] != _SigtoolVersion {
|
||||||
|
return nil, fmt.Errorf("decrypt: Unsupported version %d; this tool only supports v%d",
|
||||||
|
b[_MagicLen], _SigtoolVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
varSize := binary.BigEndian.Uint32(b[_MagicLen+1:])
|
||||||
|
|
||||||
|
// sanity check on variable segment length
|
||||||
|
if varSize > 1048576 {
|
||||||
|
return nil, ErrHeaderTooBig
|
||||||
|
}
|
||||||
|
if varSize < 32 {
|
||||||
|
return nil, ErrHeaderTooSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA256 is the trailer part of the file-header
|
||||||
|
varBuf := make([]byte, varSize+sha256.Size)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(rd, varBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt: error while reading header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checksum in the header
|
||||||
|
verify := varBuf[varSize:]
|
||||||
|
|
||||||
|
// the checksum we calculated
|
||||||
|
var csum [sha256.Size]byte
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(b[:])
|
||||||
|
h.Write(varBuf[:varSize])
|
||||||
|
cksum := h.Sum(csum[:0])
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(verify, cksum) == 0 {
|
||||||
|
return nil, ErrBadHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &Decryptor{
|
||||||
|
rd: rd,
|
||||||
|
hdrsum: cksum,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.Unmarshal(varBuf[:varSize])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt: decode error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.ChunkSize == 0 || d.ChunkSize >= maxChunkSize {
|
||||||
|
return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Salt) != _SaltSize {
|
||||||
|
return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Keys) == 0 {
|
||||||
|
return nil, ErrNoWrappedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity check on the wrapped keys
|
||||||
|
for i, w := range d.Keys {
|
||||||
|
if len(w.DKey) <= _AesKeySize {
|
||||||
|
return nil, fmt.Errorf("decrypt: wrapped key %d: wrong-size encrypted key", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Private Key 'sk' to decrypt the encrypted keys in the header and optionally validate
|
||||||
|
// the sender
|
||||||
|
func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
|
||||||
|
var err error
|
||||||
|
var key []byte
|
||||||
|
|
||||||
|
for i, w := range d.Keys {
|
||||||
|
key, err = d.unwrapKey(w, sk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt: can't unwrap key %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if key != nil {
|
||||||
|
d.key = key
|
||||||
|
d.sender = senderPk
|
||||||
|
goto havekey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrBadKey
|
||||||
|
|
||||||
|
havekey:
|
||||||
|
if err := d.verifySender(key, senderPk); err != nil {
|
||||||
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outbuf := make([]byte, sha256.Size+_AesKeySize+_AEADNonceSize)
|
||||||
|
|
||||||
|
buf := expand(outbuf, d.key, d.hdrsum, []byte(_DataKeyExpansion))
|
||||||
|
|
||||||
|
var dkey, hmackey []byte
|
||||||
|
|
||||||
|
d.nonce, buf = buf[:_AEADNonceSize], buf[_AEADNonceSize:]
|
||||||
|
dkey, buf = buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
hmackey = buf
|
||||||
|
|
||||||
|
d.hmac = hmac.New(sha256.New, hmackey)
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(dkey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.ae, err = cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("decrypt:\n\thdr-cksum: %x\n\taes-key: %x\n\tnonce: %x\n\thmac-key: %x\n",
|
||||||
|
d.hdrsum, dkey, d.nonce, hmackey)
|
||||||
|
|
||||||
|
// We have a separate on-stack buffer for reading the header (4 bytes).
|
||||||
|
// Thus, the actual I/O buf will never be larger than the chunksize + AEAD Overhead
|
||||||
|
d.buf = make([]byte, int(d.ChunkSize)+d.ae.Overhead())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticatedSender returns true if the sender authenticated themselves
|
||||||
|
// (the data-encryption key is signed).
|
||||||
|
func (d *Decryptor) AuthenticatedSender() bool {
|
||||||
|
return d.auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the file and write to 'wr'
|
||||||
|
func (d *Decryptor) Decrypt(wr io.Writer) error {
|
||||||
|
if d.key == nil {
|
||||||
|
return ErrNoKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.stream {
|
||||||
|
return ErrDecStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.eof {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
var i uint32
|
||||||
|
for i = 0; ; i++ {
|
||||||
|
c, eof, err := d.decrypt(i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(c) > 0 {
|
||||||
|
err = fullwrite(c, wr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if eof {
|
||||||
|
d.eof = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt exactly one chunk of data
|
||||||
|
func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
||||||
|
var ovh uint32 = uint32(d.ae.Overhead())
|
||||||
|
var b [4]byte
|
||||||
|
var nonce [_AEADNonceSize]byte
|
||||||
|
|
||||||
|
n, err := io.ReadFull(d.rd, b[:])
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := binary.BigEndian.Uint32(b[:])
|
||||||
|
eof := (m & _EOF) > 0
|
||||||
|
m &= (_EOF - 1)
|
||||||
|
|
||||||
|
// Sanity check - in case of corrupt header
|
||||||
|
switch {
|
||||||
|
case m > uint32(d.ChunkSize):
|
||||||
|
return nil, false, fmt.Errorf("decrypt: chunksize is too large (%d)", m)
|
||||||
|
|
||||||
|
case m == 0:
|
||||||
|
if !eof {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: block %d: zero-sized chunk without EOF", i)
|
||||||
|
}
|
||||||
|
return nil, eof, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the nonce - top 4 bytes are the counter
|
||||||
|
copy(nonce[:], d.nonce)
|
||||||
|
binary.BigEndian.PutUint32(nonce[:4], i)
|
||||||
|
|
||||||
|
z := m + ovh
|
||||||
|
n, err = io.ReadFull(d.rd, d.buf[:z])
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pt, err := d.ae.Open(d.buf[:0], nonce[:], d.buf[:n], b[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(len(pt)) != m {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: partial unsealed bytes; exp %d, saw %d", m, len(pt))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.hmac.Write(b[:])
|
||||||
|
d.hmac.Write(pt)
|
||||||
|
|
||||||
|
if eof {
|
||||||
|
return d.processTrailer(pt, eof)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt, eof, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decryptor) processTrailer(pt []byte, eof bool) ([]byte, bool, error) {
|
||||||
|
var rd [ed25519.SignatureSize]byte
|
||||||
|
|
||||||
|
_, err := io.ReadFull(d.rd, rd[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("decrypt: premature EOF while reading trailer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.auth {
|
||||||
|
// these are random bytes; ignore em
|
||||||
|
return pt, eof, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hmac [sha256.Size]byte
|
||||||
|
|
||||||
|
cksum := d.hmac.Sum(hmac[:0])
|
||||||
|
ss := &Signature{
|
||||||
|
Sig: rd[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := d.sender.VerifyMessage(cksum, ss); !ok {
|
||||||
|
return nil, eof, ErrBadTrailer
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt, eof, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionally sign the checksum and encrypt everything
|
||||||
|
func (e *Encryptor) addSenderSig(sk *PrivateKey) error {
|
||||||
|
var zero [ed25519.SignatureSize]byte
|
||||||
|
var auth bool
|
||||||
|
sig := zero[:]
|
||||||
|
|
||||||
|
if e.sender != nil {
|
||||||
|
var csum [sha256.Size]byte
|
||||||
|
|
||||||
|
// We capture essential meta-data from the sender; viz:
|
||||||
|
// - Sender tool version
|
||||||
|
// - Sender generated curve25519 PK
|
||||||
|
// - session salt, root key
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(_Magic))
|
||||||
|
h.Write([]byte{_SigtoolVersion})
|
||||||
|
h.Write(e.Pk)
|
||||||
|
h.Write(e.Salt)
|
||||||
|
h.Write(e.key)
|
||||||
|
cksum := h.Sum(csum[:0])
|
||||||
|
|
||||||
|
xsig, err := e.sender.SignMessage(cksum, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wrap: can't sign: %w", err)
|
||||||
|
}
|
||||||
|
sig = xsig.Sig
|
||||||
|
auth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, _AesKeySize+_AEADNonceSize)
|
||||||
|
buf = expand(buf, e.key, e.Salt, []byte(_WrapSender))
|
||||||
|
|
||||||
|
ekey, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(ekey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("senderId: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("senderId: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outbuf := make([]byte, ed25519.SignatureSize+ae.Overhead())
|
||||||
|
buf = ae.Seal(outbuf[:0], nonce, sig, nil)
|
||||||
|
|
||||||
|
e.auth = auth
|
||||||
|
e.Sender = buf
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unwrap sender's signature using 'key' and extract the signature
|
||||||
|
// Optionally, verify the signature using the sender's PK (if provided).
|
||||||
|
func (d *Decryptor) verifySender(key []byte, senderPk *PublicKey) error {
|
||||||
|
outbuf := make([]byte, _AEADNonceSize+_AesKeySize)
|
||||||
|
buf := expand(outbuf, key, d.Salt, []byte(_WrapSender))
|
||||||
|
|
||||||
|
ekey, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(ekey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unwrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unwrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigbuf [ed25519.SignatureSize]byte
|
||||||
|
var zero [ed25519.SignatureSize]byte
|
||||||
|
|
||||||
|
sig, err := ae.Open(sigbuf[:0], nonce, d.Sender, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unwrap: can't open sender info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did the sender actually sign anything?
|
||||||
|
if subtle.ConstantTimeCompare(zero[:], sig) == 0 {
|
||||||
|
if senderPk == nil {
|
||||||
|
return ErrNoSenderPK
|
||||||
|
}
|
||||||
|
|
||||||
|
var csum [sha256.Size]byte
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(_Magic))
|
||||||
|
h.Write([]byte{_SigtoolVersion})
|
||||||
|
h.Write(d.Pk)
|
||||||
|
h.Write(d.Salt)
|
||||||
|
h.Write(key)
|
||||||
|
cksum := h.Sum(csum[:0])
|
||||||
|
|
||||||
|
ss := &Signature{
|
||||||
|
Sig: sig,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := senderPk.VerifyMessage(cksum, ss); !ok {
|
||||||
|
return ErrBadSender
|
||||||
|
}
|
||||||
|
|
||||||
|
// we set this to indicate that the sender authenticated themselves;
|
||||||
|
d.auth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK
|
||||||
|
//
|
||||||
|
// basically, we do a scalarmult: Ephemeral encryption/decryption SK x receiver PK
|
||||||
|
func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
|
||||||
|
rxPK := pk.ToCurve25519PK()
|
||||||
|
sekrit, err := curve25519.X25519(e.encSK, rxPK)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shasum [sha256.Size]byte
|
||||||
|
|
||||||
|
rbuf := randBuf(_RxNonceSize)
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(e.Salt)
|
||||||
|
h.Write(rbuf[:])
|
||||||
|
h.Sum(shasum[:0])
|
||||||
|
|
||||||
|
out := make([]byte, _AesKeySize+_RxNonceSize)
|
||||||
|
buf := expand(out[:], sekrit, shasum[:], []byte(_WrapReceiver))
|
||||||
|
|
||||||
|
kek, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(kek)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ekey := make([]byte, ae.Overhead()+len(e.key))
|
||||||
|
w := &pb.WrappedKey{
|
||||||
|
DKey: ae.Seal(ekey[:0], nonce, e.key, pk.Pk),
|
||||||
|
Nonce: rbuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and
|
||||||
|
// senders ephemeral PublicKey
|
||||||
|
func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) {
|
||||||
|
ourSK := sk.ToCurve25519SK()
|
||||||
|
sekrit, err := curve25519.X25519(ourSK, d.Pk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unwrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shasum [sha256.Size]byte
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(d.Salt)
|
||||||
|
h.Write(w.Nonce)
|
||||||
|
h.Sum(shasum[:0])
|
||||||
|
|
||||||
|
out := make([]byte, _AesKeySize+_RxNonceSize)
|
||||||
|
buf := expand(out[:], sekrit, shasum[:], []byte(_WrapReceiver))
|
||||||
|
|
||||||
|
kek, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(kek)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wrap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := _AesKeySize + ae.Overhead()
|
||||||
|
if len(w.DKey) != want {
|
||||||
|
return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(w.DKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := sk.PublicKey()
|
||||||
|
dkey := make([]byte, _AesKeySize) // decrypted data decryption key
|
||||||
|
|
||||||
|
// we indicate incorrect receiver SK by returning a nil key
|
||||||
|
dkey, err = ae.Open(dkey[:0], nonce, w.DKey, pk.Pk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have successfully found the correct recipient
|
||||||
|
return dkey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write _all_ bytes of buffer 'buf'
|
||||||
|
func fullwrite(buf []byte, wr io.Writer) error {
|
||||||
|
n := len(buf)
|
||||||
|
|
||||||
|
for n > 0 {
|
||||||
|
m, err := wr.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n -= m
|
||||||
|
buf = buf[m:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a KEK from a shared DH key and a Pub Key
|
||||||
|
func expand(out []byte, shared, salt, ad []byte) []byte {
|
||||||
|
h := hkdf.New(sha512.New, shared, salt, ad)
|
||||||
|
_, err := io.ReadFull(h, out)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("hkdf: failed to generate %d bytes: %s", len(out), err))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSender() (sk, pk []byte, err error) {
|
||||||
|
var csk [32]byte
|
||||||
|
|
||||||
|
randRead(csk[:])
|
||||||
|
clamp(csk[:])
|
||||||
|
pk, err = curve25519.X25519(csk[:], curve25519.Basepoint)
|
||||||
|
sk = csk[:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// do sha256 on a list of byte slices
|
||||||
|
func sha256Slices(v ...[]byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
for _, x := range v {
|
||||||
|
h.Write(x)
|
||||||
|
}
|
||||||
|
return h.Sum(nil)[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _debug int = 0
|
||||||
|
|
||||||
|
// Enable debugging of this module;
|
||||||
|
// level > 0 elicits debug messages on os.Stderr
|
||||||
|
func Debug(level int) {
|
||||||
|
_debug = level
|
||||||
|
}
|
||||||
|
|
||||||
|
func debug(s string, v ...interface{}) {
|
||||||
|
if _debug <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
z := fmt.Sprintf(s, v...)
|
||||||
|
if n := len(z); z[n-1] != '\n' {
|
||||||
|
z += "\n"
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(z)
|
||||||
|
os.Stderr.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOF
|
477
sign/encrypt_test.go
Normal file
477
sign/encrypt_test.go
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
// crypt_test.go -- Test harness for encrypt/decrypt bits
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// one sender, one receiver no verification of sender
|
||||||
|
func TestEncryptSimple(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
sk, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "SK gen failed: %s", err)
|
||||||
|
|
||||||
|
pk := sk.PublicKey()
|
||||||
|
|
||||||
|
var blkSize int = 1024
|
||||||
|
var size int = (blkSize * 10)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
buf := make([]byte, size)
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
buf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(nil, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(pk)
|
||||||
|
assert(err == nil, "can't add recipient: %s", err)
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(buf)
|
||||||
|
wr := Buffer{}
|
||||||
|
|
||||||
|
err = ee.Encrypt(rd, &wr)
|
||||||
|
assert(err == nil, "encrypt fail: %s", err)
|
||||||
|
|
||||||
|
rd = bytes.NewBuffer(wr.Bytes())
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = dd.SetPrivateKey(sk, nil)
|
||||||
|
assert(err == nil, "decryptor can't add SK: %s", err)
|
||||||
|
|
||||||
|
wr = Buffer{}
|
||||||
|
err = dd.Decrypt(&wr)
|
||||||
|
assert(err == nil, "decrypt fail: %s", err)
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(len(b) == len(buf), "decrypt length mismatch: exp %d, saw %d", len(buf), len(b))
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt content mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// one sender, one receiver - small blocks
|
||||||
|
func TestEncryptSmallSizes(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
sk, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "SK gen failed: %s", err)
|
||||||
|
|
||||||
|
pk := sk.PublicKey()
|
||||||
|
|
||||||
|
var blkSize int = 8
|
||||||
|
var size int = (blkSize * 4)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
bigbuf := make([]byte, size)
|
||||||
|
for i := 0; i < len(bigbuf); i++ {
|
||||||
|
bigbuf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt progressively larger bufs
|
||||||
|
for i := 1; i < len(bigbuf); i++ {
|
||||||
|
buf := bigbuf[:i]
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(nil, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor-%d create fail: %s", i, err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(pk)
|
||||||
|
assert(err == nil, "encryptor-%d: can't add recipient: %s", i, err)
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(buf)
|
||||||
|
wr := Buffer{}
|
||||||
|
|
||||||
|
err = ee.Encrypt(rd, &wr)
|
||||||
|
assert(err == nil, "encrypt-%d fail: %s", i, err)
|
||||||
|
|
||||||
|
rd = bytes.NewBuffer(wr.Bytes())
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor-%d create fail: %s", i, err)
|
||||||
|
|
||||||
|
err = dd.SetPrivateKey(sk, nil)
|
||||||
|
assert(err == nil, "decryptor-%d can't add SK: %s", i, err)
|
||||||
|
|
||||||
|
wr = Buffer{}
|
||||||
|
err = dd.Decrypt(&wr)
|
||||||
|
assert(err == nil, "decrypt-%d fail: %s", i, err)
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(len(b) == len(buf), "decrypt-%d length mismatch: exp %d, saw %d", i, len(buf), len(b))
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt-%d content mismatch", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test corrupted header or corrupted input
|
||||||
|
func TestEncryptCorrupted(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
sk, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "SK gen failed: %s", err)
|
||||||
|
|
||||||
|
pk := sk.PublicKey()
|
||||||
|
|
||||||
|
var blkSize int = 1024
|
||||||
|
var size int = (blkSize * 23) + randmod(blkSize)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
buf := make([]byte, size)
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
buf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(nil, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(pk)
|
||||||
|
assert(err == nil, "can't add recipient: %s", err)
|
||||||
|
|
||||||
|
rd := bytes.NewReader(buf)
|
||||||
|
wr := Buffer{}
|
||||||
|
|
||||||
|
err = ee.Encrypt(rd, &wr)
|
||||||
|
assert(err == nil, "encrypt fail: %s", err)
|
||||||
|
|
||||||
|
rb := wr.Bytes()
|
||||||
|
n := len(rb)
|
||||||
|
|
||||||
|
// corrupt the input
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
j := randint() % n
|
||||||
|
rb[j] = byte(randint() & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
rd = bytes.NewReader(rb)
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err != nil, "decryptor works on bad input")
|
||||||
|
assert(dd == nil, "decryptor not nil for bad input")
|
||||||
|
}
|
||||||
|
|
||||||
|
// one sender, one receiver with verification of sender
|
||||||
|
func TestEncryptSenderVerified(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
sender, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "sender SK gen failed: %s", err)
|
||||||
|
|
||||||
|
receiver, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "receiver SK gen failed: %s", err)
|
||||||
|
|
||||||
|
var blkSize int = 1024
|
||||||
|
var size int = (blkSize * 23) + randmod(blkSize)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
buf := make([]byte, size)
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
buf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(sender, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(receiver.PublicKey())
|
||||||
|
assert(err == nil, "can't add recipient: %s", err)
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(buf)
|
||||||
|
wr := Buffer{}
|
||||||
|
|
||||||
|
err = ee.Encrypt(rd, &wr)
|
||||||
|
assert(err == nil, "encrypt fail: %s", err)
|
||||||
|
|
||||||
|
rd = bytes.NewBuffer(wr.Bytes())
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor create fail: %s", err)
|
||||||
|
|
||||||
|
randkey, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "rand SK gen failed: %s", err)
|
||||||
|
|
||||||
|
// first send a wrong sender PK
|
||||||
|
err = dd.SetPrivateKey(receiver, randkey.PublicKey())
|
||||||
|
assert(err != nil, "decryptor failed to verify sender")
|
||||||
|
|
||||||
|
// then the correct sender PK
|
||||||
|
err = dd.SetPrivateKey(receiver, sender.PublicKey())
|
||||||
|
assert(err == nil, "decryptor can't add SK: %s", err)
|
||||||
|
|
||||||
|
wr = Buffer{}
|
||||||
|
err = dd.Decrypt(&wr)
|
||||||
|
assert(err == nil, "decrypt fail: %s", err)
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(len(b) == len(buf), "decrypt length mismatch: exp %d, saw %d", len(buf), len(b))
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt content mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// one sender, multiple receivers, each decrypting the blob
|
||||||
|
func TestEncryptMultiReceiver(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
sender, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "sender SK gen failed: %s", err)
|
||||||
|
|
||||||
|
var blkSize int = 1024
|
||||||
|
var size int = (blkSize * 23) + randmod(blkSize)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
buf := make([]byte, size)
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
buf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(sender, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
n := 4
|
||||||
|
rx := make([]*PrivateKey, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
r, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "can't make receiver SK %d: %s", i, err)
|
||||||
|
rx[i] = r
|
||||||
|
|
||||||
|
err = ee.AddRecipient(r.PublicKey())
|
||||||
|
assert(err == nil, "can't add recipient %d: %s", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(buf)
|
||||||
|
wr := Buffer{}
|
||||||
|
|
||||||
|
err = ee.Encrypt(rd, &wr)
|
||||||
|
assert(err == nil, "encrypt fail: %s", err)
|
||||||
|
|
||||||
|
encBytes := wr.Bytes()
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
rd = bytes.NewBuffer(encBytes)
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor %d create fail: %s", i, err)
|
||||||
|
|
||||||
|
err = dd.SetPrivateKey(rx[i], sender.PublicKey())
|
||||||
|
assert(err == nil, "decryptor can't add SK %d: %s", i, err)
|
||||||
|
|
||||||
|
wr = Buffer{}
|
||||||
|
err = dd.Decrypt(&wr)
|
||||||
|
assert(err == nil, "decrypt %d fail: %s", i, err)
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(len(b) == len(buf), "decrypt %d length mismatch: exp %d, saw %d", i, len(buf), len(b))
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt %d content mismatch", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stream write and read
|
||||||
|
func TestStreamIO(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
receiver, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "receiver keypair gen failed: %s", err)
|
||||||
|
|
||||||
|
var blkSize int = 1024
|
||||||
|
var size int = (blkSize * 10)
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
buf := make([]byte, size)
|
||||||
|
for i := 0; i < len(buf); i++ {
|
||||||
|
buf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(nil, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(receiver.PublicKey())
|
||||||
|
assert(err == nil, "can't add recipient: %s", err)
|
||||||
|
|
||||||
|
wr := Buffer{}
|
||||||
|
wio, err := ee.NewStreamWriter(&wr)
|
||||||
|
assert(err == nil, "can't start stream writer: %s", err)
|
||||||
|
|
||||||
|
// chunksize for writing to stream
|
||||||
|
csize := 19
|
||||||
|
rbuf := buf
|
||||||
|
for len(rbuf) > 0 {
|
||||||
|
m := csize
|
||||||
|
if len(rbuf) < m {
|
||||||
|
m = len(rbuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := wio.Write(rbuf[:m])
|
||||||
|
assert(err == nil, "stream write failed: %s", err)
|
||||||
|
assert(n == m, "stream write mismatch: exp %d, saw %d", m, n)
|
||||||
|
|
||||||
|
rbuf = rbuf[m:]
|
||||||
|
}
|
||||||
|
err = wio.Close()
|
||||||
|
assert(err == nil, "stream close failed: %s", err)
|
||||||
|
|
||||||
|
_, err = wio.Write(buf[:csize])
|
||||||
|
assert(err != nil, "stream write accepted I/O after close: %s", err)
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(wr.Bytes())
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = dd.SetPrivateKey(receiver, nil)
|
||||||
|
assert(err == nil, "decryptor can't add SK: %s", err)
|
||||||
|
|
||||||
|
rio, err := dd.NewStreamReader()
|
||||||
|
assert(err == nil, "stream reader failed: %s", err)
|
||||||
|
|
||||||
|
rbuf = make([]byte, csize)
|
||||||
|
wr = Buffer{}
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
m, err := rio.Read(rbuf)
|
||||||
|
assert(err == nil || err == io.EOF, "streamread fail: %s", err)
|
||||||
|
|
||||||
|
if m > 0 {
|
||||||
|
wr.Write(rbuf[:m])
|
||||||
|
n += m
|
||||||
|
}
|
||||||
|
if err == io.EOF || m == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(n == len(b), "streamread: bad buflen; exp %d, saw %d", n, len(b))
|
||||||
|
assert(n == len(buf), "streamread: decrypt len mismatch; exp %d, saw %d", len(buf), n)
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt content mismatch")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stream write and read with small sizes
|
||||||
|
func TestSmallSizeStreamIO(t *testing.T) {
|
||||||
|
assert := newAsserter(t)
|
||||||
|
|
||||||
|
receiver, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "receiver SK gen failed: %s", err)
|
||||||
|
|
||||||
|
var blkSize int = 8
|
||||||
|
var size int = blkSize * 10
|
||||||
|
|
||||||
|
// cleartext
|
||||||
|
bigbuf := make([]byte, size)
|
||||||
|
for i := 0; i < len(bigbuf); i++ {
|
||||||
|
bigbuf[i] = byte(i & 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(bigbuf); i++ {
|
||||||
|
buf := bigbuf[:i]
|
||||||
|
t.Logf("small-size-stream: size %d, chunksize %d\n", i, blkSize)
|
||||||
|
|
||||||
|
ee, err := NewEncryptor(nil, uint64(blkSize))
|
||||||
|
assert(err == nil, "encryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = ee.AddRecipient(receiver.PublicKey())
|
||||||
|
assert(err == nil, "can't add recipient: %s", err)
|
||||||
|
|
||||||
|
wr := Buffer{}
|
||||||
|
wio, err := ee.NewStreamWriter(&wr)
|
||||||
|
assert(err == nil, "can't start stream writer: %s", err)
|
||||||
|
|
||||||
|
// chunksize for writing to stream
|
||||||
|
csize := blkSize - 1
|
||||||
|
rbuf := buf
|
||||||
|
for len(rbuf) > 0 {
|
||||||
|
m := csize
|
||||||
|
if len(rbuf) < m {
|
||||||
|
m = len(rbuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := wio.Write(rbuf[:m])
|
||||||
|
assert(err == nil, "stream write failed: %s", err)
|
||||||
|
assert(n == m, "stream write mismatch: exp %d, saw %d", m, n)
|
||||||
|
|
||||||
|
rbuf = rbuf[m:]
|
||||||
|
}
|
||||||
|
err = wio.Close()
|
||||||
|
assert(err == nil, "stream close failed: %s", err)
|
||||||
|
|
||||||
|
_, err = wio.Write(buf[:csize])
|
||||||
|
assert(err != nil, "stream write accepted I/O after close: %s", err)
|
||||||
|
|
||||||
|
rd := bytes.NewBuffer(wr.Bytes())
|
||||||
|
|
||||||
|
dd, err := NewDecryptor(rd)
|
||||||
|
assert(err == nil, "decryptor create fail: %s", err)
|
||||||
|
|
||||||
|
err = dd.SetPrivateKey(receiver, nil)
|
||||||
|
assert(err == nil, "decryptor can't add SK: %s", err)
|
||||||
|
|
||||||
|
rio, err := dd.NewStreamReader()
|
||||||
|
assert(err == nil, "stream reader failed: %s", err)
|
||||||
|
|
||||||
|
rbuf = make([]byte, csize)
|
||||||
|
wr = Buffer{}
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
m, err := rio.Read(rbuf)
|
||||||
|
assert(err == nil || err == io.EOF, "streamread fail: %s", err)
|
||||||
|
|
||||||
|
if m > 0 {
|
||||||
|
wr.Write(rbuf[:m])
|
||||||
|
n += m
|
||||||
|
}
|
||||||
|
if err == io.EOF || m == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b := wr.Bytes()
|
||||||
|
assert(n == len(b), "streamread: bad buflen; exp %d, saw %d", n, len(b))
|
||||||
|
assert(n == len(buf), "streamread: decrypt len mismatch; exp %d, saw %d", len(buf), n)
|
||||||
|
|
||||||
|
assert(byteEq(b, buf), "decrypt content mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func randint() int {
|
||||||
|
var b [4]byte
|
||||||
|
|
||||||
|
_, err := io.ReadFull(rand.Reader, b[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("can't read 4 rand bytes: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
u := binary.BigEndian.Uint32(b[:])
|
||||||
|
|
||||||
|
return int(u & 0x7fffffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randmod(m int) int {
|
||||||
|
return randint() % m
|
||||||
|
}
|
45
sign/errors.go
Normal file
45
sign/errors.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// errors.go - list of all exportable errors in this module
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
//
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrClosed = errors.New("encrypt: stream already closed")
|
||||||
|
ErrNoKey = errors.New("decrypt: no private key set for decryption")
|
||||||
|
ErrEncStarted = errors.New("encrypt: can't add new recipient after encryption has started")
|
||||||
|
ErrDecStarted = errors.New("decrypt: can't add new recipient after decryption has started")
|
||||||
|
ErrEncIsStream = errors.New("encrypt: can't use Encrypt() after using streaming I/O")
|
||||||
|
ErrNotSigTool = errors.New("decrypt: not a sigtool encrypted file?")
|
||||||
|
ErrHeaderTooBig = errors.New("decrypt: header too large (max 1048576)")
|
||||||
|
ErrHeaderTooSmall = errors.New("decrypt: header too small (min 32)")
|
||||||
|
ErrBadHeader = errors.New("decrypt: header corrupted")
|
||||||
|
ErrNoWrappedKeys = errors.New("decrypt: no wrapped keys in encrypted file")
|
||||||
|
ErrBadKey = errors.New("decrypt: wrong key")
|
||||||
|
ErrBadTrailer = errors.New("decrypt: message integrity failed (bad trailer)")
|
||||||
|
ErrBadSender = errors.New("unwrap: sender verification failed")
|
||||||
|
ErrNoSenderPK = errors.New("unwrap: missing sender public key")
|
||||||
|
|
||||||
|
ErrIncorrectPassword = errors.New("ssh: invalid passphrase")
|
||||||
|
ErrNoPEMFound = errors.New("ssh: no PEM block found")
|
||||||
|
ErrBadPublicKey = errors.New("ssh: malformed public key")
|
||||||
|
ErrKeyTooShort = errors.New("ssh: public key too short")
|
||||||
|
ErrBadTrailers = errors.New("ssh: trailing junk in public key")
|
||||||
|
ErrBadFormat = errors.New("ssh: invalid openssh private key format")
|
||||||
|
ErrBadLength = errors.New("ssh: private key unexpected length")
|
||||||
|
ErrBadPadding = errors.New("ssh: padding not as expected")
|
||||||
|
)
|
67
sign/iomisc.go
Normal file
67
sign/iomisc.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// iomisc.go -- misc i/o functions
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"github.com/opencoff/go-fio"
|
||||||
|
"github.com/opencoff/go-mmap"
|
||||||
|
"hash"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simple function to reliably write data to a file.
|
||||||
|
// Does MORE than ioutil.WriteFile() - in that it doesn't trash the
|
||||||
|
// existing file with an incomplete write.
|
||||||
|
func writeFile(fn string, b []byte, ovwrite bool, mode uint32) error {
|
||||||
|
var opts uint32
|
||||||
|
if ovwrite {
|
||||||
|
opts |= fio.OPT_OVERWRITE
|
||||||
|
}
|
||||||
|
sf, err := fio.NewSafeFile(fn, opts, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sf.Abort()
|
||||||
|
if _, err = sf.Write(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate file checksum out of hash function h
|
||||||
|
func fileCksum(fn string, h hash.Hash) ([]byte, error) {
|
||||||
|
fd, err := os.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't open %s: %s", fn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
sz, err := mmap.Reader(fd, func(b []byte) error {
|
||||||
|
h.Write(b)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var b [8]byte
|
||||||
|
binary.BigEndian.PutUint64(b[:], uint64(sz))
|
||||||
|
h.Write(b[:])
|
||||||
|
|
||||||
|
return h.Sum(nil)[:], nil
|
||||||
|
}
|
529
sign/keys.go
Normal file
529
sign/keys.go
Normal file
|
@ -0,0 +1,529 @@
|
||||||
|
// keys.go -- Ed25519 keys management
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
// This file implements:
|
||||||
|
// - key generation, and key I/O
|
||||||
|
// - sign/verify of files and byte strings
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
Ed "crypto/ed25519"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/scrypt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Private Ed25519 key
|
||||||
|
type PrivateKey struct {
|
||||||
|
Sk []byte
|
||||||
|
|
||||||
|
// Encryption key: Curve25519 point corresponding to this Ed25519 key
|
||||||
|
ck []byte
|
||||||
|
|
||||||
|
// Cached copy of the public key
|
||||||
|
pk *PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Ed25519 key
|
||||||
|
type PublicKey struct {
|
||||||
|
Pk []byte
|
||||||
|
|
||||||
|
// Comment string
|
||||||
|
Comment string
|
||||||
|
|
||||||
|
// Curve25519 point corresponding to this Ed25519 key
|
||||||
|
ck []byte
|
||||||
|
|
||||||
|
hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length of Ed25519 Public Key Hash
|
||||||
|
const PKHashLength = 16
|
||||||
|
|
||||||
|
// constants we use in this module
|
||||||
|
const (
|
||||||
|
// Scrypt parameters
|
||||||
|
_N int = 1 << 19
|
||||||
|
_r int = 8
|
||||||
|
_p int = 1
|
||||||
|
|
||||||
|
// Algorithm used in the encrypted private key
|
||||||
|
sk_algo = "scrypt-sha256"
|
||||||
|
sig_algo = "sha512-ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypted Private key
|
||||||
|
type serializedPrivKey struct {
|
||||||
|
Comment string `yaml:"comment,omitempty"`
|
||||||
|
|
||||||
|
// Encrypted Sk
|
||||||
|
Esk string `yaml:"esk"`
|
||||||
|
Salt string `yaml:"salt,omitempty"`
|
||||||
|
|
||||||
|
// Algorithm used for checksum and KDF
|
||||||
|
Algo string `yaml:"algo,omitempty"`
|
||||||
|
|
||||||
|
// These are params for scrypt.Key()
|
||||||
|
// CPU Cost parameter; must be a power of 2
|
||||||
|
N int `yaml:"Z,flow,omitempty"`
|
||||||
|
|
||||||
|
// r * p should be less than 2^30
|
||||||
|
R int `yaml:"r,flow,omitempty"`
|
||||||
|
P int `yaml:"p,flow,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialized representation of public key
|
||||||
|
type serializedPubKey struct {
|
||||||
|
Comment string `yaml:"comment,omitempty"`
|
||||||
|
Pk string `yaml:"pk"`
|
||||||
|
Hash string `yaml:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialized signature
|
||||||
|
type signature struct {
|
||||||
|
Comment string `yaml:"comment,omitempty"`
|
||||||
|
Pkhash string `yaml:"pkhash,omitempty"`
|
||||||
|
Signature string `yaml:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a public key, generate a deterministic short-hash of it.
|
||||||
|
func pkhash(pk []byte) []byte {
|
||||||
|
z := sha256.Sum256(pk)
|
||||||
|
return z[:PKHashLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey generates a new Ed25519 private key
|
||||||
|
func NewPrivateKey() (*PrivateKey, error) {
|
||||||
|
pkb, skb, err := Ed.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sk := &PrivateKey{
|
||||||
|
Sk: []byte(skb),
|
||||||
|
pk: &PublicKey{
|
||||||
|
Pk: []byte(pkb),
|
||||||
|
hash: pkhash([]byte(pkb)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the private key in 'fn', optionally decrypting it using
|
||||||
|
// password 'pw' and create new instance of PrivateKey
|
||||||
|
func ReadPrivateKey(fn string, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||||
|
yml, err := ioutil.ReadFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sk PrivateKey
|
||||||
|
if err = sk.UnmarshalBinary(yml, getpw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a private key from bytes 'yml' using optional caller provided
|
||||||
|
// getpw() function to read the password if needed.
|
||||||
|
// are assumed to be serialized version of the private key.
|
||||||
|
func MakePrivateKey(yml []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||||
|
var sk PrivateKey
|
||||||
|
|
||||||
|
err := sk.UnmarshalBinary(yml, getpw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a PrivateKey from a byte array containing ed25519 raw SK
|
||||||
|
func makePrivateKeyFromBytes(sk *PrivateKey, buf []byte) error {
|
||||||
|
if len(buf) != 64 {
|
||||||
|
return fmt.Errorf("private key is malformed (len %d!)", len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
skb := make([]byte, 64)
|
||||||
|
copy(skb, buf)
|
||||||
|
|
||||||
|
edsk := Ed.PrivateKey(skb)
|
||||||
|
edpk := edsk.Public().(Ed.PublicKey)
|
||||||
|
|
||||||
|
pk := &PublicKey{
|
||||||
|
Pk: []byte(edpk),
|
||||||
|
hash: pkhash([]byte(edpk)),
|
||||||
|
}
|
||||||
|
sk.Sk = skb
|
||||||
|
sk.pk = pk
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a private key from 64-bytes of extended Ed25519 key
|
||||||
|
func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) {
|
||||||
|
var sk PrivateKey
|
||||||
|
if err := makePrivateKeyFromBytes(&sk, buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a secret key, return the corresponding Public Key
|
||||||
|
func (sk *PrivateKey) PublicKey() *PublicKey {
|
||||||
|
return sk.pk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert an Ed25519 Private Key to Curve25519 Private key
|
||||||
|
func (sk *PrivateKey) ToCurve25519SK() []byte {
|
||||||
|
if sk.ck == nil {
|
||||||
|
var ek [64]byte
|
||||||
|
|
||||||
|
h := sha512.New()
|
||||||
|
h.Write(sk.Sk[:32])
|
||||||
|
h.Sum(ek[:0])
|
||||||
|
|
||||||
|
sk.ck = clamp(ek[:32])
|
||||||
|
}
|
||||||
|
|
||||||
|
return sk.ck
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the private key to file 'fn' using human readable
|
||||||
|
// 'comment' and encrypt the key with supplied passphrase 'pw'.
|
||||||
|
func (sk *PrivateKey) Serialize(fn, comment string, ovwrite bool, pw []byte) error {
|
||||||
|
b, err := sk.MarshalBinary(comment, pw)
|
||||||
|
if err == nil {
|
||||||
|
return writeFile(fn, b, ovwrite, 0600)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary marshals the private key with a caller provided
|
||||||
|
// passphrase 'pw' and human readable 'comment'
|
||||||
|
func (sk *PrivateKey) MarshalBinary(comment string, pw []byte) ([]byte, error) {
|
||||||
|
// expand the password into 64 bytes
|
||||||
|
pass := sha512.Sum512(pw)
|
||||||
|
salt := make([]byte, 32)
|
||||||
|
|
||||||
|
randRead(salt)
|
||||||
|
|
||||||
|
// "32" == Length of AES-256 key
|
||||||
|
key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal: can't derive scrypt key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tl := ae.Overhead()
|
||||||
|
buf := make([]byte, tl+len(sk.Sk))
|
||||||
|
esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil)
|
||||||
|
|
||||||
|
enc := base64.StdEncoding.EncodeToString
|
||||||
|
|
||||||
|
ssk := serializedPrivKey{
|
||||||
|
Comment: comment,
|
||||||
|
Esk: enc(esk),
|
||||||
|
Salt: enc(salt),
|
||||||
|
Algo: sk_algo,
|
||||||
|
N: _N,
|
||||||
|
R: _r,
|
||||||
|
P: _p,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We won't protect the Scrypt parameters with the hash above
|
||||||
|
// because it is not needed. If the parameters are wrong, the
|
||||||
|
// derived key will be wrong and thus, the hash will not match.
|
||||||
|
|
||||||
|
return yaml.Marshal(&ssk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary unmarshals the private key and optionally invokes the
|
||||||
|
// caller provided getpw() function to read the password if needed. If the
|
||||||
|
// input byte stream 'b' is an OpenSSH ed25519 key, this function transparently
|
||||||
|
// decodes it.
|
||||||
|
func (sk *PrivateKey) UnmarshalBinary(b []byte, getpw func() ([]byte, error)) error {
|
||||||
|
if bytes.Index(b, []byte("OPENSSH PRIVATE KEY-")) > 0 {
|
||||||
|
xk, err := parseSSHPrivateKey(b, getpw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*sk = *xk
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pw []byte
|
||||||
|
if getpw != nil {
|
||||||
|
var err error
|
||||||
|
pw, err = getpw()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We take short passwords and extend them
|
||||||
|
pwb := sha512.Sum512(pw)
|
||||||
|
|
||||||
|
var ssk serializedPrivKey
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(b, &ssk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: can't parse YAML: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ssk.Salt) == 0 || len(ssk.Esk) == 0 {
|
||||||
|
return fmt.Errorf("unmarshal priv key: not YAML format")
|
||||||
|
}
|
||||||
|
|
||||||
|
b64 := base64.StdEncoding.DecodeString
|
||||||
|
|
||||||
|
salt, err := b64(ssk.Salt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: can't decode salt: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
esk, err := b64(ssk.Esk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: can't decode key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "32" == Length of AES-256 key
|
||||||
|
key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: can't derive key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aes, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: aes failure: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ae, err := cipher.NewGCM(aes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: aes failure: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
skb := make([]byte, 64)
|
||||||
|
skb, err = ae.Open(skb[:0], salt[:ae.NonceSize()], esk, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshal priv key: wrong password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return makePrivateKeyFromBytes(sk, skb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public Key Methods ---
|
||||||
|
|
||||||
|
// Read the public key from 'fn' and create new instance of
|
||||||
|
// PublicKey
|
||||||
|
func ReadPublicKey(fn string) (*PublicKey, error) {
|
||||||
|
var err error
|
||||||
|
var yml []byte
|
||||||
|
|
||||||
|
if yml, err = ioutil.ReadFile(fn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pk PublicKey
|
||||||
|
if err = pk.UnmarshalBinary(yml); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a serialized public in 'yml' and return the resulting
|
||||||
|
// public key instance
|
||||||
|
func MakePublicKey(yml []byte) (*PublicKey, error) {
|
||||||
|
var pk PublicKey
|
||||||
|
if err := pk.UnmarshalBinary(yml); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a public key from a string
|
||||||
|
func MakePublicKeyFromString(s string) (*PublicKey, error) {
|
||||||
|
// first try to decode it as a openssh key
|
||||||
|
if pk2, err := parseEncPubKey([]byte(s), "command-line-pk"); err == nil {
|
||||||
|
return pk2, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try to decode as an sigtool key
|
||||||
|
b64 := base64.StdEncoding.DecodeString
|
||||||
|
|
||||||
|
pkb, err := b64(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pk PublicKey
|
||||||
|
err = makePublicKeyFromBytes(&pk, pkb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePublicKeyFromBytes(pk *PublicKey, b []byte) error {
|
||||||
|
if len(b) != 32 {
|
||||||
|
return fmt.Errorf("public key is malformed (len %d!)", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.Pk = make([]byte, 32)
|
||||||
|
pk.hash = pkhash(b)
|
||||||
|
copy(pk.Pk, b)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a public key from a byte string
|
||||||
|
func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
|
||||||
|
var pk PublicKey
|
||||||
|
if err := makePublicKeyFromBytes(&pk, b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize a PublicKey into file 'fn' with a human readable 'comment'.
|
||||||
|
// If 'ovwrite' is true, overwrite the file if it exists.
|
||||||
|
func (pk *PublicKey) Serialize(fn, comment string, ovwrite bool) error {
|
||||||
|
out, err := pk.MarshalBinary(comment)
|
||||||
|
if err == nil {
|
||||||
|
return writeFile(fn, out, ovwrite, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// from github.com/FiloSottile/age
|
||||||
|
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
|
||||||
|
|
||||||
|
// Convert an Ed25519 Public Key to Curve25519 public key
|
||||||
|
// from github.com/FiloSottile/age
|
||||||
|
func (pk *PublicKey) ToCurve25519PK() []byte {
|
||||||
|
if pk.ck != nil {
|
||||||
|
return pk.ck
|
||||||
|
}
|
||||||
|
|
||||||
|
// ed25519.PublicKey is a little endian representation of the y-coordinate,
|
||||||
|
// with the most significant bit set based on the sign of the x-ccordinate.
|
||||||
|
bigEndianY := make([]byte, Ed.PublicKeySize)
|
||||||
|
for i, b := range pk.Pk {
|
||||||
|
bigEndianY[Ed.PublicKeySize-i-1] = b
|
||||||
|
}
|
||||||
|
bigEndianY[0] &= 0b0111_1111
|
||||||
|
|
||||||
|
// The Montgomery u-coordinate is derived through the bilinear map
|
||||||
|
//
|
||||||
|
// u = (1 + y) / (1 - y)
|
||||||
|
//
|
||||||
|
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
|
||||||
|
y := new(big.Int).SetBytes(bigEndianY)
|
||||||
|
denom := big.NewInt(1)
|
||||||
|
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
|
||||||
|
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
|
||||||
|
u.Mod(u, curve25519P)
|
||||||
|
|
||||||
|
out := make([]byte, 32)
|
||||||
|
uBytes := u.Bytes()
|
||||||
|
n := len(uBytes)
|
||||||
|
for i, b := range uBytes {
|
||||||
|
out[n-i-1] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.ck = out
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Key Hash
|
||||||
|
func (pk *PublicKey) Hash() []byte {
|
||||||
|
return pk.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary marshals a PublicKey into a byte array
|
||||||
|
func (pk *PublicKey) MarshalBinary(comment string) ([]byte, error) {
|
||||||
|
b64 := base64.StdEncoding.EncodeToString
|
||||||
|
spk := &serializedPubKey{
|
||||||
|
Comment: comment,
|
||||||
|
Pk: b64(pk.Pk),
|
||||||
|
Hash: b64(pk.hash),
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Marshal(spk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary constructs a PublicKey from a previously
|
||||||
|
// marshaled byte stream instance. In addition, it is also
|
||||||
|
// capable of parsing an OpenSSH ed25519 public key.
|
||||||
|
func (pk *PublicKey) UnmarshalBinary(yml []byte) error {
|
||||||
|
|
||||||
|
// first try to parse as a ssh key
|
||||||
|
if xk, err := parseSSHPublicKey(yml); err == nil {
|
||||||
|
*pk = *xk
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK Yaml it is.
|
||||||
|
|
||||||
|
var spk serializedPubKey
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if err = yaml.Unmarshal(yml, &spk); err != nil {
|
||||||
|
return fmt.Errorf("can't parse YAML: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(spk.Pk) == 0 {
|
||||||
|
return fmt.Errorf("sign: not a YAML public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
b64 := base64.StdEncoding.DecodeString
|
||||||
|
var pkb []byte
|
||||||
|
|
||||||
|
if pkb, err = b64(spk.Pk); err != nil {
|
||||||
|
return fmt.Errorf("can't decode YAML:Pk: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return makePublicKeyFromBytes(pk, pkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Internal Utility Functions --
|
||||||
|
|
||||||
|
func clamp(k []byte) []byte {
|
||||||
|
k[0] &= 248
|
||||||
|
k[31] &= 127
|
||||||
|
k[31] |= 64
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOF
|
||||||
|
// vim: noexpandtab:ts=8:sw=8:tw=92:
|
45
sign/rand.go
Normal file
45
sign/rand.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// rand.go - utility functions to generate random quantities
|
||||||
|
//
|
||||||
|
// (c) 2018 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randu32() uint32 {
|
||||||
|
var b [4]byte
|
||||||
|
|
||||||
|
_, err := io.ReadFull(rand.Reader, b[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("can't read 4 rand bytes: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return binary.LittleEndian.Uint32(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func randRead(b []byte) []byte {
|
||||||
|
_, err := io.ReadFull(rand.Reader, b)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("can't read %d bytes of random data: %s", len(b), err))
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func randBuf(sz int) []byte {
|
||||||
|
b := make([]byte, sz)
|
||||||
|
return randRead(b)
|
||||||
|
}
|
464
sign/sign.go
464
sign/sign.go
|
@ -11,309 +11,56 @@
|
||||||
// warranty; it is provided "as is". No claim is made to its
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
// suitability for any purpose.
|
// suitability for any purpose.
|
||||||
|
|
||||||
// Package sign implements Ed25519 signing, verification on files.
|
// This file implements:
|
||||||
// It builds upon golang.org/x/crypto/ed25519 by adding methods
|
// - key generation, and key I/O
|
||||||
// for serializing and deserializing Ed25519 private & public keys.
|
// - sign/verify of files and byte strings
|
||||||
// In addition, it works with large files - by precalculating their
|
|
||||||
// SHA512 checksum in mmap'd mode and sending the 64 byte signature
|
|
||||||
// for Ed25519 signing.
|
|
||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
|
|
||||||
Ed "golang.org/x/crypto/ed25519"
|
Ed "crypto/ed25519"
|
||||||
"golang.org/x/crypto/scrypt"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
|
|
||||||
"github.com/opencoff/go-utils"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Private Ed25519 key
|
|
||||||
type PrivateKey struct {
|
|
||||||
Sk []byte
|
|
||||||
|
|
||||||
// Cached copy of the public key
|
|
||||||
// In reality, it is a pointer to Sk[32:]
|
|
||||||
pk []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public Ed25519 key
|
|
||||||
type PublicKey struct {
|
|
||||||
Pk []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ed25519 key pair
|
|
||||||
type Keypair struct {
|
|
||||||
Sec PrivateKey
|
|
||||||
Pub PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// An Ed25519 Signature
|
// An Ed25519 Signature
|
||||||
type Signature struct {
|
type Signature struct {
|
||||||
Sig []byte // 32 byte digital signature
|
Sig []byte // Ed25519 sig bytes
|
||||||
pkhash []byte // [0:16] SHA256 hash of public key needed for verification
|
pkhash []byte // [0:16] SHA256 hash of public key needed for verification
|
||||||
}
|
}
|
||||||
|
|
||||||
// Algorithm used in the encrypted private key
|
|
||||||
const sk_algo = "scrypt-sha256"
|
|
||||||
const sig_algo = "sha512-ed25519"
|
|
||||||
|
|
||||||
// Scrypt parameters
|
|
||||||
const _N = 1 << 17
|
|
||||||
const _r = 16
|
|
||||||
const _p = 1
|
|
||||||
|
|
||||||
// Encrypted Private key
|
|
||||||
type encPrivKey struct {
|
|
||||||
// Encrypted Sk
|
|
||||||
Esk []byte
|
|
||||||
|
|
||||||
// parameters for Sk serialization
|
|
||||||
Salt []byte
|
|
||||||
|
|
||||||
// Algorithm used for checksum and KDF
|
|
||||||
Algo string
|
|
||||||
|
|
||||||
// Checksum to verify passphrase before we xor it
|
|
||||||
Verify []byte
|
|
||||||
|
|
||||||
// These are params for scrypt.Key()
|
|
||||||
// CPU Cost parameter; must be a power of 2
|
|
||||||
N uint32
|
|
||||||
// r * p should be less than 2^30
|
|
||||||
r uint32
|
|
||||||
p uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialized representation of private key
|
|
||||||
type serializedPrivKey struct {
|
|
||||||
Comment string `yaml:"comment,omitempty"`
|
|
||||||
Esk string `yaml:"esk"`
|
|
||||||
Salt string `yaml:"salt,omitempty"`
|
|
||||||
Algo string `yaml:"algo,omitempty"`
|
|
||||||
Verify string `yaml:"verify,omitempty"`
|
|
||||||
N uint32 `yaml:"Z,flow,omitempty"`
|
|
||||||
R uint32 `yaml:"r,flow,omitempty"`
|
|
||||||
P uint32 `yaml:"p,flow,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialized representation of public key
|
|
||||||
type serializedPubKey struct {
|
|
||||||
Comment string `yaml:"comment,omitempty"`
|
|
||||||
Pk string `yaml:"pk"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialized signature
|
|
||||||
type signature struct {
|
|
||||||
Comment string `yaml:"comment,omitempty"`
|
|
||||||
Pkhash string `yaml:"pkhash,omitempty"`
|
|
||||||
Signature string `yaml:"signature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new Ed25519 keypair
|
|
||||||
func NewKeypair() (*Keypair, error) {
|
|
||||||
//kp := &Keypair{Sec: PrivateKey{N: 1 << 17, r: 64, p: 1}}
|
|
||||||
kp := &Keypair{}
|
|
||||||
sk := &kp.Sec
|
|
||||||
pk := &kp.Pub
|
|
||||||
|
|
||||||
p, s, err := Ed.GenerateKey(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Can't generate Ed25519 keys: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pk.Pk = []byte(p)
|
|
||||||
sk.Sk = []byte(s)
|
|
||||||
|
|
||||||
return kp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the keypair to two separate files. The basename of the
|
|
||||||
// file is 'bn'; the public key goes in $bn.pub and the private key
|
|
||||||
// goes in $bn.key.
|
|
||||||
// If password is non-empty, then the private key is encrypted
|
|
||||||
// before writing to disk.
|
|
||||||
func (kp *Keypair) Serialize(bn, comment string, pw string) error {
|
|
||||||
|
|
||||||
sk := &kp.Sec
|
|
||||||
pk := &kp.Pub
|
|
||||||
|
|
||||||
skf := fmt.Sprintf("%s.key", bn)
|
|
||||||
pkf := fmt.Sprintf("%s.pub", bn)
|
|
||||||
|
|
||||||
err := pk.serialize(pkf, comment)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't serialize to %s: %s", pkf, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = sk.serialize(skf, comment, pw)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't serialize to %s: %s", pkf, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the private key in 'fn', optionally decrypting it using
|
|
||||||
// password 'pw' and create new instance of PrivateKey
|
|
||||||
func ReadPrivateKey(fn string, pw string) (*PrivateKey, error) {
|
|
||||||
yml, err := ioutil.ReadFile(fn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return MakePrivateKey(yml, pw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a private key from bytes 'yml' and password 'pw'. The bytes
|
|
||||||
// are assumed to be serialized version of the private key.
|
|
||||||
func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) {
|
|
||||||
var ssk serializedPrivKey
|
|
||||||
|
|
||||||
err := yaml.Unmarshal(yml, &ssk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse YAML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
esk := &encPrivKey{N: ssk.N, r: ssk.R, p: ssk.P, Algo: ssk.Algo}
|
|
||||||
b64 := base64.StdEncoding.DecodeString
|
|
||||||
|
|
||||||
esk.Esk, err = b64(ssk.Esk)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't decode YAML:Esk: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
esk.Salt, err = b64(ssk.Salt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't decode YAML:Salt: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
esk.Verify, err = b64(ssk.Verify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't decode YAML:Verify: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sk := &PrivateKey{}
|
|
||||||
|
|
||||||
// We take short passwords and extend them
|
|
||||||
pwb := sha512.Sum512([]byte(pw))
|
|
||||||
|
|
||||||
xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(esk.Esk))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't derive key: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hh := sha256.New()
|
|
||||||
hh.Write(esk.Salt)
|
|
||||||
hh.Write(xork)
|
|
||||||
ck := hh.Sum(nil)
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(esk.Verify, ck) != 1 {
|
|
||||||
return nil, fmt.Errorf("incorrect private key password")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything works. Now, decode the key
|
|
||||||
sk.Sk = make([]byte, len(esk.Esk))
|
|
||||||
for i := 0; i < len(esk.Esk); i++ {
|
|
||||||
sk.Sk[i] = esk.Esk[i] ^ xork[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return sk, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize the private key to a file
|
|
||||||
// Format: YAML
|
|
||||||
// All []byte are in base64 (RawEncoding)
|
|
||||||
func (sk *PrivateKey) serialize(fn, comment string, pw string) error {
|
|
||||||
|
|
||||||
b64 := base64.StdEncoding.EncodeToString
|
|
||||||
esk := &encPrivKey{}
|
|
||||||
ssk := &serializedPrivKey{Comment: comment}
|
|
||||||
|
|
||||||
// Even with an empty password, we still encrypt and store.
|
|
||||||
|
|
||||||
// expand the password into 64 bytes
|
|
||||||
pwb := sha512.Sum512([]byte(pw))
|
|
||||||
|
|
||||||
esk.N = _N
|
|
||||||
esk.r = _r
|
|
||||||
esk.p = _p
|
|
||||||
|
|
||||||
esk.Salt = make([]byte, 32)
|
|
||||||
esk.Esk = make([]byte, len(sk.Sk))
|
|
||||||
|
|
||||||
_, err := rand.Read(esk.Salt)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't read random salt: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(sk.Sk))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't derive scrypt key: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hh := sha256.New()
|
|
||||||
hh.Write(esk.Salt)
|
|
||||||
hh.Write(xork)
|
|
||||||
esk.Verify = hh.Sum(nil)
|
|
||||||
|
|
||||||
// We won't protect the Scrypt parameters with the hash above
|
|
||||||
// because it is not needed. If the parameters are wrong, the
|
|
||||||
// derived key will be wrong and thus, the hash will not match.
|
|
||||||
|
|
||||||
esk.Algo = sk_algo // global var
|
|
||||||
|
|
||||||
// Finally setup the encrypted key
|
|
||||||
for i := 0; i < len(sk.Sk); i++ {
|
|
||||||
esk.Esk[i] = sk.Sk[i] ^ xork[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
ssk.Esk = b64(esk.Esk)
|
|
||||||
ssk.Salt = b64(esk.Salt)
|
|
||||||
ssk.Verify = b64(esk.Verify)
|
|
||||||
ssk.Algo = esk.Algo
|
|
||||||
ssk.N = esk.N
|
|
||||||
ssk.R = esk.r
|
|
||||||
ssk.P = esk.p
|
|
||||||
|
|
||||||
out, err := yaml.Marshal(ssk)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't marahal to YAML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return writeFile(fn, out, 0600)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign a prehashed Message; return the signature as opaque bytes
|
// Sign a prehashed Message; return the signature as opaque bytes
|
||||||
// Signature is an YAML file:
|
// Signature is an YAML file:
|
||||||
// Comment: source file path
|
//
|
||||||
// Signature: Ed25519 signature
|
// Comment: source file path
|
||||||
|
// Signature: Ed25519 signature
|
||||||
func (sk *PrivateKey) SignMessage(ck []byte, comment string) (*Signature, error) {
|
func (sk *PrivateKey) SignMessage(ck []byte, comment string) (*Signature, error) {
|
||||||
x := Ed.PrivateKey(sk.Sk)
|
h := sha512.New()
|
||||||
|
h.Write([]byte("sigtool signed message"))
|
||||||
|
h.Write(ck)
|
||||||
|
ck = h.Sum(nil)[:]
|
||||||
|
|
||||||
|
x := Ed.PrivateKey(sk.Sk)
|
||||||
sig, err := x.Sign(rand.Reader, ck, crypto.Hash(0))
|
sig, err := x.Sign(rand.Reader, ck, crypto.Hash(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't sign %x: %s", ck, err)
|
return nil, fmt.Errorf("can't sign %x: %s", ck, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
esk := Ed.PrivateKey(sk.Sk) // type cast
|
ss := &Signature{
|
||||||
epk := esk.Public() // interface
|
Sig: sig,
|
||||||
xpk := epk.(Ed.PublicKey) // type assertion
|
pkhash: make([]byte, len(sk.pk.hash)),
|
||||||
pk := []byte(xpk) // cast
|
}
|
||||||
pkh := sha256.Sum256(pk)
|
|
||||||
|
|
||||||
return &Signature{Sig: sig, pkhash: pkh[:16]}, nil
|
copy(ss.pkhash, sk.pk.hash)
|
||||||
|
return ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and sign a file
|
// Read and sign a file
|
||||||
|
@ -341,12 +88,13 @@ func ReadSignature(fn string) (*Signature, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return MakeSignature(yml)
|
var sig Signature
|
||||||
|
return makeSignature(&sig, yml)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse serialized signature from bytes 'b' and construct a
|
// Parse serialized signature from bytes 'b' and construct a
|
||||||
// Signature object
|
// Signature object
|
||||||
func MakeSignature(b []byte) (*Signature, error) {
|
func makeSignature(sig *Signature, b []byte) (*Signature, error) {
|
||||||
var ss signature
|
var ss signature
|
||||||
err := yaml.Unmarshal(b, &ss)
|
err := yaml.Unmarshal(b, &ss)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -365,29 +113,33 @@ func MakeSignature(b []byte) (*Signature, error) {
|
||||||
return nil, fmt.Errorf("can't decode Base64:Pkhash <%s>: %s", ss.Pkhash, err)
|
return nil, fmt.Errorf("can't decode Base64:Pkhash <%s>: %s", ss.Pkhash, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Signature{Sig: s, pkhash: p}, nil
|
sig.Sig = s
|
||||||
|
sig.pkhash = p
|
||||||
|
return sig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize a signature suitable for storing in durable media
|
// MarshalBinary marshals a signature into a byte stream with
|
||||||
func (sig *Signature) Serialize(comment string) ([]byte, error) {
|
// an optional caller supplied comment.
|
||||||
|
func (sig *Signature) MarshalBinary(comment string) ([]byte, error) {
|
||||||
sigs := base64.StdEncoding.EncodeToString(sig.Sig)
|
sigs := base64.StdEncoding.EncodeToString(sig.Sig)
|
||||||
pks := base64.StdEncoding.EncodeToString(sig.pkhash)
|
pks := base64.StdEncoding.EncodeToString(sig.pkhash)
|
||||||
ss := &signature{Comment: comment, Pkhash: pks, Signature: sigs}
|
ss := &signature{Comment: comment, Pkhash: pks, Signature: sigs}
|
||||||
|
|
||||||
out, err := yaml.Marshal(ss)
|
return yaml.Marshal(ss)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't marshal signature of %x to YAML: %s", sig.Sig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeFile serializes the signature to an output file 'f'
|
// UnmarshalBinary constructs a Signature from a previously
|
||||||
func (sig *Signature) SerializeFile(fn, comment string) error {
|
// serialized bytestream
|
||||||
b, err := sig.Serialize(comment)
|
func (sig *Signature) UnmarshalBinary(b []byte) error {
|
||||||
|
_, err := makeSignature(sig, b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize a signature suitable for storing in durable media
|
||||||
|
func (sig *Signature) Serialize(fn, comment string, ovwrite bool) error {
|
||||||
|
b, err := sig.MarshalBinary(comment)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = writeFile(fn, b, 0644)
|
err = writeFile(fn, b, ovwrite, 0644)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -396,146 +148,30 @@ func (sig *Signature) SerializeFile(fn, comment string) error {
|
||||||
// the signature. It does this by comparing the hash of 'pk' against
|
// the signature. It does this by comparing the hash of 'pk' against
|
||||||
// 'Pkhash' of 'sig'.
|
// 'Pkhash' of 'sig'.
|
||||||
func (sig *Signature) IsPKMatch(pk *PublicKey) bool {
|
func (sig *Signature) IsPKMatch(pk *PublicKey) bool {
|
||||||
h := sha256.Sum256(pk.Pk)
|
return subtle.ConstantTimeCompare(pk.hash, sig.pkhash) == 1
|
||||||
return subtle.ConstantTimeCompare(h[:16], sig.pkhash) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Public Key Methods ---
|
|
||||||
|
|
||||||
// Read the public key from 'fn' and create new instance of
|
|
||||||
// PublicKey
|
|
||||||
func ReadPublicKey(fn string) (*PublicKey, error) {
|
|
||||||
var err error
|
|
||||||
var yml []byte
|
|
||||||
|
|
||||||
if yml, err = ioutil.ReadFile(fn); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return MakePublicKey(yml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a serialized public in 'yml' and return the resulting
|
|
||||||
// public key instance
|
|
||||||
func MakePublicKey(yml []byte) (*PublicKey, error) {
|
|
||||||
var spk serializedPubKey
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if err = yaml.Unmarshal(yml, &spk); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse YAML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pk := &PublicKey{}
|
|
||||||
b64 := base64.StdEncoding.DecodeString
|
|
||||||
|
|
||||||
if pk.Pk, err = b64(spk.Pk); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't decode YAML:Pk: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple sanity checks
|
|
||||||
if len(pk.Pk) == 0 {
|
|
||||||
return nil, fmt.Errorf("public key data is empty?")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pk, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize Public Keys
|
|
||||||
func (pk *PublicKey) serialize(fn, comment string) error {
|
|
||||||
b64 := base64.StdEncoding.EncodeToString
|
|
||||||
spk := &serializedPubKey{Comment: comment}
|
|
||||||
|
|
||||||
spk.Pk = b64(pk.Pk)
|
|
||||||
|
|
||||||
out, err := yaml.Marshal(spk)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't marahal to YAML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return writeFile(fn, out, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a signature 'sig' for file 'fn' against public key 'pk'
|
// Verify a signature 'sig' for file 'fn' against public key 'pk'
|
||||||
// Return True if signature matches, False otherwise
|
// Return True if signature matches, False otherwise
|
||||||
func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) {
|
func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) {
|
||||||
|
|
||||||
ck, err := fileCksum(fn, sha512.New())
|
ck, err := fileCksum(fn, sha512.New())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return pk.VerifyMessage(ck, sig)
|
return pk.VerifyMessage(ck, sig), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify a signature 'sig' for a pre-calculated checksum 'ck' against public key 'pk'
|
// Verify a signature 'sig' for a pre-calculated checksum 'ck' against public key 'pk'
|
||||||
// Return True if signature matches, False otherwise
|
// Return True if signature matches, False otherwise
|
||||||
func (pk *PublicKey) VerifyMessage(ck []byte, sig *Signature) (bool, error) {
|
func (pk *PublicKey) VerifyMessage(ck []byte, sig *Signature) bool {
|
||||||
|
h := sha512.New()
|
||||||
|
h.Write([]byte("sigtool signed message"))
|
||||||
|
h.Write(ck)
|
||||||
|
ck = h.Sum(nil)[:]
|
||||||
|
|
||||||
x := Ed.PublicKey(pk.Pk)
|
x := Ed.PublicKey(pk.Pk)
|
||||||
return Ed.Verify(x, ck, sig.Sig), nil
|
return Ed.Verify(x, ck, sig.Sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Internal Utility Functions --
|
|
||||||
|
|
||||||
// Unlink a file.
|
|
||||||
func unlink(f string) {
|
|
||||||
st, err := os.Stat(f)
|
|
||||||
if err == nil {
|
|
||||||
if !st.Mode().IsRegular() {
|
|
||||||
panic(fmt.Sprintf("%s can't be unlinked. Not a regular file?", f))
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Remove(f)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple function to reliably write data to a file.
|
|
||||||
// Does MORE than ioutil.WriteFile() - in that it doesn't trash the
|
|
||||||
// existing file with an incomplete write.
|
|
||||||
func writeFile(fn string, b []byte, mode uint32) error {
|
|
||||||
tmp := fmt.Sprintf("%s.tmp", fn)
|
|
||||||
unlink(tmp)
|
|
||||||
|
|
||||||
fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(mode))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Can't create file %s: %s", tmp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fd.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
fd.Close()
|
|
||||||
// XXX Do we delete the tmp file?
|
|
||||||
return fmt.Errorf("Can't write %v bytes to %s: %s", len(b), tmp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fd.Close() // we ignore close(2) errors; unrecoverable anyway.
|
|
||||||
|
|
||||||
os.Rename(tmp, fn)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate file checksum out of hash function h
|
|
||||||
func fileCksum(fn string, h hash.Hash) ([]byte, error) {
|
|
||||||
|
|
||||||
fd, err := os.Open(fn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't open %s: %s", fn, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
sz, err := utils.MmapReader(fd, 0, 0, h)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var b [8]byte
|
|
||||||
binary.BigEndian.PutUint64(b[:], uint64(sz))
|
|
||||||
h.Write(b[:])
|
|
||||||
|
|
||||||
return h.Sum(nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EOF
|
|
||||||
// vim: noexpandtab:ts=8:sw=8:tw=92:
|
// vim: noexpandtab:ts=8:sw=8:tw=92:
|
||||||
|
|
|
@ -14,40 +14,13 @@
|
||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
// module under test
|
|
||||||
//"github.com/sign"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) {
|
|
||||||
return func(cond bool, msg string, args ...interface{}) {
|
|
||||||
if cond {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, file, line, ok := runtime.Caller(1)
|
|
||||||
if !ok {
|
|
||||||
file = "???"
|
|
||||||
line = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
s := fmt.Sprintf(msg, args...)
|
|
||||||
t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if two byte arrays are equal
|
|
||||||
func byteEq(x, y []byte) bool {
|
|
||||||
return subtle.ConstantTimeCompare(x, y) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a temp dir in a temp-dir
|
// Return a temp dir in a temp-dir
|
||||||
func tempdir(t *testing.T) string {
|
func tempdir(t *testing.T) string {
|
||||||
assert := newAsserter(t)
|
assert := newAsserter(t)
|
||||||
|
@ -55,7 +28,7 @@ func tempdir(t *testing.T) string {
|
||||||
var b [10]byte
|
var b [10]byte
|
||||||
|
|
||||||
dn := os.TempDir()
|
dn := os.TempDir()
|
||||||
rand.Read(b[:])
|
randRead(b[:])
|
||||||
|
|
||||||
tmp := path.Join(dn, fmt.Sprintf("%x", b[:]))
|
tmp := path.Join(dn, fmt.Sprintf("%x", b[:]))
|
||||||
err := os.MkdirAll(tmp, 0755)
|
err := os.MkdirAll(tmp, 0755)
|
||||||
|
@ -65,6 +38,22 @@ func tempdir(t *testing.T) string {
|
||||||
return tmp
|
return tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fixedPw = []byte("abc")
|
||||||
|
var badPw = []byte("def")
|
||||||
|
var nilPw []byte
|
||||||
|
|
||||||
|
// return a hardcoded password
|
||||||
|
func hardcodedPw() ([]byte, error) {
|
||||||
|
return fixedPw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrongPw() ([]byte, error) {
|
||||||
|
return badPw, nil
|
||||||
|
}
|
||||||
|
func emptyPw() ([]byte, error) {
|
||||||
|
return nilPw, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Return true if file exists, false otherwise
|
// Return true if file exists, false otherwise
|
||||||
func fileExists(fn string) bool {
|
func fileExists(fn string) bool {
|
||||||
st, err := os.Stat(fn)
|
st, err := os.Stat(fn)
|
||||||
|
@ -92,119 +81,109 @@ p: 1
|
||||||
`
|
`
|
||||||
|
|
||||||
// #1. Create new key pair, and read them back.
|
// #1. Create new key pair, and read them back.
|
||||||
func Test0(t *testing.T) {
|
func TestSignSimple(t *testing.T) {
|
||||||
assert := newAsserter(t)
|
assert := newAsserter(t)
|
||||||
|
|
||||||
kp, err := NewKeypair()
|
sk, err := NewPrivateKey()
|
||||||
assert(err == nil, "NewKeyPair() fail")
|
assert(err == nil, "NewPrivateKey() fail")
|
||||||
|
|
||||||
dn := tempdir(t)
|
pk := sk.PublicKey()
|
||||||
|
|
||||||
|
dn := t.TempDir()
|
||||||
bn := fmt.Sprintf("%s/t0", dn)
|
bn := fmt.Sprintf("%s/t0", dn)
|
||||||
|
|
||||||
err = kp.Serialize(bn, "", "abc")
|
|
||||||
assert(err == nil, "keyPair.Serialize() fail")
|
|
||||||
|
|
||||||
pkf := fmt.Sprintf("%s.pub", bn)
|
pkf := fmt.Sprintf("%s.pub", bn)
|
||||||
skf := fmt.Sprintf("%s.key", bn)
|
skf := fmt.Sprintf("%s.key", bn)
|
||||||
|
|
||||||
|
err = pk.Serialize(pkf, "", true)
|
||||||
|
assert(err == nil, "can't serialize pk %s", pkf)
|
||||||
|
|
||||||
|
// try to overwrite
|
||||||
|
err = pk.Serialize(pkf, "", false)
|
||||||
|
assert(err != nil, "pk %s overwritten!", pkf)
|
||||||
|
|
||||||
|
err = sk.Serialize(skf, "", true, fixedPw)
|
||||||
|
assert(err == nil, "can't serialize sk %s", skf)
|
||||||
|
|
||||||
|
err = sk.Serialize(skf, "", false, nilPw)
|
||||||
|
assert(err != nil, "sk %s overwritten!", skf)
|
||||||
|
|
||||||
// We must find these two files
|
// We must find these two files
|
||||||
assert(fileExists(pkf), "missing pkf")
|
assert(fileExists(pkf), "missing pkf %s", pkf)
|
||||||
assert(fileExists(skf), "missing skf")
|
assert(fileExists(skf), "missing skf %s", skf)
|
||||||
|
|
||||||
// send wrong file and see what happens
|
npk, err := ReadPublicKey(pkf)
|
||||||
pk, err := ReadPublicKey(skf)
|
|
||||||
assert(err != nil, "bad PK ReadPK fail")
|
|
||||||
|
|
||||||
pk, err = ReadPublicKey(pkf)
|
|
||||||
assert(err == nil, "ReadPK() fail")
|
assert(err == nil, "ReadPK() fail")
|
||||||
|
|
||||||
// -ditto- for Sk
|
// send the public key as private key
|
||||||
sk, err := ReadPrivateKey(pkf, "")
|
nsk, err := ReadPrivateKey(pkf, emptyPw)
|
||||||
assert(err != nil, "bad SK ReadSK fail")
|
assert(err != nil, "bad SK ReadSK fail: %s", err)
|
||||||
|
|
||||||
sk, err = ReadPrivateKey(skf, "")
|
nsk, err = ReadPrivateKey(skf, emptyPw)
|
||||||
assert(err != nil, "ReadSK() empty pw fail")
|
assert(err != nil, "ReadSK() worked with empty pw")
|
||||||
|
|
||||||
sk, err = ReadPrivateKey(skf, "abcdef")
|
nsk, err = ReadPrivateKey(skf, wrongPw)
|
||||||
assert(err != nil, "ReadSK() wrong pw fail")
|
assert(err != nil, "ReadSK() worked with wrong pw")
|
||||||
|
|
||||||
badf := fmt.Sprintf("%s/badf.key", dn)
|
badf := fmt.Sprintf("%s/badf.key", dn)
|
||||||
err = ioutil.WriteFile(badf, []byte(badsk), 0600)
|
err = ioutil.WriteFile(badf, []byte(badsk), 0600)
|
||||||
assert(err == nil, "write badsk")
|
assert(err == nil, "can't write badsk: %s", err)
|
||||||
|
|
||||||
sk, err = ReadPrivateKey(badf, "abc")
|
nsk, err = ReadPrivateKey(badf, hardcodedPw)
|
||||||
assert(err != nil, "badsk read fail")
|
assert(err != nil, "decoded bad SK")
|
||||||
|
|
||||||
// Finally, with correct password it should work.
|
// Finally, with correct password it should work.
|
||||||
sk, err = ReadPrivateKey(skf, "abc")
|
nsk, err = ReadPrivateKey(skf, hardcodedPw)
|
||||||
assert(err == nil, "ReadSK() correct pw fail")
|
assert(err == nil, "ReadSK() correct pw fail: %s", err)
|
||||||
|
|
||||||
// And, deserialized keys should be identical
|
// And, deserialized keys should be identical
|
||||||
assert(byteEq(pk.Pk, kp.Pub.Pk), "pkbytes unequal")
|
assert(byteEq(pk.Pk, npk.Pk), "pkbytes unequal")
|
||||||
assert(byteEq(sk.Sk, kp.Sec.Sk), "skbytes unequal")
|
assert(byteEq(sk.Sk, nsk.Sk), "skbytes unequal")
|
||||||
|
|
||||||
os.RemoveAll(dn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #2. Create new key pair, sign a rand buffer and verify
|
// #2. Create new key pair, sign a rand buffer and verify
|
||||||
func Test1(t *testing.T) {
|
func TestSignRandBuf(t *testing.T) {
|
||||||
assert := newAsserter(t)
|
assert := newAsserter(t)
|
||||||
kp, err := NewKeypair()
|
|
||||||
assert(err == nil, "NewKeyPair() fail")
|
sk, err := NewPrivateKey()
|
||||||
|
assert(err == nil, "NewPrivateKey() fail: %s", err)
|
||||||
|
|
||||||
var ck [64]byte // simulates sha512 sum
|
var ck [64]byte // simulates sha512 sum
|
||||||
|
|
||||||
rand.Read(ck[:])
|
randRead(ck[:])
|
||||||
|
|
||||||
pk := &kp.Pub
|
pk := sk.PublicKey()
|
||||||
sk := &kp.Sec
|
|
||||||
|
|
||||||
ss, err := sk.SignMessage(ck[:], "")
|
ss, err := sk.SignMessage(ck[:], "")
|
||||||
assert(err == nil, "sk.sign fail")
|
assert(err == nil, "sk.sign fail: %s", err)
|
||||||
assert(ss != nil, "sig is null")
|
assert(ss != nil, "sig is null")
|
||||||
|
|
||||||
// verify sig
|
// verify sig
|
||||||
assert(ss.IsPKMatch(pk), "pk match fail")
|
assert(ss.IsPKMatch(pk), "pk match fail")
|
||||||
|
|
||||||
// Corrupt the pkhash and see
|
// Corrupt the pkhash and see
|
||||||
rand.Read(ss.pkhash[:])
|
randRead(ss.pkhash)
|
||||||
assert(!ss.IsPKMatch(pk), "corrupt pk match fail")
|
assert(!ss.IsPKMatch(pk), "corrupt pk match fail")
|
||||||
|
|
||||||
// Incorrect checksum == should fail verification
|
// Incorrect checksum == should fail verification
|
||||||
ok, err := pk.VerifyMessage(ck[:16], ss)
|
ok := pk.VerifyMessage(ck[:16], ss)
|
||||||
assert(err == nil, "bad ck verify err fail")
|
|
||||||
assert(!ok, "bad ck verify fail")
|
assert(!ok, "bad ck verify fail")
|
||||||
|
|
||||||
// proper checksum == should work
|
// proper checksum == should work
|
||||||
ok, err = pk.VerifyMessage(ck[:], ss)
|
ok = pk.VerifyMessage(ck[:], ss)
|
||||||
assert(err == nil, "verify err")
|
|
||||||
assert(ok, "verify fail")
|
assert(ok, "verify fail")
|
||||||
|
|
||||||
// Now sign a file
|
// Now sign a file
|
||||||
dn := tempdir(t)
|
dn := t.TempDir()
|
||||||
bn := fmt.Sprintf("%s/k", dn)
|
|
||||||
|
|
||||||
pkf := fmt.Sprintf("%s.pub", bn)
|
|
||||||
skf := fmt.Sprintf("%s.key", bn)
|
|
||||||
|
|
||||||
err = kp.Serialize(bn, "", "")
|
|
||||||
assert(err == nil, "keyPair.Serialize() fail")
|
|
||||||
|
|
||||||
// Now read the private key and sign
|
|
||||||
sk, err = ReadPrivateKey(skf, "")
|
|
||||||
assert(err == nil, "readSK fail")
|
|
||||||
|
|
||||||
pk, err = ReadPublicKey(pkf)
|
|
||||||
assert(err == nil, "ReadPK fail")
|
|
||||||
|
|
||||||
var buf [8192]byte
|
var buf [8192]byte
|
||||||
|
|
||||||
zf := fmt.Sprintf("%s/file.dat", dn)
|
zf := fmt.Sprintf("%s/file.dat", dn)
|
||||||
fd, err := os.OpenFile(zf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
fd, err := os.OpenFile(zf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
assert(err == nil, "file.dat creat file")
|
assert(err == nil, "file.dat creat file: %s", err)
|
||||||
|
|
||||||
for i := 0; i < 8; i++ {
|
for i := 0; i < 8; i++ {
|
||||||
rand.Read(buf[:])
|
randRead(buf[:])
|
||||||
n, err := fd.Write(buf[:])
|
n, err := fd.Write(buf[:])
|
||||||
assert(err == nil, fmt.Sprintf("file.dat write fail: %s", err))
|
assert(err == nil, fmt.Sprintf("file.dat write fail: %s", err))
|
||||||
assert(n == 8192, fmt.Sprintf("file.dat i/o fail: exp 8192 saw %v", n))
|
assert(n == 8192, fmt.Sprintf("file.dat i/o fail: exp 8192 saw %v", n))
|
||||||
|
@ -213,27 +192,31 @@ func Test1(t *testing.T) {
|
||||||
fd.Close()
|
fd.Close()
|
||||||
|
|
||||||
sig, err := sk.SignFile(zf)
|
sig, err := sk.SignFile(zf)
|
||||||
assert(err == nil, "file.dat sign fail")
|
assert(err == nil, "file.dat sign fail: %s", err)
|
||||||
assert(sig != nil, "file.dat sign nil")
|
assert(sig != nil, "file.dat sign nil")
|
||||||
|
|
||||||
ok, err = pk.VerifyFile(zf, sig)
|
ok, err = pk.VerifyFile(zf, sig)
|
||||||
assert(err == nil, "file.dat verify fail")
|
assert(err == nil, "file.dat verify fail: %s", err)
|
||||||
assert(ok, "file.dat verify false")
|
assert(ok, "file.dat verify false")
|
||||||
|
|
||||||
// Now, serialize the signature and read it back
|
// Now, serialize the signature and read it back
|
||||||
sf := fmt.Sprintf("%s/file.sig", dn)
|
sf := fmt.Sprintf("%s/file.sig", dn)
|
||||||
err = sig.SerializeFile(sf, "")
|
err = sig.Serialize(sf, "", true)
|
||||||
assert(err == nil, "sig serialize fail")
|
assert(err == nil, "sig serialize fail: %s", err)
|
||||||
|
|
||||||
|
// now try to overwrite it
|
||||||
|
err = sig.Serialize(sf, "", false)
|
||||||
|
assert(err != nil, "sig serialize overwrote?!")
|
||||||
|
|
||||||
s2, err := ReadSignature(sf)
|
s2, err := ReadSignature(sf)
|
||||||
assert(err == nil, "file.sig read fail")
|
assert(err == nil, "file.sig read fail: %s", err)
|
||||||
assert(s2 != nil, "file.sig sig nil")
|
assert(s2 != nil, "file.sig sig nil")
|
||||||
|
|
||||||
assert(byteEq(s2.Sig, sig.Sig), "sig compare fail")
|
assert(byteEq(s2.Sig, sig.Sig), "sig compare fail")
|
||||||
|
|
||||||
// If we give a wrong file, verify must fail
|
// If we give a wrong file, verify must fail
|
||||||
st, err := os.Stat(zf)
|
st, err := os.Stat(zf)
|
||||||
assert(err == nil, "file.dat stat fail")
|
assert(err == nil, "file.dat stat fail: %s", err)
|
||||||
|
|
||||||
n := st.Size()
|
n := st.Size()
|
||||||
assert(n == 8192*8, "file.dat size fail")
|
assert(n == 8192*8, "file.dat size fail")
|
||||||
|
@ -241,34 +224,38 @@ func Test1(t *testing.T) {
|
||||||
os.Truncate(zf, n-1)
|
os.Truncate(zf, n-1)
|
||||||
|
|
||||||
st, err = os.Stat(zf)
|
st, err = os.Stat(zf)
|
||||||
assert(err == nil, "file.dat stat2 fail")
|
assert(err == nil, "file.dat stat2 fail: %s", err)
|
||||||
assert(st.Size() == (n-1), "truncate fail")
|
assert(st.Size() == (n-1), "truncate fail")
|
||||||
|
|
||||||
// Now verify this corrupt file
|
// Now verify this corrupt file
|
||||||
ok, err = pk.VerifyFile(zf, sig)
|
ok, err = pk.VerifyFile(zf, sig)
|
||||||
assert(err == nil, "file.dat corrupt i/o fail")
|
assert(err == nil, "file.dat corrupt i/o fail: %s", err)
|
||||||
assert(!ok, "file.dat corrupt verify false")
|
assert(!ok, "file.dat corrupt verify false")
|
||||||
|
|
||||||
os.RemoveAll(dn)
|
os.RemoveAll(dn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func Benchmark_Keygen(b *testing.B) {
|
func Benchmark_Keygen(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, _ = NewKeypair()
|
_, _ = NewPrivateKey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func Benchmark_Sig(b *testing.B) {
|
func Benchmark_Sig(b *testing.B) {
|
||||||
var sizes = [...]uint{
|
var sizes = [...]uint{
|
||||||
16,
|
16,
|
||||||
32,
|
32,
|
||||||
64,
|
64,
|
||||||
|
1024,
|
||||||
|
4096,
|
||||||
|
256 * 1024,
|
||||||
|
1048576,
|
||||||
|
4 * 1048576,
|
||||||
}
|
}
|
||||||
|
|
||||||
b.StopTimer()
|
b.StopTimer()
|
||||||
kp, _ := NewKeypair()
|
sk, _ := NewPrivateKey()
|
||||||
|
pk := sk.PublicKey()
|
||||||
var sig *Signature
|
var sig *Signature
|
||||||
for _, sz := range sizes {
|
for _, sz := range sizes {
|
||||||
buf := randbuf(sz)
|
buf := randbuf(sz)
|
||||||
|
@ -277,12 +264,12 @@ func Benchmark_Sig(b *testing.B) {
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
|
|
||||||
b.Run(s0, func (b *testing.B) {
|
b.Run(s0, func(b *testing.B) {
|
||||||
sig = benchSign(b, buf, &kp.Sec)
|
sig = benchSign(b, buf, sk)
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Run(s1, func (b *testing.B) {
|
b.Run(s1, func(b *testing.B) {
|
||||||
benchVerify(b, buf, sig, &kp.Pub)
|
benchVerify(b, buf, sig, pk)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,10 +287,9 @@ func benchVerify(b *testing.B, buf []byte, sig *Signature, pk *PublicKey) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func randbuf(sz uint) []byte {
|
func randbuf(sz uint) []byte {
|
||||||
b := make([]byte, sz)
|
b := make([]byte, sz)
|
||||||
rand.Read(b)
|
randRead(b)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
384
sign/ssh.go
Normal file
384
sign/ssh.go
Normal file
|
@ -0,0 +1,384 @@
|
||||||
|
// ssh.go - support for reading ssh private and public keys
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// This file is a bastardization of github.com/ScaleFT/sshkeys and
|
||||||
|
// golang.org/x/crypto/ssh/keys.go
|
||||||
|
//
|
||||||
|
// It is licensed under the terms of the original go source code
|
||||||
|
// OR the Apache 2.0 license (terms of sshkeys).
|
||||||
|
//
|
||||||
|
// Changes from that version:
|
||||||
|
// - don't use password but call a func() to get the password as needed
|
||||||
|
// - narrowly scope the key support for ONLY ed25519 keys
|
||||||
|
// - support reading multiple public keys from authorized_keys
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dchest/bcrypt_pbkdf"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keySizeAES256 = 32
|
||||||
|
|
||||||
|
// ParseEncryptedRawPrivateKey returns a private key from an
|
||||||
|
// encrypted ed25519 private key.
|
||||||
|
func parseSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode(data)
|
||||||
|
if block == nil {
|
||||||
|
return nil, ErrNoPEMFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if x509.IsEncryptedPEMBlock(block) {
|
||||||
|
return nil, fmt.Errorf("ssh: no support for legacy PEM encrypted keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch block.Type {
|
||||||
|
case "OPENSSH PRIVATE KEY":
|
||||||
|
return parseOpenSSHPrivateKey(block.Bytes, getpw)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSSHPublicKey(in []byte) (*PublicKey, error) {
|
||||||
|
splitter := regexp.MustCompile("[ \\t]+")
|
||||||
|
v := splitter.Split(string(in), -1)
|
||||||
|
if len(v) != 3 {
|
||||||
|
return nil, ErrBadPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseEncPubKey([]byte(v[1]), v[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse a wire encoded public key
|
||||||
|
func parseEncPubKey(in []byte, comm string) (*PublicKey, error) {
|
||||||
|
in, err := base64.StdEncoding.DecodeString(string(in))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
algo, in, ok := parseString(in)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrKeyTooShort
|
||||||
|
|
||||||
|
}
|
||||||
|
if string(algo) != ssh.KeyAlgoED25519 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var w struct {
|
||||||
|
KeyBytes []byte
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(in, &w); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(w.Rest) > 0 {
|
||||||
|
return nil, ErrBadTrailers
|
||||||
|
}
|
||||||
|
|
||||||
|
var pk PublicKey
|
||||||
|
|
||||||
|
if err = makePublicKeyFromBytes(&pk, w.KeyBytes); err == nil {
|
||||||
|
pk.Comment = strings.TrimSpace(comm)
|
||||||
|
return &pk, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||||
|
if len(in) < 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint32(in)
|
||||||
|
in = in[4:]
|
||||||
|
if uint32(len(in)) < length {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out = in[:length]
|
||||||
|
rest = in[length:]
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAuthorizedKey parses a public key in OpenSSH binary format and decodes it.
|
||||||
|
// removed.
|
||||||
|
func parseAuthorizedKey(in []byte) (*PublicKey, error) {
|
||||||
|
in = bytes.TrimSpace(in)
|
||||||
|
i := bytes.IndexAny(in, " \t")
|
||||||
|
if i == -1 {
|
||||||
|
i = len(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := parseEncPubKey(in[:i], string(in[i:]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||||
|
// file used in OpenSSH according to the sshd(8) manual page.
|
||||||
|
func ParseAuthorizedKeys(in []byte) ([]*PublicKey, error) {
|
||||||
|
var pka []*PublicKey
|
||||||
|
var rest []byte
|
||||||
|
|
||||||
|
for len(in) > 0 {
|
||||||
|
end := bytes.IndexByte(in, '\n')
|
||||||
|
if end != -1 {
|
||||||
|
rest = in[end+1:]
|
||||||
|
in = in[:end]
|
||||||
|
} else {
|
||||||
|
rest = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end = bytes.IndexByte(in, '\r')
|
||||||
|
if end != -1 {
|
||||||
|
in = in[:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
in = bytes.TrimSpace(in)
|
||||||
|
if len(in) == 0 || in[0] == '#' {
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := bytes.IndexAny(in, " \t")
|
||||||
|
if i == -1 {
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk, err := parseAuthorizedKey(in[i:]); err == nil {
|
||||||
|
if pk != nil {
|
||||||
|
pka = append(pka, pk)
|
||||||
|
}
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// No key type recognised. Maybe there's an options field at
|
||||||
|
// the beginning.
|
||||||
|
var b byte
|
||||||
|
inQuote := false
|
||||||
|
var candidateOptions []string
|
||||||
|
optionStart := 0
|
||||||
|
for i, b = range in {
|
||||||
|
isEnd := !inQuote && (b == ' ' || b == '\t')
|
||||||
|
if (b == ',' && !inQuote) || isEnd {
|
||||||
|
if i-optionStart > 0 {
|
||||||
|
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
|
||||||
|
}
|
||||||
|
optionStart = i + 1
|
||||||
|
}
|
||||||
|
if isEnd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
|
||||||
|
inQuote = !inQuote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i == len(in) {
|
||||||
|
// Invalid line: unmatched quote
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
in = in[i:]
|
||||||
|
i = bytes.IndexAny(in, " \t")
|
||||||
|
if i == -1 {
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk, err := parseAuthorizedKey(in[i:]); err == nil {
|
||||||
|
if pk != nil {
|
||||||
|
pka = append(pka, pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in = rest
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return pka, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const opensshv1Magic = "openssh-key-v1"
|
||||||
|
|
||||||
|
type opensshHeader struct {
|
||||||
|
CipherName string
|
||||||
|
KdfName string
|
||||||
|
KdfOpts string
|
||||||
|
NumKeys uint32
|
||||||
|
PubKey string
|
||||||
|
PrivKeyBlock string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opensshKey struct {
|
||||||
|
Check1 uint32
|
||||||
|
Check2 uint32
|
||||||
|
Keytype string
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type opensshED25519 struct {
|
||||||
|
Pub []byte
|
||||||
|
Priv []byte
|
||||||
|
Comment string
|
||||||
|
Pad []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
|
||||||
|
magic := append([]byte(opensshv1Magic), 0)
|
||||||
|
if !bytes.Equal(magic, data[0:len(magic)]) {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
remaining := data[len(magic):]
|
||||||
|
|
||||||
|
w := opensshHeader{}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(remaining, &w); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.NumKeys != 1 {
|
||||||
|
return nil, fmt.Errorf("ssh: NumKeys must be 1: %d", w.NumKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKeyBytes []byte
|
||||||
|
var encrypted bool
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// OpenSSH supports bcrypt KDF w/ AES256-CBC or AES256-CTR mode
|
||||||
|
case w.KdfName == "bcrypt" && w.CipherName == "aes256-cbc":
|
||||||
|
pw, err := getpw()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iv, block, err := extractBcryptIvBlock(pw, &w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cbc := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||||
|
cbc.CryptBlocks(privateKeyBytes, privateKeyBytes)
|
||||||
|
|
||||||
|
encrypted = true
|
||||||
|
|
||||||
|
case w.KdfName == "bcrypt" && w.CipherName == "aes256-ctr":
|
||||||
|
pw, err := getpw()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iv, block, err := extractBcryptIvBlock(pw, &w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := cipher.NewCTR(block, iv)
|
||||||
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||||
|
stream.XORKeyStream(privateKeyBytes, privateKeyBytes)
|
||||||
|
|
||||||
|
encrypted = true
|
||||||
|
|
||||||
|
case w.KdfName == "none" && w.CipherName == "none":
|
||||||
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ssh: unknown Cipher/KDF: %s:%s", w.CipherName, w.KdfName)
|
||||||
|
}
|
||||||
|
|
||||||
|
pk1 := opensshKey{}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(privateKeyBytes, &pk1); err != nil {
|
||||||
|
if encrypted {
|
||||||
|
return nil, ErrIncorrectPassword
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk1.Check1 != pk1.Check2 {
|
||||||
|
return nil, ErrIncorrectPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only handle ed25519 and rsa keys currently
|
||||||
|
switch pk1.Keytype {
|
||||||
|
case ssh.KeyAlgoED25519:
|
||||||
|
key := opensshED25519{}
|
||||||
|
|
||||||
|
err := ssh.Unmarshal(pk1.Rest, &key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key.Priv) != ed25519.PrivateKeySize {
|
||||||
|
return nil, ErrBadLength
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range key.Pad {
|
||||||
|
if int(b) != i+1 {
|
||||||
|
return nil, ErrBadPadding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sk PrivateKey
|
||||||
|
if err = makePrivateKeyFromBytes(&sk, key.Priv); err == nil {
|
||||||
|
return &sk, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBcryptIvBlock(passphrase []byte, w *opensshHeader) ([]byte, cipher.Block, error) {
|
||||||
|
cipherKeylen := keySizeAES256
|
||||||
|
cipherIvLen := aes.BlockSize
|
||||||
|
|
||||||
|
var opts struct {
|
||||||
|
Salt []byte
|
||||||
|
Rounds uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal([]byte(w.KdfOpts), &opts); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
kdfdata, err := bcrypt_pbkdf.Key(passphrase, opts.Salt, int(opts.Rounds), cipherKeylen+cipherIvLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := kdfdata[cipherKeylen : cipherIvLen+cipherKeylen]
|
||||||
|
aeskey := kdfdata[0:cipherKeylen]
|
||||||
|
block, err := aes.NewCipher(aeskey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return iv, block, nil
|
||||||
|
}
|
162
sign/stream.go
Normal file
162
sign/stream.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
// stream.go - Streaming io.Reader, io.Writer interface to encryption/decryption
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
//
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encWriter buffers partial writes until a full chunk is accumulated.
|
||||||
|
// It's methods implement the io.WriteCloser interface.
|
||||||
|
type encWriter struct {
|
||||||
|
buf []byte
|
||||||
|
n int // # of bytes written
|
||||||
|
wr io.WriteCloser
|
||||||
|
e *Encryptor
|
||||||
|
blk uint32
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.WriteCloser = &encWriter{}
|
||||||
|
|
||||||
|
// NewStreamWriter begins stream encryption to an underlying destination writer 'wr'.
|
||||||
|
// It returns an io.WriteCloser.
|
||||||
|
func (e *Encryptor) NewStreamWriter(wr io.WriteCloser) (io.WriteCloser, error) {
|
||||||
|
if !e.started {
|
||||||
|
err := e.start(wr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &encWriter{
|
||||||
|
buf: make([]byte, e.ChunkSize),
|
||||||
|
wr: wr,
|
||||||
|
e: e,
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stream = true
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements the io.Writer interface
|
||||||
|
func (w *encWriter) Write(b []byte) (int, error) {
|
||||||
|
if w.err != nil {
|
||||||
|
return 0, w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(b)
|
||||||
|
if n == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
max := int(w.e.ChunkSize)
|
||||||
|
for len(b) > 0 {
|
||||||
|
buf := w.buf[w.n:]
|
||||||
|
z := copy(buf, b)
|
||||||
|
b = b[z:]
|
||||||
|
w.n += z
|
||||||
|
|
||||||
|
// We only flush if we have more data remaining in the input buffer.
|
||||||
|
// This way, we don't flush a potentially last block here; that happens
|
||||||
|
// when the caller eventually closes the stream.
|
||||||
|
if w.n == max && len(b) > 0 {
|
||||||
|
w.err = w.e.encrypt(w.buf, w.wr, w.blk, false)
|
||||||
|
if w.err != nil {
|
||||||
|
return 0, w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.n = 0
|
||||||
|
w.blk += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the io.Close interface
|
||||||
|
func (w *encWriter) Close() error {
|
||||||
|
if w.err != nil {
|
||||||
|
return w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.e.encrypt(w.buf[:w.n], w.wr, w.blk, true)
|
||||||
|
if err != nil {
|
||||||
|
w.err = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.n = 0
|
||||||
|
w.err = ErrClosed
|
||||||
|
return w.wr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// encReader buffers partial reads and it's methods implement the io.Reader interface.
|
||||||
|
type encReader struct {
|
||||||
|
buf []byte
|
||||||
|
unread []byte
|
||||||
|
d *Decryptor
|
||||||
|
blk uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.Reader = &encReader{}
|
||||||
|
|
||||||
|
// NewStreamReader returns an io.Reader to read from the decrypted stream
|
||||||
|
func (d *Decryptor) NewStreamReader() (io.Reader, error) {
|
||||||
|
if d.key == nil {
|
||||||
|
return nil, ErrNoKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.eof {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
d.stream = true
|
||||||
|
return &encReader{
|
||||||
|
buf: make([]byte, d.ChunkSize),
|
||||||
|
d: d,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader interface
|
||||||
|
func (r *encReader) Read(b []byte) (int, error) {
|
||||||
|
if r.d.eof && len(r.unread) == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.unread) > 0 {
|
||||||
|
n := copy(b, r.unread)
|
||||||
|
r.unread = r.unread[n:]
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, eof, err := r.d.decrypt(r.blk)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.blk += 1
|
||||||
|
|
||||||
|
n := copy(b, buf)
|
||||||
|
buf = buf[n:]
|
||||||
|
|
||||||
|
copy(r.buf, buf)
|
||||||
|
r.unread = r.buf[:len(buf)]
|
||||||
|
|
||||||
|
if eof {
|
||||||
|
r.d.eof = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
43
sign/utils_test.go
Normal file
43
sign/utils_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// utils_test.go -- Test harness utilities for sign
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package sign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) {
|
||||||
|
return func(cond bool, msg string, args ...interface{}) {
|
||||||
|
if cond {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, file, line, ok := runtime.Caller(1)
|
||||||
|
if !ok {
|
||||||
|
file = "???"
|
||||||
|
line = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s := fmt.Sprintf(msg, args...)
|
||||||
|
t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if two byte arrays are equal
|
||||||
|
func byteEq(x, y []byte) bool {
|
||||||
|
return subtle.ConstantTimeCompare(x, y) == 1
|
||||||
|
}
|
333
sigtool.go
333
sigtool.go
|
@ -1,333 +0,0 @@
|
||||||
// sigtool.go -- Tool to generate, manage Ed25519 keys and
|
|
||||||
// signatures.
|
|
||||||
//
|
|
||||||
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
|
||||||
//
|
|
||||||
// Licensing Terms: GPLv2
|
|
||||||
//
|
|
||||||
// If you need a commercial license for this work, please contact
|
|
||||||
// the author.
|
|
||||||
//
|
|
||||||
// This software does not come with any express or implied
|
|
||||||
// warranty; it is provided "as is". No claim is made to its
|
|
||||||
// suitability for any purpose.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/opencoff/go-utils"
|
|
||||||
flag "github.com/opencoff/pflag"
|
|
||||||
"github.com/opencoff/sigtool/sign"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This will be filled in by "build"
|
|
||||||
var Version string = "1.1"
|
|
||||||
|
|
||||||
var Z string = path.Base(os.Args[0])
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
var ver, help bool
|
|
||||||
|
|
||||||
mf := flag.NewFlagSet(Z, flag.ExitOnError)
|
|
||||||
mf.SetInterspersed(false)
|
|
||||||
mf.BoolVarP(&ver, "version", "v", false, "Show version info and exit")
|
|
||||||
mf.BoolVarP(&help, "help", "h", false, "Show help info exit")
|
|
||||||
mf.Parse(os.Args[1:])
|
|
||||||
|
|
||||||
if ver {
|
|
||||||
fmt.Printf("%s: %s\n", Z, Version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if help {
|
|
||||||
usage(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := mf.Args()
|
|
||||||
if len(args) < 1 {
|
|
||||||
warn("Insufficient arguments. Try '%s -h'", Z)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[0] {
|
|
||||||
case "gen", "generate", "g":
|
|
||||||
gen(args[1:])
|
|
||||||
|
|
||||||
case "sign", "s":
|
|
||||||
signify(args[1:])
|
|
||||||
|
|
||||||
case "verify", "v":
|
|
||||||
verify(args[1:])
|
|
||||||
|
|
||||||
case "help", "":
|
|
||||||
usage(0)
|
|
||||||
|
|
||||||
default:
|
|
||||||
die("Unknown command %s", args[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the generate command
|
|
||||||
func gen(args []string) {
|
|
||||||
|
|
||||||
var pw, help, force bool
|
|
||||||
var comment string
|
|
||||||
var envpw string
|
|
||||||
|
|
||||||
fs := flag.NewFlagSet("generate", flag.ExitOnError)
|
|
||||||
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
|
||||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to encrypt the private key")
|
|
||||||
fs.StringVarP(&comment, "comment", "c", "", "Use `C` as the text comment for the keys")
|
|
||||||
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
|
||||||
fs.BoolVarP(&force, "force", "F", false, "Overwrite the output file if it exists")
|
|
||||||
|
|
||||||
fs.Parse(args)
|
|
||||||
|
|
||||||
if help {
|
|
||||||
fs.SetOutput(os.Stdout)
|
|
||||||
fmt.Printf(`%s generate|gen|g [options] file-prefix
|
|
||||||
|
|
||||||
Generate a new Ed25519 public+private key pair and write public key to
|
|
||||||
FILE-PREFIX.pub and private key to FILE-PREFIX.key.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
`, Z)
|
|
||||||
fs.PrintDefaults()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
args = fs.Args()
|
|
||||||
if len(args) < 1 {
|
|
||||||
die("Insufficient arguments to 'generate'. Try '%s generate -h' ..", Z)
|
|
||||||
}
|
|
||||||
|
|
||||||
bn := args[0]
|
|
||||||
|
|
||||||
if exists(bn) && !force {
|
|
||||||
die("Public/Private key files (%s.key, %s.pub) exist. Won't overwrite!", bn, bn)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pws string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(envpw) > 0 {
|
|
||||||
pws = os.Getenv(envpw)
|
|
||||||
} else if pw {
|
|
||||||
pws, err = utils.Askpass("Enter passphrase for private key", true)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kp, err := sign.NewKeypair()
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = kp.Serialize(bn, comment, pws)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the 'sign' command.
|
|
||||||
func signify(args []string) {
|
|
||||||
var pw, help bool
|
|
||||||
var output string
|
|
||||||
var envpw string
|
|
||||||
|
|
||||||
fs := flag.NewFlagSet("sign", flag.ExitOnError)
|
|
||||||
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
|
||||||
fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key")
|
|
||||||
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
|
||||||
fs.StringVarP(&output, "output", "o", "", "Write signature to file `F`")
|
|
||||||
|
|
||||||
fs.Parse(args)
|
|
||||||
|
|
||||||
if help {
|
|
||||||
fs.SetOutput(os.Stdout)
|
|
||||||
fmt.Printf(`%s sign|s [options] privkey file
|
|
||||||
|
|
||||||
Sign FILE with a Ed25519 private key PRIVKEY and write signature to FILE.sig
|
|
||||||
|
|
||||||
Options:
|
|
||||||
`, Z)
|
|
||||||
fs.PrintDefaults()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
args = fs.Args()
|
|
||||||
if len(args) < 2 {
|
|
||||||
die("Insufficient arguments to 'sign'. Try '%s sign -h' ..", Z)
|
|
||||||
}
|
|
||||||
|
|
||||||
kn := args[0]
|
|
||||||
fn := args[1]
|
|
||||||
outf := fmt.Sprintf("%s.sig", fn)
|
|
||||||
|
|
||||||
var pws string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(envpw) > 0 {
|
|
||||||
pws = os.Getenv(envpw)
|
|
||||||
} else if pw {
|
|
||||||
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(output) > 0 {
|
|
||||||
outf = output
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, err := sign.ReadPrivateKey(kn, pws)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sig, err := pk.SignFile(fn)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sigo, err := sig.Serialize(fmt.Sprintf("input=%s", fn))
|
|
||||||
|
|
||||||
var fd io.Writer = os.Stdout
|
|
||||||
|
|
||||||
if outf != "-" {
|
|
||||||
fdx, err := os.OpenFile(outf, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
die("can't create output file %s: %s", outf, err)
|
|
||||||
}
|
|
||||||
defer fdx.Close()
|
|
||||||
fd = fdx
|
|
||||||
}
|
|
||||||
|
|
||||||
fd.Write(sigo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature on a given file
|
|
||||||
func verify(args []string) {
|
|
||||||
var help, quiet bool
|
|
||||||
|
|
||||||
fs := flag.NewFlagSet("verify", flag.ExitOnError)
|
|
||||||
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
|
||||||
fs.BoolVarP(&quiet, "quiet", "q", false, "Don't show any output; exit with status code only")
|
|
||||||
|
|
||||||
fs.Parse(args)
|
|
||||||
|
|
||||||
if help {
|
|
||||||
fs.SetOutput(os.Stdout)
|
|
||||||
fmt.Printf(`%s verify|v [options] pubkey sig file
|
|
||||||
|
|
||||||
Verify an Ed25519 signature in SIG of FILE using a public key PUBKEY.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
`, Z)
|
|
||||||
fs.PrintDefaults()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
args = fs.Args()
|
|
||||||
if len(args) < 3 {
|
|
||||||
die("Insufficient arguments to 'verify'. Try '%s verify -h' ..", Z)
|
|
||||||
}
|
|
||||||
|
|
||||||
pn := args[0]
|
|
||||||
sn := args[1]
|
|
||||||
fn := args[2]
|
|
||||||
|
|
||||||
sig, err := sign.ReadSignature(sn)
|
|
||||||
if err != nil {
|
|
||||||
die("Can't read signature '%s': %s", sn, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, err := sign.ReadPublicKey(pn)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sig.IsPKMatch(pk) {
|
|
||||||
die("Wrong public key '%s' for verifying '%s'", pn, sn)
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := pk.VerifyFile(fn, sig)
|
|
||||||
if err != nil {
|
|
||||||
die("%s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := 0
|
|
||||||
if !ok {
|
|
||||||
exit = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if !quiet {
|
|
||||||
if ok {
|
|
||||||
fmt.Printf("%s: Signature %s verified\n", fn, sn)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s: Signature %s verification failure\n", fn, sn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(exit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage(c int) {
|
|
||||||
x := fmt.Sprintf(`%s is a tool to generate, sign and verify files with Ed25519 signatures.
|
|
||||||
|
|
||||||
Usage: %s [global-options] command [options] arg [args..]
|
|
||||||
|
|
||||||
Global options:
|
|
||||||
-h, --help Show help and exit
|
|
||||||
-v, --version Show version info and exit.
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
generate, g Generate a new Ed25519 keypair
|
|
||||||
sign, s Sign a file with a private key
|
|
||||||
verify, v Verify a signature against a file and a public key
|
|
||||||
`, Z, Z)
|
|
||||||
|
|
||||||
os.Stdout.Write([]byte(x))
|
|
||||||
os.Exit(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if $bn.key or $bn.pub exist; false otherwise
|
|
||||||
func exists(bn string) bool {
|
|
||||||
pk := bn + ".pub"
|
|
||||||
sk := bn + ".key"
|
|
||||||
|
|
||||||
if _, err := os.Stat(pk); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(sk); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// die with error
|
|
||||||
func die(f string, v ...interface{}) {
|
|
||||||
warn(f, v...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func warn(f string, v ...interface{}) {
|
|
||||||
z := fmt.Sprintf("%s: %s", os.Args[0], f)
|
|
||||||
s := fmt.Sprintf(z, v...)
|
|
||||||
if n := len(s); s[n-1] != '\n' {
|
|
||||||
s += "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Stderr.WriteString(s)
|
|
||||||
os.Stderr.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// vim: ft=go:sw=8:ts=8:noexpandtab:tw=98:
|
|
399
src/crypt.go
Normal file
399
src/crypt.go
Normal file
|
@ -0,0 +1,399 @@
|
||||||
|
// crypt.go -- Encrypt/decrypt command handling
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/sign"
|
||||||
|
"github.com/opencoff/go-fio"
|
||||||
|
"github.com/opencoff/go-utils"
|
||||||
|
flag "github.com/opencoff/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sigtool encrypt [-i|--identity my.key] to.pub [to.pub] [ssh.pub] inputfile|- [-o output]
|
||||||
|
|
||||||
|
func encrypt(args []string) {
|
||||||
|
fs := flag.NewFlagSet("encrypt", flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
encryptUsage(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var outfile string
|
||||||
|
var keyfile string
|
||||||
|
var szstr string = "128k"
|
||||||
|
var envpw string
|
||||||
|
var nopw, force bool
|
||||||
|
var blksize uint64
|
||||||
|
|
||||||
|
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`")
|
||||||
|
fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`")
|
||||||
|
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
|
||||||
|
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||||
|
fs.StringVarP(&szstr, "block-size", "B", szstr, "Use `S` as the encryption block size")
|
||||||
|
fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists")
|
||||||
|
|
||||||
|
err := fs.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blksize, err = utils.ParseSize(szstr); err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pws, infile string
|
||||||
|
var sk *sign.PrivateKey
|
||||||
|
|
||||||
|
if len(keyfile) > 0 {
|
||||||
|
sk, err = sign.ReadPrivateKey(keyfile, func() ([]byte, error) {
|
||||||
|
if nopw {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(envpw) > 0 {
|
||||||
|
pws = os.Getenv(envpw)
|
||||||
|
} else {
|
||||||
|
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(pws), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = fs.Args()
|
||||||
|
if len(args) < 2 {
|
||||||
|
Die("Insufficient args. Try '%s --help'", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var infd io.Reader = os.Stdin
|
||||||
|
var outfd io.WriteCloser = os.Stdout
|
||||||
|
var inf *os.File
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
infile = args[len(args)-1]
|
||||||
|
if infile != "-" {
|
||||||
|
inf := mustOpen(infile, os.O_RDONLY)
|
||||||
|
defer inf.Close()
|
||||||
|
|
||||||
|
infd = inf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets try to read the authorized files
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
Die("can't find homedir for this user")
|
||||||
|
}
|
||||||
|
|
||||||
|
authkeys := fmt.Sprintf("%s/.ssh/authorized_keys", home)
|
||||||
|
authdata, err := ioutil.ReadFile(authkeys)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
Die("can't open %s: %s", authkeys, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pka, err := sign.ParseAuthorizedKeys(authdata)
|
||||||
|
keymap := make(map[string]*sign.PublicKey)
|
||||||
|
|
||||||
|
for _, pk := range pka {
|
||||||
|
keymap[pk.Comment] = pk
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outfile) > 0 && outfile != "-" {
|
||||||
|
var mode os.FileMode = 0600 // conservative output mode
|
||||||
|
|
||||||
|
if inf != nil {
|
||||||
|
var err error
|
||||||
|
var ist, ost os.FileInfo
|
||||||
|
|
||||||
|
if ost, err = os.Stat(outfile); err != nil {
|
||||||
|
Die("can't stat %s: %s", outfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ist, err = inf.Stat(); err != nil {
|
||||||
|
Die("can't stat %s: %s", infile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.SameFile(ist, ost) {
|
||||||
|
Die("won't create output file: same as input file!")
|
||||||
|
}
|
||||||
|
mode = ist.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts uint32
|
||||||
|
if force {
|
||||||
|
opts |= fio.OPT_OVERWRITE
|
||||||
|
}
|
||||||
|
sf, err := fio.NewSafeFile(outfile, opts, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
AtExit(sf.Abort)
|
||||||
|
defer sf.Abort()
|
||||||
|
outfd = sf
|
||||||
|
}
|
||||||
|
|
||||||
|
en, err := sign.NewEncryptor(sk, blksize)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := 0
|
||||||
|
for i := 0; i < len(args)-1; i++ {
|
||||||
|
var err error
|
||||||
|
var pk *sign.PublicKey
|
||||||
|
|
||||||
|
fn := args[i]
|
||||||
|
if strings.Index(fn, "@") > 0 {
|
||||||
|
var ok bool
|
||||||
|
pk, ok = keymap[fn]
|
||||||
|
if !ok {
|
||||||
|
Warn("can't find user %s in %s", fn, authkeys)
|
||||||
|
errs += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pk, err = sign.ReadPublicKey(fn)
|
||||||
|
if err != nil {
|
||||||
|
Warn("%s", err)
|
||||||
|
errs += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = en.AddRecipient(pk)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs > 0 {
|
||||||
|
Die("Too many errors!")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = en.Encrypt(infd, outfd)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
outfd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type nullWriter struct{}
|
||||||
|
|
||||||
|
func (w *nullWriter) Write(p []byte) (int, error) {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *nullWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.WriteCloser = &nullWriter{}
|
||||||
|
|
||||||
|
// sigtool decrypt a.key [file] [-o output]
|
||||||
|
func decrypt(args []string) {
|
||||||
|
fs := flag.NewFlagSet("decrypt", flag.ExitOnError)
|
||||||
|
fs.Usage = func() {
|
||||||
|
decryptUsage(fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var envpw string
|
||||||
|
var outfile string
|
||||||
|
var pubkey string
|
||||||
|
var nopw, test, force bool
|
||||||
|
|
||||||
|
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`")
|
||||||
|
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
|
||||||
|
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||||
|
fs.StringVarP(&pubkey, "verify-sender", "v", "", "Verify that the sender matches public key in `F`")
|
||||||
|
fs.BoolVarP(&test, "test", "t", false, "Test the encrypted file against the given key without writing to output")
|
||||||
|
fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists")
|
||||||
|
|
||||||
|
err := fs.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = fs.Args()
|
||||||
|
if len(args) < 1 {
|
||||||
|
Die("Insufficient args. Try '%s --help'", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var infd io.Reader = os.Stdin
|
||||||
|
var outfd io.WriteCloser = os.Stdout
|
||||||
|
var inf *os.File
|
||||||
|
var infile string
|
||||||
|
|
||||||
|
keyfile := args[0]
|
||||||
|
sk, err := sign.ReadPrivateKey(keyfile, func() ([]byte, error) {
|
||||||
|
var pws string
|
||||||
|
if nopw {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(envpw) > 0 {
|
||||||
|
pws = os.Getenv(envpw)
|
||||||
|
} else {
|
||||||
|
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(pws), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pk *sign.PublicKey
|
||||||
|
|
||||||
|
if len(pubkey) > 0 {
|
||||||
|
pk, err = sign.ReadPublicKey(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
infile = args[1]
|
||||||
|
if infile != "-" {
|
||||||
|
inf := mustOpen(infile, os.O_RDONLY)
|
||||||
|
defer inf.Close()
|
||||||
|
|
||||||
|
infd = inf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test {
|
||||||
|
outfd = &nullWriter{}
|
||||||
|
} else if len(outfile) > 0 && outfile != "-" {
|
||||||
|
var mode os.FileMode = 0600 // conservative mode
|
||||||
|
|
||||||
|
if inf != nil {
|
||||||
|
var ist, ost os.FileInfo
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if ost, err = os.Stat(outfile); err != nil {
|
||||||
|
Die("can't stat %s: %s", outfile, err)
|
||||||
|
}
|
||||||
|
if ist, err = inf.Stat(); err != nil {
|
||||||
|
Die("can't stat %s: %s", infile, err)
|
||||||
|
}
|
||||||
|
if os.SameFile(ist, ost) {
|
||||||
|
Die("won't create output file: same as input file!")
|
||||||
|
}
|
||||||
|
mode = ist.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts uint32
|
||||||
|
if force {
|
||||||
|
opts |= fio.OPT_OVERWRITE
|
||||||
|
}
|
||||||
|
sf, err := fio.NewSafeFile(outfile, opts, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
AtExit(sf.Abort)
|
||||||
|
defer sf.Abort()
|
||||||
|
outfd = sf
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := sign.NewDecryptor(infd)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.SetPrivateKey(sk, pk)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pk == nil && d.AuthenticatedSender() {
|
||||||
|
var fn string = infile
|
||||||
|
if len(fn) == 0 || fn == "-" {
|
||||||
|
fn = "<stdin>"
|
||||||
|
}
|
||||||
|
Warn("%s: Missing sender Public Key; can't authenticate sender ..", fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = d.Decrypt(outfd); err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outfd.Close()
|
||||||
|
|
||||||
|
if test {
|
||||||
|
Warn("Enc file OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptUsage(fs *flag.FlagSet) {
|
||||||
|
fmt.Printf(`%s encrypt: Encrypt a file to one or more recipients.
|
||||||
|
|
||||||
|
Usage: %s encrypt [options] to [to ...] infile|-
|
||||||
|
|
||||||
|
Where TO is the public key of the recipient; it can be one of:
|
||||||
|
|
||||||
|
- a file referring to an SSH or sigtool public key.
|
||||||
|
- string of the form 'a@b' - in which case the user's default
|
||||||
|
ssh/authorized_keys is consulted to find the comment matching
|
||||||
|
'a@b' - in which case the user's ssh authorized_keys file is consulted to
|
||||||
|
find the comment matching the string.
|
||||||
|
|
||||||
|
INFILE is an input file to be encrypted. If the input file is '-' then %s
|
||||||
|
reads from STDIN. Unless '-o' is used, %s writes the encrypted output to STDOUT.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`, Z, Z, Z, Z)
|
||||||
|
|
||||||
|
fs.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptUsage(fs *flag.FlagSet) {
|
||||||
|
fmt.Printf(`%s decrypt: Decrypt a file.
|
||||||
|
|
||||||
|
Usage: %s decrypt [options] key [infile]
|
||||||
|
|
||||||
|
Where KEY is the private key to be used for decryption and INFILE is
|
||||||
|
the encrypted input file. If INFILE is not provided, %s reads
|
||||||
|
from STDIN. Unless '-o' is used, %s writes the decrypted output to STDOUT.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`, Z, Z, Z, Z)
|
||||||
|
|
||||||
|
fs.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustOpen(fn string, flag int) *os.File {
|
||||||
|
fdk, err := os.OpenFile(fn, flag, 0600)
|
||||||
|
if err != nil {
|
||||||
|
Die("can't open file %s: %s", fn, err)
|
||||||
|
}
|
||||||
|
return fdk
|
||||||
|
}
|
55
src/die.go
Normal file
55
src/die.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// die.go -- die() and warn()
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var atExit []func()
|
||||||
|
|
||||||
|
// Die prints an error message to stderr
|
||||||
|
// and exits the program after calling all the registered
|
||||||
|
// at-exit functions.
|
||||||
|
func Die(f string, v ...interface{}) {
|
||||||
|
Warn(f, v...)
|
||||||
|
Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn prints an error message to stderr
|
||||||
|
func Warn(f string, v ...interface{}) {
|
||||||
|
z := fmt.Sprintf("%s: %s", os.Args[0], f)
|
||||||
|
s := fmt.Sprintf(z, v...)
|
||||||
|
if n := len(s); s[n-1] != '\n' {
|
||||||
|
s += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stderr.WriteString(s)
|
||||||
|
os.Stderr.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AtExit registers a function to be called before the program exits.
|
||||||
|
func AtExit(f func()) {
|
||||||
|
atExit = append(atExit, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit invokes the registered atexit handlers and exits with the
|
||||||
|
// given code.
|
||||||
|
func Exit(v int) {
|
||||||
|
for _, f := range atExit {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
os.Exit(v)
|
||||||
|
}
|
100
src/gen.go
Normal file
100
src/gen.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
// gen.go -- generate keys
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/sign"
|
||||||
|
"github.com/opencoff/go-utils"
|
||||||
|
flag "github.com/opencoff/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run the generate command
|
||||||
|
func gen(args []string) {
|
||||||
|
var nopw, help, force bool
|
||||||
|
var comment string
|
||||||
|
var envpw string
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("generate", flag.ExitOnError)
|
||||||
|
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
||||||
|
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key")
|
||||||
|
fs.StringVarP(&comment, "comment", "c", "", "Use `C` as the text comment for the keys")
|
||||||
|
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||||
|
fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists")
|
||||||
|
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if help {
|
||||||
|
fs.SetOutput(os.Stdout)
|
||||||
|
fmt.Printf(`%s generate|gen|g [options] file-prefix
|
||||||
|
|
||||||
|
Generate a new Ed25519 public+private key pair and write public key to
|
||||||
|
FILE-PREFIX.pub and private key to FILE-PREFIX.key.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`, Z)
|
||||||
|
fs.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = fs.Args()
|
||||||
|
if len(args) < 1 {
|
||||||
|
Die("Insufficient arguments to 'generate'. Try '%s generate -h' ..", Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
bn := args[0]
|
||||||
|
|
||||||
|
pkn := fmt.Sprintf("%s.pub", path.Clean(bn))
|
||||||
|
skn := fmt.Sprintf("%s.key", path.Clean(bn))
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
if exists(pkn) || exists(skn) {
|
||||||
|
Die("Public/Private key files (%s, %s) exist. won't overwrite!", skn, pkn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var pw []byte
|
||||||
|
|
||||||
|
if !nopw {
|
||||||
|
var pws string
|
||||||
|
if len(envpw) > 0 {
|
||||||
|
pws = os.Getenv(envpw)
|
||||||
|
} else {
|
||||||
|
pws, err = utils.Askpass("Enter passphrase for private key", true)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pw = []byte(pws)
|
||||||
|
}
|
||||||
|
|
||||||
|
sk, err := sign.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = sk.Serialize(skn, comment, force, pw); err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := sk.PublicKey()
|
||||||
|
if err = pk.Serialize(pkn, comment, force); err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
116
src/sign.go
Normal file
116
src/sign.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// sign.go -- 'sign' command implementation
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/sign"
|
||||||
|
"github.com/opencoff/go-fio"
|
||||||
|
"github.com/opencoff/go-utils"
|
||||||
|
flag "github.com/opencoff/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run the 'sign' command.
|
||||||
|
func signify(args []string) {
|
||||||
|
var nopw, help, force bool
|
||||||
|
var output string
|
||||||
|
var envpw string
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("sign", flag.ExitOnError)
|
||||||
|
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
||||||
|
fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key")
|
||||||
|
fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
|
||||||
|
fs.StringVarP(&output, "output", "o", "", "Write signature to file `F`")
|
||||||
|
fs.BoolVarP(&force, "overwrite", "", false, "Overwrite previous signature file if it exists")
|
||||||
|
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if help {
|
||||||
|
fs.SetOutput(os.Stdout)
|
||||||
|
fmt.Printf(`%s sign|s [options] privkey file
|
||||||
|
|
||||||
|
Sign FILE with a Ed25519 private key PRIVKEY and write signature to FILE.sig
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`, Z)
|
||||||
|
fs.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = fs.Args()
|
||||||
|
if len(args) < 2 {
|
||||||
|
Die("Insufficient arguments to 'sign'. Try '%s sign -h' ..", Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
kn := args[0]
|
||||||
|
fn := args[1]
|
||||||
|
outf := fmt.Sprintf("%s.sig", fn)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(output) > 0 {
|
||||||
|
outf = output
|
||||||
|
}
|
||||||
|
|
||||||
|
var fd io.WriteCloser = os.Stdout
|
||||||
|
|
||||||
|
if outf != "-" {
|
||||||
|
var opts uint32
|
||||||
|
if force {
|
||||||
|
opts |= fio.OPT_OVERWRITE
|
||||||
|
}
|
||||||
|
sf, err := fio.NewSafeFile(outf, opts, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
Die("can't create sig file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we unlink and remove temp on any error
|
||||||
|
AtExit(sf.Abort)
|
||||||
|
defer sf.Abort()
|
||||||
|
fd = sf
|
||||||
|
}
|
||||||
|
|
||||||
|
sk, err := sign.ReadPrivateKey(kn, func() ([]byte, error) {
|
||||||
|
if nopw {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pws string
|
||||||
|
if len(envpw) > 0 {
|
||||||
|
pws = os.Getenv(envpw)
|
||||||
|
} else {
|
||||||
|
pws, err = utils.Askpass("Enter passphrase for private key", false)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(pws), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := sk.SignFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigbytes, err := sig.MarshalBinary(fmt.Sprintf("input=%s", fn))
|
||||||
|
fd.Write(sigbytes)
|
||||||
|
fd.Close()
|
||||||
|
}
|
129
src/sigtool.go
Normal file
129
src/sigtool.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// sigtool.go -- Tool to generate, manage Ed25519 keys and
|
||||||
|
// signatures.
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/sign"
|
||||||
|
"github.com/opencoff/go-utils"
|
||||||
|
flag "github.com/opencoff/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Z string = path.Base(os.Args[0])
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var ver, help, debug bool
|
||||||
|
|
||||||
|
mf := flag.NewFlagSet(Z, flag.ExitOnError)
|
||||||
|
mf.SetInterspersed(false)
|
||||||
|
mf.BoolVarP(&ver, "version", "v", false, "Show version info and exit")
|
||||||
|
mf.BoolVarP(&help, "help", "h", false, "Show help info exit")
|
||||||
|
mf.BoolVarP(&debug, "debug", "", false, "Enable debug mode")
|
||||||
|
mf.Parse(os.Args[1:])
|
||||||
|
|
||||||
|
if ver {
|
||||||
|
fmt.Printf("%s - %s [%s]\n", Z, ProductVersion, RepoVersion)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if help {
|
||||||
|
usage(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := mf.Args()
|
||||||
|
if len(args) < 1 {
|
||||||
|
Die("Insufficient arguments. Try '%s -h'", Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds := map[string]func(args []string){
|
||||||
|
"generate": gen,
|
||||||
|
"sign": signify,
|
||||||
|
"verify": verify,
|
||||||
|
"encrypt": encrypt,
|
||||||
|
"decrypt": decrypt,
|
||||||
|
|
||||||
|
"help": func(_ []string) {
|
||||||
|
usage(0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
words := make([]string, 0, len(cmds))
|
||||||
|
for k := range cmds {
|
||||||
|
words = append(words, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
ab := utils.Abbrev(words)
|
||||||
|
canon, ok := ab[strings.ToLower(args[0])]
|
||||||
|
if !ok {
|
||||||
|
Die("Unknown command %s", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmds[canon]
|
||||||
|
if cmd == nil {
|
||||||
|
Die("can't map command %s", canon)
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
sign.Debug(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd(args[1:])
|
||||||
|
|
||||||
|
// always call Exit so that at-exit handlers are called.
|
||||||
|
Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature on a given file
|
||||||
|
|
||||||
|
func usage(c int) {
|
||||||
|
x := fmt.Sprintf(`%s is a tool to generate, sign and verify files with Ed25519 signatures.
|
||||||
|
|
||||||
|
Usage: %s [global-options] command [options] arg [args..]
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
-h, --help Show help and exit
|
||||||
|
-v, --version Show version info and exit
|
||||||
|
--debug Enable debug (DANGEROUS)
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
generate, g Generate a new Ed25519 keypair
|
||||||
|
sign, s Sign a file with a private key
|
||||||
|
verify, v Verify a signature against a file and a public key
|
||||||
|
encrypt, e Encrypt an input file to one or more recipients
|
||||||
|
decrypt, d Decrypt a file with a private key
|
||||||
|
`, Z, Z)
|
||||||
|
|
||||||
|
os.Stdout.Write([]byte(x))
|
||||||
|
os.Exit(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if $bn.key or $bn.pub exist; false otherwise
|
||||||
|
func exists(nm string) bool {
|
||||||
|
if _, err := os.Stat(nm); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be filled in by "build"
|
||||||
|
var RepoVersion string = "UNDEFINED"
|
||||||
|
var ProductVersion string = "UNDEFINED"
|
||||||
|
|
||||||
|
// vim: ft=go:sw=8:ts=8:noexpandtab:tw=98:
|
96
src/verify.go
Normal file
96
src/verify.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// verify.go -- Verify signatures
|
||||||
|
//
|
||||||
|
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||||
|
//
|
||||||
|
// Licensing Terms: GPLv2
|
||||||
|
//
|
||||||
|
// If you need a commercial license for this work, please contact
|
||||||
|
// the author.
|
||||||
|
//
|
||||||
|
// This software does not come with any express or implied
|
||||||
|
// warranty; it is provided "as is". No claim is made to its
|
||||||
|
// suitability for any purpose.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.rgst.io/homelab/sigtool/v3/sign"
|
||||||
|
flag "github.com/opencoff/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func verify(args []string) {
|
||||||
|
var help, quiet bool
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("verify", flag.ExitOnError)
|
||||||
|
fs.BoolVarP(&help, "help", "h", false, "Show this help and exit")
|
||||||
|
fs.BoolVarP(&quiet, "quiet", "q", false, "Don't show any output; exit with status code only")
|
||||||
|
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
if help {
|
||||||
|
fs.SetOutput(os.Stdout)
|
||||||
|
fmt.Printf(`%s verify|v [options] pubkey sig file
|
||||||
|
|
||||||
|
Verify an Ed25519 signature in SIG of FILE using a public key PUBKEY.
|
||||||
|
The pubkey can be one of:
|
||||||
|
- a file: either OpenSSH ed25519 pubkey or a sigtool pubkey
|
||||||
|
- a string: the raw OpenSSH or sigtool pubkey
|
||||||
|
|
||||||
|
%s will first parse it as a string before trying to parse it as a file.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
`, Z, Z)
|
||||||
|
fs.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = fs.Args()
|
||||||
|
if len(args) < 3 {
|
||||||
|
Die("Insufficient arguments to 'verify'. Try '%s verify -h' ..", Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pn := args[0]
|
||||||
|
sn := args[1]
|
||||||
|
fn := args[2]
|
||||||
|
|
||||||
|
// We first try to read the public key as a base64/openssh string
|
||||||
|
pk, err := sign.MakePublicKeyFromString(pn)
|
||||||
|
if err != nil {
|
||||||
|
pk, err = sign.ReadPublicKey(pn)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := sign.ReadSignature(sn)
|
||||||
|
if err != nil {
|
||||||
|
Die("Can't read signature '%s': %s", sn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sig.IsPKMatch(pk) {
|
||||||
|
Die("Wrong public key '%s' for verifying '%s'", pn, sn)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := pk.VerifyFile(fn, sig)
|
||||||
|
if err != nil {
|
||||||
|
Die("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := 0
|
||||||
|
if !ok {
|
||||||
|
exit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quiet {
|
||||||
|
if ok {
|
||||||
|
fmt.Printf("%s: Signature %s verified\n", fn, sn)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: Signature %s verification failure\n", fn, sn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(exit)
|
||||||
|
}
|
124
tests.sh
Executable file
124
tests.sh
Executable file
|
@ -0,0 +1,124 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
# simple round-trip tests to verify the tool
|
||||||
|
# Usage:
|
||||||
|
# $0 [bin=/path/to/sigtool] [tmpdir=/path/to/workdir]
|
||||||
|
|
||||||
|
Z=`basename $0`
|
||||||
|
die() {
|
||||||
|
echo "$Z: $@" 1>&2
|
||||||
|
echo "$Z: Test output in $tmpdir .." 1>&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# cmd line args processing
|
||||||
|
for a in $*; do
|
||||||
|
key=${a%=*}
|
||||||
|
val=${a#*=}
|
||||||
|
case $key in
|
||||||
|
bin)
|
||||||
|
bin=$val
|
||||||
|
;;
|
||||||
|
|
||||||
|
tmpdir)
|
||||||
|
tmpdir=$val
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Ignoring $key .."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$bin" ]; then
|
||||||
|
arch=`./build --print-arch`
|
||||||
|
bin=./bin/$arch/sigtool
|
||||||
|
|
||||||
|
[ -x $bin ] || ./build || die "can't find & build sigtool"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$tmpdir" ] && tmpdir=/tmp/sigtool$$
|
||||||
|
|
||||||
|
mkdir -p $tmpdir || die "can't mkdir $tmpdir"
|
||||||
|
|
||||||
|
# env name for reading the password
|
||||||
|
passenv=FOO
|
||||||
|
|
||||||
|
# this is the password for SKs
|
||||||
|
FOO=bar
|
||||||
|
|
||||||
|
|
||||||
|
#trap "rm -rf $tmpdir" EXIT
|
||||||
|
|
||||||
|
bn=$tmpdir/foo
|
||||||
|
sig=$tmpdir/$Z.sig
|
||||||
|
pk=$bn.pub
|
||||||
|
sk=$bn.key
|
||||||
|
bn2=$tmpdir/bar
|
||||||
|
pk2=$bn2.pub
|
||||||
|
sk2=$bn2.key
|
||||||
|
|
||||||
|
encout=$tmpdir/$Z.enc
|
||||||
|
decout=$tmpdir/$Z.dec
|
||||||
|
|
||||||
|
# exit on any failure
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Now try with ssh ed25519 keys
|
||||||
|
keygen=`which ssh-keygen`
|
||||||
|
[ -z "$keygen" ] && die "can't find ssh-keygen"
|
||||||
|
|
||||||
|
ssk1=$tmpdir/ssk1
|
||||||
|
spk1=$ssk1.pub
|
||||||
|
|
||||||
|
ssk2=$tmpdir/ssk2
|
||||||
|
spk2=$ssk2.pub
|
||||||
|
|
||||||
|
# first generate two ssh keys
|
||||||
|
$keygen -q -C 'ssk1@foo' -t ed25519 -f $ssk1 -N ""
|
||||||
|
$keygen -q -C 'ssk2@foo' -t ed25519 -f $ssk2 -N ""
|
||||||
|
|
||||||
|
# extract the pk string
|
||||||
|
spk1_str=$(cat $spk1 | awk '{ print $2 }')
|
||||||
|
|
||||||
|
$bin s --no-password $ssk1 -o $sig $0 || die "can't sign with $ssk1"
|
||||||
|
$bin v -q $spk1 $sig $0 || die "can't verify with $spk2"
|
||||||
|
$bin v -q $spk1_str $sig $0 || die "can't verify with $spk2_str"
|
||||||
|
|
||||||
|
$bin e --no-password -o $encout $spk2 $0 || die "can't encrypt to $spk2 with $ssk1"
|
||||||
|
$bin d --no-password -o $decout $ssk2 $encout || die "can't decrypt with $ssk2"
|
||||||
|
|
||||||
|
# cleanup state
|
||||||
|
rm -f $sig $encout $decout
|
||||||
|
|
||||||
|
# generate keys
|
||||||
|
$bin g -E FOO $bn || die "can't gen keypair $pk, $sk"
|
||||||
|
$bin g -E FOO $bn 2>/dev/null && die "overwrote prev keypair"
|
||||||
|
$bin g -E FOO --overwrite $bn || die "can't force gen keypair $pk, $sk"
|
||||||
|
$bin g -E FOO $bn2 || die "can't force gen keypair $pk2, $sk2"
|
||||||
|
|
||||||
|
# extract pk string
|
||||||
|
pk_str=$(cat $pk | grep 'pk:' | sed -e 's/^pk: //g')
|
||||||
|
pk2_str=$(cat $pk2 | grep 'pk:' | sed -e 's/^pk: //g')
|
||||||
|
|
||||||
|
# sign and verify
|
||||||
|
$bin s -E FOO $sk $0 -o $sig || die "can't sign $0"
|
||||||
|
$bin v -q $pk $sig $0 || die "can't verify signature of $0"
|
||||||
|
$bin v -q $pk_str $sig $0 || die "can't verify signature of $0"
|
||||||
|
$bin v -q $pk2 $sig $0 2>/dev/null && die "bad verification with wrong $pk2"
|
||||||
|
$bin v -q $pk2_str $sig $0 2>/dev/null && die "bad verification with wrong $pk2"
|
||||||
|
|
||||||
|
# encrypt/decrypt
|
||||||
|
$bin e -E FOO -o $encout $pk2 $0 || die "can't encrypt to $pk2"
|
||||||
|
$bin d -E FOO -o $decout $sk2 $encout || die "can't decrypt with $sk2"
|
||||||
|
cmp -s $decout $0 || die "decrypted file mismatch with $0"
|
||||||
|
|
||||||
|
# now with sender verification
|
||||||
|
$bin e -E FOO --overwrite -o $encout -s $sk $pk2 $0 || die "can't sender-encrypt to $pk2"
|
||||||
|
$bin d -E FOO --overwrite -o $decout -v $pk $sk2 $encout || die "can't decrypt with $sk2"
|
||||||
|
cmp -s $decout $0 || die "decrypted file mismatch with $0"
|
||||||
|
|
||||||
|
# Only delete if everything worked
|
||||||
|
echo "$Z: All tests pass!"
|
||||||
|
rm -rf $tmpdir
|
||||||
|
|
||||||
|
# vim: tw=100 sw=4 ts=4 expandtab
|
Loading…
Add table
Reference in a new issue