implement mcrypto.Signer
This commit is contained in:
parent
a2b6f56876
commit
50db101620
@ -2,3 +2,42 @@
|
||||
// cryptography, notably related to unique identifiers, signing/verifying data,
|
||||
// and encrypting/decrypting data
|
||||
package mcrypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Instead of outputing opaque hex garbage, this package opts to add a prefix to
|
||||
// the garbage. Each "type" of string returned has its own character which is
|
||||
// not found in the hex range (0-9, a-f), and in addition each also has a
|
||||
// version character prefixed as well, in case something wants to be changed
|
||||
// going forward.
|
||||
//
|
||||
// We keep the constant prefices here to ensure there's no conflicts across
|
||||
// string types in this package.
|
||||
const (
|
||||
uuidV0 = "0u" // u for uuid
|
||||
sigV0 = "0s" // s for signature
|
||||
exSigV0 = "0t" // t for time
|
||||
uniqueSigV0 = "0q" // q for uni"q"ue
|
||||
encryptedV0 = "0n" // n for "n"-crypted, harharhar
|
||||
)
|
||||
|
||||
func stripPrefix(s, prefix string) (string, bool) {
|
||||
trimmed := strings.TrimPrefix(s, prefix)
|
||||
return trimmed, len(trimmed) < len(s)
|
||||
}
|
||||
|
||||
func prefixReader(r io.Reader, prefix []byte) io.Reader {
|
||||
b := make([]byte, 0, len(prefix)+hex.EncodedLen(strconv.IntSize)+2)
|
||||
buf := bytes.NewBuffer(b)
|
||||
fmt.Fprintf(buf, "%x\n", len(prefix))
|
||||
buf.Write(prefix)
|
||||
buf.WriteByte('\n')
|
||||
return io.MultiReader(buf, r)
|
||||
}
|
||||
|
267
mcrypto/sig.go
Normal file
267
mcrypto/sig.go
Normal file
@ -0,0 +1,267 @@
|
||||
package mcrypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||
)
|
||||
|
||||
var (
|
||||
errMalformedSig = errors.New("malformed signature")
|
||||
|
||||
// ErrInvalidSig is returned by Signer related functions when an invalid
|
||||
// signature is used, e.g. it is a signature for different data, or uses a
|
||||
// different secret key, or has expired
|
||||
ErrInvalidSig = errors.New("invalid signature")
|
||||
)
|
||||
|
||||
// Signer is some entity which can generate signatures for arbitrary data and
|
||||
// can later verify those signatures
|
||||
type Signer interface {
|
||||
sign(io.Reader) (string, error)
|
||||
|
||||
// returns an error if io.Reader returns one ever, or if the signature
|
||||
// couldn't be verified
|
||||
verify(string, io.Reader) error
|
||||
}
|
||||
|
||||
// Sign reads all data from the io.Reader and signs it using the given Signer
|
||||
func Sign(s Signer, r io.Reader) (string, error) {
|
||||
return s.sign(r)
|
||||
}
|
||||
|
||||
// SignBytes uses the Signer to generate a signature for the given []bytes
|
||||
func SignBytes(s Signer, b []byte) string {
|
||||
sig, err := s.sign(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sig
|
||||
}
|
||||
|
||||
// SignString uses the Signer to generate a signature for the given string
|
||||
func SignString(s Signer, in string) string {
|
||||
return SignBytes(s, []byte(in))
|
||||
}
|
||||
|
||||
// Verify reads all data from the io.Reader and uses the Signer to verify that
|
||||
// the signature is for that data.
|
||||
//
|
||||
// Returns any errors from io.Reader, or ErrInvalidSig (use merry.Is(err,
|
||||
// mcrypto.ErrInvalidSig) to check).
|
||||
func Verify(s Signer, sig string, r io.Reader) error {
|
||||
return s.verify(sig, r)
|
||||
}
|
||||
|
||||
// VerifyBytes uses the Signer to verify that the signature is for the given
|
||||
// []bytes.
|
||||
//
|
||||
// Returns any errors from io.Reader, or ErrInvalidSig (use merry.Is(err,
|
||||
// mcrypto.ErrInvalidSig) to check).
|
||||
func VerifyBytes(s Signer, sig string, b []byte) error {
|
||||
return s.verify(sig, bytes.NewBuffer(b))
|
||||
}
|
||||
|
||||
// VerifyString uses the Signer to verify that the signature is for the given
|
||||
// string.
|
||||
//
|
||||
// Returns any errors from io.Reader, or ErrInvalidSig (use merry.Is(err,
|
||||
// mcrypto.ErrInvalidSig) to check).
|
||||
func VerifyString(s Signer, sig, in string) error {
|
||||
return VerifyBytes(s, sig, []byte(in))
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type signer struct {
|
||||
outSize uint8 // in bytes, shouldn't be more than 32, cause sha256
|
||||
secret []byte
|
||||
}
|
||||
|
||||
// NewSigner returns a Signer instance which will use the given secret to sign
|
||||
// and verify all signatures. The signatures generated by this Signer have no
|
||||
// expiration
|
||||
func NewSigner(secret []byte) Signer {
|
||||
return signer{outSize: 20, secret: secret}
|
||||
}
|
||||
|
||||
// NewWeakSigner returns a Signer, similar to how NewSigner does. The signatures
|
||||
// generated by this Signer will be smaller in text size, and therefore weaker,
|
||||
// but are still fine for most applications.
|
||||
//
|
||||
// The Signers returned by both NewSigner and NewWeakSigner can verify
|
||||
// each-other's signatures, as long as the secret is the same.
|
||||
func NewWeakSigner(secret []byte) Signer {
|
||||
return signer{outSize: 8, secret: secret}
|
||||
}
|
||||
|
||||
func (s signer) signRaw(r io.Reader) (hash.Hash, error) {
|
||||
h := hmac.New(sha256.New, s.secret)
|
||||
_, err := io.Copy(h, r)
|
||||
return h, err
|
||||
}
|
||||
|
||||
func (s signer) sign(r io.Reader) (string, error) {
|
||||
h, err := s.signRaw(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b := make([]byte, 1+h.Size())
|
||||
b[0] = s.outSize
|
||||
h.Sum(b[1:1])
|
||||
return sigV0 + hex.EncodeToString(b[:1+int(s.outSize)]), nil
|
||||
}
|
||||
|
||||
func (s signer) verify(sig string, r io.Reader) error {
|
||||
sig, ok := stripPrefix(sig, sigV0)
|
||||
if !ok || len(sig) < 2 {
|
||||
return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
sig = strings.TrimPrefix(sig, sigV0)
|
||||
|
||||
sizeStr, sig := sig[:2], sig[2:]
|
||||
sizeB, err := hex.DecodeString(sizeStr)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
size := sizeB[0]
|
||||
if hex.DecodedLen(len(sig)) != int(size) {
|
||||
return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
sigB, err := hex.DecodeString(sig)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
h, err := s.signRaw(r)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
if !hmac.Equal(sigB, h.Sum(nil)[:size]) {
|
||||
return mlog.ErrWithKV(ErrInvalidSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type expireSigner struct {
|
||||
s Signer
|
||||
timeout time.Duration
|
||||
|
||||
// only used during tests
|
||||
testNow time.Time
|
||||
}
|
||||
|
||||
// ExpireSigner wraps a Signer so that the signatures produced include timestamp
|
||||
// information about when the signature was made. That information is then used
|
||||
// during verifying to ensure the signature isn't older than the timeout.
|
||||
//
|
||||
// It is allowed to change the timeout ExpireSigner is initialized with.
|
||||
// Previously generated signatures will be verified (or rejected) using the new
|
||||
// timeout.
|
||||
func ExpireSigner(s Signer, timeout time.Duration) Signer {
|
||||
return expireSigner{s: s, timeout: timeout}
|
||||
}
|
||||
|
||||
func (es expireSigner) now() time.Time {
|
||||
if !es.testNow.IsZero() {
|
||||
return es.testNow
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (es expireSigner) sign(r io.Reader) (string, error) {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(es.now().UnixNano()))
|
||||
sig, err := es.s.sign(prefixReader(r, b))
|
||||
return exSigV0 + hex.EncodeToString(b) + sig, err
|
||||
}
|
||||
|
||||
var exSigTimeLen = hex.EncodedLen(8)
|
||||
|
||||
func (es expireSigner) verify(sig string, r io.Reader) error {
|
||||
sig, ok := stripPrefix(sig, exSigV0)
|
||||
if !ok || len(sig) < exSigTimeLen {
|
||||
return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
tStr, sig := sig[:exSigTimeLen], sig[exSigTimeLen:]
|
||||
tB, err := hex.DecodeString(tStr)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
t := time.Unix(0, int64(binary.BigEndian.Uint64(tB)))
|
||||
if es.now().Sub(t) > es.timeout {
|
||||
return mlog.ErrWithKV(ErrInvalidSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
return es.s.verify(sig, prefixReader(r, tB))
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type uniqueSigner struct {
|
||||
s Signer
|
||||
randSize uint8 // in bytes
|
||||
}
|
||||
|
||||
// UniqueSigner wraps a Signer so that when data is signed some random data is
|
||||
// included in the signed data, and that random data is included in the
|
||||
// signature as well. This ensures that even for the same input data signatures
|
||||
// produced are all unique.
|
||||
func UniqueSigner(s Signer) Signer {
|
||||
return uniqueSigner{s: s, randSize: 10}
|
||||
}
|
||||
|
||||
func (us uniqueSigner) sign(r io.Reader) (string, error) {
|
||||
b := make([]byte, 1+us.randSize)
|
||||
b[0] = us.randSize
|
||||
if _, err := rand.Read(b[1:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sig, err := us.s.sign(prefixReader(r, b[1:]))
|
||||
return uniqueSigV0 + hex.EncodeToString(b) + sig, err
|
||||
}
|
||||
|
||||
func (us uniqueSigner) verify(sig string, r io.Reader) error {
|
||||
sig, ok := stripPrefix(sig, uniqueSigV0)
|
||||
if !ok || len(sig) < 2 {
|
||||
return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
sizeStr, sig := sig[:2], sig[2:]
|
||||
sizeB, err := hex.DecodeString(sizeStr)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
size := sizeB[0]
|
||||
sizeEnc := hex.EncodedLen(int(size))
|
||||
if len(sig) < sizeEnc {
|
||||
return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
bStr, sig := sig[:sizeEnc], sig[sizeEnc:]
|
||||
b, err := hex.DecodeString(bStr)
|
||||
if err != nil {
|
||||
return mlog.ErrWithKV(err, mlog.KV{"sig": sig})
|
||||
}
|
||||
|
||||
return us.s.verify(sig, prefixReader(r, b))
|
||||
}
|
80
mcrypto/sig_test.go
Normal file
80
mcrypto/sig_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package mcrypto
|
||||
|
||||
import (
|
||||
. "testing"
|
||||
"time"
|
||||
|
||||
"github.com/ansel1/merry"
|
||||
"github.com/mediocregopher/mediocre-go-lib/mtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSigner(t *T) {
|
||||
secret := mtest.RandBytes(16)
|
||||
signer, weakSigner := NewSigner(secret), NewWeakSigner(secret)
|
||||
var prevStr, prevSig, prevWeakSig string
|
||||
for i := 0; i < 10000; i++ {
|
||||
thisStr := mtest.RandHex(512)
|
||||
thisSig := SignString(signer, thisStr)
|
||||
thisWeakSig := SignString(weakSigner, thisStr)
|
||||
|
||||
// sanity checks
|
||||
assert.NotEqual(t, thisSig, thisWeakSig)
|
||||
assert.True(t, len(thisSig) > len(thisWeakSig))
|
||||
|
||||
// Either signer should be able to verify either signature
|
||||
assert.NoError(t, VerifyString(signer, thisSig, thisStr))
|
||||
assert.NoError(t, VerifyString(weakSigner, thisWeakSig, thisStr))
|
||||
assert.NoError(t, VerifyString(signer, thisWeakSig, thisStr))
|
||||
assert.NoError(t, VerifyString(weakSigner, thisSig, thisStr))
|
||||
|
||||
if prevStr != "" {
|
||||
assert.NotEqual(t, prevSig, thisSig)
|
||||
assert.NotEqual(t, prevWeakSig, thisWeakSig)
|
||||
err := VerifyString(signer, prevSig, thisStr)
|
||||
assert.True(t, merry.Is(err, ErrInvalidSig))
|
||||
err = VerifyString(signer, prevWeakSig, thisStr)
|
||||
assert.True(t, merry.Is(err, ErrInvalidSig))
|
||||
}
|
||||
prevStr = thisStr
|
||||
prevSig = thisSig
|
||||
prevWeakSig = thisWeakSig
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpireSigner(t *T) {
|
||||
origNow := time.Now()
|
||||
s := ExpireSigner(NewSigner(mtest.RandBytes(16)), 1*time.Hour).(expireSigner)
|
||||
s.testNow = origNow
|
||||
str := mtest.RandHex(32)
|
||||
sig := SignString(s, str)
|
||||
|
||||
// in the immediate the sig should obviously work
|
||||
assert.NoError(t, VerifyString(s, sig, str))
|
||||
err := VerifyString(s, sig, mtest.RandHex(32))
|
||||
assert.True(t, merry.Is(err, ErrInvalidSig))
|
||||
|
||||
// within the timeout it should still work
|
||||
s.testNow = s.testNow.Add(1 * time.Minute)
|
||||
assert.NoError(t, VerifyString(s, sig, str))
|
||||
|
||||
// but a new "now" should then generate a different sig
|
||||
sig2 := SignString(s, str)
|
||||
assert.NotEqual(t, sig, sig2)
|
||||
assert.NoError(t, VerifyString(s, sig2, str))
|
||||
|
||||
// jumping forward an hour should expire the first sig, but not the second
|
||||
s.testNow = s.testNow.Add(1 * time.Hour)
|
||||
err = VerifyString(s, sig, str)
|
||||
assert.True(t, merry.Is(err, ErrInvalidSig))
|
||||
assert.NoError(t, VerifyString(s, sig2, str))
|
||||
}
|
||||
|
||||
func TestUniqueSigner(t *T) {
|
||||
s := UniqueSigner(NewSigner(mtest.RandBytes(16)))
|
||||
str := mtest.RandHex(32)
|
||||
sigA, sigB := SignString(s, str), SignString(s, str)
|
||||
assert.NotEqual(t, sigA, sigB)
|
||||
assert.NoError(t, VerifyString(s, sigA, str))
|
||||
assert.NoError(t, VerifyString(s, sigB, str))
|
||||
}
|
@ -13,10 +13,7 @@ import (
|
||||
"github.com/mediocregopher/mediocre-go-lib/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
uuidV0Prefix = "0u"
|
||||
uuidV0Len = 34 // prefix:2 + hexEncodedLen(time:8 + random:8)
|
||||
)
|
||||
var errMalformedUUID = errors.New("malformed UUID string")
|
||||
|
||||
// UUID is a universally unique identifier which embeds within it a timestamp.
|
||||
//
|
||||
@ -43,7 +40,7 @@ func NewUUID(t time.Time) UUID {
|
||||
panic(err)
|
||||
}
|
||||
return UUID{
|
||||
str: uuidV0Prefix + hex.EncodeToString(b),
|
||||
str: uuidV0 + hex.EncodeToString(b),
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +77,7 @@ func (u UUID) MarshalText() ([]byte, error) {
|
||||
// UnmarshalText implements the method for the encoding.TextUnmarshaler
|
||||
// interface
|
||||
func (u *UUID) UnmarshalText(b []byte) error {
|
||||
if !bytes.HasPrefix(b, []byte(uuidV0Prefix)) || len(b) != uuidV0Len {
|
||||
if !bytes.HasPrefix(b, []byte(uuidV0)) || len(b) != len(uuidV0)+32 {
|
||||
err := errors.New("malformed uuid string")
|
||||
return mlog.ErrWithKV(err, mlog.KV{"uuidStr": string(b)})
|
||||
}
|
||||
|
@ -18,8 +18,7 @@ func TestUUID(t *T) {
|
||||
this := NewUUID(thisT)
|
||||
|
||||
// basic
|
||||
assert.True(t, strings.HasPrefix(this.String(), uuidV0Prefix))
|
||||
assert.Len(t, this.String(), uuidV0Len)
|
||||
assert.True(t, strings.HasPrefix(this.String(), uuidV0))
|
||||
|
||||
// comparisons with prev
|
||||
assert.False(t, prev.Equal(this))
|
||||
@ -35,6 +34,6 @@ func TestUUID(t *T) {
|
||||
require.NoError(t, err)
|
||||
var this2 UUID
|
||||
require.NoError(t, this2.UnmarshalText(thisStr))
|
||||
assert.True(t, this.Equal(this2))
|
||||
assert.True(t, this.Equal(this2), "this:%q this2:%q", this, this2)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user