Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/pre-submit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Pre submits
on: [pull_request, workflow_dispatch]

permissions: read-all

jobs:
pre-submit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.3.4

- name: setup-go
uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2.2.0
with:
go-version: "1.18"

- name: Run tests
Comment thread
ianlewis marked this conversation as resolved.
run: |
set -euo pipefail

# Download dependencies.
go mod vendor
# Test.
go test -mod=vendor -v ./...
23 changes: 16 additions & 7 deletions github/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package github
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
Expand All @@ -34,24 +35,32 @@ type OIDCToken struct {
JobWorkflowRef string `json:"job_workflow_ref"`
}

var (
errURLEnvKeyEmpty = fmt.Errorf("%q env var is empty", requestURLEnvKey)
errResponseJSON = errors.New("invalid response JSON")
errInvalidToken = errors.New("invalid JWT token")
errInvalidTokenB64 = errors.New("invalid JWT token base64")
errInvalidTokenJSON = errors.New("invalid JWT token JSON")
)

// RequestOIDCToken requests an OIDC token from Github's provider and returns
// the token.
func RequestOIDCToken(audience string) (*OIDCToken, error) {
urlKey := os.Getenv(requestURLEnvKey)
if urlKey == "" {
return nil, fmt.Errorf("requestURLEnvKey is empty")
return nil, errURLEnvKeyEmpty
}

url := urlKey + "&audience=" + audience
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("creating request: %w", err)
}

req.Header.Add("Authorization", "bearer "+os.Getenv(requestTokenEnvKey))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("request: %w", err)
}
defer resp.Body.Close()

Expand All @@ -62,26 +71,26 @@ func RequestOIDCToken(audience string) (*OIDCToken, error) {
// Extract the value from JSON payload.
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&payload); err != nil {
return nil, err
return nil, errResponseJSON
}

// This is a JWT token with 3 parts.
parts := strings.Split(payload.Value, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid jwt token: found %d parts", len(parts))
return nil, errInvalidToken
}

content := parts[1]

// Base64-decode the content.
token, err := base64.RawURLEncoding.DecodeString(content)
if err != nil {
return nil, fmt.Errorf("base64.RawURLEncoding.DecodeString: %w", err)
return nil, errInvalidTokenB64
}

var oidc OIDCToken
if err := json.Unmarshal(token, &oidc); err != nil {
return nil, fmt.Errorf("json.Unmarshal: %w", err)
return nil, errInvalidTokenJSON
}

return &oidc, nil
Expand Down
71 changes: 71 additions & 0 deletions github/oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package github

import (
"encoding/base64"
"errors"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func TestRequestOIDCToken(t *testing.T) {
testCases := []struct {
name string
audience string
expected *OIDCToken
raw string
err error
}{
{
name: "basic token",
audience: "hoge",
expected: &OIDCToken{JobWorkflowRef: "hoge"},
},
{
name: "invalid response",
audience: "hoge",
raw: `not json`,
err: errResponseJSON,
},
{
name: "invalid parts",
audience: "hoge",
raw: `{"value": "part1"}`,
err: errInvalidToken,
},
{
name: "invalid base64",
audience: "hoge",
raw: `{"value": "part1.part2.part3"}`,
err: errInvalidTokenB64,
},
{
name: "invalid json",
audience: "hoge",
raw: fmt.Sprintf(`{"value": "part1.%s.part3"}`, base64.RawURLEncoding.EncodeToString([]byte("not json"))),
err: errInvalidTokenJSON,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.expected != nil {
_, stop := NewTestOIDCServer(tc.expected)
defer stop()
} else {
_, stop := newRawTestOIDCServer(tc.raw)
defer stop()
}

token, err := RequestOIDCToken(tc.audience)
if !errors.Is(err, tc.err) {
t.Fatalf("unexpected error: %v", cmp.Diff(err, tc.err, cmpopts.EquateErrors()))
}
if want, got := tc.expected, token; !cmp.Equal(want, got) {
t.Errorf("unexpected workflow ref\nwant: %#v\ngot: %#v\ndiff: %#v", want, got, cmp.Diff(want, got))
}
})
}
}
41 changes: 41 additions & 0 deletions github/oidctest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package github

import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
)

// NewTestOIDCServer returns an httptest.Server that can be used as the OIDC
// server in tests and a cleanup function that can be used to stop and clean up
// the server. The server returns the given token when queried.
func NewTestOIDCServer(t *OIDCToken) (*httptest.Server, func()) {
b, err := json.Marshal(t)
if err != nil {
panic(err)
}

rawResponse := fmt.Sprintf(`{"value": "part1.%s.part3"}`, base64.RawURLEncoding.EncodeToString(b))
return newRawTestOIDCServer(rawResponse)
}

func newRawTestOIDCServer(raw string) (*httptest.Server, func()) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Respond with a very basic 3-part JWT token.
fmt.Fprintln(w, raw)
}))
oldEnv, ok := os.LookupEnv(requestURLEnvKey)
// NOTE: httptest.Server.URL has no trailing slash.
os.Setenv(requestURLEnvKey, s.URL+"/")
return s, func() {
s.Close()
if ok {
os.Setenv(requestURLEnvKey, oldEnv)
} else {
os.Unsetenv(requestURLEnvKey)
}
}
}
51 changes: 51 additions & 0 deletions slsa/provenance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package slsa

import (
"reflect"
"testing"

intoto "github.com/in-toto/in-toto-golang/in_toto"
slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"

"github.com/slsa-framework/slsa-github-generator/github"
)

func TestHostedActionsProvenance(t *testing.T) {
testCases := []struct {
name string
r WorkflowRun
expected *intoto.ProvenanceStatement
}{
{
name: "empty",
r: WorkflowRun{},
expected: &intoto.ProvenanceStatement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: slsa.PredicateSLSAProvenance,
},
Predicate: slsa.ProvenancePredicate{
Builder: slsa.ProvenanceBuilder{
ID: GithubHostedActionsBuilderID,
},
Metadata: &slsa.ProvenanceMetadata{},
},
},
},
}

for _, tc := range testCases {
_, stop := github.NewTestOIDCServer(nil)
defer stop()

t.Run(tc.name, func(t *testing.T) {
if p, err := HostedActionsProvenance(tc.r); err != nil {
t.Fatalf("unexpected error: %v", err)
} else {
if want, got := tc.expected, p; !reflect.DeepEqual(want, got) {
t.Errorf("unexpected result, want: %#v, got: %#v", want, got)
}
}
})
}
}