Repository: sigstore/fulcio
Pull request #570 (2022-05-11 23:09:36)

Consume `identity.Principal` in CA abstraction
#### Summary

As part of #275, we'd like to move away from the `challenges.ChallengeResult` data structure and use `identity.Principal` and `identity.Issuer` instead. This PR changes the `ca.CertificateAuthority` to consume `identity.Principal` and a public key instead of `challenges.ChallengeResult`.

#### Ticket Link

Related to #275

#### Release Note

```release-note
NONE
```


nsmith5: @znewman01 for refactoring mentorship eyeballs 🙏🏻 
codecov-commenter: # [Codecov](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) Report
> Merging [#570](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (380c64c) into [main](https://codecov.io/gh/sigstore/fulcio/commit/8d307b598a72d28d597a54e5170b7853822a16ca?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) (8d307b5) will **decrease** coverage by `0.34%`.
> The diff coverage is `29.68%`.

```diff
@@            Coverage Diff             @@
##             main     #570      +/-   ##
==========================================
- Coverage   44.29%   43.95%   -0.35%     
==========================================
  Files          21       21              
  Lines        1517     1513       -4     
==========================================
- Hits          672      665       -7     
- Misses        771      777       +6     
+ Partials       74       71       -3     
```


| [Impacted Files](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) | Coverage Δ | |
|---|---|---|
| [pkg/ca/googleca/v1/googleca.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2NhL2dvb2dsZWNhL3YxL2dvb2dsZWNhLmdv) | `52.25% <0.00%> (ø)` | |
| [pkg/ca/x509ca/common.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2NhL3g1MDljYS9jb21tb24uZ28=) | `0.00% <0.00%> (-32.61%)` | :arrow_down: |
| [pkg/challenges/challenges.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2NoYWxsZW5nZXMvY2hhbGxlbmdlcy5nbw==) | `29.71% <30.90%> (+0.93%)` | :arrow_up: |
| [pkg/api/grpc\_server.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2FwaS9ncnBjX3NlcnZlci5nbw==) | `52.97% <50.00%> (ø)` | |
| [pkg/ca/intermediateca/intermediateca.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2NhL2ludGVybWVkaWF0ZWNhL2ludGVybWVkaWF0ZWNhLmdv) | `72.30% <50.00%> (ø)` | |
| [pkg/ca/fileca/watch.go](https://codecov.io/gh/sigstore/fulcio/pull/570/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL2NhL2ZpbGVjYS93YXRjaC5nbw==) | `100.00% <0.00%> (+50.00%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore). Last update [8d307b5...380c64c](https://codecov.io/gh/sigstore/fulcio/pull/570?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).

znewman01: > Leaving open for a bit if @znewman01 has any comments

LGTM, thanks!

Patch:
diff --git a/pkg/api/grpc_server.go b/pkg/api/grpc_server.go
index af6b6f4..f53c073 100644
--- a/pkg/api/grpc_server.go
+++ b/pkg/api/grpc_server.go
@@ -115,7 +115,7 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu
 	// For CAs that do not support embedded SCTs or if the CT log is not configured
 	if sctCa, ok := g.ca.(certauth.EmbeddedSCTCA); !ok || g.ct == nil {
 		// currently configured CA doesn't support pre-certificate flow required to embed SCT in final certificate
-		csc, err = g.ca.CreateCertificate(ctx, subject)
+		csc, err = g.ca.CreateCertificate(ctx, subject, publicKey)
 		if err != nil {
 			// if the error was due to invalid input in the request, return HTTP 400
 			if _, ok := err.(certauth.ValidationError); ok {
@@ -165,7 +165,7 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu
 			result.GetSignedCertificateDetachedSct().SignedCertificateTimestamp = sctBytes
 		}
 	} else {
-		precert, err := sctCa.CreatePrecertificate(ctx, subject)
+		precert, err := sctCa.CreatePrecertificate(ctx, subject, publicKey)
 		if err != nil {
 			// if the error was due to invalid input in the request, return HTTP 400
 			if _, ok := err.(certauth.ValidationError); ok {
diff --git a/pkg/ca/ca.go b/pkg/ca/ca.go
index b6c6f73..0f7cefc 100644
--- a/pkg/ca/ca.go
+++ b/pkg/ca/ca.go
@@ -17,13 +17,14 @@ package ca
 
 import (
 	"context"
+	"crypto"
 
-	"github.com/sigstore/fulcio/pkg/challenges"
+	"github.com/sigstore/fulcio/pkg/identity"
 )
 
 // CertificateAuthority implements certificate creation with a detached SCT and
 // fetching the CA trust bundle.
 type CertificateAuthority interface {
-	CreateCertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*CodeSigningCertificate, error)
+	CreateCertificate(context.Context, identity.Principal, crypto.PublicKey) (*CodeSigningCertificate, error)
 	Root(ctx context.Context) ([]byte, error)
 }
diff --git a/pkg/ca/embeddedca.go b/pkg/ca/embeddedca.go
index 3cf9ab8..fd4a84a 100644
--- a/pkg/ca/embeddedca.go
+++ b/pkg/ca/embeddedca.go
@@ -17,13 +17,14 @@ package ca
 
 import (
 	"context"
+	"crypto"
 
 	ct "github.com/google/certificate-transparency-go"
-	"github.com/sigstore/fulcio/pkg/challenges"
+	"github.com/sigstore/fulcio/pkg/identity"
 )
 
 // EmbeddedSCTCA implements precertificate and certificate issuance. Certificates will contain an embedded SCT.
 type EmbeddedSCTCA interface {
-	CreatePrecertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*CodeSigningPreCertificate, error)
+	CreatePrecertificate(context.Context, identity.Principal, crypto.PublicKey) (*CodeSigningPreCertificate, error)
 	IssueFinalCertificate(ctx context.Context, precert *CodeSigningPreCertificate, sct *ct.SignedCertificateTimestamp) (*CodeSigningCertificate, error)
 }
diff --git a/pkg/ca/googleca/v1/googleca.go b/pkg/ca/googleca/v1/googleca.go
index 95ee33b..6991ff2 100644
--- a/pkg/ca/googleca/v1/googleca.go
+++ b/pkg/ca/googleca/v1/googleca.go
@@ -17,6 +17,7 @@ package v1
 
 import (
 	"context"
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/rsa"
 	"crypto/x509"
@@ -30,7 +31,7 @@ import (
 	privateca "cloud.google.com/go/security/privateca/apiv1"
 	"github.com/sigstore/fulcio/pkg/ca"
 	"github.com/sigstore/fulcio/pkg/ca/x509ca"
-	"github.com/sigstore/fulcio/pkg/challenges"
+	"github.com/sigstore/fulcio/pkg/identity"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 	"google.golang.org/api/iterator"
 	privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1"
@@ -160,13 +161,13 @@ func (c *CertAuthorityService) Root(ctx context.Context) ([]byte, error) {
 	return c.cachedRoots, nil
 }
 
-func (c *CertAuthorityService) CreateCertificate(ctx context.Context, subj *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) {
-	cert, err := x509ca.MakeX509(subj)
+func (c *CertAuthorityService) CreateCertificate(ctx context.Context, principal identity.Principal, publicKey crypto.PublicKey) (*ca.CodeSigningCertificate, error) {
+	cert, err := x509ca.MakeX509(ctx, principal, publicKey)
 	if err != nil {
 		return nil, ca.ValidationError(err)
 	}
 
-	pubKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(subj.PublicKey)
+	pubKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey)
 	if err != nil {
 		return nil, ca.ValidationError(err)
 	}
diff --git a/pkg/ca/intermediateca/intermediateca.go b/pkg/ca/intermediateca/intermediateca.go
index a6848e6..31c0581 100644
--- a/pkg/ca/intermediateca/intermediateca.go
+++ b/pkg/ca/intermediateca/intermediateca.go
@@ -30,7 +30,7 @@ import (
 	ctx509 "github.com/google/certificate-transparency-go/x509"
 	"github.com/sigstore/fulcio/pkg/ca"
 	"github.com/sigstore/fulcio/pkg/ca/x509ca"
-	"github.com/sigstore/fulcio/pkg/challenges"
+	"github.com/sigstore/fulcio/pkg/identity"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
 
@@ -49,8 +49,8 @@ type IntermediateCA struct {
 	Signer crypto.Signer
 }
 
-func (ica *IntermediateCA) CreatePrecertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*ca.CodeSigningPreCertificate, error) {
-	cert, err := x509ca.MakeX509(challenge)
+func (ica *IntermediateCA) CreatePrecertificate(ctx context.Context, principal identity.Principal, publicKey crypto.PublicKey) (*ca.CodeSigningPreCertificate, error) {
+	cert, err := x509ca.MakeX509(ctx, principal, publicKey)
 	if err != nil {
 		return nil, err
 	}
@@ -64,7 +64,7 @@ func (ica *IntermediateCA) CreatePrecertificate(ctx context.Context, challenge *
 		Value:    asn1.NullBytes,
 	})
 
-	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certChain[0], challenge.PublicKey, privateKey)
+	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certChain[0], publicKey, privateKey)
 	if err != nil {
 		return nil, err
 	}
@@ -130,15 +130,15 @@ func (ica *IntermediateCA) IssueFinalCertificate(ctx context.Context, precert *c
 	return ca.CreateCSCFromDER(finalCertBytes, precert.CertChain)
 }
 
-func (ica *IntermediateCA) CreateCertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) {
-	cert, err := x509ca.MakeX509(challenge)
+func (ica *IntermediateCA) CreateCertificate(ctx context.Context, principal identity.Principal, publicKey crypto.PublicKey) (*ca.CodeSigningCertificate, error) {
+	cert, err := x509ca.MakeX509(ctx, principal, publicKey)
 	if err != nil {
 		return nil, err
 	}
 
 	certChain, privateKey := ica.getX509KeyPair()
 
-	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certChain[0], challenge.PublicKey, privateKey)
+	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certChain[0], publicKey, privateKey)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/ca/x509ca/common.go b/pkg/ca/x509ca/common.go
index 78e8295..05dba42 100644
--- a/pkg/ca/x509ca/common.go
+++ b/pkg/ca/x509ca/common.go
@@ -21,12 +21,10 @@ import (
 	"crypto/rand"
 	"crypto/x509"
 	"encoding/pem"
-	"net/url"
 	"time"
 
-	"github.com/pkg/errors"
 	"github.com/sigstore/fulcio/pkg/ca"
-	"github.com/sigstore/fulcio/pkg/challenges"
+	"github.com/sigstore/fulcio/pkg/identity"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
 
@@ -35,13 +33,13 @@ type X509CA struct {
 	PrivKey crypto.Signer
 }
 
-func MakeX509(subject *challenges.ChallengeResult) (*x509.Certificate, error) {
+func MakeX509(ctx context.Context, principal identity.Principal, publicKey crypto.PublicKey) (*x509.Certificate, error) {
 	serialNumber, err := cryptoutils.GenerateSerialNumber()
 	if err != nil {
 		return nil, err
 	}
 
-	skid, err := cryptoutils.SKID(subject.PublicKey)
+	skid, err := cryptoutils.SKID(publicKey)
 	if err != nil {
 		return nil, err
 	}
@@ -55,67 +53,9 @@ func MakeX509(subject *challenges.ChallengeResult) (*x509.Certificate, error) {
 		KeyUsage:     x509.KeyUsageDigitalSignature,
 	}
 
-	switch subject.TypeVal {
-	case challenges.EmailValue:
-		cert.EmailAddresses = []string{subject.Value}
-	case challenges.SpiffeValue:
-		challengeURL, err := url.Parse(subject.Value)
-		if err != nil {
-			return nil, ca.ValidationError(err)
-		}
-		cert.URIs = []*url.URL{challengeURL}
-	case challenges.GithubWorkflowValue:
-		jobWorkflowURI, err := url.Parse(subject.Value)
-		if err != nil {
-			return nil, ca.ValidationError(err)
-		}
-		cert.URIs = []*url.URL{jobWorkflowURI}
-	case challenges.KubernetesValue:
-		k8sURI, err := url.Parse(subject.Value)
-		if err != nil {
-			return nil, ca.ValidationError(err)
-		}
-		cert.URIs = []*url.URL{k8sURI}
-	case challenges.URIValue:
-		subjectURI, err := url.Parse(subject.Value)
-		if err != nil {
-			return nil, ca.ValidationError(err)
-		}
-		cert.URIs = []*url.URL{subjectURI}
-	case challenges.UsernameValue:
-		cert.EmailAddresses = []string{subject.Value}
-	}
-
-	exts := Extensions{
-		Issuer: subject.Issuer,
-	}
-	if subject.TypeVal == challenges.GithubWorkflowValue {
-		var ok bool
-		exts.GithubWorkflowTrigger, ok = subject.AdditionalInfo[challenges.GithubWorkflowTrigger]
-		if !ok {
-			return nil, errors.New("x509ca: github workflow missing trigger claim")
-		}
-		exts.GithubWorkflowSHA, ok = subject.AdditionalInfo[challenges.GithubWorkflowSha]
-		if !ok {
-			return nil, errors.New("x509ca: github workflow missing SHA claim")
-		}
-		exts.GithubWorkflowName, ok = subject.AdditionalInfo[challenges.GithubWorkflowName]
-		if !ok {
-			return nil, errors.New("x509ca: github workflow missing workflow name claim")
-		}
-		exts.GithubWorkflowRepository, ok = subject.AdditionalInfo[challenges.GithubWorkflowRepository]
-		if !ok {
-			return nil, errors.New("x509ca: github workflow missing repository claim")
-		}
-		exts.GithubWorkflowRef, ok = subject.AdditionalInfo[challenges.GithubWorkflowRef]
-		if !ok {
-			return nil, errors.New("x509ca: github workflow missing ref claim")
-		}
-	}
-
-	cert.ExtraExtensions, err = exts.Render()
+	err = principal.Embed(ctx, cert)
 	if err != nil {
-		return nil, err
+		return nil, ca.ValidationError(err)
 	}
 
 	return cert, nil
@@ -128,13 +68,13 @@ func (x *X509CA) Root(ctx context.Context) ([]byte, error) {
 	}), nil
 }
 
-func (x *X509CA) CreateCertificate(_ context.Context, subject *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) {
-	cert, err := MakeX509(subject)
+func (x *X509CA) CreateCertificate(ctx context.Context, principal identity.Principal, publicKey crypto.PublicKey) (*ca.CodeSigningCertificate, error) {
+	cert, err := MakeX509(ctx, principal, publicKey)
 	if err != nil {
 		return nil, err
 	}
 
-	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, x.RootCA, subject.PublicKey, x.PrivKey)
+	finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, x.RootCA, publicKey, x.PrivKey)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/challenges/challenges.go b/pkg/challenges/challenges.go
index 87f91ee..42177dd 100644
--- a/pkg/challenges/challenges.go
+++ b/pkg/challenges/challenges.go
@@ -26,7 +26,9 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/sigstore/fulcio/pkg/ca/x509ca"
 	"github.com/sigstore/fulcio/pkg/config"
+	"github.com/sigstore/fulcio/pkg/identity"
 
 	"github.com/coreos/go-oidc/v3/oidc"
 	"github.com/sigstore/fulcio/pkg/oauthflow"
@@ -61,21 +63,91 @@ const (
 )
 
 type ChallengeResult struct {
-	Issuer    string
-	TypeVal   ChallengeType
-	PublicKey crypto.PublicKey
-	Value     string
+	Issuer  string
+	TypeVal ChallengeType
+
+	// Value configures what will be set for SubjectAlternativeName in
+	// the certificate issued.
+	Value string
+
 	// Extra information from the token that can be added to extensions.
 	AdditionalInfo map[AdditionalInfo]string
+
+	// subject or email from the id token. This must be the thing
+	// signed in the proof of possession!
+	subject string
 }
 
-// preChallengeResult holds an additional value for the subject of the token,
-// used to verify the proof of poessession signature.
-type preChallengeResult struct {
-	// Result.PublicKey is not populated. Result.AdditionalInfo is optional.
-	Result *ChallengeResult
-	// Subject or email from the OIDC token
-	Subject string
+func (cr *ChallengeResult) Name(context.Context) string {
+	return cr.subject
+}
+
+func (cr *ChallengeResult) Embed(ctx context.Context, cert *x509.Certificate) error {
+	switch cr.TypeVal {
+	case EmailValue:
+		cert.EmailAddresses = []string{cr.Value}
+	case SpiffeValue:
+		challengeURL, err := url.Parse(cr.Value)
+		if err != nil {
+			return err
+		}
+		cert.URIs = []*url.URL{challengeURL}
+	case GithubWorkflowValue:
+		jobWorkflowURI, err := url.Parse(cr.Value)
+		if err != nil {
+			return err
+		}
+		cert.URIs = []*url.URL{jobWorkflowURI}
+	case KubernetesValue:
+		k8sURI, err := url.Parse(cr.Value)
+		if err != nil {
+			return err
+		}
+		cert.URIs = []*url.URL{k8sURI}
+	case URIValue:
+		subjectURI, err := url.Parse(cr.Value)
+		if err != nil {
+			return err
+		}
+		cert.URIs = []*url.URL{subjectURI}
+	case UsernameValue:
+		cert.EmailAddresses = []string{cr.Value}
+	}
+
+	exts := x509ca.Extensions{
+		Issuer: cr.Issuer,
+	}
+	if cr.TypeVal == GithubWorkflowValue {
+		var ok bool
+		exts.GithubWorkflowTrigger, ok = cr.AdditionalInfo[GithubWorkflowTrigger]
+		if !ok {
+			return errors.New("github workflow missing trigger claim")
+		}
+		exts.GithubWorkflowSHA, ok = cr.AdditionalInfo[GithubWorkflowSha]
+		if !ok {
+			return errors.New("github workflow missing SHA claim")
+		}
+		exts.GithubWorkflowName, ok = cr.AdditionalInfo[GithubWorkflowName]
+		if !ok {
+			return errors.New("github workflow missing workflow name claim")
+		}
+		exts.GithubWorkflowRepository, ok = cr.AdditionalInfo[GithubWorkflowRepository]
+		if !ok {
+			return errors.New("github workflow missing repository claim")
+		}
+		exts.GithubWorkflowRef, ok = cr.AdditionalInfo[GithubWorkflowRef]
+		if !ok {
+			return errors.New("github workflow missing ref claim")
+		}
+	}
+
+	var err error
+	cert.ExtraExtensions, err = exts.Render()
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // CheckSignature verifies a challenge, a signature over the subject or email
@@ -89,7 +161,7 @@ func CheckSignature(pub crypto.PublicKey, proof []byte, subject string) error {
 	return verifier.VerifySignature(bytes.NewReader(proof), strings.NewReader(subject))
 }
 
-func email(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func email(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 	emailAddress, emailVerified, err := oauthflow.EmailFromIDToken(principal)
 	if !emailVerified {
 		return nil, errors.New("email_verified claim was false")
@@ -107,17 +179,15 @@ func email(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, e
 		return nil, err
 	}
 
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:  issuer,
-			TypeVal: EmailValue,
-			Value:   emailAddress,
-		},
-		Subject: emailAddress,
+	return &ChallengeResult{
+		Issuer:  issuer,
+		TypeVal: EmailValue,
+		Value:   emailAddress,
+		subject: emailAddress,
 	}, nil
 }
 
-func spiffe(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func spiffe(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 
 	spiffeID := principal.Subject
 
@@ -143,17 +213,15 @@ func spiffe(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult,
 	}
 
 	// Now issue cert!
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:  issuer,
-			TypeVal: SpiffeValue,
-			Value:   spiffeID,
-		},
-		Subject: spiffeID,
+	return &ChallengeResult{
+		Issuer:  issuer,
+		TypeVal: SpiffeValue,
+		Value:   spiffeID,
+		subject: spiffeID,
 	}, nil
 }
 
-func kubernetes(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func kubernetes(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 	k8sURI, err := kubernetesToken(principal)
 	if err != nil {
 		return nil, err
@@ -169,17 +237,15 @@ func kubernetes(ctx context.Context, principal *oidc.IDToken) (*preChallengeResu
 		return nil, err
 	}
 
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:  issuer,
-			TypeVal: KubernetesValue,
-			Value:   k8sURI,
-		},
-		Subject: principal.Subject,
+	return &ChallengeResult{
+		Issuer:  issuer,
+		TypeVal: KubernetesValue,
+		Value:   k8sURI,
+		subject: principal.Subject,
 	}, nil
 }
 
-func githubWorkflow(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func githubWorkflow(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 	workflowRef, err := workflowFromIDToken(principal)
 	if err != nil {
 		return nil, err
@@ -199,18 +265,16 @@ func githubWorkflow(ctx context.Context, principal *oidc.IDToken) (*preChallenge
 		return nil, err
 	}
 
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:         issuer,
-			TypeVal:        GithubWorkflowValue,
-			Value:          workflowRef,
-			AdditionalInfo: additionalInfo,
-		},
-		Subject: principal.Subject,
+	return &ChallengeResult{
+		Issuer:         issuer,
+		TypeVal:        GithubWorkflowValue,
+		Value:          workflowRef,
+		AdditionalInfo: additionalInfo,
+		subject:        principal.Subject,
 	}, nil
 }
 
-func uri(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func uri(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 	uriWithSubject := principal.Subject
 
 	cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
@@ -259,17 +323,15 @@ func uri(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, err
 		return nil, err
 	}
 
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:  issuer,
-			TypeVal: URIValue,
-			Value:   uriWithSubject,
-		},
-		Subject: uriWithSubject,
+	return &ChallengeResult{
+		Issuer:  issuer,
+		TypeVal: URIValue,
+		Value:   uriWithSubject,
+		subject: uriWithSubject,
 	}, nil
 }
 
-func username(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult, error) {
+func username(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
 	username := principal.Subject
 
 	if strings.Contains(username, "@") {
@@ -303,13 +365,11 @@ func username(ctx context.Context, principal *oidc.IDToken) (*preChallengeResult
 
 	emailSubject := fmt.Sprintf("%s@%s", username, cfg.SubjectDomain)
 
-	return &preChallengeResult{
-		Result: &ChallengeResult{
-			Issuer:  issuer,
-			TypeVal: UsernameValue,
-			Value:   emailSubject,
-		},
-		Subject: username,
+	return &ChallengeResult{
+		Issuer:  issuer,
+		TypeVal: UsernameValue,
+		Value:   emailSubject,
+		subject: username,
 	}, nil
 }
 
@@ -434,26 +494,26 @@ func validateAllowedDomain(subjectHostname, issuerHostname string) error {
 	return fmt.Errorf("hostname top-level and second-level domains do not match: %s, %s", subjectHostname, issuerHostname)
 }
 
-func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.PublicKey, csr *x509.CertificateRequest, challenge []byte) (*ChallengeResult, error) {
+func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.PublicKey, csr *x509.CertificateRequest, challenge []byte) (identity.Principal, error) {
 	iss, ok := config.FromContext(ctx).GetIssuer(tok.Issuer)
 	if !ok {
 		return nil, fmt.Errorf("configuration can not be loaded for issuer %v", tok.Issuer)
 	}
-	var result *preChallengeResult
+	var principal identity.Principal
 	var err error
 	switch iss.Type {
 	case config.IssuerTypeEmail:
-		result, err = email(ctx, tok)
+		principal, err = email(ctx, tok)
 	case config.IssuerTypeSpiffe:
-		result, err = spiffe(ctx, tok)
+		principal, err = spiffe(ctx, tok)
 	case config.IssuerTypeGithubWorkflow:
-		result, err = githubWorkflow(ctx, tok)
+		principal, err = githubWorkflow(ctx, tok)
 	case config.IssuerTypeKubernetes:
-		result, err = kubernetes(ctx, tok)
+		principal, err = kubernetes(ctx, tok)
 	case config.IssuerTypeURI:
-		result, err = uri(ctx, tok)
+		principal, err = uri(ctx, tok)
 	case config.IssuerTypeUsername:
-		result, err = username(ctx, tok)
+		principal, err = username(ctx, tok)
 	default:
 		return nil, fmt.Errorf("unsupported issuer: %s", iss.Type)
 	}
@@ -468,13 +528,12 @@ func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.Pub
 			return nil, err
 		}
 	} else {
-		if err := CheckSignature(publicKey, challenge, result.Subject); err != nil {
+		if err := CheckSignature(publicKey, challenge, principal.Name(ctx)); err != nil {
 			return nil, err
 		}
 	}
 
-	result.Result.PublicKey = publicKey
-	return result.Result, nil
+	return principal, nil
 }
 
 // TODO: Move to sigstore/sigstore


Test patch:
diff --git a/pkg/api/grpc_server_test.go b/pkg/api/grpc_server_test.go
index d226680..2a9d184 100644
--- a/pkg/api/grpc_server_test.go
+++ b/pkg/api/grpc_server_test.go
@@ -17,6 +17,7 @@ package api
 
 import (
 	"context"
+	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
@@ -42,9 +43,9 @@ import (
 	"github.com/google/certificate-transparency-go/jsonclient"
 	"github.com/sigstore/fulcio/pkg/ca"
 	"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
-	"github.com/sigstore/fulcio/pkg/challenges"
 	"github.com/sigstore/fulcio/pkg/config"
 	"github.com/sigstore/fulcio/pkg/generated/protobuf"
+	"github.com/sigstore/fulcio/pkg/identity"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
@@ -1250,7 +1251,7 @@ func verifyResponse(resp *protobuf.SigningCertificate, eca *ephemeralca.Ephemera
 type FailingCertificateAuthority struct {
 }
 
-func (fca *FailingCertificateAuthority) CreateCertificate(ctx context.Context, challenge *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) {
+func (fca *FailingCertificateAuthority) CreateCertificate(context.Context, identity.Principal, crypto.PublicKey) (*ca.CodeSigningCertificate, error) {
 	return nil, errors.New("CreateCertificate always fails for testing")
 }
 func (fca *FailingCertificateAuthority) Root(ctx context.Context) ([]byte, error) {
diff --git a/pkg/ca/intermediateca/intermediateca_test.go b/pkg/ca/intermediateca/intermediateca_test.go
index 2d33fdb..2721620 100644
--- a/pkg/ca/intermediateca/intermediateca_test.go
+++ b/pkg/ca/intermediateca/intermediateca_test.go
@@ -165,11 +165,10 @@ func TestCreatePrecertificateAndIssueFinalCertificate(t *testing.T) {
 
 	ica := IntermediateCA{Certs: certChain, Signer: subKey}
 	precsc, err := ica.CreatePrecertificate(context.TODO(), &challenges.ChallengeResult{
-		Issuer:    "iss",
-		TypeVal:   challenges.EmailValue,
-		Value:     "foo@example.com",
-		PublicKey: priv.Public(),
-	})
+		Issuer:  "iss",
+		TypeVal: challenges.EmailValue,
+		Value:   "foo@example.com",
+	}, priv.Public())
 
 	if err != nil {
 		t.Fatalf("error generating precertificate: %v", err)
diff --git a/pkg/ca/x509ca/common_test.go b/pkg/ca/x509ca/common_test.go
deleted file mode 100644
index 3f09317..0000000
--- a/pkg/ca/x509ca/common_test.go
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2022 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 x509ca
-
-import (
-	"bytes"
-	"crypto"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/x509"
-	"encoding/asn1"
-	"fmt"
-	"testing"
-
-	"github.com/pkg/errors"
-	"github.com/sigstore/fulcio/pkg/challenges"
-)
-
-func mustNewTestPublicKey() crypto.PublicKey {
-	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		panic(err)
-	}
-	return priv.Public()
-}
-
-func TestMakeCert(t *testing.T) {
-	tests := map[string]struct {
-		Challenge challenges.ChallengeResult
-		WantErr   bool
-		WantFacts map[string]func(x509.Certificate) error
-	}{
-		`Github workflow challenge should have all Github workflow extensions and issuer set`: {
-			Challenge: challenges.ChallengeResult{
-				Issuer:    `https://token.actions.githubusercontent.com`,
-				TypeVal:   challenges.GithubWorkflowValue,
-				PublicKey: mustNewTestPublicKey(),
-				Value:     `https://github.com/foo/bar/`,
-				AdditionalInfo: map[challenges.AdditionalInfo]string{
-					challenges.GithubWorkflowSha:        "sha",
-					challenges.GithubWorkflowTrigger:    "trigger",
-					challenges.GithubWorkflowName:       "workflowname",
-					challenges.GithubWorkflowRepository: "repository",
-					challenges.GithubWorkflowRef:        "ref",
-				},
-			},
-			WantErr: false,
-			WantFacts: map[string]func(x509.Certificate) error{
-				`Certifificate should have correct issuer`:     factIssuerIs(`https://token.actions.githubusercontent.com`),
-				`Certificate has correct trigger extension`:    factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, "trigger"),
-				`Certificate has correct SHA extension`:        factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, "sha"),
-				`Certificate has correct workflow extension`:   factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, "workflowname"),
-				`Certificate has correct repository extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, "repository"),
-				`Certificate has correct ref extension`:        factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, "ref"),
-			},
-		},
-		`Email challenges should set issuer extension and email subject`: {
-			Challenge: challenges.ChallengeResult{
-				Issuer:    `example.com`,
-				TypeVal:   challenges.EmailValue,
-				PublicKey: mustNewTestPublicKey(),
-				Value:     `alice@example.com`,
-			},
-			WantErr: false,
-			WantFacts: map[string]func(x509.Certificate) error{
-				`Certificate should have alice@example.com email subject`: func(cert x509.Certificate) error {
-					if len(cert.EmailAddresses) != 1 {
-						return errors.New("no email SAN set for email challenge")
-					}
-					if cert.EmailAddresses[0] != `alice@example.com` {
-						return errors.New("bad email. expected alice@example.com")
-					}
-					return nil
-				},
-				`Certificate should have issuer extension set`: factIssuerIs("example.com"),
-			},
-		},
-	}
-
-	for name, test := range tests {
-		t.Run(name, func(t *testing.T) {
-			cert, err := MakeX509(&test.Challenge)
-			if err != nil {
-				if !test.WantErr {
-					t.Error(err)
-				}
-				return
-			}
-			for factName, fact := range test.WantFacts {
-				t.Run(factName, func(t *testing.T) {
-					if err := fact(*cert); err != nil {
-						t.Error(err)
-					}
-				})
-			}
-		})
-	}
-}
-
-func factIssuerIs(issuer string) func(x509.Certificate) error {
-	return factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, issuer)
-}
-
-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) {
-				if !bytes.Equal(ext.Value, []byte(value)) {
-					return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, ext.Value)
-				}
-				return nil
-			}
-		}
-		return errors.New("extension not set")
-	}
-}
diff --git a/pkg/challenges/challenges_test.go b/pkg/challenges/challenges_test.go
index 18d4c73..c23e1e6 100644
--- a/pkg/challenges/challenges_test.go
+++ b/pkg/challenges/challenges_test.go
@@ -16,6 +16,7 @@
 package challenges
 
 import (
+	"bytes"
 	"context"
 	"crypto"
 	"crypto/ecdsa"
@@ -25,7 +26,9 @@ import (
 	"crypto/sha256"
 	"crypto/x509"
 	"crypto/x509/pkix"
+	"encoding/asn1"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"net/url"
 	"strings"
@@ -36,6 +39,95 @@ import (
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
 )
 
+func TestEmbedChallengeResult(t *testing.T) {
+	tests := map[string]struct {
+		Challenge ChallengeResult
+		WantErr   bool
+		WantFacts map[string]func(x509.Certificate) error
+	}{
+		`Github workflow challenge should have all Github workflow extensions and issuer set`: {
+			Challenge: ChallengeResult{
+				Issuer:  `https://token.actions.githubusercontent.com`,
+				TypeVal: GithubWorkflowValue,
+				Value:   `https://github.com/foo/bar/`,
+				AdditionalInfo: map[AdditionalInfo]string{
+					GithubWorkflowSha:        "sha",
+					GithubWorkflowTrigger:    "trigger",
+					GithubWorkflowName:       "workflowname",
+					GithubWorkflowRepository: "repository",
+					GithubWorkflowRef:        "ref",
+				},
+			},
+			WantErr: false,
+			WantFacts: map[string]func(x509.Certificate) error{
+				`Certifificate should have correct issuer`:     factIssuerIs(`https://token.actions.githubusercontent.com`),
+				`Certificate has correct trigger extension`:    factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, "trigger"),
+				`Certificate has correct SHA extension`:        factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, "sha"),
+				`Certificate has correct workflow extension`:   factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, "workflowname"),
+				`Certificate has correct repository extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, "repository"),
+				`Certificate has correct ref extension`:        factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, "ref"),
+			},
+		},
+		`Email challenges should set issuer extension and email subject`: {
+			Challenge: ChallengeResult{
+				Issuer:  `example.com`,
+				TypeVal: EmailValue,
+				Value:   `alice@example.com`,
+			},
+			WantErr: false,
+			WantFacts: map[string]func(x509.Certificate) error{
+				`Certificate should have alice@example.com email subject`: func(cert x509.Certificate) error {
+					if len(cert.EmailAddresses) != 1 {
+						return errors.New("no email SAN set for email challenge")
+					}
+					if cert.EmailAddresses[0] != `alice@example.com` {
+						return errors.New("bad email. expected alice@example.com")
+					}
+					return nil
+				},
+				`Certificate should have issuer extension set`: factIssuerIs("example.com"),
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			var cert x509.Certificate
+			err := test.Challenge.Embed(context.TODO(), &cert)
+			if err != nil {
+				if !test.WantErr {
+					t.Error(err)
+				}
+				return
+			}
+			for factName, fact := range test.WantFacts {
+				t.Run(factName, func(t *testing.T) {
+					if err := fact(cert); err != nil {
+						t.Error(err)
+					}
+				})
+			}
+		})
+	}
+}
+
+func factIssuerIs(issuer string) func(x509.Certificate) error {
+	return factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, issuer)
+}
+
+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) {
+				if !bytes.Equal(ext.Value, []byte(value)) {
+					return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, ext.Value)
+				}
+				return nil
+			}
+		}
+		return errors.New("extension not set")
+	}
+}
 func Test_isSpiffeIDAllowed(t *testing.T) {
 	tests := []struct {
 		name     string
@@ -93,21 +185,28 @@ func TestURI(t *testing.T) {
 	issuer := "https://accounts.example.com"
 	token := &oidc.IDToken{Subject: subject, Issuer: issuer}
 
-	result, err := uri(ctx, token)
+	principal, err := uri(ctx, token)
 	if err != nil {
 		t.Errorf("Expected test success, got %v", err)
 	}
-	if result.Result.Issuer != issuer {
-		t.Errorf("Expected issuer %s, got %s", issuer, result.Result.Issuer)
+	if principal.Name(ctx) != token.Subject {
+		t.Errorf("Expected subject %v, got %v", token.Subject, principal.Name(ctx))
+	}
+	raw, ok := principal.(*ChallengeResult)
+	if !ok {
+		t.Fatal("expected principal to be a ChallengeResult")
+	}
+	if raw.Issuer != issuer {
+		t.Errorf("Expected issuer %s, got %s", issuer, raw.Issuer)
 	}
-	if result.Result.Value != subject {
-		t.Errorf("Expected subject value %s, got %s", subject, result.Result.Value)
+	if raw.Value != subject {
+		t.Errorf("Expected subject value %s, got %s", subject, raw.Value)
 	}
-	if result.Result.TypeVal != URIValue {
-		t.Errorf("Expected type %v, got %v", URIValue, result.Result.TypeVal)
+	if raw.TypeVal != URIValue {
+		t.Errorf("Expected type %v, got %v", URIValue, raw.TypeVal)
 	}
-	if result.Subject != token.Subject {
-		t.Errorf("Expected subject %v, got %v", token.Subject, result.Subject)
+	if raw.subject != token.Subject {
+		t.Errorf("Expected subject %v, got %v", token.Subject, raw.subject)
 	}
 }
 
@@ -128,21 +227,29 @@ func TestUsername(t *testing.T) {
 	issuer := "https://accounts.example.com"
 	token := &oidc.IDToken{Subject: usernameVal, Issuer: issuer}
 
-	result, err := username(ctx, token)
+	principal, err := username(ctx, token)
 	if err != nil {
 		t.Errorf("Expected test success, got %v", err)
 	}
-	if result.Result.Issuer != issuer {
-		t.Errorf("Expected issuer %s, got %s", issuer, result.Result.Issuer)
+	if principal.Name(ctx) != token.Subject {
+		t.Errorf("Expected subject %s, got %s", token.Subject, principal.Name(ctx))
+	}
+	raw, ok := principal.(*ChallengeResult)
+	if !ok {
+		t.Fatal("expected principal to be a ChallengeResult")
+	}
+
+	if raw.Issuer != issuer {
+		t.Errorf("Expected issuer %s, got %s", issuer, raw.Issuer)
 	}
-	if result.Result.Value != usernameWithEmail {
-		t.Errorf("Expected subject value %s, got %s", usernameWithEmail, result.Result.Value)
+	if raw.Value != usernameWithEmail {
+		t.Errorf("Expected subject value %s, got %s", usernameWithEmail, raw.Value)
 	}
-	if result.Result.TypeVal != UsernameValue {
-		t.Errorf("Expected type %v, got %v", UsernameValue, result.Result.TypeVal)
+	if raw.TypeVal != UsernameValue {
+		t.Errorf("Expected type %v, got %v", UsernameValue, raw.TypeVal)
 	}
-	if result.Subject != token.Subject {
-		t.Errorf("Expected subject %s, got %s", token.Subject, result.Subject)
+	if raw.subject != token.Subject {
+		t.Errorf("Expected subject %s, got %s", token.Subject, raw.subject)
 	}
 }