diff --git a/cmd/apk/main.go b/cmd/apk/main.go index 569b654..30c2dae 100644 --- a/cmd/apk/main.go +++ b/cmd/apk/main.go @@ -47,13 +47,15 @@ func main() { } log.Printf("listening on %s", port) - // TODO: Auth. opt := []apk.Option{apk.WithUserAgent(userAgent)} if *auth || os.Getenv("AUTH") == "keychain" { opt = append(opt, apk.WithKeychain(gcrane.Keychain)) } if cgid := os.Getenv("CHAINGUARD_IDENTITY"); cgid != "" { - cgauth := apk.NewChainguardIdentityAuth(cgid, "https://issuer.enforce.dev", "apk.cgr.dev") + cgauth, err := apk.NewChainguardMultiKeychain(cgid, "https://issuer.enforce.dev", "apk.cgr.dev") + if err != nil { + log.Fatalf("error creating apk auth keychain: %v", err) + } opt = append(opt, apk.WithAuth(cgauth)) } if eg := os.Getenv("EXAMPLES"); eg != "" { diff --git a/cmd/oci/main.go b/cmd/oci/main.go index 009dc63..5741457 100644 --- a/cmd/oci/main.go +++ b/cmd/oci/main.go @@ -50,7 +50,10 @@ func main() { opt := []explore.Option{explore.WithUserAgent(userAgent)} kcs := []authn.Keychain{} if cgid := os.Getenv("CHAINGUARD_IDENTITY"); cgid != "" { - cgauth := explore.NewChainguardIdentityAuth(cgid, "https://issuer.enforce.dev", "cgr.dev") + cgauth, err := explore.NewChainguardMultiKeychain(cgid, "https://issuer.enforce.dev", "cgr.dev") + if err != nil { + log.Fatalf("error creating OCI auth keychain: %v", err) + } kcs = append(kcs, cgauth) } if *auth || os.Getenv("AUTH") == "keychain" { diff --git a/internal/apk/auth.go b/internal/apk/auth.go index fb600ca..d0d02d5 100644 --- a/internal/apk/auth.go +++ b/internal/apk/auth.go @@ -8,6 +8,7 @@ import ( "time" "chainguard.dev/sdk/sts" + "github.com/jonjohnsonjr/dagdotdev/internal/chainguard" "golang.org/x/time/rate" "google.golang.org/api/idtoken" ) @@ -33,6 +34,33 @@ func NewChainguardIdentityAuth(identity, issuer, audience string) Authenticator } } +// NewChainguardIdentityAuthFromURL parses a URL of the form uidp@cgr.dev?iss=issuer.enforce.dev +func NewChainguardIdentityAuthFromURL(raw string) (Authenticator, error) { + id, err := chainguard.ParseIdentity(raw) + if err != nil { + return nil, err + } + + return NewChainguardIdentityAuth(id.ID, id.Issuer, id.Audience), nil +} + +func NewChainguardMultiKeychain(raw string, defaultIssuer string, defaultAudience string) (Authenticator, error) { + var ks []Authenticator + for _, s := range strings.Split(raw, ",") { + if strings.HasPrefix(s, "chainguard://") { + k, err := NewChainguardIdentityAuthFromURL(s) + if err != nil { + return nil, fmt.Errorf("parsing %q: %w", s, err) + } + ks = append(ks, k) + } else { + // Not URL format, fallback to basic identity format. + ks = append(ks, NewChainguardIdentityAuth(s, defaultIssuer, defaultAudience)) + } + } + return NewMultiAuthenticator(ks...), nil +} + type cgAuth struct { id, iss, aud string @@ -73,3 +101,24 @@ func (a *cgAuth) AddAuth(ctx context.Context, req *http.Request) error { req.SetBasicAuth("user", a.cgtok) return nil } + +type multiAuthenticator struct { + auths []Authenticator +} + +func NewMultiAuthenticator(auth ...Authenticator) Authenticator { + return &multiAuthenticator{auths: auth} +} + +func (a *multiAuthenticator) AddAuth(ctx context.Context, req *http.Request) error { + for _, auth := range a.auths { + if err := auth.AddAuth(ctx, req); err != nil { + return err + } + if req.Header.Get("Authorization") != "" { + // Auth was set, we're done. + return nil + } + } + return nil +} diff --git a/internal/apk/auth_test.go b/internal/apk/auth_test.go new file mode 100644 index 0000000..a930fda --- /dev/null +++ b/internal/apk/auth_test.go @@ -0,0 +1,61 @@ +package apk + +import ( + "context" + "encoding/base64" + "net/http" + "net/http/httptest" + "testing" +) + +type mockAuth struct { + token string +} + +func (m *mockAuth) AddAuth(ctx context.Context, req *http.Request) error { + if m.token != "" { + req.SetBasicAuth("user", m.token) + } + return nil +} + +func TestNewMultiAuthenticator(t *testing.T) { + auth := NewMultiAuthenticator(&mockAuth{}, &mockAuth{token: "foo"}) + req := httptest.NewRequest("GET", "/", nil) + if err := auth.AddAuth(context.Background(), req); err != nil { + t.Fatalf("NewMultiAuthenticator() error = %v", err) + } + want := "Basic " + base64.StdEncoding.EncodeToString([]byte("user:foo")) + got := req.Header.Get("Authorization") + if got != want { + t.Errorf("Authorization = %v, want %v", string(got), want) + } +} + +func TestNewChainguardMultiKeychain(t *testing.T) { + _, err := NewChainguardMultiKeychain("uidp,chainguard://uidp@apk.cgr.dev?iss=issuer.enforce.dev", "foo", "bar") + if err != nil { + t.Fatal(err) + } +} + +func TestNewChainguardIdentityAuthFromURL(t *testing.T) { + auth, err := NewChainguardIdentityAuthFromURL("chainguard://uidp@apk.cgr.dev?iss=issuer.enforce.dev") + if err != nil { + t.Fatal(err) + } + cgauth, ok := auth.(*cgAuth) + if !ok { + t.Fatalf("NewChainguardIdentityAuthFromURL() = %T, want *cgAuth", auth) + } + + if cgauth.id != "uidp" { + t.Errorf("id = %v, want uidp", cgauth.id) + } + if cgauth.iss != "https://issuer.enforce.dev" { + t.Errorf("iss = %v, want https://issuer.enforce.dev", cgauth.iss) + } + if cgauth.aud != "apk.cgr.dev" { + t.Errorf("aud = %v, want apk.cgr.dev", cgauth.aud) + } +} diff --git a/internal/chainguard/auth.go b/internal/chainguard/auth.go new file mode 100644 index 0000000..824ae05 --- /dev/null +++ b/internal/chainguard/auth.go @@ -0,0 +1,35 @@ +package chainguard + +import ( + "fmt" + "net/url" + "strings" +) + +type Identity struct { + ID, Issuer, Audience string +} + +func ParseIdentity(raw string) (*Identity, error) { + u, err := url.Parse(raw) + if err != nil { + return nil, fmt.Errorf("parsing URL: %w", err) + } + + if u.Scheme != "chainguard" { + return nil, fmt.Errorf("invalid scheme %q", u.Scheme) + } + + iss := u.Query().Get("iss") + if iss == "" { + return nil, fmt.Errorf("missing issuer query parameter") + } + if !strings.HasPrefix(iss, "https://") { + iss = "https://" + iss + } + return &Identity{ + ID: u.User.Username(), + Issuer: iss, + Audience: u.Hostname(), + }, nil +} diff --git a/internal/chainguard/auth_test.go b/internal/chainguard/auth_test.go new file mode 100644 index 0000000..5ce728d --- /dev/null +++ b/internal/chainguard/auth_test.go @@ -0,0 +1,47 @@ +package chainguard + +import ( + "testing" +) + +func TestNewChainguardIdentityAuthFromURL(t *testing.T) { + cases := []struct { + rawURL string + wantErr bool + wantID string + wantIss string + wantAud string + }{ + { + rawURL: "chainguard://uidp@cgr.dev?iss=issuer.enforce.dev", + wantErr: false, + wantID: "uidp", + wantIss: "https://issuer.enforce.dev", + wantAud: "cgr.dev", + }, + { + rawURL: "invalid-url", + wantErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.rawURL, func(t *testing.T) { + got, err := ParseIdentity(tc.rawURL) + if (err != nil) != tc.wantErr { + t.Fatalf("NewChainguardIdentityAuthFromURL() error = %v, wantErr %v", err, tc.wantErr) + } + if err == nil { + if got.ID != tc.wantID { + t.Errorf("id = %v, want %v", got.ID, tc.wantID) + } + if got.Issuer != tc.wantIss { + t.Errorf("iss = %v, want %v", got.Issuer, tc.wantIss) + } + if got.Audience != tc.wantAud { + t.Errorf("aud = %v, want %v", got.Audience, tc.wantAud) + } + } + }) + } +} diff --git a/internal/explore/auth.go b/internal/explore/auth.go index 6765c63..39c91f9 100644 --- a/internal/explore/auth.go +++ b/internal/explore/auth.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "log" + "strings" "time" "chainguard.dev/sdk/sts" "github.com/google/go-containerregistry/pkg/authn" + "github.com/jonjohnsonjr/dagdotdev/internal/chainguard" "golang.org/x/time/rate" "google.golang.org/api/idtoken" ) @@ -22,6 +24,32 @@ func NewChainguardIdentityAuth(identity, issuer, audience string) authn.Keychain } } +// NewChainguardIdentityAuthFromURL parses a URL of the form uidp@cgr.dev?iss=issuer.enforce.dev +func NewChainguardIdentityAuthFromURL(raw string) (authn.Keychain, error) { + id, err := chainguard.ParseIdentity(raw) + if err != nil { + return nil, err + } + return NewChainguardIdentityAuth(id.ID, id.Issuer, id.Audience), nil +} + +func NewChainguardMultiKeychain(raw string, defaultIssuer string, defaultAudience string) (authn.Keychain, error) { + var ks []authn.Keychain + for _, s := range strings.Split(raw, ",") { + if strings.HasPrefix(s, "chainguard://") { + k, err := NewChainguardIdentityAuthFromURL(s) + if err != nil { + return nil, fmt.Errorf("parsing %q: %w", s, err) + } + ks = append(ks, k) + } else { + // Not URL format, fallback to basic identity format. + ks = append(ks, NewChainguardIdentityAuth(s, defaultIssuer, defaultAudience)) + } + } + return authn.NewMultiKeychain(ks...), nil +} + type keychain struct { id, iss, aud string @@ -42,8 +70,8 @@ func (k *keychain) ResolveContext(ctx context.Context, res authn.Resource) (auth return authn.Anonymous, nil } - if res.RegistryStr() != "cgr.dev" { - log.Printf("%q != %q", res.RegistryStr(), "cgr.dev") + if res.RegistryStr() != k.aud { + log.Printf("%q != %q", res.RegistryStr(), k.aud) return authn.Anonymous, nil } diff --git a/internal/explore/auth_test.go b/internal/explore/auth_test.go new file mode 100644 index 0000000..9962853 --- /dev/null +++ b/internal/explore/auth_test.go @@ -0,0 +1,31 @@ +package explore + +import "testing" + +func TestNewChainguardIdentityAuthFromURL(t *testing.T) { + auth, err := NewChainguardIdentityAuthFromURL("chainguard://uidp@apk.cgr.dev?iss=issuer.enforce.dev") + if err != nil { + t.Fatal(err) + } + cgauth, ok := auth.(*keychain) + if !ok { + t.Fatalf("NewChainguardIdentityAuthFromURL() = %T, want *cgAuth", auth) + } + + if cgauth.id != "uidp" { + t.Errorf("id = %v, want uidp", cgauth.id) + } + if cgauth.iss != "https://issuer.enforce.dev" { + t.Errorf("iss = %v, want https://issuer.enforce.dev", cgauth.iss) + } + if cgauth.aud != "apk.cgr.dev" { + t.Errorf("aud = %v, want apk.cgr.dev", cgauth.aud) + } +} + +func TestNewChainguardMultiKeychain(t *testing.T) { + _, err := NewChainguardMultiKeychain("uidp,chainguard://uidp@cgr.dev?iss=issuer.enforce.dev", "foo", "bar") + if err != nil { + t.Fatalf("NewChainguardMultiKeychain() error = %v", err) + } +} diff --git a/main.go b/main.go index d0d039d..b28e6e9 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,10 @@ func run(args []string) error { opt = append(opt, apk.WithKeychain(gcrane.Keychain)) } if cgid := os.Getenv("CHAINGUARD_IDENTITY"); cgid != "" { - cgauth := apk.NewChainguardIdentityAuth(cgid, "https://issuer.enforce.dev", "apk.cgr.dev") + cgauth, err := apk.NewChainguardMultiKeychain(cgid, "https://issuer.enforce.dev", "apk.cgr.dev") + if err != nil { + return fmt.Errorf("error creating apk auth keychain: %w", err) + } opt = append(opt, apk.WithAuth(cgauth)) } if eg := os.Getenv("EXAMPLES"); eg != "" { @@ -68,7 +71,10 @@ func run(args []string) error { kcs := []authn.Keychain{} if cgid := os.Getenv("CHAINGUARD_IDENTITY"); cgid != "" { log.Printf("saw CHAINGUARD_IDENTITY=%q", cgid) - cgauth := explore.NewChainguardIdentityAuth(cgid, "https://issuer.enforce.dev", "cgr.dev") + cgauth, err := explore.NewChainguardMultiKeychain(cgid, "https://issuer.enforce.dev", "cgr.dev") + if err != nil { + return fmt.Errorf("error creating OCI auth keychain: %w", err) + } kcs = append(kcs, cgauth) } if *auth || os.Getenv("AUTH") == "keychain" {