Skip to content

Commit

Permalink
Change gRPC proto to have oneof for pubkey and CSR
Browse files Browse the repository at this point in the history
This provides a cleaner API.

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper committed Apr 18, 2022
1 parent e671fa8 commit 100ac76
Show file tree
Hide file tree
Showing 5 changed files with 453 additions and 226 deletions.
46 changes: 28 additions & 18 deletions fulcio.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,21 @@ message CreateSigningCertificateRequest {
* Identity information about who possesses the private / public key pair presented
*/
Credentials credentials = 1 [(google.api.field_behavior) = REQUIRED];
/*
* The public key to be stored in the requested certificate
*/
PublicKey public_key = 2 [(google.api.field_behavior) = OPTIONAL];
/*
* Proof that the client possesses the private key; must be verifiable by provided public key
*
* This is a currently a signature over the `sub` claim from the OIDC identity token
*/
bytes proof_of_possession = 3 [(google.api.field_behavior) = OPTIONAL];
/*
* PKCS#10 encoded certificate signing request
*
* Contains the public key to be stored in the requested certificate. All other CSR fields
* are ignored. Since the CSR is self-signed, it also acts as a proof of posession of
* the private key.
*/
bytes certificate_signing_request = 4 [(google.api.field_behavior) = OPTIONAL];
oneof key {
/*
* The public key to be stored in the requested certificate along with a signed
* challenge as proof of possession of the private key.
*/
PublicKeyRequest public_key_request = 2 [(google.api.field_behavior) = REQUIRED];
/*
* PKCS#10 encoded certificate signing request
*
* Contains the public key to be stored in the requested certificate. All other CSR fields
* are ignored. Since the CSR is self-signed, it also acts as a proof of posession of
* the private key.
*/
bytes certificate_signing_request = 3 [(google.api.field_behavior) = REQUIRED];
}
}

message Credentials {
Expand All @@ -78,6 +75,19 @@ message Credentials {
}
}

message PublicKeyRequest {
/*
* The public key to be stored in the requested certificate
*/
PublicKey public_key = 1 [(google.api.field_behavior) = REQUIRED];
/*
* Proof that the client possesses the private key; must be verifiable by provided public key
*
* This is a currently a signature over the `sub` claim from the OIDC identity token
*/
bytes proof_of_possession = 2 [(google.api.field_behavior) = REQUIRED];
}

