Skip to content

Commit

Permalink
Merge pull request #2 from junohq/fix_timestamps
Browse files Browse the repository at this point in the history
Make timestamped signatures compatible with Python
  • Loading branch information
alext authored Oct 7, 2024
2 parents 351de4d + 6200e3f commit 28100d0
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 25 deletions.
13 changes: 7 additions & 6 deletions itsdangerous.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import (
"time"
)

// 2011/01/01 in UTC
const EPOCH = 1293840000

// Encodes a single string. The resulting string is safe for putting into URLs.
func base64Encode(src []byte) string {
return base64.RawURLEncoding.EncodeToString(src)
Expand All @@ -25,8 +22,12 @@ func base64Decode(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}

// Function used to obtain the current time. Defaults to time.Now, but can be
// overridden eg for unit tests to simulate a different current time.
var NowFunc = time.Now

// Returns the current timestamp. This implementation returns the
// seconds since 1/1/2011.
func getTimestamp() uint32 {
return uint32(time.Now().Unix() - EPOCH)
// seconds since January 1, 1970 UTC.
func getTimestamp() int64 {
return NowFunc().Unix()
}
29 changes: 15 additions & 14 deletions signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,12 @@ type TimestampSignature struct {

// Sign the given string.
func (s *TimestampSignature) Sign(value string) (string, error) {
buf := new(bytes.Buffer)
tsBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tsBytes, uint64(getTimestamp()))
// trim leading zeroes
tsBytes = bytes.TrimLeft(tsBytes, "\x00")

if err := binary.Write(buf, binary.BigEndian, getTimestamp()); err != nil {
return "", err
}

ts := base64Encode(buf.Bytes())
ts := base64Encode(tsBytes)
val := value + s.Sep + ts

sig, err := s.Get(val)
Expand All @@ -160,8 +159,6 @@ func (s *TimestampSignature) Sign(value string) (string, error) {

// Unsign the given string.
func (s *TimestampSignature) Unsign(value string, maxAge uint32) (string, error) {
var timestamp uint32

result, err := s.Signature.Unsign(value)
if err != nil {
return "", err
Expand All @@ -175,18 +172,22 @@ func (s *TimestampSignature) Unsign(value string, maxAge uint32) (string, error)
li := strings.LastIndex(result, s.Sep)
val, ts := result[:li], result[li+len(s.Sep):]

sig, err := base64Decode(ts)
tsBytes, err := base64Decode(ts)
if err != nil {
return "", err
}

buf := bytes.NewReader([]byte(sig))
if err = binary.Read(buf, binary.BigEndian, &timestamp); 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; age > maxAge {
if age := getTimestamp() - timestamp; uint32(age) > maxAge {
return "", fmt.Errorf("signature age %d > %d seconds", age, maxAge)
}
}
Expand Down
100 changes: 95 additions & 5 deletions signature_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package itsdangerous

import "testing"
import (
"testing"
"time"
)

func assert(t *testing.T, actual, expected string) {
if actual != expected {
Expand All @@ -22,9 +25,96 @@ func TestSignatureUnsign(t *testing.T) {
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) {
s := NewTimestampSignature("secret-key", "", "", "", nil, nil)
expected := "my string"
actual, _ := s.Unsign("my string.BpSAPw.NnKk1nQ206g1c1aJAS1Nxkt4aug", 0)
assert(t, actual, expected)
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)
}
}
})
}
}

0 comments on commit 28100d0

Please sign in to comment.