Repository: sigstore/fulcio
Pull request #1712 (2024-06-24 22:13:51)

Bump github.com/hashicorp/go-retryablehttp from 0.7.6 to 0.7.7 in the go_modules group
Bumps the go_modules group with 1 update: [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp).

Updates `github.com/hashicorp/go-retryablehttp` from 0.7.6 to 0.7.7
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/hashicorp/go-retryablehttp/blob/main/CHANGELOG.md">github.com/hashicorp/go-retryablehttp's changelog</a>.</em></p>
<blockquote>
<h2>0.7.7 (May 30, 2024)</h2>
<p>BUG FIXES:</p>
<ul>
<li>client: avoid potentially leaking URL-embedded basic authentication credentials in logs (<a href="https://redirect.github.com/hashicorp/go-retryablehttp/issues/158">#158</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/1542b31176d3973a6ecbc06c05a2d0df89b59afb"><code>1542b31</code></a> v0.7.7</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/defb9f441dcf67a2a56fae733482836ea83349ac"><code>defb9f4</code></a> v0.7.7</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/a99f07beb3c5faaa0a283617e6eb6bcf25f5049a"><code>a99f07b</code></a> Merge pull request <a href="https://redirect.github.com/hashicorp/go-retryablehttp/issues/158">#158</a> from dany74q/danny/redacted-url-in-logs</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/8a28c574da4098c0612fe1c7135f1f6de113d411"><code>8a28c57</code></a> Merge branch 'main' into danny/redacted-url-in-logs</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/86e852df43aa0d94150c4629d74e5116d1ff3348"><code>86e852d</code></a> Merge pull request <a href="https://redirect.github.com/hashicorp/go-retryablehttp/issues/227">#227</a> from hashicorp/dependabot/github_actions/actions/chec...</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/47fe99e6460cddc5f433aad2b54dcf32281f8a53"><code>47fe99e</code></a> Bump actions/checkout from 4.1.5 to 4.1.6</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/490fc06be0931548d3523a4245d15e9dc5d9214d"><code>490fc06</code></a> Merge pull request <a href="https://redirect.github.com/hashicorp/go-retryablehttp/issues/226">#226</a> from testwill/ioutil</li>
<li><a href="https://github.com/hashicorp/go-retryablehttp/commit/f3e9417dbfcd0dc2b4a02a1dfdeb75f1e636b692"><code>f3e9417</code></a> chore: remove refs to deprecated io/ioutil</li>
<li>See full diff in <a href="https://github.com/hashicorp/go-retryablehttp/compare/v0.7.6...v0.7.7">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/hashicorp/go-retryablehttp&package-manager=go_modules&previous-version=0.7.6&new-version=0.7.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/sigstore/fulcio/network/alerts).

</details>


Patch:
diff --git a/config/config.jsn b/config/config.jsn
index 8e66be7..27fae54 100644
--- a/config/config.jsn
+++ b/config/config.jsn
@@ -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"
     }
   }
 }
diff --git a/config/fulcio-config.yaml b/config/fulcio-config.yaml
index 44a7107..0f7a0ad 100644
--- a/config/fulcio-config.yaml
+++ b/config/fulcio-config.yaml
@@ -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",
diff --git a/federation/issuer.enforce.dev/config.yaml b/federation/issuer.enforce.dev/config.yaml
new file mode 100644
index 0000000..45e252a
--- /dev/null
+++ b/federation/issuer.enforce.dev/config.yaml
@@ -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"
diff --git a/go.mod b/go.mod
index 05cf248..8caa0c7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.22.4
 
 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
