diff --git a/algorithm.go b/algorithm.go index 35a7fa1..8157d90 100644 --- a/algorithm.go +++ b/algorithm.go @@ -2,14 +2,13 @@ package itsdangerous import ( "crypto/hmac" - "crypto/subtle" "hash" ) // SigningAlgorithm provides interfaces to generate and verify signature type SigningAlgorithm interface { - GetSignature(key, value string) []byte - VerifySignature(key, value string, sig []byte) bool + GetSignature(key []byte, value string) []byte + VerifySignature(key []byte, value string, signature []byte) bool } // HMACAlgorithm provides signature generation using HMACs. @@ -18,15 +17,16 @@ type HMACAlgorithm struct { } // GetSignature returns the signature for the given key and value. -func (a *HMACAlgorithm) GetSignature(key, value string) []byte { - a.DigestMethod().Reset() - h := hmac.New(func() hash.Hash { return a.DigestMethod() }, []byte(key)) +func (a *HMACAlgorithm) GetSignature(key []byte, value string) []byte { + h := hmac.New(a.DigestMethod, key) h.Write([]byte(value)) return h.Sum(nil) } // VerifySignature verifies the given signature matches the expected signature. -func (a *HMACAlgorithm) VerifySignature(key, value string, sig []byte) bool { - eq := subtle.ConstantTimeCompare(sig, []byte(a.GetSignature(key, value))) - return eq == 1 +func (a *HMACAlgorithm) VerifySignature(key []byte, value string, signature []byte) bool { + return hmac.Equal( + signature, + a.GetSignature(key, value), + ) } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..011f91c --- /dev/null +++ b/errors.go @@ -0,0 +1,22 @@ +package itsdangerous + +import "fmt" + +type InvalidSignatureError struct { + err error +} + +func (e InvalidSignatureError) Error() string { return e.err.Error() } +func (e InvalidSignatureError) Unwrap() error { return e.err } + +type SignatureExpiredError struct { + age, maxAge int64 +} + +func (e SignatureExpiredError) Error() string { + return fmt.Sprintf("signature age %d > %d seconds", e.age, e.maxAge) +} + +func signatureExpired(age, maxAge int64) error { + return InvalidSignatureError{SignatureExpiredError{age: age, maxAge: maxAge}} +} diff --git a/python_examples/generate_examples.py b/python_examples/generate_examples.py new file mode 100644 index 0000000..f7a2908 --- /dev/null +++ b/python_examples/generate_examples.py @@ -0,0 +1,18 @@ +from freezegun import freeze_time +from itsdangerous import Signer, TimestampSigner + +key = "secret_key" +salt = "salt" + +print(f"Signer examples {key=} {salt=}") +s = Signer(key, salt) +print(" 'my string' ->", s.sign("my string")) +print(" 'aaaaaaaaaaaaaaaa' ->", s.sign("aaaaaaaaaaaaaaaa")) +print() + +print(f"TimestampSigner examples {key=} {salt=}") +s = TimestampSigner(key, salt) +with freeze_time("2024-09-27T14:00:00Z"): + print(" 'my string' ->", s.sign("my string"), "at time 2024-09-27T14:00:00Z") +with freeze_time("2024-09-27T15:00:00Z"): + print(" 'my string' ->", s.sign("my string"), "at time 2024-09-27T15:00:00Z") diff --git a/signature.go b/signature.go deleted file mode 100644 index 268e081..0000000 --- a/signature.go +++ /dev/null @@ -1,201 +0,0 @@ -package itsdangerous - -import ( - "bytes" - "crypto/hmac" - "crypto/sha1" - "encoding/binary" - "errors" - "fmt" - "hash" - "strings" -) - -// Signature can sign bytes and unsign it and validate the signature -// provided. -// -// Salt can be used to namespace the hash, so that a signed string is only -// valid for a given namespace. Leaving this at the default value or re-using -// a salt value across different parts of your application where the same -// signed value in one part can mean something different in another part -// is a security risk. -type Signature struct { - SecretKey string - Sep string - Salt string - KeyDerivation string - DigestMethod func() hash.Hash - Algorithm SigningAlgorithm -} - -// DeriveKey generates a key derivation. Keep in mind that the key derivation in itsdangerous -// is not intended to be used as a security method to make a complex key out of a short password. -// Instead you should use large random secret keys. -func (s *Signature) DeriveKey() (string, error) { - var key string - var err error - - s.DigestMethod().Reset() - - switch s.KeyDerivation { - case "concat": - h := s.DigestMethod() - h.Write([]byte(s.Salt + s.SecretKey)) - key = string(h.Sum(nil)) - case "django-concat": - h := s.DigestMethod() - h.Write([]byte(s.Salt + "signer" + s.SecretKey)) - key = string(h.Sum(nil)) - case "hmac": - h := hmac.New(func() hash.Hash { return s.DigestMethod() }, []byte(s.SecretKey)) - h.Write([]byte(s.Salt)) - key = string(h.Sum(nil)) - case "none": - key = s.SecretKey - default: - key, err = "", errors.New("unknown key derivation method") - } - return key, err -} - -// Get returns the signature for the given value. -func (s *Signature) Get(value string) (string, error) { - key, err := s.DeriveKey() - if err != nil { - return "", err - } - - sig := s.Algorithm.GetSignature(key, value) - return base64Encode(sig), err -} - -// Verify verifies the signature for the given value. -func (s *Signature) Verify(value, sig string) (bool, error) { - key, err := s.DeriveKey() - if err != nil { - return false, err - } - - signed, err := base64Decode(sig) - if err != nil { - return false, err - } - return s.Algorithm.VerifySignature(key, value, signed), nil -} - -// Sign the given string. -func (s *Signature) Sign(value string) (string, error) { - sig, err := s.Get(value) - if err != nil { - return "", err - } - return value + s.Sep + sig, nil -} - -// Unsign the given string. -func (s *Signature) Unsign(signed string) (string, error) { - if !strings.Contains(signed, s.Sep) { - return "", fmt.Errorf("no %s found in value", s.Sep) - } - - li := strings.LastIndex(signed, s.Sep) - value, sig := signed[:li], signed[li+len(s.Sep):] - - if ok, _ := s.Verify(value, sig); ok == true { - return value, nil - } - return "", fmt.Errorf("signature %s does not match", sig) -} - -// NewSignature creates a new Signature -func NewSignature(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) *Signature { - if salt == "" { - salt = "itsdangerous.Signer" - } - if sep == "" { - sep = "." - } - if derivation == "" { - derivation = "django-concat" - } - if digest == nil { - digest = sha1.New - } - if algo == nil { - algo = &HMACAlgorithm{DigestMethod: digest} - } - return &Signature{ - SecretKey: secret, - Salt: salt, - Sep: sep, - KeyDerivation: derivation, - DigestMethod: digest, - Algorithm: algo, - } -} - -// TimestampSignature works like the regular Signature but also records the time -// of the signing and can be used to expire signatures. -type TimestampSignature struct { - Signature -} - -// Sign the given string. -func (s *TimestampSignature) Sign(value string) (string, error) { - tsBytes := make([]byte, 8) - binary.BigEndian.PutUint64(tsBytes, uint64(getTimestamp())) - // trim leading zeroes - tsBytes = bytes.TrimLeft(tsBytes, "\x00") - - ts := base64Encode(tsBytes) - val := value + s.Sep + ts - - sig, err := s.Get(val) - if err != nil { - return "", err - } - return val + s.Sep + sig, nil -} - -// Unsign the given string. -func (s *TimestampSignature) Unsign(value string, maxAge uint32) (string, error) { - result, err := s.Signature.Unsign(value) - if err != nil { - return "", err - } - - // If there is no timestamp in the result there is something seriously wrong. - if !strings.Contains(result, s.Sep) { - return "", errors.New("timestamp missing") - } - - li := strings.LastIndex(result, s.Sep) - val, ts := result[:li], result[li+len(s.Sep):] - - tsBytes, err := base64Decode(ts) - if err != nil { - return "", err - } - // left pad up to 8 bytes - if len(tsBytes) < 8 { - tsBytes = append( - make([]byte, 8-len(tsBytes)), - tsBytes..., - ) - } - - var timestamp = int64(binary.BigEndian.Uint64(tsBytes)) - - if maxAge > 0 { - if age := getTimestamp() - timestamp; uint32(age) > maxAge { - return "", fmt.Errorf("signature age %d > %d seconds", age, maxAge) - } - } - return val, nil -} - -// NewTimestampSignature creates a new TimestampSignature -func NewTimestampSignature(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) *TimestampSignature { - s := NewSignature(secret, salt, sep, derivation, digest, algo) - return &TimestampSignature{Signature: *s} -} diff --git a/signature_test.go b/signature_test.go deleted file mode 100644 index 5e39044..0000000 --- a/signature_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package itsdangerous - -import ( - "testing" - "time" -) - -func assert(t *testing.T, actual, expected string) { - if actual != expected { - t.Errorf("expecting %s, got %s instead", expected, actual) - } -} - -func TestSignatureSign(t *testing.T) { - s := NewSignature("secret-key", "", "", "", nil, nil) - expected := "my string.wh6tMHxLgJqB6oY1uT73iMlyrOA" - actual, _ := s.Sign("my string") - assert(t, actual, expected) -} - -func TestSignatureUnsign(t *testing.T) { - s := NewSignature("secret-key", "", "", "", nil, nil) - expected := "my string" - actual, _ := s.Unsign("my string.wh6tMHxLgJqB6oY1uT73iMlyrOA") - assert(t, actual, expected) -} - -/* -Examples generated in Python as follows: - from freezegun import freeze_time - from itsdangerous import TimestampSigner - with freeze_time("2024-09-27T14:00:00Z"): - s = TimestampSigner("secret_key", "salt") - print(s.sign("my string")) -*/ - -func TestTimestampSignatureSign(t *testing.T) { - tests := []struct { - input string - now time.Time - expected string - }{ - {input: "my string", now: time.Date(2024, 9, 27, 14, 0, 0, 0, time.UTC), - expected: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU"}, - {input: "my string", now: time.Date(2024, 9, 27, 15, 0, 0, 0, time.UTC), - expected: "my string.ZvbIcA.VVQqPkaZ-YQaLHomuudMzTiw45Q"}, - // Test with timestamp > 4 bytes - {input: "my string", now: time.Date(2124, 9, 27, 15, 0, 0, 0, time.UTC), - expected: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY"}, - } - for _, test := range tests { - test := test - t.Run(test.input, func(t *testing.T) { - if !test.now.IsZero() { - NowFunc = func() time.Time { return test.now } - defer func() { NowFunc = time.Now }() - } - - sig := NewTimestampSignature("secret_key", "salt", "", "", nil, nil) - - actual, err := sig.Sign(test.input) - if err != nil { - t.Fatalf("Sign(%s) returned error: %s", test.input, err) - } - if actual != test.expected { - t.Errorf("Sign(%s) got %#v; want %#v", test.input, actual, test.expected) - } - }) - } -} - -func TestTimestampSignatureUnsign(t *testing.T) { - tests := []struct { - input string - expected string - now time.Time - maxAge uint32 - expectError bool - }{ - // Signature within maxAge - {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", - now: time.Date(2024, 9, 27, 14, 4, 59, 0, time.UTC), maxAge: 5 * 60}, - // signature expired - {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expectError: true, - now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 5 * 60}, - // maxAge zero always validates - {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", - now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 0}, - // Test with timestamp > 4 bytes - {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expected: "my string", - now: time.Date(2124, 9, 27, 15, 4, 59, 0, time.UTC), maxAge: 5 * 60}, - {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expectError: true, - now: time.Date(2124, 9, 27, 15, 5, 1, 0, time.UTC), maxAge: 5 * 60}, - } - for _, test := range tests { - test := test - t.Run(test.input, func(t *testing.T) { - if !test.now.IsZero() { - NowFunc = func() time.Time { return test.now } - defer func() { NowFunc = time.Now }() - } - - sig := NewTimestampSignature("secret_key", "salt", "", "", nil, nil) - - actual, err := sig.Unsign(test.input, test.maxAge) - if test.expectError { - if err == nil { - t.Fatalf("Unsign(%s) expected error; got no error", test.input) - } - } else { - if err != nil { - t.Fatalf("Unsign(%s) returned error: %s", test.input, err) - } - if actual != test.expected { - t.Errorf("Unsign(%s) got %#v; want %#v", test.input, actual, test.expected) - } - } - }) - } -} diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..4f1b1bf --- /dev/null +++ b/signer.go @@ -0,0 +1,202 @@ +package itsdangerous + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/binary" + "errors" + "fmt" + "hash" + "strings" + "time" +) + +// Signer can sign bytes and unsign it and validate the signature +// provided. +// +// Salt can be used to namespace the hash, so that a signed string is only +// valid for a given namespace. Leaving this at the default value or re-using +// a salt value across different parts of your application where the same +// signed value in one part can mean something different in another part +// is a security risk. +type Signer struct { + sep string + key []byte + algorithm SigningAlgorithm +} + +// NewSigner creates a new Signer with the given secret and salt. All other +// properties will be set to match the Python itsdangerous defaults. +func NewSigner(secret, salt string) *Signer { + s, err := NewSignerWithOptions(secret, salt, "", "", nil, nil) + if err != nil { + // This shouldn't be possible with default arguments. + panic(err) + } + return s +} + +// NewSignerWithOptions creates a new Signer allowing overiding the default +// properties. +func NewSignerWithOptions(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) (*Signer, error) { + if salt == "" { + salt = "itsdangerous.Signer" + } + if sep == "" { + sep = "." + } + if derivation == "" { + derivation = "django-concat" + } + if digest == nil { + digest = sha1.New + } + if algo == nil { + algo = &HMACAlgorithm{DigestMethod: digest} + } + s := &Signer{ + sep: sep, + algorithm: algo, + } + var err error + s.key, err = deriveKey(secret, salt, derivation, digest) + return s, err +} + +// deriveKey generates a key derivation. Keep in mind that the key derivation in itsdangerous +// is not intended to be used as a security method to make a complex key out of a short password. +// Instead you should use large random secret keys. +func deriveKey(secretKey, salt, keyDerivation string, digestMethod func() hash.Hash) ([]byte, error) { + var key []byte + var err error + + switch keyDerivation { + case "concat": + h := digestMethod() + h.Write([]byte(salt + secretKey)) + key = h.Sum(nil) + case "django-concat": + h := digestMethod() + h.Write([]byte(salt + "signer" + secretKey)) + key = h.Sum(nil) + case "hmac": + h := hmac.New(digestMethod, []byte(secretKey)) + h.Write([]byte(salt)) + key = h.Sum(nil) + case "none": + key = []byte(secretKey) + default: + err = errors.New("unknown key derivation method " + keyDerivation) + } + return key, err +} + +// getSignature returns the signature for the given value. +func (s *Signer) getSignature(value string) string { + sig := s.algorithm.GetSignature(s.key, value) + return base64Encode(sig) +} + +// verifySignature verifies the signature for the given value. +func (s *Signer) verifySignature(value, signature string) (bool, error) { + signed, err := base64Decode(signature) + if err != nil { + return false, err + } + return s.algorithm.VerifySignature(s.key, value, signed), nil +} + +// Sign the given string. +func (s *Signer) Sign(value string) string { + sig := s.getSignature(value) + return value + s.sep + sig +} + +// Unsign the given string. +func (s *Signer) Unsign(signed string) (string, error) { + li := strings.LastIndex(signed, s.sep) + if li < 0 { + return "", InvalidSignatureError{fmt.Errorf("no %s found in value", s.sep)} + } + value, sig := signed[:li], signed[li+len(s.sep):] + + if ok, _ := s.verifySignature(value, sig); ok == true { + return value, nil + } + return "", InvalidSignatureError{fmt.Errorf("signature does not match")} +} + +// TimestampSigner works like the regular Signer but also records the time +// of the signing and can be used to expire signatures. +type TimestampSigner struct { + Signer +} + +// NewTimestampSigner creates a new TimestampSigner with the given secret and +// salt. All other properties will be set to match the Python itsdangerous +// defaults. +func NewTimestampSigner(secret, salt string) *TimestampSigner { + s := NewSigner(secret, salt) + return &TimestampSigner{Signer: *s} +} + +// NewTimestampSignerWithOptions creates a new TimestampSigner allowing +// overiding the default properties. +func NewTimestampSignerWithOptions(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) (*TimestampSigner, error) { + s, err := NewSignerWithOptions(secret, salt, sep, derivation, digest, algo) + if err != nil { + return nil, err + } + return &TimestampSigner{Signer: *s}, nil +} + +// Sign the given string. +func (s *TimestampSigner) Sign(value string) string { + tsBytes := make([]byte, 8) + binary.BigEndian.PutUint64(tsBytes, uint64(getTimestamp())) + // trim leading zeroes + tsBytes = bytes.TrimLeft(tsBytes, "\x00") + + ts := base64Encode(tsBytes) + val := value + s.sep + ts + + return s.Signer.Sign(val) +} + +// Unsign the given string. +func (s *TimestampSigner) Unsign(value string, maxAge time.Duration) (string, error) { + result, err := s.Signer.Unsign(value) + if err != nil { + return "", err + } + + li := strings.LastIndex(result, s.sep) + if li < 0 { + // If there is no timestamp in the result there is something seriously wrong. + return "", InvalidSignatureError{errors.New("timestamp missing")} + } + val, ts := result[:li], result[li+len(s.sep):] + + tsBytes, err := base64Decode(ts) + if err != nil { + return "", err + } + // left pad up to 8 bytes + if len(tsBytes) < 8 { + tsBytes = append( + make([]byte, 8-len(tsBytes)), + tsBytes..., + ) + } + + var timestamp = int64(binary.BigEndian.Uint64(tsBytes)) + + if maxAge > 0 { + maxAgeSecs := int64(maxAge.Seconds()) + if age := getTimestamp() - timestamp; age > maxAgeSecs { + return "", signatureExpired(age, maxAgeSecs) + } + } + return val, nil +} diff --git a/signer_test.go b/signer_test.go new file mode 100644 index 0000000..4d67c17 --- /dev/null +++ b/signer_test.go @@ -0,0 +1,166 @@ +package itsdangerous_test + +import ( + "errors" + "testing" + "time" + + "github.com/junohq/go-itsdangerous" +) + +// Example values here generated from Python using generate_examples.py script + +func TestSignerSign(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {input: "my string", expected: "my string.xv0r21ogoygusbkJA01c4OxsAio"}, + {input: "aaaaaaaaaaaaaaaa", expected: "aaaaaaaaaaaaaaaa.Ot23yopX-I7Y6_e0hoZg6VKAcLk"}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + sig := itsdangerous.NewSigner("secret_key", "salt") + + actual := sig.Sign(test.input) + if actual != test.expected { + t.Errorf("Sign(%s) got %s; want %s", test.input, actual, test.expected) + } + }) + } +} + +func TestSignerUnsign(t *testing.T) { + tests := []struct { + input string + expected string + expectError bool + }{ + {input: "my string.xv0r21ogoygusbkJA01c4OxsAio", expected: "my string"}, + {input: "altered string.xv0r21ogoygusbkJA01c4OxsAio", expectError: true}, + // missing separator + {input: "my stringxv0r21ogoygusbkJA01c4OxsAio", expectError: true}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + sig := itsdangerous.NewSigner("secret_key", "salt") + + actual, err := sig.Unsign(test.input) + if test.expectError { + if err == nil { + t.Fatalf("Unsign(%s) expected error; got no error", test.input) + } + if !errors.As(err, &itsdangerous.InvalidSignatureError{}) { + t.Fatalf("Unsign(%s) expected InvalidSignatureError; got %T(%s)", test.input, err, err.Error()) + } + } else { + if err != nil { + t.Fatalf("Unsign(%s) returned error: %s", test.input, err) + } + if actual != test.expected { + t.Errorf("Unsign(%s) got %s; want %s", test.input, actual, test.expected) + } + } + }) + } +} + +func TestTimestampSignerSign(t *testing.T) { + tests := []struct { + input string + now time.Time + expected string + }{ + {input: "my string", now: time.Date(2024, 9, 27, 14, 0, 0, 0, time.UTC), + expected: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU"}, + {input: "my string", now: time.Date(2024, 9, 27, 15, 0, 0, 0, time.UTC), + expected: "my string.ZvbIcA.VVQqPkaZ-YQaLHomuudMzTiw45Q"}, + // Test with timestamp > 4 bytes + {input: "my string", now: time.Date(2124, 9, 27, 15, 0, 0, 0, time.UTC), + expected: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY"}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + if !test.now.IsZero() { + itsdangerous.NowFunc = func() time.Time { return test.now } + defer func() { itsdangerous.NowFunc = time.Now }() + } + + sig := itsdangerous.NewTimestampSigner("secret_key", "salt") + + actual := sig.Sign(test.input) + if actual != test.expected { + t.Errorf("Sign(%s) got %#v; want %#v", test.input, actual, test.expected) + } + }) + } +} + +func TestTimestampSignerUnsign(t *testing.T) { + tests := []struct { + input string + expected string + now time.Time + maxAge time.Duration + expectError bool + expectExpired bool + }{ + // Signature within maxAge + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", + now: time.Date(2024, 9, 27, 14, 4, 59, 0, time.UTC), maxAge: 5 * time.Minute}, + // signature expired + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expectError: true, expectExpired: true, + now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 5 * time.Minute}, + // maxAge zero always validates + {input: "my string.Zva6YA.aqBNzGvNEDkO6RGFPEX1HIhz0vU", expected: "my string", + now: time.Date(2024, 9, 27, 14, 5, 1, 0, time.UTC), maxAge: 0}, + // Test with timestamp > 4 bytes + {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expected: "my string", + now: time.Date(2124, 9, 27, 15, 4, 59, 0, time.UTC), maxAge: 5 * time.Minute}, + {input: "my string.ASMOinA.eGqsFVFmYbv8t7tXD8PX7LHSXdY", expectError: true, expectExpired: true, + now: time.Date(2124, 9, 27, 15, 5, 1, 0, time.UTC), maxAge: 5 * time.Minute}, + // Test with missing timestamp + {input: "my string.xv0r21ogoygusbkJA01c4OxsAio", expectError: true, + now: time.Date(2024, 9, 27, 14, 4, 59, 0, time.UTC), maxAge: 5 * time.Minute}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + if !test.now.IsZero() { + itsdangerous.NowFunc = func() time.Time { return test.now } + defer func() { itsdangerous.NowFunc = time.Now }() + } + + sig := itsdangerous.NewTimestampSigner("secret_key", "salt") + + actual, err := sig.Unsign(test.input, test.maxAge) + if test.expectError { + if err == nil { + t.Fatalf("Unsign(%s) expected error; got no error", test.input) + } + if !errors.As(err, &itsdangerous.InvalidSignatureError{}) { + t.Fatalf("Unsign(%s) expected InvalidSignatureError; got %T(%s)", test.input, err, err.Error()) + } + if test.expectExpired { + if !errors.As(err, &itsdangerous.SignatureExpiredError{}) { + t.Fatalf("Unsign(%s) expected SignatureExpiredError; got %T(%s)", test.input, err, err.Error()) + } + } else { + if errors.As(err, &itsdangerous.SignatureExpiredError{}) { + t.Fatalf("Unsign(%s) expected not to get a SignatureExpiredError; got %s", test.input, err.Error()) + } + } + } else { + if err != nil { + t.Fatalf("Unsign(%s) returned error: %s", test.input, err) + } + if actual != test.expected { + t.Errorf("Unsign(%s) got %#v; want %#v", test.input, actual, test.expected) + } + } + }) + } +}