Skip to content

Commit

Permalink
Add Chainguard OIDC provider. (sigstore#1703)
Browse files Browse the repository at this point in the history
This adds support for Chainguard issued tokens, so that users can sign with their Chainguard-issued identity, and so that we can explore signing our own content with our internal service principal construct (see issue).

Related: sigstore#1702

Signed-off-by: Matt Moore <mattmoor@chainguard.dev>
  • Loading branch information
mattmoor authored and lance committed Sep 5, 2024
1 parent 1e9cd3e commit b278019
Show file tree
Hide file tree
Showing 13 changed files with 628 additions and 5 deletions.
5 changes: 5 additions & 0 deletions config/config.jsn
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"IssuerURL": "https://oidc.codefresh.io",
"ClientID": "sigstore",
"Type": "codefresh-workflow"
},
"https://issuer.enforce.dev": {
"IssuerURL": "https://issuer.enforce.dev",
"ClientID": "sigstore",
"Type": "chainguard-identity"
}
}
}
5 changes: 5 additions & 0 deletions config/fulcio-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ data:
"ClientID": "sigstore",
"Type": "gitlab-pipeline"
},
"https://issuer.enforce.dev": {
"IssuerURL": "https://issuer.enforce.dev",
"ClientID": "sigstore",
"Type": "chainguard-identity"
},
"https://oauth2.sigstore.dev/auth": {
"IssuerURL": "https://oauth2.sigstore.dev/auth",
"ClientID": "sigstore",
Expand Down
19 changes: 19 additions & 0 deletions federation/issuer.enforce.dev/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2024 The Sigstore Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

url: https://issuer.enforce.dev
# TODO(mattmoor): Change to a group.
contact: mattmoor@chainguard.dev
description: "Chainguard identity tokens"
type: "chainguard-identity"
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ toolchain go1.21.12

require (
chainguard.dev/go-grpc-kit v0.17.5
chainguard.dev/sdk v0.1.20
cloud.google.com/go/security v1.17.0
github.com/PaesslerAG/jsonpath v0.1.1
github.com/ThalesIgnite/crypto11 v1.2.5
Expand Down Expand Up @@ -144,7 +145,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
goa.design/goa v2.2.5+incompatible // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
chainguard.dev/go-grpc-kit v0.17.5 h1:y0MHgqm3v0LKKQfxPJV57wkXxa8uMSpNTjhtHbNh1DY=
chainguard.dev/go-grpc-kit v0.17.5/go.mod h1:vQGcwZiX6jXwhyLPCZwVMvjITD+XcrSmQzuCTW/XcVc=
chainguard.dev/sdk v0.1.20 h1:x46/Nd+DbfvaO4F0NEHUYIkGFa/B3wA+0KKByVNd61I=
chainguard.dev/sdk v0.1.20/go.mod h1:UO+3bmvsha1UoXxvgNnMze1kfNLuADe2WWi3AirvvxE=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
Expand Down Expand Up @@ -398,8 +400,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ const (
IssuerTypeGithubWorkflow = "github-workflow"
IssuerTypeCodefreshWorkflow = "codefresh-workflow"
IssuerTypeGitLabPipeline = "gitlab-pipeline"
IssuerTypeChainguard = "chainguard-identity"
IssuerTypeKubernetes = "kubernetes"
IssuerTypeSpiffe = "spiffe"
IssuerTypeURI = "uri"
Expand Down Expand Up @@ -517,6 +518,8 @@ func issuerToChallengeClaim(issType IssuerType, challengeClaim string) string {
return "sub"
case IssuerTypeCodefreshWorkflow:
return "sub"
case IssuerTypeChainguard:
return "sub"
case IssuerTypeKubernetes:
return "sub"
case IssuerTypeSpiffe:
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ func Test_issuerToChallengeClaim(t *testing.T) {
if claim := issuerToChallengeClaim(IssuerTypeCodefreshWorkflow, ""); claim != "sub" {
t.Fatalf("expected sub subject claim for Codefresh issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeChainguard, ""); claim != "sub" {
t.Fatalf("expected sub subject claim for Chainguard issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeKubernetes, ""); claim != "sub" {
t.Fatalf("expected sub subject claim for K8S issuer, got %s", claim)
}
Expand Down
40 changes: 40 additions & 0 deletions pkg/identity/chainguard/issuer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chainguard

import (
"context"
"fmt"

"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/identity"
"github.com/sigstore/fulcio/pkg/identity/base"
)

type issuer struct {
identity.Issuer
}

func Issuer(issuerURL string) identity.Issuer {
return &issuer{base.Issuer(issuerURL)}
}

func (e *issuer) Authenticate(ctx context.Context, token string, opts ...config.InsecureOIDCConfigOption) (identity.Principal, error) {
idtoken, err := identity.Authorize(ctx, token, opts...)
if err != nil {
return nil, fmt.Errorf("authorizing chainguard issuer: %w", err)
}
return PrincipalFromIDToken(ctx, idtoken)
}
97 changes: 97 additions & 0 deletions pkg/identity/chainguard/issuer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chainguard

import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
"unsafe"

"chainguard.dev/sdk/uidp"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/identity"
)

func TestIssuer(t *testing.T) {
ctx := context.Background()
url := "test-issuer-url"
issuer := Issuer(url)

// test the Match function
t.Run("match", func(t *testing.T) {
if matches := issuer.Match(ctx, url); !matches {
t.Fatal("expected url to match but it doesn't")
}
if matches := issuer.Match(ctx, "some-other-url"); matches {
t.Fatal("expected match to fail but it didn't")
}
})

t.Run("authenticate", func(t *testing.T) {
group := uidp.NewUIDP("")
id := group.NewChild()

token := &oidc.IDToken{
Issuer: "https://iss.example.com",
Subject: id.String(),
}
claims, err := json.Marshal(map[string]interface{}{
"iss": "https://iss.example.com",
"sub": id.String(),

// Actor claims track the identity that was used to assume the
// Chainguard identity. In this case, it is the Catalog Syncer
// service principal.
"act": map[string]string{
"iss": "https://iss.example.com/",
"sub": fmt.Sprintf("catalog-syncer:%s", group.String()),
"aud": "chainguard",
},
"internal": map[string]interface{}{
"service-principal": "CATALOG_SYNCER",
},
})
if err != nil {
t.Fatal(err)
}
withClaims(token, claims)

identity.Authorize = func(_ context.Context, _ string, _ ...config.InsecureOIDCConfigOption) (*oidc.IDToken, error) {
return token, nil
}
principal, err := issuer.Authenticate(ctx, "token")
if err != nil {
t.Fatal(err)
}

if principal.Name(ctx) != id.String() {
t.Fatalf("got unexpected name %s", principal.Name(ctx))
}
})
}

// reflect hack because "claims" field is unexported by oidc IDToken
// https://github.com/coreos/go-oidc/pull/329
func withClaims(token *oidc.IDToken, data []byte) {
val := reflect.Indirect(reflect.ValueOf(token))
member := val.FieldByName("claims")
pointer := unsafe.Pointer(member.UnsafeAddr())
realPointer := (*[]byte)(pointer)
*realPointer = data
}
80 changes: 80 additions & 0 deletions pkg/identity/chainguard/principal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package chainguard

import (
"context"
"crypto/x509"
"net/url"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/sigstore/fulcio/pkg/certificate"
"github.com/sigstore/fulcio/pkg/identity"
)

type workflowPrincipal struct {
issuer string
subject string

actor map[string]string
servicePrincipal string
}

var _ identity.Principal = (*workflowPrincipal)(nil)

func (w workflowPrincipal) Name(_ context.Context) string {
return w.subject
}

func PrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.Principal, error) {
var claims struct {
Actor map[string]string `json:"act"`
Internal struct {
ServicePrincipal string `json:"service-principal,omitempty"`
} `json:"internal"`
}

if err := token.Claims(&claims); err != nil {
return nil, err
}

return &workflowPrincipal{
issuer: token.Issuer,
subject: token.Subject,
actor: claims.Actor,
servicePrincipal: claims.Internal.ServicePrincipal,
}, nil
}

func (w workflowPrincipal) Embed(_ context.Context, cert *x509.Certificate) error {
baseURL, err := url.Parse(w.issuer)
if err != nil {
return err
}

// Set SAN to the <issuer>/<subject>
cert.URIs = []*url.URL{baseURL.JoinPath(w.subject)}

cert.ExtraExtensions, err = certificate.Extensions{
Issuer: w.issuer,

// TODO(mattmoor): Embed more of the Chainguard token structure via OIDs.
}.Render()
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit b278019

Please sign in to comment.