@@ -95,7 +96,7 @@ require (
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.6 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect
 	github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
 	github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
@@ -140,7 +141,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
diff --git a/go.sum b/go.sum
index 71dc3ca..e17dcba 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
@@ -189,8 +191,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
-github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
 github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
@@ -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=
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 900ecef..6a6aca7 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -279,6 +279,7 @@ const (
 	IssuerTypeGithubWorkflow    = "github-workflow"
 	IssuerTypeCodefreshWorkflow = "codefresh-workflow"
 	IssuerTypeGitLabPipeline    = "gitlab-pipeline"
+	IssuerTypeChainguard        = "chainguard-identity"
 	IssuerTypeKubernetes        = "kubernetes"
 	IssuerTypeSpiffe            = "spiffe"
 	IssuerTypeURI               = "uri"
@@ -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:
diff --git a/pkg/identity/chainguard/issuer.go b/pkg/identity/chainguard/issuer.go
new file mode 100644
index 0000000..41df8c4
--- /dev/null
+++ b/pkg/identity/chainguard/issuer.go
@@ -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)
+}
diff --git a/pkg/identity/chainguard/principal.go b/pkg/identity/chainguard/principal.go
new file mode 100644
index 0000000..d2940c6
--- /dev/null
+++ b/pkg/identity/chainguard/principal.go
@@ -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
+}
diff --git a/pkg/server/issuer_pool.go b/pkg/server/issuer_pool.go
index 9905df5..18349f2 100644
--- a/pkg/server/issuer_pool.go
+++ b/pkg/server/issuer_pool.go
@@ -18,6 +18,7 @@ import (
 	"github.com/sigstore/fulcio/pkg/config"
 	"github.com/sigstore/fulcio/pkg/identity"
 	"github.com/sigstore/fulcio/pkg/identity/buildkite"
+	"github.com/sigstore/fulcio/pkg/identity/chainguard"
 	"github.com/sigstore/fulcio/pkg/identity/codefresh"
 	"github.com/sigstore/fulcio/pkg/identity/email"
 	"github.com/sigstore/fulcio/pkg/identity/github"
@@ -62,6 +63,8 @@ func getIssuer(meta string, i config.OIDCIssuer) identity.Issuer {
 		return buildkite.Issuer(issuerURL)
 	case config.IssuerTypeCodefreshWorkflow:
 		return codefresh.Issuer(issuerURL)
+	case config.IssuerTypeChainguard:
+		return chainguard.Issuer(issuerURL)
 	case config.IssuerTypeKubernetes:
 		return kubernetes.Issuer(issuerURL)
 	case config.IssuerTypeSpiffe:


Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 1c926a0..4c09676 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -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)
 	}
