From ba5fdc873820283bfa70e6bb7d191695f9b99f64 Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Wed, 29 Sep 2021 12:18:13 +0900 Subject: [PATCH 1/2] support non integer timestamp --- provider/github-app-token/github/oidc/date.go | 56 ++++++++++++ .../github-app-token/github/oidc/date_test.go | 90 +++++++++++++++++++ .../github-app-token/github/parse_id_token.go | 28 +++--- .../github/parse_id_token_test.go | 6 +- 4 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 provider/github-app-token/github/oidc/date.go create mode 100644 provider/github-app-token/github/oidc/date_test.go diff --git a/provider/github-app-token/github/oidc/date.go b/provider/github-app-token/github/oidc/date.go new file mode 100644 index 00000000..041163ca --- /dev/null +++ b/provider/github-app-token/github/oidc/date.go @@ -0,0 +1,56 @@ +package oidc + +import ( + "math" + "math/big" + "strconv" + "time" +) + +// NumericDate represents a JSON numeric date value, as referenced at +// https://datatracker.ietf.org/doc/html/rfc7519#section-2. +type NumericDate struct { + time.Time +} + +func (date NumericDate) MarshalJSON() (b []byte, err error) { + // the maximum time.Time that in Go + const maxTime = "9223371974719179007.999999999" + + buf := make([]byte, 0, len(maxTime)) + sec := date.Unix() + buf = strconv.AppendInt(buf, sec, 10) + + if nsec := date.Nanosecond(); nsec != 0 { + buf = append(buf, '.') + digits := 100_000_000 + for nsec != 0 { + d := nsec / digits + buf = append(buf, byte('0'+d)) + nsec = nsec % digits + digits /= 10 + } + } + return buf, nil +} + +var v1_000_000_000 = new(big.Float).SetInt64(1_000_000_000) + +func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { + z := new(big.Float).SetPrec(128) + if err := z.UnmarshalText(b); err != nil { + return err + } + sec, acc := z.Int64() + if acc == big.Exact { + // z is an integer, we don't need to parse nsec. + date.Time = time.Unix(sec, 0) + return nil + } + + z = z.Sub(z, new(big.Float).SetInt64(sec)) + z = z.Mul(z, v1_000_000_000) + nsec, _ := z.Float64() + date.Time = time.Unix(sec, int64(math.RoundToEven(nsec))) + return nil +} diff --git a/provider/github-app-token/github/oidc/date_test.go b/provider/github-app-token/github/oidc/date_test.go new file mode 100644 index 00000000..92166e79 --- /dev/null +++ b/provider/github-app-token/github/oidc/date_test.go @@ -0,0 +1,90 @@ +package oidc + +import ( + "encoding/json" + "testing" + "time" +) + +func TestNumericDate_MarshalJSON(t *testing.T) { + testCases := []struct { + output string + date time.Time + }{ + { + output: "1234567890", + date: time.Unix(1234567890, 0), + }, + { + output: "1234567890.123456789", + date: time.Unix(1234567890, 123_456_789), + }, + { + output: "1234567890.123456", + date: time.Unix(1234567890, 123_456_000), + }, + { + output: "1234567890.1", + date: time.Unix(1234567890, 100_000_000), + }, + { + // the maximum time.Time that Go can marshal to JSON. + output: "253402300799.999999999", + date: time.Date(9999, time.December, 31, 23, 59, 59, 999_999_999, time.UTC), + }, + { + // the maximum time.Time that in Go + // https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go + output: "9223371974719179007.999999999", + date: time.Unix(1<<63-62135596801, 999999999), + }, + } + + for _, tc := range testCases { + got, err := json.Marshal(NumericDate{tc.date}) + if err != nil { + t.Errorf("failed to marshal %s", tc.date) + continue + } + if string(got) != tc.output { + t.Errorf("mashal %s not match: want %s, got %s", tc.date, tc.output, string(got)) + } + } +} + +func TestNumericDate_UnmarshalJSON(t *testing.T) { + testCases := []struct { + input string + date time.Time + }{ + { + input: "1234567890", + date: time.Unix(1234567890, 0), + }, + { + input: "1234567890.123456789", + date: time.Unix(1234567890, 123456789), + }, + { + // the maximum time.Time that Go can marshal to JSON. + input: "253402300799.999999999", + date: time.Date(9999, time.December, 31, 23, 59, 59, 999_999_999, time.UTC), + }, + { + // the maximum time.Time that in Go + // https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go + input: "9223371974719179007.999999999", + date: time.Unix(1<<63-62135596801, 999999999), + }, + } + + for _, tc := range testCases { + var got NumericDate + if err := json.Unmarshal([]byte(tc.input), &got); err != nil { + t.Errorf("failed parse %q: %v", tc.input, err) + } + if !got.Equal(tc.date) { + t.Errorf("the result of %q is unexpected: want %s, got %s", tc.input, tc.date, got) + } + } +} diff --git a/provider/github-app-token/github/parse_id_token.go b/provider/github-app-token/github/parse_id_token.go index 6129a437..d8cc37d0 100644 --- a/provider/github-app-token/github/parse_id_token.go +++ b/provider/github-app-token/github/parse_id_token.go @@ -5,17 +5,19 @@ import ( "errors" "fmt" "time" + + "github.com/shogo82148/actions-github-app-token/provider/github-app-token/github/oidc" ) type ActionsIDToken struct { // common jwt parameters - Audience string `json:"aud,omitempty"` - ExpiresAt int64 `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` + Audience string `json:"aud,omitempty"` + ExpiresAt *oidc.NumericDate `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt *oidc.NumericDate `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore *oidc.NumericDate `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` // GitHub's extara parameters Ref string `json:"ref,omitempty"` @@ -51,22 +53,18 @@ func (token *ActionsIDToken) Valid() error { return fmt.Errorf("github: unexpected issuer: %q", token.Issuer) } - if token.ExpiresAt == 0 { + if token.ExpiresAt == nil { return errors.New("github: the exp (expires at) parameter is not set") } - truncatedTime := now.Truncate(time.Second).Unix() - if truncatedTime >= token.ExpiresAt { + if !token.ExpiresAt.Before(now) { return errors.New("github: the token is already expired") } - if token.NotBefore == 0 { + if token.NotBefore == nil { return errors.New("github: the nbf (not before) paremeter is not set") } - // the not before parameter might be a future time, because GitHub rounds off it. - // we rounds up the current time here to accept such a case. - roundedUpTime := now.Add(time.Second - 1).Truncate(time.Second).Unix() - if roundedUpTime < token.NotBefore { + if now.Before(token.NotBefore.Time) { return errors.New("github: the token is not valid yet") } diff --git a/provider/github-app-token/github/parse_id_token_test.go b/provider/github-app-token/github/parse_id_token_test.go index 51bf8e62..82f0c1a4 100644 --- a/provider/github-app-token/github/parse_id_token_test.go +++ b/provider/github-app-token/github/parse_id_token_test.go @@ -45,9 +45,9 @@ func TestParseIDToken_Intergrated(t *testing.T) { t.Logf("sub: %s", id.Subject) t.Logf("job_workflow_ref: %s", id.JobWorkflowRef) t.Logf("aud: %s", id.Audience) - t.Logf("issued at %s", time.Unix(id.IssuedAt, 0)) - t.Logf("not before %s", time.Unix(id.NotBefore, 0)) - t.Logf("expires at %s", time.Unix(id.ExpiresAt, 0)) + t.Logf("issued at %s", id.IssuedAt) + t.Logf("not before %s", id.NotBefore) + t.Logf("expires at %s", id.ExpiresAt) if got, want := id.Actor, os.Getenv("GITHUB_ACTOR"); got != want { t.Errorf("unexpected actor: want %q, got %q", want, got) From b6cf0f572ad77488235e5c0a61386a82c497a4b0 Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Wed, 29 Sep 2021 12:33:15 +0900 Subject: [PATCH 2/2] fix expire condition --- provider/github-app-token/github/parse_id_token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/github-app-token/github/parse_id_token.go b/provider/github-app-token/github/parse_id_token.go index d8cc37d0..234e7775 100644 --- a/provider/github-app-token/github/parse_id_token.go +++ b/provider/github-app-token/github/parse_id_token.go @@ -56,7 +56,7 @@ func (token *ActionsIDToken) Valid() error { if token.ExpiresAt == nil { return errors.New("github: the exp (expires at) parameter is not set") } - if !token.ExpiresAt.Before(now) { + if token.ExpiresAt.Before(now) { return errors.New("github: the token is already expired") }