message PublicKey {
/*
* The cryptographic algorithm to use with the key material
Expand Down
19 changes: 9 additions & 10 deletions pkg/api/grpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -76,23 +75,23 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu
return nil, handleFulcioGRPCError(ctx, codes.Unauthenticated, err, invalidCredentials)
}

if request.PublicKey == nil && len(request.CertificateSigningRequest) == 0 {
return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, errors.New("public key not provided"), invalidPublicKey)
}

// optionally parse CSR
var csr *x509.CertificateRequest
if len(request.CertificateSigningRequest) > 0 {
csr, err = challenges.ParseCSR(request.CertificateSigningRequest)
if len(request.GetCertificateSigningRequest()) > 0 {
csr, err = challenges.ParseCSR(request.GetCertificateSigningRequest())
if err != nil {
return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, invalidCSR)
}
}

// fetch public key from request or CSR
var pubKeyContent string
if request.PublicKey != nil {
pubKeyContent = request.PublicKey.Content
var proofOfPossession []byte
if request.GetPublicKeyRequest() != nil {
if request.GetPublicKeyRequest().PublicKey != nil {
pubKeyContent = request.GetPublicKeyRequest().PublicKey.Content
}
proofOfPossession = request.GetPublicKeyRequest().ProofOfPossession
}
publicKey, err := challenges.ParsePublicKey(pubKeyContent, csr)
if err != nil {
Expand All @@ -105,7 +104,7 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu
}

// verify challenge
subject, err := challenges.ExtractSubject(ctx, principal, publicKey, csr, request.ProofOfPossession)
subject, err := challenges.ExtractSubject(ctx, principal, publicKey, csr, proofOfPossession)
if err != nil {
return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, invalidSignature)
}
Expand Down
148 changes: 128 additions & 20 deletions pkg/api/grpc_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,14 @@ func TestAPIWithEmail(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
},
ProofOfPossession: proof,
},
},
ProofOfPossession: proof,
})
if err != nil {
t.Fatalf("SigningCert() = %v", err)
Expand Down Expand Up @@ -345,10 +349,14 @@ func TestAPIWithUriSubject(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
},
ProofOfPossession: proof,
},
},
ProofOfPossession: proof,
})
if err != nil {
t.Fatalf("SigningCert() = %v", err)
Expand Down Expand Up @@ -435,10 +443,14 @@ func TestAPIWithKubernetes(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
},
ProofOfPossession: proof,
},
},
ProofOfPossession: proof,
})
if err != nil {
t.Fatalf("SigningCert() = %v", err)
Expand Down Expand Up @@ -528,10 +540,14 @@ func TestAPIWithGitHub(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
},
ProofOfPossession: proof,
},
},
ProofOfPossession: proof,
})
if err != nil {
t.Fatalf("SigningCert() = %v", err)
Expand Down Expand Up @@ -651,7 +667,9 @@ func TestAPIWithCSRChallenge(t *testing.T) {
OidcIdentityToken: tok,
},
},
CertificateSigningRequest: pemCSR,
Key: &protobuf.CreateSigningCertificateRequest_CertificateSigningRequest{
CertificateSigningRequest: pemCSR,
},
})
if err != nil {
t.Fatalf("SigningCert() = %v", err)
Expand Down Expand Up @@ -726,10 +744,14 @@ func TestAPIWithInsecurePublicKey(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: string(cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, pubBytes)),
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: string(cryptoutils.PEMEncode(cryptoutils.CertificatePEMType, pubBytes)),
},
ProofOfPossession: []byte{},
},
},
ProofOfPossession: []byte{},
})
if err == nil || !strings.Contains(err.Error(), "The public key supplied in the request is insecure") {
t.Fatalf("expected insecure public key error, got %v", err)
Expand Down Expand Up @@ -781,12 +803,31 @@ func TestAPIWithoutPublicKey(t *testing.T) {

client := protobuf.NewCAClient(conn)

// Test with no key proto specified
_, err = client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
Credentials: &protobuf.Credentials{
Credentials: &protobuf.Credentials_OidcIdentityToken{
OidcIdentityToken: tok,
},
},
})
if err == nil || !strings.Contains(err.Error(), "The public key supplied in the request could not be parsed") {
t.Fatalf("expected parsing public key error, got %v", err)
}
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected invalid argument, got %v", status.Code(err))
}

// Test with no public key specified
_, err = client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
Credentials: &protobuf.Credentials{
Credentials: &protobuf.Credentials_OidcIdentityToken{
OidcIdentityToken: tok,
},
},
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{},
},
})
if err == nil || !strings.Contains(err.Error(), "The public key supplied in the request could not be parsed") {
t.Fatalf("expected parsing public key error, got %v", err)
Expand Down Expand Up @@ -847,10 +888,14 @@ func TestAPIWithInvalidChallenge(t *testing.T) {
OidcIdentityToken: tok,
},
},
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &protobuf.PublicKeyRequest{
PublicKey: &protobuf.PublicKey{
Content: pubBytes,
},
ProofOfPossession: invalidProof,
},
},
ProofOfPossession: invalidProof,
})
if err == nil || !strings.Contains(err.Error(), "The signature supplied in the request could not be verified") {
t.Fatalf("expected invalid signature error, got %v", err)
Expand All @@ -860,6 +905,67 @@ func TestAPIWithInvalidChallenge(t *testing.T) {
}
}

// Tests API with an invalid CSR.
func TestAPIWithInvalidCSR(t *testing.T) {
emailSigner, emailIssuer := newOIDCIssuer(t)

// Create a FulcioConfig that supports this issuer.
cfg, err := config.Read([]byte(fmt.Sprintf(`{
"OIDCIssuers": {
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"Type": "email"
}
}
}`, emailIssuer, emailIssuer)))
if err != nil {
t.Fatalf("config.Read() = %v", err)
}

emailSubject := "foo@example.com"

// Create an OIDC token using this issuer's signer.
tok, err := jwt.Signed(emailSigner).Claims(jwt.Claims{
Issuer: emailIssuer,
IssuedAt: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
Subject: emailSubject,
Audience: jwt.Audience{"sigstore"},
}).Claims(customClaims{Email: emailSubject, EmailVerified: true}).CompactSerialize()
if err != nil {
t.Fatalf("CompactSerialize() = %v", err)
}

ctClient, eca := createCA(cfg, t)
ctx := context.Background()
server, conn := setupGRPCForTest(ctx, t, cfg, ctClient, eca)
defer func() {
server.Stop()
conn.Close()
}()

client := protobuf.NewCAClient(conn)

_, err = client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
Credentials: &protobuf.Credentials{
Credentials: &protobuf.Credentials_OidcIdentityToken{
OidcIdentityToken: tok,
},
},
Key: &protobuf.CreateSigningCertificateRequest_CertificateSigningRequest{
CertificateSigningRequest: []byte("invalid"),
},
})

if err == nil || !strings.Contains(err.Error(), "The certificate signing request could not be parsed") {
t.Fatalf("expected invalid signature error, got %v", err)
}
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected invalid argument, got %v", status.Code(err))
}
}

// Tests API with unsigned CSR, which will fail signature verification.
func TestAPIWithInvalidCSRSignature(t *testing.T) {
emailSigner, emailIssuer := newOIDCIssuer(t)
Expand Down Expand Up @@ -925,7 +1031,9 @@ func TestAPIWithInvalidCSRSignature(t *testing.T) {
OidcIdentityToken: tok,
},
},
CertificateSigningRequest: pemCSR,
Key: &protobuf.CreateSigningCertificateRequest_CertificateSigningRequest{
CertificateSigningRequest: pemCSR,
},
})

if err == nil || !strings.Contains(err.Error(), "The signature supplied in the request could not be verified") {
Expand Down
22 changes: 15 additions & 7 deletions pkg/api/legacy_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ func (l *legacyGRPCCAServer) CreateSigningCertificate(ctx context.Context, reque

var v2Request fulciogrpc.CreateSigningCertificateRequest
if len(request.CertificateSigningRequest) > 0 {
v2Request = fulciogrpc.CreateSigningCertificateRequest{
Credentials: &creds,
key := fulciogrpc.CreateSigningCertificateRequest_CertificateSigningRequest{
CertificateSigningRequest: request.CertificateSigningRequest, //lint:ignore SA1019 this is valid because we're converting from v1beta to v1 API
}
v2Request = fulciogrpc.CreateSigningCertificateRequest{
Credentials: &creds,
Key: &key,
}
} else {
// the CSR and the public key have not been set
if request.PublicKey == nil {
Expand All @@ -79,13 +82,18 @@ func (l *legacyGRPCCAServer) CreateSigningCertificate(ctx context.Context, reque
if !ok {
algorithmEnum = int32(fulciogrpc.PublicKeyAlgorithm_PUBLIC_KEY_ALGORITHM_UNSPECIFIED)
}
key := fulciogrpc.CreateSigningCertificateRequest_PublicKeyRequest{
PublicKeyRequest: &fulciogrpc.PublicKeyRequest{
PublicKey: &fulciogrpc.PublicKey{
Algorithm: fulciogrpc.PublicKeyAlgorithm(algorithmEnum),
Content: string(request.PublicKey.Content), //lint:ignore SA1019 this is valid because we're converting from v1beta to v1 API
},
ProofOfPossession: request.SignedEmailAddress, //lint:ignore SA1019 this is valid because we're converting from v1beta to v1 API,
},
}
v2Request = fulciogrpc.CreateSigningCertificateRequest{
Credentials: &creds,
PublicKey: &fulciogrpc.PublicKey{
Algorithm: fulciogrpc.PublicKeyAlgorithm(algorithmEnum),
Content: string(request.PublicKey.Content), //lint:ignore SA1019 this is valid because we're converting from v1beta to v1 API
},
ProofOfPossession: request.SignedEmailAddress, //lint:ignore SA1019 this is valid because we're converting from v1beta to v1 API
Key: &key,
}
}

Expand Down
Loading

0 comments on commit 100ac76

Please sign in to comment.