diff --git a/pkg/identity/chainguard/issuer_test.go b/pkg/identity/chainguard/issuer_test.go
new file mode 100644
index 0000000..6e3412a
--- /dev/null
+++ b/pkg/identity/chainguard/issuer_test.go
@@ -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
+}
diff --git a/pkg/identity/chainguard/principal_test.go b/pkg/identity/chainguard/principal_test.go
new file mode 100644
index 0000000..db995db
--- /dev/null
+++ b/pkg/identity/chainguard/principal_test.go
@@ -0,0 +1,232 @@
+// 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"
+	"encoding/asn1"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+	"reflect"
+	"strings"
+	"testing"
+
+	"chainguard.dev/sdk/uidp"
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/sigstore/fulcio/pkg/identity"
+)
+
+func TestJobPrincipalFromIDToken(t *testing.T) {
+	group := uidp.NewUIDP("")
+	id := group.NewChild()
+
+	tests := map[string]struct {
+		Claims          map[string]interface{}
+		ExpectPrincipal workflowPrincipal
+		WantErr         bool
+		ErrContains     string
+	}{
+		`Service principal token`: {
+			Claims: map[string]interface{}{
+				"iss": "https://issuer.enforce.dev",
+				"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",
+				},
+			},
+			ExpectPrincipal: workflowPrincipal{
+				issuer:  "https://issuer.enforce.dev",
+				subject: id.String(),
+				actor: map[string]string{
+					"iss": "https://iss.example.com/",
+					"sub": fmt.Sprintf("catalog-syncer:%s", group.String()),
+					"aud": "chainguard",
+				},
+				servicePrincipal: "CATALOG_SYNCER",
+			},
+			WantErr: false,
+		},
+		`Human SSO token`: {
+			Claims: map[string]interface{}{
+				"iss": "https://issuer.enforce.dev",
+				"sub": group.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://auth.chainguard.dev/",
+					"sub": "google-oauth2|1234567890",
+					"aud": "fdsaldfkjhasldf",
+				},
+			},
+			ExpectPrincipal: workflowPrincipal{
+				issuer:  "https://issuer.enforce.dev",
+				subject: group.String(),
+				actor: map[string]string{
+					"iss": "https://auth.chainguard.dev/",
+					"sub": "google-oauth2|1234567890",
+					"aud": "fdsaldfkjhasldf",
+				},
+			},
+			WantErr: false,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			token := &oidc.IDToken{
+				Issuer:  test.Claims["iss"].(string),
+				Subject: test.Claims["sub"].(string),
+			}
+			claims, err := json.Marshal(test.Claims)
+			if err != nil {
+				t.Fatal(err)
+			}
+			withClaims(token, claims)
+
+			untyped, err := PrincipalFromIDToken(context.TODO(), token)
+			if err != nil {
+				if !test.WantErr {
+					t.Fatal("didn't expect error", err)
+				}
+				if !strings.Contains(err.Error(), test.ErrContains) {
+					t.Fatalf("expected error %s to contain %s", err, test.ErrContains)
+				}
+				return
+			}
+			if err == nil && test.WantErr {
+				t.Fatal("expected error but got none")
+			}
+
+			principal, ok := untyped.(*workflowPrincipal)
+			if !ok {
+				t.Errorf("Got wrong principal type %v", untyped)
+			}
+			if !reflect.DeepEqual(*principal, test.ExpectPrincipal) {
+				t.Errorf("got %v principal and expected %v", *principal, test.ExpectPrincipal)
+			}
+		})
+	}
+}
+
+func TestEmbed(t *testing.T) {
+	group := uidp.NewUIDP("")
+	id := group.NewChild()
+
+	tests := map[string]struct {
+		Principal identity.Principal
+		WantErr   bool
+		WantFacts map[string]func(x509.Certificate) error
+	}{
+		`Chainguard Service Principal`: {
+			Principal: &workflowPrincipal{
+				issuer:  "https://issuer.enforce.dev",
+				subject: id.String(),
+				actor: map[string]string{
+					"iss": "https://iss.example.com/",
+					"sub": fmt.Sprintf("catalog-syncer:%s", group.String()),
+					"aud": "chainguard",
+				},
+				servicePrincipal: "CATALOG_SYNCER",
+			},
+			WantErr: false,
+			WantFacts: map[string]func(x509.Certificate) error{
+				`Certificate SAN has correct value`:             factSanURIIs(fmt.Sprintf("https://issuer.enforce.dev/%s", id.String())),
+				`Certificate has correct issuer (v2) extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}, "https://issuer.enforce.dev"),
+			},
+		},
+		`Chainguard Human SSO`: {
+			Principal: &workflowPrincipal{
+				issuer:  "https://issuer.enforce.dev",
+				subject: group.String(),
+				actor: map[string]string{
+					"iss": "https://auth.chainguard.dev/",
+					"sub": "google-oauth2|1234567890",
+					"aud": "fdsaldfkjhasldf",
+				},
+			},
+			WantErr: false,
+			WantFacts: map[string]func(x509.Certificate) error{
+				`Certificate SAN has correct value`:             factSanURIIs(fmt.Sprintf("https://issuer.enforce.dev/%s", group.String())),
+				`Certificate has correct issuer (v2) extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}, "https://issuer.enforce.dev"),
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			var cert x509.Certificate
+			err := test.Principal.Embed(context.TODO(), &cert)
+			if err != nil {
+				if !test.WantErr {
+					t.Error(err)
+				}
+				return
+			} else if test.WantErr {
+				t.Error("expected error")
+			}
+			for factName, fact := range test.WantFacts {
+				t.Run(factName, func(t *testing.T) {
+					if err := fact(cert); err != nil {
+						t.Error(err)
+					}
+				})
+			}
+		})
+	}
+}
+
+func factExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error {
+	return func(cert x509.Certificate) error {
+		for _, ext := range cert.ExtraExtensions {
+			if ext.Id.Equal(oid) {
+				var strVal string
+				_, _ = asn1.Unmarshal(ext.Value, &strVal)
+				if value != strVal {
+					return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, strVal)
+				}
+				return nil
+			}
+		}
+		return errors.New("extension not set")
+	}
+}
+
+func factSanURIIs(value string) func(x509.Certificate) error {
+	return func(cert x509.Certificate) error {
+		url, err := url.Parse(value)
+
+		if err != nil {
+			return err
+		}
+
+		if cert.URIs[0].String() != url.String() {
+			return fmt.Errorf("expected SAN o be %s, but got %s", value, cert.URIs[0].String())
+		}
+
+		return nil
+	}
+}
diff --git a/pkg/server/grpc_server_test.go b/pkg/server/grpc_server_test.go
index 06d6c9e..9990831 100644
--- a/pkg/server/grpc_server_test.go
+++ b/pkg/server/grpc_server_test.go
@@ -39,6 +39,7 @@ import (
 	"testing"
 	"time"
 
+	"chainguard.dev/sdk/uidp"
 	"github.com/go-jose/go-jose/v4"
 	"github.com/go-jose/go-jose/v4/jwt"
 	ctclient "github.com/google/certificate-transparency-go/client"
@@ -197,6 +198,7 @@ func TestGetConfiguration(t *testing.T) {
 	_, gitHubIssuer := newOIDCIssuer(t)
 	_, gitLabIssuer := newOIDCIssuer(t)
 	_, codefreshIssuer := newOIDCIssuer(t)
+	_, chainguardIssuer := newOIDCIssuer(t)
 
 	issuerDomain, err := url.Parse(usernameIssuer)
 	if err != nil {
@@ -247,6 +249,11 @@ func TestGetConfiguration(t *testing.T) {
 				"IssuerURL": %q,
 				"ClientID": "sigstore",
 				"Type": "codefresh-workflow"
+			},
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "chainguard-identity"
 			}
 		},
 		"MetaIssuers": {
@@ -263,6 +270,7 @@ func TestGetConfiguration(t *testing.T) {
 		gitHubIssuer, gitHubIssuer,
 		gitLabIssuer, gitLabIssuer,
 		codefreshIssuer, codefreshIssuer,
+		chainguardIssuer, chainguardIssuer,
 		k8sIssuer)))
 	if err != nil {
 		t.Fatalf("config.Read() = %v", err)
@@ -283,14 +291,15 @@ func TestGetConfiguration(t *testing.T) {
 		t.Fatal("GetConfiguration failed", err)
 	}
 
-	if len(config.Issuers) != 9 {
-		t.Fatalf("expected 9 issuers, got %v", len(config.Issuers))
+	if got, want := len(config.Issuers), 10; got != want {
+		t.Fatalf("expected %d issuers, got %d", want, got)
 	}
 
 	expectedIssuers := map[string]bool{
 		emailIssuer: true, spiffeIssuer: true, uriIssuer: true,
 		usernameIssuer: true, k8sIssuer: true, gitHubIssuer: true,
 		buildkiteIssuer: true, gitLabIssuer: true, codefreshIssuer: true,
+		chainguardIssuer: true,
 	}
 	for _, iss := range config.Issuers {
 		var issURL string
@@ -1400,6 +1409,130 @@ func TestAPIWithCodefresh(t *testing.T) {
 	}
 }
 
+// chainguardClaims holds the additional JWT claims for Chainguard OIDC tokens
+type chainguardClaims struct {
+	Actor    map[string]string `json:"act"`
+	Internal struct {
+		ServicePrincipal string `json:"service-principal,omitempty"`
+	} `json:"internal"`
+}
+
+// Tests API for Chainguard subject types
+func TestAPIWithChainguard(t *testing.T) {
+	chainguardSigner, chainguardIssuer := newOIDCIssuer(t)
+
+	// Create a FulcioConfig that supports these issuers.
+	cfg, err := config.Read([]byte(fmt.Sprintf(`{
+		"OIDCIssuers": {
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "chainguard-identity"
+			}
+        }
+	}`, chainguardIssuer, chainguardIssuer)))
+	if err != nil {
+		t.Fatalf("config.Read() = %v", err)
+	}
+
+	group := uidp.NewUIDP("")
+	chainguardSubject := group.NewChild()
+	claims := chainguardClaims{
+		Actor: map[string]string{
+			"iss": chainguardIssuer,
+			"sub": fmt.Sprintf("catalog-syncer:%s", group.String()),
+			"aud": "chainguard",
+		},
+		Internal: struct {
+			ServicePrincipal string `json:"service-principal,omitempty"`
+		}{
+			ServicePrincipal: "CATALOG_SYNCER",
+		},
+	}
+
+	// Create an OIDC token using this issuer's signer.
+	tok, err := jwt.Signed(chainguardSigner).Claims(jwt.Claims{
+		Issuer:   chainguardIssuer,
+		IssuedAt: jwt.NewNumericDate(time.Now()),
+		Expiry:   jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
+		Subject:  chainguardSubject.String(),
+		Audience: jwt.Audience{"sigstore"},
+	}).Claims(&claims).Serialize()
+	if err != nil {
+		t.Fatalf("CompactSerialize() = %v", err)
+	}
+
+	ctClient, eca := createCA(cfg, t)
+	ctx := context.Background()
+	server, conn := setupGRPCForTest(t, cfg, ctClient, eca)
+	defer func() {
+		server.Stop()
+		conn.Close()
+	}()
+
+	client := protobuf.NewCAClient(conn)
+
+	pubBytes, proof := generateKeyAndProof(chainguardSubject.String(), t)
+
+	// Hit the API to have it sign our certificate.
+	resp, err := client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
+		Credentials: &protobuf.Credentials{
+			Credentials: &protobuf.Credentials_OidcIdentityToken{
+				OidcIdentityToken: tok,
+			},
+		},
+		Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
+			PublicKeyRequest: &protobuf.PublicKeyRequest{
+				PublicKey: &protobuf.PublicKey{
+					Content: pubBytes,
+				},
+				ProofOfPossession: proof,
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("SigningCert() = %v", err)
+	}
+
+	leafCert := verifyResponse(resp, eca, chainguardIssuer, t)
+
+	// Expect URI values
+	if len(leafCert.URIs) != 1 {
+		t.Fatalf("unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs))
+	}
+	chainguardURL := fmt.Sprintf("%s/%s", chainguardIssuer, chainguardSubject)
+	chainguardURI, err := url.Parse(chainguardURL)
+	if err != nil {
+		t.Fatalf("failed to parse expected url")
+	}
+	if *leafCert.URIs[0] != *chainguardURI {
+		t.Fatalf("URIs do not match: Expected %v, got %v", chainguardURI, leafCert.URIs[0])
+	}
+
+	expectedExts := map[int]string{
+		8: chainguardIssuer,
+
+		// TODO(mattmoor): Embed more of the Chainguard token structure via OIDs.
+	}
+	for o, value := range expectedExts {
+		ext, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, o})
+		if !found {
+			t.Fatalf("expected extension in custom OID 1.3.6.1.4.1.57264.1.%d", o)
+		}
+		var extValue string
+		rest, err := asn1.Unmarshal(ext.Value, &extValue)
+		if err != nil {
+			t.Fatalf("error unmarshalling extension: :%v", err)
+		}
+		if len(rest) != 0 {
+			t.Fatal("error unmarshalling extension, rest is not 0")
+		}
+		if string(extValue) != value {
+			t.Fatalf("unexpected extension value, expected %s, got %s", value, extValue)
+		}
+	}
+}
+
 // Tests API with issuer claim in different field in the OIDC token
 func TestAPIWithIssuerClaimConfig(t *testing.T) {
 	emailSigner, emailIssuer := newOIDCIssuer(t)