diff --git a/mcrypto/mcrypto.go b/mcrypto/mcrypto.go index 0c18d2d..458982f 100644 --- a/mcrypto/mcrypto.go +++ b/mcrypto/mcrypto.go @@ -4,11 +4,6 @@ package mcrypto import ( - "bytes" - "encoding/hex" - "fmt" - "io" - "strconv" "strings" ) @@ -23,8 +18,6 @@ import ( 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 ) @@ -32,12 +25,3 @@ 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) -} diff --git a/mcrypto/sig.go b/mcrypto/sig.go index 4e8331c..b1dec22 100644 --- a/mcrypto/sig.go +++ b/mcrypto/sig.go @@ -7,10 +7,9 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "encoding/json" "errors" - "hash" "io" - "strings" "time" "github.com/mediocregopher/mediocre-go-lib/mlog" @@ -25,23 +24,131 @@ var ( ErrInvalidSig = errors.New("invalid signature") ) +// Signature marshals/unmarshals an actual signature, produced internally by a +// Signer, along with the timestamp the signing took place and a random salt. +// +// All signatures produced in this package will have had the timestamp and salt +// included in the signature's input data, and so are also checked by the +// Verifier. +type Signature struct { + sig, salt []byte // neither of these should ever be more than 255 bytes long + t time.Time +} + +// Time returns the timestamp the Signature was generated at +func (s Signature) Time() time.Time { + return s.t +} + +func (s Signature) String() string { + // ts:8 + saltHeader:1 + salt + sigHeader:1 + sig + b := make([]byte, 10+len(s.salt)+len(s.sig)) + // It will be year 2286 before the nano doesn't fit in uint64 + binary.BigEndian.PutUint64(b, uint64(s.t.UnixNano())) + ptr := 8 + b[ptr], ptr = uint8(len(s.salt)), ptr+1 + ptr += copy(b[ptr:], s.salt) + b[ptr], ptr = uint8(len(s.sig)), ptr+1 + copy(b[ptr:], s.sig) + return sigV0 + hex.EncodeToString(b) +} + +// KV implements the method for the mlog.KVer interface +func (s Signature) KV() mlog.KV { + return mlog.KV{"sig": s.String()} +} + +// MarshalText implements the method for the encoding.TextMarshaler interface +func (s Signature) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +// UnmarshalText implements the method for the encoding.TextUnmarshaler +// interface +func (s *Signature) UnmarshalText(b []byte) error { + str := string(b) + strEnc, ok := stripPrefix(str, sigV0) + if !ok || len(strEnc) < hex.EncodedLen(10) { + return mlog.ErrWithKV(errMalformedSig, mlog.KV{"sigStr": str}) + } + + b, err := hex.DecodeString(strEnc) + if err != nil { + return mlog.ErrWithKV(err, mlog.KV{"sigStr": str}) + } + + unixNano, b := int64(binary.BigEndian.Uint64(b[:8])), b[8:] + s.t = time.Unix(0, unixNano).Local() + + readBytes := func() []byte { + if err != nil { + return nil + } else if len(b) < 1+int(b[0]) { + err = mlog.ErrWithKV(errMalformedSig, mlog.KV{"sigStr": str}) + return nil + } + out := b[1 : 1+b[0]] + b = b[1+b[0]:] + return out + } + + s.salt = readBytes() + s.sig = readBytes() + return err +} + +// MarshalJSON implements the method for the json.Marshaler interface +func (s Signature) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +// UnmarshalJSON implements the method for the json.Unmarshaler interface +func (s *Signature) UnmarshalJSON(b []byte) error { + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + return s.UnmarshalText([]byte(str)) +} + +// returns an io.Reader which will first read out information about the +// Signature which is going to be generated for the data, and then the data from +// the io.Reader itself. When used in conjunction with the Signer/Verifier's +// hashing algorithm this ensures that the other data encoded in the Signature +// (the time and salt) are also encompassed in the sig. +func sigPrefixReader(r io.Reader, sigLen uint8, salt []byte, t time.Time) io.Reader { + // ts:8 + saltHeader:1 + salt + sigLen:1 + b := make([]byte, 10+len(salt)) + binary.BigEndian.PutUint64(b, uint64(t.UnixNano())) + b[9] = uint8(len(salt)) + copy(b[9:9+len(salt)], salt) + b[9+len(salt)] = sigLen + return io.MultiReader(bytes.NewBuffer(b), r) +} + +//////////////////////////////////////////////////////////////////////////////// + // 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) + sign(io.Reader) (Signature, error) +} - // returns an error if io.Reader returns one ever, or if the signature +// Verifier is some entity which can verify Signatures produced by a Signer for +// some arbitrary data +type Verifier interface { + // returns an error if io.Reader returns one ever, or if the Signature // couldn't be verified - verify(string, io.Reader) error + verify(Signature, 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) { +func Sign(s Signer, r io.Reader) (Signature, error) { return s.sign(r) } -// SignBytes uses the Signer to generate a signature for the given []bytes -func SignBytes(s Signer, b []byte) string { +// SignBytes uses the Signer to generate a Signature for the given []bytes +func SignBytes(s Signer, b []byte) Signature { sig, err := s.sign(bytes.NewBuffer(b)) if err != nil { panic(err) @@ -49,219 +156,104 @@ func SignBytes(s Signer, b []byte) string { return sig } -// SignString uses the Signer to generate a signature for the given string -func SignString(s Signer, in string) string { +// SignString uses the Signer to generate a Signature for the given string +func SignString(s Signer, in string) Signature { 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. +// Verify reads all data from the io.Reader and uses the Verifier 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) +func Verify(v Verifier, s Signature, r io.Reader) error { + return v.verify(s, r) } -// VerifyBytes uses the Signer to verify that the signature is for the given +// VerifyBytes uses the Verifier 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)) +func VerifyBytes(v Verifier, s Signature, b []byte) error { + return v.verify(s, bytes.NewBuffer(b)) } -// VerifyString uses the Signer to verify that the signature is for the given +// VerifyString uses the Verifier 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)) +func VerifyString(v Verifier, s Signature, in string) error { + return VerifyBytes(v, s, []byte(in)) } //////////////////////////////////////////////////////////////////////////////// -type signer struct { +type signVerifier 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} +// NewSignerVerifier returns Signer and Verifier instances which will use the +// given secret to sign and verify all Signatures +func NewSignerVerifier(secret []byte) (Signer, Verifier) { + sv := signVerifier{outSize: 20, secret: secret} + return sv, sv } -func (es expireSigner) now() time.Time { - if !es.testNow.IsZero() { - return es.testNow +// NewWeakSignerVerifier returns Signer and Verifier instances, similar to how +// NewSignVerifier does. The Signatures generated by this Signer will be smaller +// in text size, and therefore weaker, but are still fine for most applications. +// +// The Verifiers returned by both NewSignVerifier and NewWeakSignVerifier can +// verify each-other's signatures, as long as the secret is the same. +func NewWeakSignerVerifier(secret []byte) (Signer, Verifier) { + sv := signVerifier{outSize: 8, secret: secret} + return sv, sv +} + +func (sv signVerifier) now() time.Time { + if !sv.testNow.IsZero() { + return sv.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}) +func (sv signVerifier) signRaw( + r io.Reader, + sigLen uint8, salt []byte, t time.Time, +) ( + []byte, error, +) { + h := hmac.New(sha256.New, sv.secret) + r = sigPrefixReader(r, sigLen, salt, t) + if _, err := io.Copy(h, r); err != nil { + return nil, err } - - 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)) + return h.Sum(nil)[:sigLen], nil } -//////////////////////////////////////////////////////////////////////////////// - -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 { +func (sv signVerifier) sign(r io.Reader) (Signature, error) { + salt := make([]byte, 8) + if _, err := rand.Read(salt); err != nil { panic(err) } - sig, err := us.s.sign(prefixReader(r, b[1:])) - return uniqueSigV0 + hex.EncodeToString(b) + sig, err + + t := sv.now() + sig, err := sv.signRaw(r, sv.outSize, salt, t) + return Signature{sig: sig, salt: salt, t: t}, 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) +func (sv signVerifier) verify(s Signature, r io.Reader) error { + sig, err := sv.signRaw(r, uint8(len(s.sig)), s.salt, s.t) if err != nil { - return mlog.ErrWithKV(err, mlog.KV{"sig": sig}) + return mlog.ErrWithKV(err, s) + } else if !hmac.Equal(sig, s.sig) { + return mlog.ErrWithKV(ErrInvalidSig, s) } - - 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)) + return nil } diff --git a/mcrypto/sig_test.go b/mcrypto/sig_test.go index 50d5713..400c211 100644 --- a/mcrypto/sig_test.go +++ b/mcrypto/sig_test.go @@ -9,31 +9,55 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSigner(t *T) { +func TestSignerVerifier(t *T) { secret := mtest.RandBytes(16) - signer, weakSigner := NewSigner(secret), NewWeakSigner(secret) - var prevStr, prevSig, prevWeakSig string + sigI, ver := NewSignerVerifier(secret) + sig := sigI.(signVerifier) + weakSigI, weakVer := NewWeakSignerVerifier(secret) + weakSig := weakSigI.(signVerifier) + var prevStr string + var prevSig, prevWeakSig Signature for i := 0; i < 10000; i++ { + now := time.Now().Round(0) + sig.testNow = now + weakSig.testNow = now + thisStr := mtest.RandHex(512) - thisSig := SignString(signer, thisStr) - thisWeakSig := SignString(weakSigner, thisStr) + thisSig := SignString(sig, thisStr) + thisWeakSig := SignString(weakSig, thisStr) + thisSigStr, thisWeakSigStr := thisSig.String(), thisWeakSig.String() + + // checking the times made it + assert.Equal(t, now, thisSig.Time()) + assert.Equal(t, now, thisWeakSig.Time()) // sanity checks - assert.NotEqual(t, thisSig, thisWeakSig) - assert.True(t, len(thisSig) > len(thisWeakSig)) + assert.NotEmpty(t, thisSigStr) + assert.NotEmpty(t, thisWeakSigStr) + assert.NotEqual(t, thisSigStr, thisWeakSigStr) + assert.True(t, len(thisSigStr) > len(thisWeakSigStr)) - // 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)) + // marshaling/unmarshaling + var thisSig2, thisWeakSig2 Signature + assert.NoError(t, thisSig2.UnmarshalText([]byte(thisSigStr))) + assert.Equal(t, thisSigStr, thisSig2.String()) + assert.NoError(t, thisWeakSig2.UnmarshalText([]byte(thisWeakSigStr))) + assert.Equal(t, thisWeakSigStr, thisWeakSig2.String()) + assert.Equal(t, now, thisSig2.Time()) + assert.Equal(t, now, thisWeakSig2.Time()) + + // Either sigVer should be able to verify either signature + assert.NoError(t, VerifyString(ver, thisSig, thisStr)) + assert.NoError(t, VerifyString(weakVer, thisWeakSig, thisStr)) + assert.NoError(t, VerifyString(ver, thisWeakSig, thisStr)) + assert.NoError(t, VerifyString(weakVer, thisSig, thisStr)) if prevStr != "" { - assert.NotEqual(t, prevSig, thisSig) - assert.NotEqual(t, prevWeakSig, thisWeakSig) - err := VerifyString(signer, prevSig, thisStr) + assert.NotEqual(t, prevSig.String(), thisSigStr) + assert.NotEqual(t, prevWeakSig.String(), thisWeakSigStr) + err := VerifyString(ver, prevSig, thisStr) assert.True(t, merry.Is(err, ErrInvalidSig)) - err = VerifyString(signer, prevWeakSig, thisStr) + err = VerifyString(ver, prevWeakSig, thisStr) assert.True(t, merry.Is(err, ErrInvalidSig)) } prevStr = thisStr @@ -41,40 +65,3 @@ func TestSigner(t *T) { 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)) -} diff --git a/mcrypto/uuid.go b/mcrypto/uuid.go index 9308330..790a2e1 100644 --- a/mcrypto/uuid.go +++ b/mcrypto/uuid.go @@ -78,8 +78,7 @@ func (u UUID) MarshalText() ([]byte, error) { // interface func (u *UUID) UnmarshalText(b []byte) error { 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)}) + return mlog.ErrWithKV(errMalformedUUID, mlog.KV{"uuidStr": string(b)}) } u.str = string(b) return nil