Compare commits

...

31 commits

Author SHA1 Message Date
aa96303d43
chore(deps): update module golang.org/x/crypto to v0.36.0 (#5)
Reviewed-on: #5
Co-authored-by: renovate[bot] <renovate@rgst.io>
Co-committed-by: renovate[bot] <renovate@rgst.io>
2025-03-06 17:33:08 +00:00
8d3686eb14
chore: add mise fix yaml.v3 2025-02-27 21:07:47 -08:00
6b2e5526b2
Update module gopkg.in/yaml.v2 to v3 (#4)
Reviewed-on: #4
Co-authored-by: renovate[bot] <renovate@rgst.io>
Co-committed-by: renovate[bot] <renovate@rgst.io>
2025-02-28 05:05:32 +00:00
d128cfa7ce
Update module github.com/opencoff/go-fio to v0.5.14 (#2)
Reviewed-on: #2
Co-authored-by: renovate[bot] <renovate@rgst.io>
Co-committed-by: renovate[bot] <renovate@rgst.io>
2025-02-28 04:37:29 +00:00
3f6b9cd2af
Update module gopkg.in/yaml.v2 to v3 (#3)
Reviewed-on: #3
Co-authored-by: renovate[bot] <renovate@rgst.io>
Co-committed-by: renovate[bot] <renovate@rgst.io>
2025-02-28 04:37:18 +00:00
6a16789886 Configure Renovate (#1)
Co-authored-by: renovate[bot] <renovate@rgst.io>
Co-committed-by: renovate[bot] <renovate@rgst.io>
2025-02-28 04:03:09 +00:00
1823aaa5e0
add back from bytes 2025-02-25 11:28:39 -08:00
810aa02bdd
update import path and deps 2025-02-25 11:08:20 -08:00
Sudhi Herle
1786734c0a Updated dependencies; updated ABI compat 2024-12-18 19:13:24 -08:00
Sudhi Herle
d47a4596ef Remove build timestamp 2024-08-30 09:37:59 -07:00
Sudhi Herle
fc94d7cd7d Updated build script to make reproducible builds
* fixed mk-rel.sh; put all release artifiacts in releases/ dir
* updated vt-proto dependencies
2024-08-29 09:42:16 -07:00
Sudhi Herle
2e6d92c753 Updated build script to parse VCS version# correctly 2024-04-09 14:33:38 -07:00
Sudhi Herle
5c6152b4ed Updated to latest safefile lib - fixed up caller sites. 2024-03-10 12:48:16 -07:00
Sudhi Herle
e3053142f5 Teach sigtool verify to use public key as a command line string (in lieu of a file).
- Reorganized the code a bit and split each of sigtool's
  commands into a separate file.
- Added extra tests to validate verify's new capabilities
- Updated README
2024-01-13 10:34:24 -08:00
Sudhi Herle
d49f732c71 Updated go-mmap; added release script 2024-01-12 13:37:20 -08:00
Sudhi Herle
15053202a1 Updated sigtool to use portable mmap lib 2024-01-07 11:55:21 -08:00
Sudhi Herle
c5400a6b18 Updated x/crypto (security fix) 2023-12-19 10:34:46 -08:00
Sudhi Herle
eae20abd24 Updated to vtproto inlieu of gogo-slick; verified that protobuf enc/dec
are same across both.

* updated 'build' to use new vtproto toolchain;
* updated go.mod to use vtproto
2023-11-23 21:06:30 -08:00
Sudhi Herle
c4f79962c9 Updated dependencies; use SafeFile from go-utils. Teach tests.sh to use
a user supplied binary & tmpdir
2023-11-12 12:21:31 -08:00
Sudhi Herle
a538ac8e5c Don't try to be clever with GoRoot; use what's in the path. 2023-09-10 12:28:49 -07:00
Sudhi Herle
c95515af0e Update dependencies; move go version to 1.20
* update build script to use a diff go-root
* update tests.sh to use diff go-root
* move pflag to opencoff/pflag
* use common lib to parse string size..
2023-03-09 17:20:50 +00:00
Sudhi Herle
bbd7afd496 Updated README 2022-11-15 09:16:06 -08:00
Sudhi Herle
f343d45a8e Add sender authenticated message integrity; fixup KDF
- use HKDF for producing keys, nonces
- add running hmac of plaintext; sender-sign the hmac as trailer
- use header checksum as "salt" for data encryption keys, nonces
- generate explicit nonce for wrapping root keys for each recipient
  (previous impl had brittleness)
2022-11-13 11:53:00 -08:00
Sudhi Herle
a428db8feb Added ssh tests 2022-06-05 13:47:07 -07:00
Sudhi Herle
0ddf48c92f Minor cleanups and one bugfix:
* bugfix: use os.IsNotExist() instead of comparing errors for equality;
  this fixes incorrect handling of missing authorized_keys file.
* move die() and warn() into die.go - and make them public functions.
* teach die.go to also provide atexit() like functionality
* teach all callers of sign.SafeFile{} to use AtExit() to delete
  temporary artifacts
* symbol renaming: die->Die, warn->Warn.
2022-05-12 16:53:27 -07:00
Sudhi Herle
42bbe5ddeb Refactored the core signing & encryption library, teach sigtool to use safe I/O.
* Added new SafeFile (io.WriteCloser) class + methods to atomically write a file.
* Teach core lib to use SafeFile for all file I/O
* Teach sigtool to use SafeFile for all file I/O
* Cleaned up the public interfaces of sign/ to be more coherent:
   - with uniform APIs for marshaling, unmarshaling, serialization.
   - removed KeyPair class/interface and stick to PrivateKey as the primary
     interface.
* collected common rand utility functions into rand.go
* Teach sigtool to NOT overwrite existing output files (keys, signatures etc.)
* Teach sigtool to use a new --overwrite option for every command that creates
  files (generate, sign, encrypt, decrypt)
* encrypt/decrypt will try to use the input file mode/perm where possible
  (unless input is stdin).
* Added more tests
2022-04-29 21:36:39 +05:30
Sudhi Herle
f180079586 Updated README with dependencies 2022-04-27 08:59:18 +05:30
Sudhi Herle
445c13ca6f Closes #5 and #6
- removed spurious check in decrypt() against blocksize
- added additional tests for small sized blocks and inputs smaller than
  the blocksize.
- updated README to capture dependencies (protobuf tools)
2022-04-27 08:54:44 +05:30
Sudhi Herle
bce89dacb0 Updated to go1.18; minor code cleanups; updated dependencies 2022-03-20 20:15:15 -07:00
Sudhi Herle
460f1cf703
Merge pull request #4 from uli-heller/ssh-pubkey
Fixed handling of ssh pubkey files; use regexp to split
2021-11-19 19:57:55 -08:00
Uli Heller
43a9f38592 Fixed handling of ssh pubkey files 2021-11-19 13:10:30 +01:00
31 changed files with 2857 additions and 2014 deletions

5
.gitignore vendored
View file

@ -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
View file

@ -0,0 +1,2 @@
[tools]
golang = "1.24"

View file

@ -26,9 +26,21 @@ The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys
files to any recipient identified by their comment in `~/.ssh/authorized_keys`. 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.13 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
@ -89,6 +101,10 @@ 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
You can also pass a public key as a string (instead of a file):
sigtool verify iF84Dymq/bAEnUMK6DRIHWAQDRD8FwDDDfsgFfzdjWM= archive.sig archive.tar.gz
Note that signing and verifying can also work with OpenSSH ed25519 Note that signing and verifying can also work with OpenSSH ed25519
keys. keys.
@ -140,19 +156,32 @@ recipient can decrypt using their private key.
### How is the file encryption done? ### How is the file encryption done?
The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses
a random 32-byte AES-256 key. This key is mixed in with the header checksum a random 32-byte AES-256 key. This root key is expanded via
as a safeguard to protect the header against accidental or malicious corruption. 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 input is broken into chunks and each chunk is individually AEAD encrypted.
The default chunk size is 4MB (4 * 1048576 bytes). Each chunk generates The default chunk size is 4MB (4 * 1048576 bytes). Each chunk generates
its own nonce from a global salt. The nonce is calculated as follows: 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
- v1: SHA256 of the salt, the chunk length and the block number. "AD" of "AEAD").
- v2: Last 8 bytes of a 32-byte salt is the big-endian encoding of
the chunk-length and block number
The last block has its most-signficant-bit set to 1 to denote EOF. Thus, the The last block has its most-signficant-bit set to 1 to denote EOF. Thus, the
maximum chunk size is set to 1GB. 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? ### What is the public-key cryptography in sigtool?
`sigtool` uses ephemeral Curve25519 keys to generate shared secrets `sigtool` uses ephemeral Curve25519 keys to generate shared secrets
between pairs of sender & one or more recipients. This pairwise shared between pairs of sender & one or more recipients. This pairwise shared
@ -161,7 +190,7 @@ data-encryption key in AEAD mode. Thus, each recipient has their own
individual encrypted key blob - that **only** they can decrypt. individual encrypted key blob - that **only** they can decrypt.
If the sender authenticates the encryption by providing their secret If the sender authenticates the encryption by providing their secret
key, the data-encryption key is signed via Ed25519 and the signature key, the encryption key material is signed via Ed25519 and the signature
is encrypted (using the data-encryption key) and stored in the is encrypted (using the data-encryption key) and stored in the
header. If the sender opts to not authenticate, a "signature" of all header. If the sender opts to not authenticate, a "signature" of all
zeroes is encrypted instead. zeroes is encrypted instead.
@ -191,7 +220,7 @@ described as a protobuf file (sign/hdr.proto):
uint32 chunk_size = 1; uint32 chunk_size = 1;
bytes salt = 2; bytes salt = 2;
bytes pk = 3; // sender's ephemeral curve PK bytes pk = 3; // sender's ephemeral curve PK
bytes sender_sig = 4; // ed25519 signature of the key bytes sender = 4; // ed25519 signature of key material
repeated wrapped_key keys = 5; repeated wrapped_key keys = 5;
} }
@ -201,6 +230,7 @@ described as a protobuf file (sign/hdr.proto):
*/ */
message wrapped_key { message wrapped_key {
bytes d_key = 1; bytes d_key = 1;
bytes nonce = 2;
} }
``` ```
@ -221,8 +251,8 @@ decryption.
### How is the private key protected? ### How is the private key protected?
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key 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 derived from the user's pass-phrase. The user pass phrase is expanded via
SHA256; this expanded pass phrase is fed to `scrypt()` to SHA256; this expanded pass phrase is fed to `scrypt()` to
generate a key-encryption-key. In pseudo code, this operation looks generate a key-encryption-key. In pseudo code, this operation looks
like below: like below:
@ -251,6 +281,9 @@ etc.
* `src/ssh.go` contains code to parse SSH Ed25519 key files * `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 * `src/stream.go` contains code that provides an `io.Reader` and `io.WriteCloser` interface
for encryption and decryption. 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.
@ -263,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

257
build
View file

@ -4,7 +4,7 @@
# #
# - it tacks on a version number for use by the individual tools # - it tacks on a version number for use by the individual tools
# - it supports git and mercurial version# # - it supports git and mercurial version#
# #
# NB: # NB:
# o the attempt at decoding dirty repo state for mercurial is # o the attempt at decoding dirty repo state for mercurial is
# borked. It doesn't know about untracked files # borked. It doesn't know about untracked files
@ -13,12 +13,13 @@
# #
# License: GPLv2 # License: GPLv2
# #
Progs=".:sigtool" Progs="src:sigtool"
# Relative path to protobuf sources # Relative path to protobuf sources
# e.g. src/foo/a.proto # e.g. src/foo/a.proto
Protobufs="internal/pb/hdr.proto" Protobufs="internal/pb/hdr.proto"
#set -x
# -- DO NOT CHANGE ANYTHING AFTER THIS -- # -- DO NOT CHANGE ANYTHING AFTER THIS --
@ -28,10 +29,10 @@ PWD=`pwd`
Static=0 Static=0
Dryrun=0 Dryrun=0
Prodver="" Prodver=""
Repover=""
Verbose=0 Verbose=0
Go=`which go`
hostos=$(go env GOHOSTOS) || exit 1 Bindir=$PWD/bin
hostcpu=$(go env GOHOSTARCH) || exit 1
die() { die() {
echo "$Z: $@" 1>&2 echo "$Z: $@" 1>&2
@ -49,47 +50,56 @@ case $BASH_VERSION in
;; ;;
esac esac
getvcs_version() {
local rev=
local prodv=
local git=`which git`
local hg=`which hg`
# build a tool that runs on the host - if needed. if [ -n "$git" ]; then
hosttool() { local xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1
local tool=$1 rev="git:$xrev"
local bindir=$2 prodv=$(git tag --list | sort -V | tail -1)
local src=$3 elif [ -n "$hg" ]; then
local xrev=$(hg id --id) || exit 1
local p=$(type -P $tool) local brev=${xrev%+}
if [ -n "$p" ]; then if [ "$brev" != "$xrev" ]; then
echo $p rev="hg:${brev}-dirty"
return 0 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 fi
# from here - we want this dir to find all build artifacts [ -n "$Prodver" ] && prodv=$Prodver
PATH=$PATH:$bindir
export PATH
p=$bindir/$tool echo "$rev $prodv"
if [ -x $p ]; then
echo $p
return 0
fi
# build it and stash it in the hostdir
echo "Building tool $tool from $src .."
$e go get -d $src || exit 1
$e go build -o $p $src || exit 1
echo $p
return 0 return 0
} }
read -r Repover Prodver <<< $(getvcs_version)
usage() { 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 cat <<EOF
$0 - A Go production build tool that adds git-repository information, $0 - A Go production build tool that adds git-repository information,
product version, build-timestamp etc. It supports cross-compilation, product version, build-timestamp etc. It supports cross-compilation,
static linking and generating protobuf output. static linking and generating protobuf output.
If needed, it uses the gogo-slick protobuf compiler [github.com/gogo/protobuf].
Build output is in bin/\$OS-\$CPU for a given OS, CPU combination. Build output is in bin/\$OS-\$CPU for a given OS, CPU combination.
Usage: $0 Usage: $0
@ -98,13 +108,14 @@ Usage: $0
Where OS-ARCH denotes one of the valid OS, ARCH combinations supported by 'go'. Where OS-ARCH denotes one of the valid OS, ARCH combinations supported by 'go'.
And, PROGS is one or more go programs. And, PROGS is one or more go programs.
With no arguments, $0 builds: $Progs (source in ./src/) With no arguments, $0 builds: $pstr
The repository's latest tag is used as the default version of the software being The repository's latest tag is used as the default version of the software being
built. built. The current repository version is $Repover.
Options: Options:
-h, --help Show this help message and quit -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] -s, --static Build a statically linked binary [False]
-V N, --version=N Use 'N' as the product version string [$Prodver] -V N, --version=N Use 'N' as the product version string [$Prodver]
-a X, --arch=X Cross compile for OS-CPU 'X' [$hostos-$hostcpu] -a X, --arch=X Cross compile for OS-CPU 'X' [$hostos-$hostcpu]
@ -112,14 +123,16 @@ Options:
-t, --test Run "go test" on modules named on the command line [False] -t, --test Run "go test" on modules named on the command line [False]
-v, --verbose Build verbosely (adds "-v" to go tooling) [False] -v, --verbose Build verbosely (adds "-v" to go tooling) [False]
--vet Run "go vet" on modules named on the command line [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] -x Run in debug/trace mode [False]
--print-arch Print the target architecture and exit
EOF EOF
exit 0 exit 0
} }
host=`uname|tr '[A-Z]' '[a-z]'` host=`uname|tr '[A-Z]' '[a-z]'`
export GO15VENDOREXPERIMENT=1
declare -A oses declare -A oses
declare -A cpus declare -A cpus
@ -180,13 +193,23 @@ do
--arch=*) --arch=*)
Arch=$ac_optarg Arch=$ac_optarg
;; ;;
-a|--arch) -a|--arch)
ac_prev=Arch ac_prev=Arch
;; ;;
-b|--bindir)
ac_prev=Bindir
;;
--bindir=*)
Bindir=$ac_optarg
;;
--version=*) --version=*)
Prodver=$ac_optarg Prodver=$ac_optarg
;; ;;
--test|-t) --test|-t)
Tool=test Tool=test
;; ;;
@ -195,9 +218,14 @@ do
Tool=vet Tool=vet
;; ;;
--mod)
Tool=mod
;;
-V|--version) -V|--version)
ac_prev=Prodver ac_prev=Prodver
;; ;;
-v|--verbose) -v|--verbose)
Verbose=1 Verbose=1
;; ;;
@ -214,6 +242,10 @@ do
set -x set -x
;; ;;
--go-root=*)
GoRoot=$ac_optarg
;;
--print-arch) --print-arch)
Printarch=1 Printarch=1
;; ;;
@ -229,11 +261,81 @@ do
;; ;;
esac esac
done done
[ $Dryrun -gt 0 ] && e=echo [ $Dryrun -gt 0 ] && e=echo
# let every error abort # let every error abort
set -e 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 # This fragment can't be in a function - since it exports several vars
if [ -n "$Arch" ]; then if [ -n "$Arch" ]; then
ox=${Arch%%-*} ox=${Arch%%-*}
@ -285,72 +387,25 @@ fi
# This is where build outputs go # This is where build outputs go
Bindir=$PWD/bin/$cross Outdir=$Bindir/$cross
Hostbindir=$PWD/bin/$hostos-$hostcpu Hostbindir=$Bindir/$hostos-$hostcpu
export PATH=$Hostbindir:$PATH
[ -d $Bindir ] || mkdir -p $Bindir [ -d $Outdir ] || mkdir -p $Outdir
[ -d $Hostbindir ] || mkdir -p $Hostbindir [ -d $Hostbindir ] || mkdir -p $Hostbindir
# Get git/hg version info for the build
if [ -d "./.hg" ]; then
xrev=$(hg id --id) || exit 1
brev=${xrev%+}
if [ "$brev" != "$xrev" ]; then
rev="hg:${brev}-dirty"
else
rev="hg:${brev}"
fi
if [ -z "$Prodver" ]; then
Prodver=$(hg log -r "branch(stable) and tag()" -T "{tags}\n" | tail -1)
fi
elif [ -d "./.git" ]; then
xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1
rev="git:$xrev"
if [ -z "$Prodver" ]; then
Prodver=$(git tag --list | tail -1)
fi
else
rev="UNKNOWN-VER"
echo "$0: Can't find version info" 1>&2
fi
# Do Protobufs if needed # Do Protobufs if needed
if [ -n "$Protobufs" ]; then if [ -n "$Protobufs" ]; then
set +e set +e
slick=$Hostbindir/protoc-gen-gogoslick buildproto $Protobufs
slicksrc=github.com/gogo/protobuf/protoc-gen-gogoslick
for pc in protoc protoc-c; do
pc=`which $pc`
[ -n "$pc" ] && break
done
[ -z "$pc" ] && die "Need 'protoc' for building .."
slick=$(hosttool protoc-gen-gogoslick $Hostbindir $slicksrc) || exit 1
#if [ ! -f $slick ]; then
# echo "Building $slick .."
# $e go build -o $slick github.com/gogo/protobuf/protoc-gen-gogoslick || exit 1
#i
export PATH=$PATH:$Hostbindir
for f in $Protobufs; do
dn=$(dirname $f)
bn=$(basename $f .proto)
of=$dn/${bn}.pb.go
if [ $f -nt $of ]; then
echo "Running $pc .."
$e $pc --gogoslick_out=. $f || exit 1
fi
done
set -e set -e
fi fi
repover="main.RepoVersion=$rev" # Get git/hg version info for the build
repover="main.RepoVersion=$Repover"
prodver="main.ProductVersion=$Prodver" prodver="main.ProductVersion=$Prodver"
date="main.Buildtime=`date -u '+%Y-%m-%dT%H:%M.%SZ'`" ldflags="-ldflags \"-X $repover -X $prodver $ldflags -buildid=\""
ldflags="-ldflags \"-X $repover -X $prodver -X $date $ldflags\""
vflag="" vflag=""
[ $Verbose -gt 0 ] && vflag="-v" [ $Verbose -gt 0 ] && vflag="-v"
@ -358,12 +413,17 @@ vflag=""
case $Tool in case $Tool in
test) test)
set -- $args set -- $args
$e go test $vflag "$@" $e $Go test $vflag "$@"
;; ;;
vet) vet)
set -- $args set -- $args
$e go vet $vflag "$@" $e $Go vet $vflag "$@"
;;
mod)
set -- $args
$e $Go mod $vflag "$@"
;; ;;
*) # Default is to build programs *) # Default is to build programs
@ -374,9 +434,11 @@ case $Tool in
all="$@" all="$@"
fi fi
echo "Building $Prodver ($rev), $cross $msg .." [ -z "$all" ] && die "No programs specified. Try '$Z --help'"
for p in $all; do echo "Building $Prodver ($Repover), $cross $msg .."
for p in $all; do
if echo $p | grep -q ':' ; then if echo $p | grep -q ':' ; then
out=${p##*:} out=${p##*:}
dir=${p%%:*} dir=${p%%:*}
@ -384,8 +446,15 @@ case $Tool in
out=$p out=$p
dir=$p dir=$p
fi fi
# Add .exe suffix to out if needed
if [ "$GOOS" = "windows" ]; then
base=${out%%.exe}
out="${base}.exe"
fi
echo " $dir: $out .. " echo " $dir: $out .. "
$e eval go build $vflag -o $Bindir/$out $isuffix "$ldflags" ./$dir || exit 1 $e eval $Go build $vflag -trimpath -o $Outdir/$out $isuffix "$ldflags" ./$dir || exit 1
done done
;; ;;
esac esac

27
go.mod
View file

@ -1,14 +1,25 @@
module github.com/opencoff/sigtool module git.rgst.io/homelab/sigtool/v3
go 1.17 go 1.24.0
require ( require (
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a
github.com/gogo/protobuf v1.3.2 github.com/opencoff/go-fio v0.5.14
github.com/opencoff/go-utils v0.4.1 github.com/opencoff/go-mmap v0.1.5
github.com/opencoff/pflag v0.5.0 github.com/opencoff/go-utils v1.0.2
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 github.com/opencoff/pflag v1.0.7
gopkg.in/yaml.v2 v2.2.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 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect 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

73
go.sum
View file

@ -1,44 +1,39 @@
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/opencoff/go-fio v0.5.14 h1:PGi4XLLO4RSuc3m5exY0G2vweov6w3UThhScehBfM8c=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/opencoff/go-fio v0.5.14/go.mod h1:hoSySYpavRnfQUsxzUgadk31kYiNQhMDvA2MObsXKf8=
github.com/opencoff/go-utils v0.4.1 h1:Ke4Q1Tl2GKMI+dwleuPNHH713ngRiNMOFIkymncHqXg= github.com/opencoff/go-mmap v0.1.5 h1:RKPtevC4mOW5bi9skBPPo4nFTIH4lVWAL20Tff+FjLg=
github.com/opencoff/go-utils v0.4.1/go.mod h1:c+7QUAiCCHcNH6OGvsZ0fviG7cgse8Y3ucg+xy7sGXM= github.com/opencoff/go-mmap v0.1.5/go.mod h1:y/6Jk/tDUc00k3oSQpiJX++20Nw7xFSlc5kLkhGnRXw=
github.com/opencoff/pflag v0.5.0 h1:kK3cSTlGj0fHby/PoFzHkf+Jx3PdiACJwzYDWEWlEKQ= github.com/opencoff/go-utils v1.0.2 h1:BANRL8ZxgHpuo8gQBAzT3M9Im3aNFhaWW28jhc86LNs=
github.com/opencoff/pflag v0.5.0/go.mod h1:mTLzGGUGda1Av3d34iAJlh0JIlRxmFZtmc6qoWPspK0= github.com/opencoff/go-utils v1.0.2/go.mod h1:eZkEVQVzNfuE8uGepyhscMsqcXq7liGbBHYYwgYaoy8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/opencoff/pflag v1.0.7 h1:o5cQIuX75bDcdJ6AXl68gzpA72a3CJ2MPStaMnEuwi4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/opencoff/pflag v1.0.7/go.mod h1:2bXtpAD/5h/2LarkbsRwiUxqnvB1nZBzn9Xjad1P41A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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 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.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,8 @@
syntax="proto3"; syntax="proto3";
//import "gogoproto/gogo.proto"
package pb; option go_package = "internal/pb";
//option (gogoproto.marshaler_all) = true;
//option (gogoproto.sizer_all) = true;
//option (gogoproto.unmarshaler_all) = true;
//option (gogoproto.goproto_getters_all) = false;
/* /*
* Every encrypted file starts with a header describing the * Every encrypted file starts with a header describing the
@ -16,11 +11,11 @@ package pb;
* protobuf format before writing to disk. * protobuf format before writing to disk.
*/ */
message header { message header {
uint32 chunk_size = 1; // encryption block size uint32 chunk_size = 1; // encryption block size
bytes salt = 2; // master salt (nonces are derived from this) bytes salt = 2; // master salt (nonces are derived from this)
bytes pk = 3; // ephemeral curve PK bytes pk = 3; // ephemeral curve PK
bytes sender_sign = 4; // signature block of sender bytes sender = 4; // sender signed artifacts
repeated wrapped_key keys = 5; // list of wrapped receiver blocks repeated wrapped_key keys = 5; // list of wrapped receiver blocks
} }
/* /*
@ -28,5 +23,7 @@ message header {
* key. WrappedKey describes such a wrapped key. * key. WrappedKey describes such a wrapped key.
*/ */
message wrapped_key { message wrapped_key {
bytes d_key = 1; // encrypted data key bytes d_key = 1; // encrypted data key
bytes nonce = 2; // nonce used for encryption
} }

View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View file

@ -1,10 +1,10 @@
[![GoDoc](https://godoc.org/github.com/opencoff/sigtool/sign?status.svg)](https://godoc.org/github.com/opencoff/sigtool/sign) [![GoDoc](https://godoc.org/git.rgst.io/homelab/sigtool/v3/sign?status.svg)](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

View file

@ -26,29 +26,36 @@
// //
// Variable Length Segment: // Variable Length Segment:
// - Protobuf encoded, per-recipient wrapped keys // - Protobuf encoded, per-recipient wrapped keys
// - Shasum: 32 bytes (SHA256 of full header)
// //
// The variable length segment consists of one or more // The variable length segment consists of one or more
// recipients, each with their wrapped keys. This is encoded as // recipients, each with their individually wrapped keys.
// a protobuf message. This protobuf encoded message immediately
// follows the fixed length header.
// //
// The input data is encrypted with an expanded random 32-byte key: // The input data is encrypted with an expanded random 32-byte key:
// - Prefix_string = "Encrypt Nonce" // - hkdf-sha512 of random key, salt, context
// - datakey = SHA256(Prefix_string || header_checksum || random_key) // - the hkdf process yields a data-encryption key, nonce and hmac key.
// - The header checksum is mixed in the above process to ensure we // - we use the header checksum as the 'salt' for HKDF; this ensures that
// catch any malicious modification of the header. // 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 // The input data is broken up into "chunks"; each no larger than
// maxChunkSize. The default block size is "chunkSize". Each block // maxChunkSize. The default block size is "chunkSize". Each block
// is AEAD encrypted: // is AEAD encrypted:
// AEAD nonce = header.salt || block# || block-size // AEAD nonce = header.nonce || block#
// AD of AEAD = chunk length+eof marker
// //
// The encrypted block (includes the AEAD tag) length is written // The encrypted block (includes the AEAD tag) length is written
// as a big-endian 4-byte prefix. The high-order bit of this length // as a big-endian 4-byte prefix. The high-order bit of this length
// field is set for the last-block (denoting EOF). // field is set for the last-block (denoting EOF).
// //
// The encrypted blocks use an opinionated nonce length of 32 (_AEADNonceLen).
package sign package sign
@ -57,50 +64,65 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/ed25519" "crypto/ed25519"
"crypto/hmac"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/subtle" "crypto/subtle"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"hash"
"io"
"os"
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"io"
"github.com/opencoff/sigtool/internal/pb" "git.rgst.io/homelab/sigtool/v3/internal/pb"
) )
// Encryption chunk size = 4MB // Encryption chunk size = 4MB
const ( const (
chunkSize uint32 = 4 * 1048576 // The latest version of the tool's output file format
_SigtoolVersion = 3
chunkSize uint32 = 4 * 1048576 // 4 MB
maxChunkSize uint32 = 1 << 30 maxChunkSize uint32 = 1 << 30
_EOF uint32 = 1 << 31 _EOF uint32 = 1 << 31
_Magic = "SigTool" _Magic = "SigTool"
_MagicLen = len(_Magic) _MagicLen = len(_Magic)
_SigtoolVersion = 2 _FixedHdrLen = _MagicLen + 1 + 4 // 1: version, 4: len of variable segment
_AEADNonceLen = 32
_FixedHdrLen = _MagicLen + 1 + 4
_WrapReceiverNonce = "Receiver Key Nonce" _AesKeySize = 32
_WrapSenderNonce = "Sender Sig Nonce" _AEADNonceSize = 12
_EncryptNonce = "Encrypt Nonce" _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 // Encryptor holds the encryption context
type Encryptor struct { type Encryptor struct {
pb.Header pb.Header
key []byte // file encryption key key []byte // root key
ae cipher.AEAD nonce []byte // nonce for the data encrypting cipher
buf []byte // I/O buf (chunk-sized)
ae cipher.AEAD
hmac hash.Hash
// ephemeral key // ephemeral key
encSK []byte encSK []byte
started bool // sender identity
sender *PrivateKey
hdrsum []byte auth bool // set if the sender idetity is sent
buf []byte started bool
stream bool stream bool
} }
// Create a new Encryption context for encrypting blocks of size 'blksize'. // Create a new Encryption context for encrypting blocks of size 'blksize'.
@ -123,42 +145,23 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
return nil, fmt.Errorf("encrypt: %w", err) return nil, fmt.Errorf("encrypt: %w", err)
} }
key := make([]byte, 32) key := randBuf(_AesKeySize)
salt := make([]byte, _AEADNonceLen) salt := randBuf(_SaltSize)
randRead(key)
randRead(salt)
// if sender has provided their identity to authenticate, we sign the data-enc key
// and encrypt the signature. At no point will we send the sender's identity.
var senderSig []byte
if sk != nil {
sig, err := sk.SignMessage(key, "")
if err != nil {
return nil, fmt.Errorf("encrypt: can't sign: %w", err)
}
senderSig = sig.Sig
} else {
var zero [ed25519.SignatureSize]byte
senderSig = zero[:]
}
wSig, err := wrapSenderSig(senderSig, key, salt)
if err != nil {
return nil, fmt.Errorf("encrypt: %w", err)
}
e := &Encryptor{ e := &Encryptor{
Header: pb.Header{ Header: pb.Header{
ChunkSize: blksz, ChunkSize: blksz,
Salt: salt, Salt: salt,
Pk: epk, Pk: epk,
SenderSign: wSig,
}, },
key: key, key: key,
encSK: esk, encSK: esk,
sender: sk,
}
if err = e.addSenderSig(sk); err != nil {
return nil, fmt.Errorf("encrypt: %w", err)
} }
return e, nil return e, nil
@ -225,8 +228,8 @@ func (e *Encryptor) start(wr io.Writer) error {
buffer := make([]byte, _FixedHdrLen+varSize+sha256.Size) buffer := make([]byte, _FixedHdrLen+varSize+sha256.Size)
fixHdr := buffer[:_FixedHdrLen] fixHdr := buffer[:_FixedHdrLen]
varHdr := buffer[_FixedHdrLen:] varHdr := buffer[_FixedHdrLen : _FixedHdrLen+varSize]
sumHdr := varHdr[varSize:] sumHdr := buffer[_FixedHdrLen+varSize:]
// Now assemble the fixed header // Now assemble the fixed header
copy(fixHdr[:], []byte(_Magic)) copy(fixHdr[:], []byte(_Magic))
@ -239,70 +242,117 @@ func (e *Encryptor) start(wr io.Writer) error {
return fmt.Errorf("encrypt: can't marshal header: %w", err) return fmt.Errorf("encrypt: can't marshal header: %w", err)
} }
// Now calculate checksum of everything
h := sha256.New() h := sha256.New()
h.Write(buffer[:_FixedHdrLen+varSize]) h.Write(buffer[:_FixedHdrLen+varSize])
h.Sum(sumHdr[:0]) cksum := h.Sum(sumHdr[:0])
// Finally write it out // 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) err = fullwrite(buffer, wr)
if err != nil { if err != nil {
return fmt.Errorf("encrypt: %w", err) return fmt.Errorf("encrypt: %w", err)
} }
// we mix the header checksum to create the encryption key e.hmac = hmac.New(sha256.New, hmackey)
h = sha256.New() e.buf = make([]byte, e.ChunkSize+4+uint32(e.ae.Overhead()))
h.Write([]byte(_EncryptNonce))
h.Write(e.key)
h.Write(sumHdr)
key := h.Sum(nil)
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
e.buf = make([]byte, e.ChunkSize+4+uint32(ae.Overhead()))
e.ae = ae
e.started = true 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 return nil
} }
// encrypt exactly _one_ block of data // encrypt exactly _one_ block of data
// The nonce for the block is: sha256(salt || chunkLen || block#) // The nonce is constructed from the salt, block# and block-size.
// This protects the output stream from re-ordering attacks and length // This protects the output stream from re-ordering attacks and length
// modification attacks. The encoded length & block number is used as // modification attacks. The encoded length & block number is used as
// additional data in the AEAD construction. // additional data in the AEAD construction.
func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error { func (e *Encryptor) encrypt(pt []byte, wr io.Writer, i uint32, eof bool) error {
var z uint32 = uint32(len(buf)) var z uint32 = uint32(len(pt))
var nbuf [_AEADNonceLen]byte var nonce [_AEADNonceSize]byte
// mark last block // mark last block
if eof { if eof {
z |= _EOF z |= _EOF
} }
b := e.buf[:8] copy(nonce[:], e.nonce)
binary.BigEndian.PutUint32(b[:4], z)
binary.BigEndian.PutUint32(b[4:], i)
nonce := makeNonceV2(nbuf[:], e.Salt, b) // now change the upper bytes to track the block#; we use the len+eof as AD
binary.BigEndian.PutUint32(nonce[:4], i)
cbuf := e.buf[4:] // put the encoded length+eof at the start of the output buf
c := e.ae.Seal(cbuf[:0], nonce, buf, b[:]) 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 // total number of bytes written
n := len(c) + 4 n := len(ct) + 4
err := fullwrite(e.buf[:n], wr) err := fullwrite(e.buf[:n], wr)
if err != nil { if err != nil {
return fmt.Errorf("encrypt: %w", err) 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 return nil
} }
@ -310,16 +360,18 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error
type Decryptor struct { type Decryptor struct {
pb.Header pb.Header
ae cipher.AEAD ae cipher.AEAD
rd io.Reader hmac hash.Hash
buf []byte
hdrsum []byte
// flag set to true if sender signed the key sender *PublicKey
auth bool
// Decrypted key rd io.Reader
key []byte 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 eof bool
stream bool stream bool
} }
@ -362,14 +414,18 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
return nil, fmt.Errorf("decrypt: error while reading header: %w", err) return nil, fmt.Errorf("decrypt: error while reading header: %w", err)
} }
// The checksum in the header
verify := varBuf[varSize:] verify := varBuf[varSize:]
// the checksum we calculated
var csum [sha256.Size]byte
h := sha256.New() h := sha256.New()
h.Write(b[:]) h.Write(b[:])
h.Write(varBuf[:varSize]) h.Write(varBuf[:varSize])
cksum := h.Sum(nil) cksum := h.Sum(csum[:0])
if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 { if subtle.ConstantTimeCompare(verify, cksum) == 0 {
return nil, ErrBadHeader return nil, ErrBadHeader
} }
@ -387,7 +443,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize) return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize)
} }
if len(d.Salt) != _AEADNonceLen { if len(d.Salt) != _SaltSize {
return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt)) return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt))
} }
@ -397,7 +453,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
// sanity check on the wrapped keys // sanity check on the wrapped keys
for i, w := range d.Keys { for i, w := range d.Keys {
if len(w.DKey) <= 32 { if len(w.DKey) <= _AesKeySize {
return nil, fmt.Errorf("decrypt: wrapped key %d: wrong-size encrypted key", i) return nil, fmt.Errorf("decrypt: wrapped key %d: wrong-size encrypted key", i)
} }
} }
@ -417,6 +473,8 @@ func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
return fmt.Errorf("decrypt: can't unwrap key %d: %w", i, err) return fmt.Errorf("decrypt: can't unwrap key %d: %w", i, err)
} }
if key != nil { if key != nil {
d.key = key
d.sender = senderPk
goto havekey goto havekey
} }
} }
@ -424,28 +482,37 @@ func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
return ErrBadKey return ErrBadKey
havekey: havekey:
if err := d.verifySender(key, sk, senderPk); err != nil { if err := d.verifySender(key, senderPk); err != nil {
return fmt.Errorf("decrypt: %w", err) return fmt.Errorf("decrypt: %w", err)
} }
d.key = key outbuf := make([]byte, sha256.Size+_AesKeySize+_AEADNonceSize)
// we mix the header checksum into the key buf := expand(outbuf, d.key, d.hdrsum, []byte(_DataKeyExpansion))
h := sha256.New()
h.Write([]byte(_EncryptNonce))
h.Write(d.key)
h.Write(d.hdrsum)
key = h.Sum(nil)
aes, err := aes.NewCipher(key) 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 { if err != nil {
return fmt.Errorf("decrypt: %w", err) return fmt.Errorf("decrypt: %w", err)
} }
d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen) d.ae, err = cipher.NewGCM(aes)
if err != nil { if err != nil {
return fmt.Errorf("decrypt: %w", err) 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()) d.buf = make([]byte, int(d.ChunkSize)+d.ae.Overhead())
return nil return nil
} }
@ -488,24 +555,21 @@ func (d *Decryptor) Decrypt(wr io.Writer) error {
return nil return nil
} }
} }
return nil
} }
// Decrypt exactly one chunk of data // Decrypt exactly one chunk of data
func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) { func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
var b [8]byte
var nonceb [32]byte
var ovh uint32 = uint32(d.ae.Overhead()) var ovh uint32 = uint32(d.ae.Overhead())
var p []byte var b [4]byte
var nonce [_AEADNonceSize]byte
n, err := io.ReadFull(d.rd, b[:4]) n, err := io.ReadFull(d.rd, b[:])
if err != nil || n == 0 { if err != nil || n == 0 {
return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i) return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i)
} }
m := binary.BigEndian.Uint32(b[:4]) m := binary.BigEndian.Uint32(b[:])
eof := (m & _EOF) > 0 eof := (m & _EOF) > 0
m &= (_EOF - 1) m &= (_EOF - 1)
// Sanity check - in case of corrupt header // Sanity check - in case of corrupt header
@ -517,16 +581,14 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
if !eof { if !eof {
return nil, false, fmt.Errorf("decrypt: block %d: zero-sized chunk without EOF", i) return nil, false, fmt.Errorf("decrypt: block %d: zero-sized chunk without EOF", i)
} }
return p, eof, nil return nil, eof, nil
case m < ovh:
return nil, false, fmt.Errorf("decrypt: chunksize is too small (%d)", m)
default: default:
} }
binary.BigEndian.PutUint32(b[4:], i) // make the nonce - top 4 bytes are the counter
nonce := makeNonceV2(nonceb[:], d.Salt, b[:]) copy(nonce[:], d.nonce)
binary.BigEndian.PutUint32(nonce[:4], i)
z := m + ovh z := m + ovh
n, err = io.ReadFull(d.rd, d.buf[:z]) n, err = io.ReadFull(d.rd, d.buf[:z])
@ -534,39 +596,115 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %w", i, err) return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %w", i, err)
} }
p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:]) pt, err := d.ae.Open(d.buf[:0], nonce[:], d.buf[:n], b[:])
if err != nil { if err != nil {
return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %w", i, err) return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %w", i, err)
} }
return p[:m], eof, nil 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
} }
// Wrap sender's signature of the encryption key func (d *Decryptor) processTrailer(pt []byte, eof bool) ([]byte, bool, error) {
func wrapSenderSig(sig []byte, key, salt []byte) ([]byte, error) { var rd [ed25519.SignatureSize]byte
aes, err := aes.NewCipher(key)
_, err := io.ReadFull(d.rd, rd[:])
if err != nil { if err != nil {
return nil, fmt.Errorf("wrap: %w", err) 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) ae, err := cipher.NewGCM(aes)
if err != nil { if err != nil {
return nil, fmt.Errorf("wrap: %w", err) return fmt.Errorf("senderId: %w", err)
} }
tagsize := ae.Overhead() outbuf := make([]byte, ed25519.SignatureSize+ae.Overhead())
nonceSize := ae.NonceSize() buf = ae.Seal(outbuf[:0], nonce, sig, nil)
nonce := sha256Slices([]byte(_WrapSenderNonce), salt)[:nonceSize] e.auth = auth
esig := make([]byte, tagsize+len(sig)) e.Sender = buf
return ae.Seal(esig[:0], nonce, sig, nil), nil return nil
} }
// unwrap sender's signature using 'key' and extract the signature // unwrap sender's signature using 'key' and extract the signature
// Optionally, verify the signature using the sender's PK (if provided). // Optionally, verify the signature using the sender's PK (if provided).
func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey) error { func (d *Decryptor) verifySender(key []byte, senderPk *PublicKey) error {
aes, err := aes.NewCipher(key) 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 { if err != nil {
return fmt.Errorf("unwrap: %w", err) return fmt.Errorf("unwrap: %w", err)
} }
@ -576,46 +714,70 @@ func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey
return fmt.Errorf("unwrap: %w", err) return fmt.Errorf("unwrap: %w", err)
} }
nonceSize := ae.NonceSize() var sigbuf [ed25519.SignatureSize]byte
nonce := sha256Slices([]byte(_WrapSenderNonce), d.Salt)[:nonceSize] var zero [ed25519.SignatureSize]byte
sig := make([]byte, ed25519.SignatureSize)
sig, err = ae.Open(sig[:0], nonce, d.SenderSign, nil) sig, err := ae.Open(sigbuf[:0], nonce, d.Sender, nil)
if err != nil { if err != nil {
return fmt.Errorf("unwrap: can't open sender info: %w", err) return fmt.Errorf("unwrap: can't open sender info: %w", err)
} }
var zero [ed25519.SignatureSize]byte
// Did the sender actually sign anything? // Did the sender actually sign anything?
if subtle.ConstantTimeCompare(zero[:], sig) == 0 { if subtle.ConstantTimeCompare(zero[:], sig) == 0 {
d.auth = true if senderPk == nil {
return ErrNoSenderPK
if senderPK != nil {
ss := &Signature{
Sig: sig,
}
ok := senderPK.VerifyMessage(key, ss)
if !ok {
return fmt.Errorf("unwrap: sender verification failed")
}
} }
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 return nil
} }
// Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK // Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK
// basically, we do two scalarmults: //
// a) Ephemeral encryption/decryption SK x receiver PK // basically, we do a scalarmult: Ephemeral encryption/decryption SK x receiver PK
// b) Sender's SK x receiver PK
func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) { func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
rxPK := pk.toCurve25519PK() rxPK := pk.ToCurve25519PK()
dkek, err := curve25519.X25519(e.encSK, rxPK) sekrit, err := curve25519.X25519(e.encSK, rxPK)
if err != nil { if err != nil {
return nil, fmt.Errorf("wrap: %w", err) return nil, fmt.Errorf("wrap: %w", err)
} }
aes, err := aes.NewCipher(dkek) 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 { if err != nil {
return nil, fmt.Errorf("wrap: %w", err) return nil, fmt.Errorf("wrap: %w", err)
} }
@ -625,14 +787,10 @@ func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
return nil, fmt.Errorf("wrap: %w", err) return nil, fmt.Errorf("wrap: %w", err)
} }
tagsize := ae.Overhead() ekey := make([]byte, ae.Overhead()+len(e.key))
nonceSize := ae.NonceSize()
nonceR := sha256Slices([]byte(_WrapReceiverNonce), e.Salt)[:nonceSize]
ekey := make([]byte, tagsize+len(e.key))
w := &pb.WrappedKey{ w := &pb.WrappedKey{
DKey: ae.Seal(ekey[:0], nonceR, e.key, pk.Pk), DKey: ae.Seal(ekey[:0], nonce, e.key, pk.Pk),
Nonce: rbuf,
} }
return w, nil return w, nil
@ -641,41 +799,49 @@ func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
// Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and // Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and
// senders ephemeral PublicKey // senders ephemeral PublicKey
func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) { func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) {
ourSK := sk.toCurve25519SK() ourSK := sk.ToCurve25519SK()
dkek, err := curve25519.X25519(ourSK, d.Pk) sekrit, err := curve25519.X25519(ourSK, d.Pk)
if err != nil { if err != nil {
return nil, fmt.Errorf("unwrap: %w", err) return nil, fmt.Errorf("unwrap: %w", err)
} }
aes, err := aes.NewCipher(dkek) 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 { if err != nil {
return nil, fmt.Errorf("unwrap: %w", err) return nil, fmt.Errorf("wrap: %w", err)
} }
ae, err := cipher.NewGCM(aes) ae, err := cipher.NewGCM(aes)
if err != nil { if err != nil {
return nil, fmt.Errorf("unwrap: %w", err) return nil, fmt.Errorf("wrap: %w", err)
} }
// 32 == AES-256 key size want := _AesKeySize + ae.Overhead()
want := 32 + ae.Overhead()
if len(w.DKey) != want { if len(w.DKey) != want {
return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(w.DKey)) return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(w.DKey))
} }
nonceSize := ae.NonceSize()
nonceR := sha256Slices([]byte(_WrapReceiverNonce), d.Salt)[:nonceSize]
pk := sk.PublicKey() pk := sk.PublicKey()
dkey := make([]byte, _AesKeySize) // decrypted data decryption key
dkey := make([]byte, 32) // decrypted data decryption key
// we indicate incorrect receiver SK by returning a nil key // we indicate incorrect receiver SK by returning a nil key
dkey, err = ae.Open(dkey[:0], nonceR, w.DKey, pk.Pk) dkey, err = ae.Open(dkey[:0], nonce, w.DKey, pk.Pk)
if err != nil { if err != nil {
return nil, nil return nil, nil
} }
// we have successfully found the correct recipient
return dkey, nil return dkey, nil
} }
@ -695,30 +861,14 @@ func fullwrite(buf []byte, wr io.Writer) error {
return nil return nil
} }
// make aead nonce from salt, chunk-size and block#
// First 8 bytes are chunk-size and nonce (in 'ad')
func makeNonceV2(dest []byte, salt []byte, ad []byte) []byte {
n := len(ad)
copy(dest, ad)
copy(dest[n:], salt)
return dest
}
// make aead nonce from salt, chunk-size and block# for v1
// This is here for historical documentation purposes
func makeNonceV1(dest []byte, salt []byte, ad []byte) []byte {
h := sha256.New()
h.Write(salt)
h.Write(ad)
return h.Sum(dest[:0])
}
// generate a KEK from a shared DH key and a Pub Key // generate a KEK from a shared DH key and a Pub Key
func expand(shared, pk []byte) ([]byte, error) { func expand(out []byte, shared, salt, ad []byte) []byte {
kek := make([]byte, 32) h := hkdf.New(sha512.New, shared, salt, ad)
h := hkdf.New(sha512.New, shared, pk, nil) _, err := io.ReadFull(h, out)
_, err := io.ReadFull(h, kek) if err != nil {
return kek, err panic(fmt.Sprintf("hkdf: failed to generate %d bytes: %s", len(out), err))
}
return out
} }
func newSender() (sk, pk []byte, err error) { func newSender() (sk, pk []byte, err error) {
@ -740,4 +890,25 @@ func sha256Slices(v ...[]byte) []byte {
return h.Sum(nil)[:] 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 // EOF

View file

@ -34,8 +34,10 @@ func (b *Buffer) Close() error {
func TestEncryptSimple(t *testing.T) { func TestEncryptSimple(t *testing.T) {
assert := newAsserter(t) assert := newAsserter(t)
receiver, err := NewKeypair() sk, err := NewPrivateKey()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "SK gen failed: %s", err)
pk := sk.PublicKey()
var blkSize int = 1024 var blkSize int = 1024
var size int = (blkSize * 10) var size int = (blkSize * 10)
@ -49,7 +51,7 @@ func TestEncryptSimple(t *testing.T) {
ee, err := NewEncryptor(nil, uint64(blkSize)) ee, err := NewEncryptor(nil, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(pk)
assert(err == nil, "can't add recipient: %s", err) assert(err == nil, "can't add recipient: %s", err)
rd := bytes.NewBuffer(buf) rd := bytes.NewBuffer(buf)
@ -63,7 +65,7 @@ func TestEncryptSimple(t *testing.T) {
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err == nil, "decryptor create fail: %s", err) assert(err == nil, "decryptor create fail: %s", err)
err = dd.SetPrivateKey(&receiver.Sec, nil) err = dd.SetPrivateKey(sk, nil)
assert(err == nil, "decryptor can't add SK: %s", err) assert(err == nil, "decryptor can't add SK: %s", err)
wr = Buffer{} wr = Buffer{}
@ -76,12 +78,67 @@ func TestEncryptSimple(t *testing.T) {
assert(byteEq(b, buf), "decrypt content mismatch") 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 // test corrupted header or corrupted input
func TestEncryptCorrupted(t *testing.T) { func TestEncryptCorrupted(t *testing.T) {
assert := newAsserter(t) assert := newAsserter(t)
receiver, err := NewKeypair() sk, err := NewPrivateKey()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "SK gen failed: %s", err)
pk := sk.PublicKey()
var blkSize int = 1024 var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize) var size int = (blkSize * 23) + randmod(blkSize)
@ -95,7 +152,7 @@ func TestEncryptCorrupted(t *testing.T) {
ee, err := NewEncryptor(nil, uint64(blkSize)) ee, err := NewEncryptor(nil, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(pk)
assert(err == nil, "can't add recipient: %s", err) assert(err == nil, "can't add recipient: %s", err)
rd := bytes.NewReader(buf) rd := bytes.NewReader(buf)
@ -107,6 +164,7 @@ func TestEncryptCorrupted(t *testing.T) {
rb := wr.Bytes() rb := wr.Bytes()
n := len(rb) n := len(rb)
// corrupt the input
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
j := randint() % n j := randint() % n
rb[j] = byte(randint() & 0xff) rb[j] = byte(randint() & 0xff)
@ -122,11 +180,11 @@ func TestEncryptCorrupted(t *testing.T) {
func TestEncryptSenderVerified(t *testing.T) { func TestEncryptSenderVerified(t *testing.T) {
assert := newAsserter(t) assert := newAsserter(t)
sender, err := NewKeypair() sender, err := NewPrivateKey()
assert(err == nil, "sender keypair gen failed: %s", err) assert(err == nil, "sender SK gen failed: %s", err)
receiver, err := NewKeypair() receiver, err := NewPrivateKey()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "receiver SK gen failed: %s", err)
var blkSize int = 1024 var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize) var size int = (blkSize * 23) + randmod(blkSize)
@ -137,10 +195,10 @@ func TestEncryptSenderVerified(t *testing.T) {
buf[i] = byte(i & 0xff) buf[i] = byte(i & 0xff)
} }
ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) ee, err := NewEncryptor(sender, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(receiver.PublicKey())
assert(err == nil, "can't add recipient: %s", err) assert(err == nil, "can't add recipient: %s", err)
rd := bytes.NewBuffer(buf) rd := bytes.NewBuffer(buf)
@ -154,14 +212,15 @@ func TestEncryptSenderVerified(t *testing.T) {
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err == nil, "decryptor create fail: %s", err) assert(err == nil, "decryptor create fail: %s", err)
// first send a wrong sender key randkey, err := NewPrivateKey()
randkey, err := NewKeypair() assert(err == nil, "rand SK gen failed: %s", err)
assert(err == nil, "receiver rand keypair gen failed: %s", err)
err = dd.SetPrivateKey(&receiver.Sec, &randkey.Pub) // first send a wrong sender PK
err = dd.SetPrivateKey(receiver, randkey.PublicKey())
assert(err != nil, "decryptor failed to verify sender") assert(err != nil, "decryptor failed to verify sender")
err = dd.SetPrivateKey(&receiver.Sec, &sender.Pub) // then the correct sender PK
err = dd.SetPrivateKey(receiver, sender.PublicKey())
assert(err == nil, "decryptor can't add SK: %s", err) assert(err == nil, "decryptor can't add SK: %s", err)
wr = Buffer{} wr = Buffer{}
@ -178,8 +237,8 @@ func TestEncryptSenderVerified(t *testing.T) {
func TestEncryptMultiReceiver(t *testing.T) { func TestEncryptMultiReceiver(t *testing.T) {
assert := newAsserter(t) assert := newAsserter(t)
sender, err := NewKeypair() sender, err := NewPrivateKey()
assert(err == nil, "sender keypair gen failed: %s", err) assert(err == nil, "sender SK gen failed: %s", err)
var blkSize int = 1024 var blkSize int = 1024
var size int = (blkSize * 23) + randmod(blkSize) var size int = (blkSize * 23) + randmod(blkSize)
@ -190,17 +249,17 @@ func TestEncryptMultiReceiver(t *testing.T) {
buf[i] = byte(i & 0xff) buf[i] = byte(i & 0xff)
} }
ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) ee, err := NewEncryptor(sender, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
n := 4 n := 4
rx := make([]*Keypair, n) rx := make([]*PrivateKey, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
r, err := NewKeypair() r, err := NewPrivateKey()
assert(err == nil, "can't make receiver key %d: %s", i, err) assert(err == nil, "can't make receiver SK %d: %s", i, err)
rx[i] = r rx[i] = r
err = ee.AddRecipient(&r.Pub) err = ee.AddRecipient(r.PublicKey())
assert(err == nil, "can't add recipient %d: %s", i, err) assert(err == nil, "can't add recipient %d: %s", i, err)
} }
@ -217,7 +276,7 @@ func TestEncryptMultiReceiver(t *testing.T) {
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err == nil, "decryptor %d create fail: %s", i, err) assert(err == nil, "decryptor %d create fail: %s", i, err)
err = dd.SetPrivateKey(&rx[i].Sec, &sender.Pub) err = dd.SetPrivateKey(rx[i], sender.PublicKey())
assert(err == nil, "decryptor can't add SK %d: %s", i, err) assert(err == nil, "decryptor can't add SK %d: %s", i, err)
wr = Buffer{} wr = Buffer{}
@ -235,7 +294,7 @@ func TestEncryptMultiReceiver(t *testing.T) {
func TestStreamIO(t *testing.T) { func TestStreamIO(t *testing.T) {
assert := newAsserter(t) assert := newAsserter(t)
receiver, err := NewKeypair() receiver, err := NewPrivateKey()
assert(err == nil, "receiver keypair gen failed: %s", err) assert(err == nil, "receiver keypair gen failed: %s", err)
var blkSize int = 1024 var blkSize int = 1024
@ -250,7 +309,7 @@ func TestStreamIO(t *testing.T) {
ee, err := NewEncryptor(nil, uint64(blkSize)) ee, err := NewEncryptor(nil, uint64(blkSize))
assert(err == nil, "encryptor create fail: %s", err) assert(err == nil, "encryptor create fail: %s", err)
err = ee.AddRecipient(&receiver.Pub) err = ee.AddRecipient(receiver.PublicKey())
assert(err == nil, "can't add recipient: %s", err) assert(err == nil, "can't add recipient: %s", err)
wr := Buffer{} wr := Buffer{}
@ -283,7 +342,7 @@ func TestStreamIO(t *testing.T) {
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err == nil, "decryptor create fail: %s", err) assert(err == nil, "decryptor create fail: %s", err)
err = dd.SetPrivateKey(&receiver.Sec, nil) err = dd.SetPrivateKey(receiver, nil)
assert(err == nil, "decryptor can't add SK: %s", err) assert(err == nil, "decryptor can't add SK: %s", err)
rio, err := dd.NewStreamReader() rio, err := dd.NewStreamReader()
@ -313,6 +372,93 @@ func TestStreamIO(t *testing.T) {
} }
// 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 { func randint() int {
var b [4]byte var b [4]byte

View file

@ -20,17 +20,19 @@ import (
var ( var (
ErrClosed = errors.New("encrypt: stream already closed") ErrClosed = errors.New("encrypt: stream already closed")
ErrNoKey = errors.New("decrypt: No private key set for decryption") ErrNoKey = errors.New("decrypt: no private key set for decryption")
ErrEncStarted = errors.New("encrypt: can't add new recipient after encryption has started") 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") 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") ErrEncIsStream = errors.New("encrypt: can't use Encrypt() after using streaming I/O")
ErrNotSigTool = errors.New("decrypt: Not a sigtool encrypted file?") ErrNotSigTool = errors.New("decrypt: not a sigtool encrypted file?")
ErrHeaderTooBig = errors.New("decrypt: header too large (max 1048576)") ErrHeaderTooBig = errors.New("decrypt: header too large (max 1048576)")
ErrHeaderTooSmall = errors.New("decrypt: header too small (min 32)") ErrHeaderTooSmall = errors.New("decrypt: header too small (min 32)")
ErrBadHeader = errors.New("decrypt: header corrupted") ErrBadHeader = errors.New("decrypt: header corrupted")
ErrNoWrappedKeys = errors.New("decrypt: No wrapped keys in encrypted file") ErrNoWrappedKeys = errors.New("decrypt: no wrapped keys in encrypted file")
ErrBadKey = errors.New("decrypt: wrong key") ErrBadKey = errors.New("decrypt: wrong key")
ErrBadTrailer = errors.New("decrypt: message integrity failed (bad trailer)")
ErrBadSender = errors.New("unwrap: sender verification failed") ErrBadSender = errors.New("unwrap: sender verification failed")
ErrNoSenderPK = errors.New("unwrap: missing sender public key")
ErrIncorrectPassword = errors.New("ssh: invalid passphrase") ErrIncorrectPassword = errors.New("ssh: invalid passphrase")
ErrNoPEMFound = errors.New("ssh: no PEM block found") ErrNoPEMFound = errors.New("ssh: no PEM block found")

67
sign/iomisc.go Normal file
View 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
}

View file

@ -25,19 +25,14 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/binary"
"fmt" "fmt"
"hash"
"io"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"os"
Ed "crypto/ed25519" Ed "crypto/ed25519"
"golang.org/x/crypto/scrypt"
"gopkg.in/yaml.v2"
"github.com/opencoff/go-utils" "golang.org/x/crypto/scrypt"
"gopkg.in/yaml.v3"
) )
// Private Ed25519 key // Private Ed25519 key
@ -64,15 +59,10 @@ type PublicKey struct {
hash []byte hash []byte
} }
// Ed25519 key pair
type Keypair struct {
Sec PrivateKey
Pub PublicKey
}
// Length of Ed25519 Public Key Hash // Length of Ed25519 Public Key Hash
const PKHashLength = 16 const PKHashLength = 16
// constants we use in this module
const ( const (
// Scrypt parameters // Scrypt parameters
_N int = 1 << 19 _N int = 1 << 19
@ -118,55 +108,27 @@ type signature struct {
Signature string `yaml:"signature"` Signature string `yaml:"signature"`
} }
// given a public key, generate a deterministic short-hash of it.
func pkhash(pk []byte) []byte { func pkhash(pk []byte) []byte {
z := sha256.Sum256(pk) z := sha256.Sum256(pk)
return z[:PKHashLength] return z[:PKHashLength]
} }
// Generate a new Ed25519 keypair // NewPrivateKey generates a new Ed25519 private key
func NewKeypair() (*Keypair, error) { func NewPrivateKey() (*PrivateKey, error) {
//kp := &Keypair{Sec: PrivateKey{N: 1 << 17, r: 64, p: 1}} pkb, skb, err := Ed.GenerateKey(rand.Reader)
kp := &Keypair{}
sk := &kp.Sec
pk := &kp.Pub
sk.pk = pk
p, s, err := Ed.GenerateKey(rand.Reader)
if err != nil { if err != nil {
return nil, fmt.Errorf("Can't generate Ed25519 keys: %s", err) return nil, err
} }
pk.Pk = []byte(p) sk := &PrivateKey{
sk.Sk = []byte(s) Sk: []byte(skb),
pk.hash = pkhash(pk.Pk) pk: &PublicKey{
Pk: []byte(pkb),
return kp, nil hash: pkhash([]byte(pkb)),
} },
// 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, getpw func() ([]byte, error)) 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)
} }
return sk, nil
err = sk.serialize(skf, comment, getpw)
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 // Read the private key in 'fn', optionally decrypting it using
@ -177,73 +139,30 @@ func ReadPrivateKey(fn string, getpw func() ([]byte, error)) (*PrivateKey, error
return nil, err return nil, err
} }
if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 { var sk PrivateKey
return parseSSHPrivateKey(yml, getpw) if err = sk.UnmarshalBinary(yml, getpw); err != nil {
return nil, err
} }
return &sk, nil
if pw, err := getpw(); err == nil {
return MakePrivateKey(yml, pw)
}
return nil, err
} }
// Make a private key from bytes 'yml' and password 'pw'. The bytes // 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. // are assumed to be serialized version of the private key.
func MakePrivateKey(yml []byte, pw []byte) (*PrivateKey, error) { func MakePrivateKey(yml []byte, getpw func() ([]byte, error)) (*PrivateKey, error) {
var ssk serializedPrivKey var sk PrivateKey
err := yaml.Unmarshal(yml, &ssk) err := sk.UnmarshalBinary(yml, getpw)
if err != nil { if err != nil {
return nil, fmt.Errorf("make priv key: can't parse YAML: %s", err) return nil, err
} }
return &sk, nil
if len(ssk.Salt) == 0 || len(ssk.Esk) == 0 {
return nil, fmt.Errorf("sign: not YAML private key")
}
b64 := base64.StdEncoding.DecodeString
salt, err := b64(ssk.Salt)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode salt: %s", err)
}
esk, err := b64(ssk.Esk)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode key: %s", err)
}
// We take short passwords and extend them
pwb := sha512.Sum512(pw)
// "32" == Length of AES-256 key
key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32)
if err != nil {
return nil, fmt.Errorf("make priv key: can't derive key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
skb, err := ae.Open(nil, salt[:ae.NonceSize()], esk, nil)
if err != nil {
return nil, fmt.Errorf("make priv key: wrong password")
}
return PrivateKeyFromBytes(skb)
} }
// Make a private key from 64-bytes of extended Ed25519 key // make a PrivateKey from a byte array containing ed25519 raw SK
func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) { func makePrivateKeyFromBytes(sk *PrivateKey, buf []byte) error {
if len(buf) != 64 { if len(buf) != 64 {
return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf)) return fmt.Errorf("private key is malformed (len %d!)", len(buf))
} }
skb := make([]byte, 64) skb := make([]byte, 64)
@ -256,12 +175,18 @@ func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) {
Pk: []byte(edpk), Pk: []byte(edpk),
hash: pkhash([]byte(edpk)), hash: pkhash([]byte(edpk)),
} }
sk := &PrivateKey{ sk.Sk = skb
Sk: skb, sk.pk = pk
pk: pk, return nil
} }
return sk, 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 // Given a secret key, return the corresponding Public Key
@ -270,7 +195,7 @@ func (sk *PrivateKey) PublicKey() *PublicKey {
} }
// Convert an Ed25519 Private Key to Curve25519 Private key // Convert an Ed25519 Private Key to Curve25519 Private key
func (sk *PrivateKey) toCurve25519SK() []byte { func (sk *PrivateKey) ToCurve25519SK() []byte {
if sk.ck == nil { if sk.ck == nil {
var ek [64]byte var ek [64]byte
@ -284,12 +209,228 @@ func (sk *PrivateKey) toCurve25519SK() []byte {
return sk.ck 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 // from github.com/FiloSottile/age
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10) var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
// Convert an Ed25519 Public Key to Curve25519 public key // Convert an Ed25519 Public Key to Curve25519 public key
// from github.com/FiloSottile/age // from github.com/FiloSottile/age
func (pk *PublicKey) toCurve25519PK() []byte { func (pk *PublicKey) ToCurve25519PK() []byte {
if pk.ck != nil { if pk.ck != nil {
return pk.ck return pk.ck
} }
@ -329,131 +470,8 @@ func (pk *PublicKey) Hash() []byte {
return pk.hash return pk.hash
} }
// Serialize the private key to a file // MarshalBinary marshals a PublicKey into a byte array
// AEAD encryption for protecting the private key func (pk *PublicKey) MarshalBinary(comment string) ([]byte, error) {
// Format: YAML
// All []byte are in base64 (RawEncoding)
func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error {
pw, err := getpw()
if err != nil {
return err
}
// 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 fmt.Errorf("marshal: can't derive scrypt key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("marshal: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return 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.
out, err := yaml.Marshal(&ssk)
if err != nil {
return fmt.Errorf("can't marahal to YAML: %s", err)
}
return writeFile(fn, out, 0600)
}
// --- 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
}
// first try to parse as a ssh key
pk, err := parseSSHPublicKey(yml)
if err != nil {
pk, err = MakePublicKey(yml)
}
return pk, err
}
// 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)
}
if len(spk.Pk) == 0 {
return nil, fmt.Errorf("sign: not a YAML public key")
}
b64 := base64.StdEncoding.DecodeString
var pkb []byte
if pkb, err = b64(spk.Pk); err != nil {
return nil, fmt.Errorf("can't decode YAML:Pk: %s", err)
}
if pk, err := PublicKeyFromBytes(pkb); err == nil {
pk.Comment = spk.Comment
return pk, nil
}
return nil, err
}
// Make a public key from a byte string
func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
if len(b) != 32 {
return nil, fmt.Errorf("public key is malformed (len %d!)", len(b))
}
pk := &PublicKey{
Pk: make([]byte, 32),
hash: pkhash(b),
}
copy(pk.Pk, b)
return pk, nil
}
// Serialize Public Keys
func (pk *PublicKey) serialize(fn, comment string) error {
b64 := base64.StdEncoding.EncodeToString b64 := base64.StdEncoding.EncodeToString
spk := &serializedPubKey{ spk := &serializedPubKey{
Comment: comment, Comment: comment,
@ -461,76 +479,45 @@ func (pk *PublicKey) serialize(fn, comment string) error {
Hash: b64(pk.hash), Hash: b64(pk.hash),
} }
out, err := yaml.Marshal(spk) return yaml.Marshal(spk)
if err != nil { }
return fmt.Errorf("can't marahal to YAML: %s", err)
// 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
} }
return writeFile(fn, out, 0644) // 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 -- // -- 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
}
func clamp(k []byte) []byte { func clamp(k []byte) []byte {
k[0] &= 248 k[0] &= 248
k[31] &= 127 k[31] &= 127
@ -538,13 +525,5 @@ func clamp(k []byte) []byte {
return k return k
} }
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
}
// EOF // EOF
// vim: noexpandtab:ts=8:sw=8:tw=92: // vim: noexpandtab:ts=8:sw=8:tw=92:

45
sign/rand.go Normal file
View 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)
}

View file

@ -27,7 +27,8 @@ import (
"io/ioutil" "io/ioutil"
Ed "crypto/ed25519" Ed "crypto/ed25519"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
) )
// An Ed25519 Signature // An Ed25519 Signature
@ -38,8 +39,9 @@ type Signature struct {
// 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) {
h := sha512.New() h := sha512.New()
h.Write([]byte("sigtool signed message")) h.Write([]byte("sigtool signed message"))
@ -86,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 {
@ -110,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
} }
@ -147,7 +154,6 @@ func (sig *Signature) IsPKMatch(pk *PublicKey) bool {
// 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

View file

@ -38,16 +38,20 @@ 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 // return a hardcoded password
func hardcodedPw() ([]byte, error) { func hardcodedPw() ([]byte, error) {
return []byte("abc"), nil return fixedPw, nil
} }
func wrongPw() ([]byte, error) { func wrongPw() ([]byte, error) {
return []byte("xyz"), nil return badPw, nil
} }
func emptyPw() ([]byte, error) { func emptyPw() ([]byte, error) {
return nil, nil return nilPw, nil
} }
// Return true if file exists, false otherwise // Return true if file exists, false otherwise
@ -80,68 +84,78 @@ p: 1
func TestSignSimple(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, "", hardcodedPw)
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)
// We must find these two files err = pk.Serialize(pkf, "", true)
assert(fileExists(pkf), "missing pkf") assert(err == nil, "can't serialize pk %s", pkf)
assert(fileExists(skf), "missing skf")
pk, err := ReadPublicKey(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
assert(fileExists(pkf), "missing pkf %s", pkf)
assert(fileExists(skf), "missing skf %s", skf)
npk, 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, emptyPw) nsk, err := ReadPrivateKey(pkf, emptyPw)
assert(err != nil, "bad SK ReadSK fail: %s", err) assert(err != nil, "bad SK ReadSK fail: %s", err)
sk, err = ReadPrivateKey(skf, emptyPw) nsk, err = ReadPrivateKey(skf, emptyPw)
assert(err != nil, "ReadSK() empty pw fail: ks", err) assert(err != nil, "ReadSK() worked with empty pw")
sk, err = ReadPrivateKey(skf, wrongPw) nsk, err = ReadPrivateKey(skf, wrongPw)
assert(err != nil, "ReadSK() wrong pw fail: %s", err) 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, hardcodedPw) nsk, err = ReadPrivateKey(badf, hardcodedPw)
assert(err != nil, "badsk read fail: %s", err) assert(err != nil, "decoded bad SK")
// Finally, with correct password it should work. // Finally, with correct password it should work.
sk, err = ReadPrivateKey(skf, hardcodedPw) 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 TestSignRandBuf(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
randRead(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
@ -160,27 +174,13 @@ func TestSignRandBuf(t *testing.T) {
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, "", emptyPw)
assert(err == nil, "keyPair.Serialize() fail")
// Now read the private key and sign
sk, err = ReadPrivateKey(skf, emptyPw)
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++ {
randRead(buf[:]) randRead(buf[:])
@ -192,27 +192,31 @@ func TestSignRandBuf(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")
@ -220,12 +224,12 @@ func TestSignRandBuf(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)
@ -233,7 +237,7 @@ func TestSignRandBuf(t *testing.T) {
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()
} }
} }
@ -250,7 +254,8 @@ func Benchmark_Sig(b *testing.B) {
} }
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)
@ -260,11 +265,11 @@ 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)
}) })
} }
} }

View file

@ -26,6 +26,7 @@ import (
"encoding/binary" "encoding/binary"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/dchest/bcrypt_pbkdf" "github.com/dchest/bcrypt_pbkdf"
@ -56,12 +57,13 @@ func parseSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey,
} }
func parseSSHPublicKey(in []byte) (*PublicKey, error) { func parseSSHPublicKey(in []byte) (*PublicKey, error) {
v := bytes.Split(in, []byte(" \t")) splitter := regexp.MustCompile("[ \\t]+")
v := splitter.Split(string(in), -1)
if len(v) != 3 { if len(v) != 3 {
return nil, ErrBadPublicKey return nil, ErrBadPublicKey
} }
return parseEncPubKey(v[1], string(v[2])) return parseEncPubKey([]byte(v[1]), v[2])
} }
// parse a wire encoded public key // parse a wire encoded public key
@ -93,11 +95,13 @@ func parseEncPubKey(in []byte, comm string) (*PublicKey, error) {
return nil, ErrBadTrailers return nil, ErrBadTrailers
} }
pk, err := PublicKeyFromBytes(w.KeyBytes) var pk PublicKey
if err == nil {
if err = makePublicKeyFromBytes(&pk, w.KeyBytes); err == nil {
pk.Comment = strings.TrimSpace(comm) pk.Comment = strings.TrimSpace(comm)
return &pk, nil
} }
return pk, err return nil, err
} }
func parseString(in []byte) (out, rest []byte, ok bool) { func parseString(in []byte) (out, rest []byte, ok bool) {
@ -341,8 +345,11 @@ func parseOpenSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*Private
} }
} }
pk, err := PrivateKeyFromBytes(key.Priv) var sk PrivateKey
return pk, err if err = makePrivateKeyFromBytes(&sk, key.Priv); err == nil {
return &sk, nil
}
return nil, err
default: default:
return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype) return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype)
} }

View file

@ -29,6 +29,8 @@ type encWriter struct {
err error err error
} }
var _ io.WriteCloser = &encWriter{}
// NewStreamWriter begins stream encryption to an underlying destination writer 'wr'. // NewStreamWriter begins stream encryption to an underlying destination writer 'wr'.
// It returns an io.WriteCloser. // It returns an io.WriteCloser.
func (e *Encryptor) NewStreamWriter(wr io.WriteCloser) (io.WriteCloser, error) { func (e *Encryptor) NewStreamWriter(wr io.WriteCloser) (io.WriteCloser, error) {
@ -108,6 +110,8 @@ type encReader struct {
blk uint32 blk uint32
} }
var _ io.Reader = &encReader{}
// NewStreamReader returns an io.Reader to read from the decrypted stream // NewStreamReader returns an io.Reader to read from the decrypted stream
func (d *Decryptor) NewStreamReader() (io.Reader, error) { func (d *Decryptor) NewStreamReader() (io.Reader, error) {
if d.key == nil { if d.key == nil {

View file

@ -1,362 +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"
"strings"
"github.com/opencoff/go-utils"
flag "github.com/opencoff/pflag"
"github.com/opencoff/sigtool/sign"
)
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 [%s; %s]\n", Z, ProductVersion, RepoVersion, Buildtime)
os.Exit(0)
}
if help {
usage(0)
}
args := mf.Args()
if len(args) < 1 {
warn("Insufficient arguments. Try '%s -h'", Z)
os.Exit(1)
}
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)
}
cmd(args[1:])
}
// 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, "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 err error
kp, err := sign.NewKeypair()
if err != nil {
die("%s", err)
}
err = kp.Serialize(bn, comment, 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", true)
if err != nil {
die("%s", err)
}
}
return []byte(pws), nil
})
if err != nil {
die("%s", err)
}
}
// Run the 'sign' command.
func signify(args []string) {
var nopw, 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(&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.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
}
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)
}
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
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(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()
}
// This will be filled in by "build"
var RepoVersion string = "UNDEFINED"
var Buildtime string = "UNDEFINED"
var ProductVersion string = "UNDEFINED"
// vim: ft=go:sw=8:ts=8:noexpandtab:tw=98:

View file

@ -20,9 +20,10 @@ import (
"os" "os"
"strings" "strings"
"git.rgst.io/homelab/sigtool/v3/sign"
"github.com/opencoff/go-fio"
"github.com/opencoff/go-utils" "github.com/opencoff/go-utils"
flag "github.com/opencoff/pflag" flag "github.com/opencoff/pflag"
"github.com/opencoff/sigtool/sign"
) )
// sigtool encrypt [-i|--identity my.key] to.pub [to.pub] [ssh.pub] inputfile|- [-o output] // sigtool encrypt [-i|--identity my.key] to.pub [to.pub] [ssh.pub] inputfile|- [-o output]
@ -35,19 +36,25 @@ func encrypt(args []string) {
var outfile string var outfile string
var keyfile string var keyfile string
var szstr string = "128k"
var envpw string var envpw string
var nopw bool var nopw, force bool
var blksize uint64 var blksize uint64
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`")
fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`") 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.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`")
fs.SizeVarP(&blksize, "block-size", "B", 128*1024, "Use `S` as the encryption block size") 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) err := fs.Parse(args)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
}
if blksize, err = utils.ParseSize(szstr); err != nil {
Die("%s", err)
} }
var pws, infile string var pws, infile string
@ -63,19 +70,19 @@ func encrypt(args []string) {
} else { } else {
pws, err = utils.Askpass("Enter passphrase for private key", false) pws, err = utils.Askpass("Enter passphrase for private key", false)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
} }
return []byte(pws), nil return []byte(pws), nil
}) })
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
} }
args = fs.Args() args = fs.Args()
if len(args) < 2 { if len(args) < 2 {
die("Insufficient args. Try '%s --help'", os.Args[0]) Die("Insufficient args. Try '%s --help'", os.Args[0])
} }
var infd io.Reader = os.Stdin var infd io.Reader = os.Stdin
@ -95,14 +102,14 @@ func encrypt(args []string) {
// Lets try to read the authorized files // Lets try to read the authorized files
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
die("can't find homedir for this user") Die("can't find homedir for this user")
} }
authkeys := fmt.Sprintf("%s/.ssh/authorized_keys", home) authkeys := fmt.Sprintf("%s/.ssh/authorized_keys", home)
authdata, err := ioutil.ReadFile(authkeys) authdata, err := ioutil.ReadFile(authkeys)
if err != nil { if err != nil {
if err != os.ErrNotExist { if !os.IsNotExist(err) {
die("can't open %s: %s", authkeys, err) Die("can't open %s: %s", authkeys, err)
} }
} }
@ -114,31 +121,43 @@ func encrypt(args []string) {
} }
if len(outfile) > 0 && outfile != "-" { if len(outfile) > 0 && outfile != "-" {
var mode os.FileMode = 0600 // conservative output mode
if inf != nil { if inf != nil {
ost, err := os.Stat(outfile) var err error
if err != nil { var ist, ost os.FileInfo
die("can't stat %s: %s", outfile, err)
if ost, err = os.Stat(outfile); err != nil {
Die("can't stat %s: %s", outfile, err)
} }
ist, err := inf.Stat() if ist, err = inf.Stat(); err != nil {
if err != nil { Die("can't stat %s: %s", infile, err)
die("can't stat %s: %s", infile, err)
} }
if os.SameFile(ist, ost) { if os.SameFile(ist, ost) {
die("won't create output file: same as input file!") Die("won't create output file: same as input file!")
} }
mode = ist.Mode()
} }
outf := mustOpen(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) var opts uint32
defer outf.Close() 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)
}
outfd = outf AtExit(sf.Abort)
defer sf.Abort()
outfd = sf
} }
en, err := sign.NewEncryptor(sk, blksize) en, err := sign.NewEncryptor(sk, blksize)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
errs := 0 errs := 0
@ -151,14 +170,14 @@ func encrypt(args []string) {
var ok bool var ok bool
pk, ok = keymap[fn] pk, ok = keymap[fn]
if !ok { if !ok {
warn("can't find user %s in %s", fn, authkeys) Warn("can't find user %s in %s", fn, authkeys)
errs += 1 errs += 1
continue continue
} }
} else { } else {
pk, err = sign.ReadPublicKey(fn) pk, err = sign.ReadPublicKey(fn)
if err != nil { if err != nil {
warn("%s", err) Warn("%s", err)
errs += 1 errs += 1
continue continue
} }
@ -166,18 +185,19 @@ func encrypt(args []string) {
err = en.AddRecipient(pk) err = en.AddRecipient(pk)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
} }
if errs > 0 { if errs > 0 {
die("Too many errors!") Die("Too many errors!")
} }
err = en.Encrypt(infd, outfd) err = en.Encrypt(infd, outfd)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
outfd.Close()
} }
type nullWriter struct{} type nullWriter struct{}
@ -202,26 +222,27 @@ func decrypt(args []string) {
var envpw string var envpw string
var outfile string var outfile string
var pubkey string var pubkey string
var nopw, test bool var nopw, test, force bool
fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") 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.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key")
fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") 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.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(&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) err := fs.Parse(args)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
args = fs.Args() args = fs.Args()
if len(args) < 1 { if len(args) < 1 {
die("Insufficient args. Try '%s --help'", os.Args[0]) Die("Insufficient args. Try '%s --help'", os.Args[0])
} }
var infd io.Reader = os.Stdin var infd io.Reader = os.Stdin
var outfd io.Writer = os.Stdout var outfd io.WriteCloser = os.Stdout
var inf *os.File var inf *os.File
var infile string var infile string
@ -237,13 +258,13 @@ func decrypt(args []string) {
} else { } else {
pws, err = utils.Askpass("Enter passphrase for private key", false) pws, err = utils.Askpass("Enter passphrase for private key", false)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
} }
return []byte(pws), nil return []byte(pws), nil
}) })
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
var pk *sign.PublicKey var pk *sign.PublicKey
@ -251,7 +272,7 @@ func decrypt(args []string) {
if len(pubkey) > 0 { if len(pubkey) > 0 {
pk, err = sign.ReadPublicKey(pubkey) pk, err = sign.ReadPublicKey(pubkey)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
} }
@ -268,34 +289,46 @@ func decrypt(args []string) {
if test { if test {
outfd = &nullWriter{} outfd = &nullWriter{}
} else if len(outfile) > 0 && outfile != "-" { } else if len(outfile) > 0 && outfile != "-" {
var mode os.FileMode = 0600 // conservative mode
if inf != nil { if inf != nil {
ost, err := os.Stat(outfile) var ist, ost os.FileInfo
if err != nil { var err error
die("can't stat %s: %s", outfile, err)
if ost, err = os.Stat(outfile); err != nil {
Die("can't stat %s: %s", outfile, err)
} }
ist, err := inf.Stat() if ist, err = inf.Stat(); err != nil {
if err != nil { Die("can't stat %s: %s", infile, err)
die("can't stat %s: %s", infile, err)
} }
if os.SameFile(ist, ost) { if os.SameFile(ist, ost) {
die("won't create output file: same as input file!") Die("won't create output file: same as input file!")
} }
mode = ist.Mode()
} }
outf := mustOpen(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) var opts uint32
defer outf.Close() 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)
}
outfd = outf AtExit(sf.Abort)
defer sf.Abort()
outfd = sf
} }
d, err := sign.NewDecryptor(infd) d, err := sign.NewDecryptor(infd)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
err = d.SetPrivateKey(sk, pk) err = d.SetPrivateKey(sk, pk)
if err != nil { if err != nil {
die("%s", err) Die("%s", err)
} }
if pk == nil && d.AuthenticatedSender() { if pk == nil && d.AuthenticatedSender() {
@ -303,17 +336,19 @@ func decrypt(args []string) {
if len(fn) == 0 || fn == "-" { if len(fn) == 0 || fn == "-" {
fn = "<stdin>" fn = "<stdin>"
} }
warn("%s: Missing sender Public Key; can't authenticate sender ..", fn) Warn("%s: Missing sender Public Key; can't authenticate sender ..", fn)
} }
err = d.Decrypt(outfd) if err = d.Decrypt(outfd); err != nil {
if err != nil { Die("%s", err)
die("%s", err)
} }
outfd.Close()
if test { if test {
warn("Enc file OK") Warn("Enc file OK")
} }
} }
func encryptUsage(fs *flag.FlagSet) { func encryptUsage(fs *flag.FlagSet) {
@ -321,9 +356,16 @@ func encryptUsage(fs *flag.FlagSet) {
Usage: %s encrypt [options] to [to ...] infile|- Usage: %s encrypt [options] to [to ...] infile|-
Where TO is the public key of the recipient and INFILE is an input file. Where TO is the public key of the recipient; it can be one of:
If the input file is '-' then %s reads from STDIN. Unless '-o' is used,
%s writes the encrypted output to STDOUT. - 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: Options:
`, Z, Z, Z, Z) `, Z, Z, Z, Z)
@ -351,7 +393,7 @@ Options:
func mustOpen(fn string, flag int) *os.File { func mustOpen(fn string, flag int) *os.File {
fdk, err := os.OpenFile(fn, flag, 0600) fdk, err := os.OpenFile(fn, flag, 0600)
if err != nil { if err != nil {
die("can't open file %s: %s", fn, err) Die("can't open file %s: %s", fn, err)
} }
return fdk return fdk
} }

55
src/die.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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