Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to generate trusted_root.json by the TUF server #1191

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions cmd/tuf/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ var (
// Name of the "secret" where we create two entries, one for:
// root = Which holds 1.root.json
// repository - Compressed repo, which has been tar/gzipped.
secretName = flag.String("rootsecret", "tuf-root", "Name of the secret to create for the initial root file")
noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment")
secretName = flag.String("rootsecret", "tuf-root", "Name of the secret to create for the initial root file")
noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment")
metadataTargets = flag.Bool("metadata-targets", true, "Serve individual targets with custom Sigstore metadata")
trustedRoot = flag.Bool("trusted-root", true, "Serve trusted_root.json")
)

func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) {
Expand Down Expand Up @@ -118,7 +120,7 @@ func main() {
}

// Create a new TUF root with the listed artifacts.
local, dir, err := repo.CreateRepo(ctx, files)
local, dir, err := repo.CreateRepoWithOptions(ctx, files, repo.CreateRepoOptions{AddMetadataTargets: *metadataTargets, AddTrustedRoot: *trustedRoot})
if err != nil {
logging.FromContext(ctx).Panicf("Failed to create repo: %v", err)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/protobuf-specs v0.3.2 // indirect
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 // indirect
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 // indirect
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,8 @@ github.com/sigstore/cosign/v2 v2.2.4 h1:iY4vtEacmu2hkNj1Fh+8EBqBwKs2DHM27/lbNWDF
github.com/sigstore/cosign/v2 v2.2.4/go.mod h1:JZlRD2uaEjVAvZ1XJ3QkkZJhTqSDVtLaet+C/TMR81Y=
github.com/sigstore/fulcio v1.4.5 h1:WWNnrOknD0DbruuZWCbN+86WRROpEl3Xts+WT2Ek1yc=
github.com/sigstore/fulcio v1.4.5/go.mod h1:oz3Qwlma8dWcSS/IENR/6SjbW4ipN0cxpRVfgdsjMU8=
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
github.com/sigstore/sigstore v1.8.7 h1:L7/zKauHTg0d0Hukx7qlR4nifh6T6O6UIt9JBwAmTIg=
Expand Down
302 changes: 284 additions & 18 deletions pkg/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,46 @@ import (
"archive/tar"
"compress/gzip"
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"

v1Common "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
v1 "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1"
"github.com/theupdateframework/go-tuf"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
"knative.dev/pkg/logging"
)

const (
FulcioTarget = "Fulcio"
RekorTarget = "Rekor"
CTFETarget = "CTFE"
TSATarget = "TSA"
UnknownTarget = "Unknown"
)

type CreateRepoOptions struct {
AddMetadataTargets bool
AddTrustedRoot bool
}

// TargetWithMetadata describes a TUF target with the given Name, Bytes, and
// CustomMetadata
type TargetWithMetadata struct {
Expand Down Expand Up @@ -110,7 +135,14 @@ func CreateRepoWithMetadata(ctx context.Context, targets []TargetWithMetadata) (
return local, dir, nil
}

// CreateRepo creates and initializes a TUF repo for Sigstore by adding
// CreateRepo calls CreateRepoWithOptions, while setting:
// * CreateRepoOptions.AddMetadataTargets: true
// * CreateRepoOptions.AddTrustedRoot: false
func CreateRepo(ctx context.Context, files map[string][]byte) (tuf.LocalStore, string, error) {
return CreateRepoWithOptions(ctx, files, CreateRepoOptions{AddMetadataTargets: true, AddTrustedRoot: false})
}

// CreateRepoWithOptions creates and initializes a TUF repo for Sigstore by adding
// keys to bytes. keys are typically for a basic setup like:
// "fulcio_v1.crt.pem" - Fulcio root cert in PEM format
// "ctfe.pub" - CTLog public key in PEM format
Expand All @@ -127,36 +159,270 @@ func CreateRepoWithMetadata(ctx context.Context, targets []TargetWithMetadata) (
// - `rekor` = it will get Usage set to `Rekor`
// - `tsa` = it will get Usage set to `tsa`.
// - Anything else will get set to `Unknown`
func CreateRepo(ctx context.Context, files map[string][]byte) (tuf.LocalStore, string, error) {
targets := make([]TargetWithMetadata, 0, len(files))
//
// The targets will be added individually to the TUF repo if CreateRepoOptions.AddMetadataTargets
// is set to true. The trusted_root.json file will be added if CreateRepoOptions.AddTrustedRoot
// is set to true. At least one of these has to be true.
func CreateRepoWithOptions(ctx context.Context, files map[string][]byte, options CreateRepoOptions) (tuf.LocalStore, string, error) {
if !options.AddMetadataTargets && !options.AddTrustedRoot {
return nil, "", errors.New("failed to create TUF repo: At least one of metadataTargets, trustedRoot must be true")
}

metadataTargets := make([]TargetWithMetadata, 0, len(files))
for name, bytes := range files {
var usage string
switch {
case strings.Contains(name, "fulcio"):
usage = "Fulcio"
case strings.Contains(name, "ctfe"):
usage = "CTFE"
case strings.Contains(name, "rekor"):
usage = "Rekor"
case strings.Contains(name, "tsa"):
usage = "TSA"
default:
usage = "Unknown"
}
scmActive, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: CustomMetadata{Usage: usage, Status: "Active"}})
scmActive, err := json.Marshal(&sigstoreCustomMetadata{Sigstore: CustomMetadata{Usage: getTargetUsage(name), Status: "Active"}})
if err != nil {
return nil, "", fmt.Errorf("failed to marshal custom metadata for %s: %w", name, err)
}
targets = append(targets, TargetWithMetadata{
metadataTargets = append(metadataTargets, TargetWithMetadata{
Name: name,
Bytes: bytes,
CustomMetadata: scmActive,
})
}

targets := make([]TargetWithMetadata, 0, len(files)+1)
if options.AddMetadataTargets {
targets = append(targets, metadataTargets...)
}
if options.AddTrustedRoot {
trustedRootTarget, err := constructTrustedRoot(metadataTargets)
if err != nil {
return nil, "", fmt.Errorf("failed to construct trust root: %w", err)
}
targets = append(targets, *trustedRootTarget)
}

return CreateRepoWithMetadata(ctx, targets)
}

func constructTrustedRoot(targets []TargetWithMetadata) (*TargetWithMetadata, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to sigstore-go? There is some overlap with sigstore/cosign#3794, and I'd prefer to have this centrally located right off the bat.

@cmurphy FYI, since you were asking about utilities for trust root generation as well.

tr := v1.TrustedRoot{
MediaType: "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
}

var fulcioLeaf, fulcioRoot, tsaLeaf, tsaRoot []byte
var fulcioIntermed, tsaIntermed [][]byte

// we sort the targets by Name, this results in intermediary certs being sorted correctly,
// as long as there is less than 10, which is ok to assume for the purposes of this code
sort.Slice(targets, func(i, j int) bool {
return targets[i].Name < targets[j].Name
})

for _, target := range targets {
// NOTE: in the below switch, we are able to process whole certificate chains, but we also support
// if they're passed in as individual certificates, already split in individual targets
switch getTargetUsage(target.Name) {
case FulcioTarget:
switch {
case strings.Contains(target.Name, "leaf"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no leaves in the trust root for Fulcio, it should only be intermediates and roots. The leaf would be the code signing certificate.

fulcioLeaf = target.Bytes
case strings.Contains(target.Name, "intermediate"):
fulcioIntermed = append(fulcioIntermed, target.Bytes)
default:
fulcioRoot = target.Bytes
}
case TSATarget:
switch {
case strings.Contains(target.Name, "leaf"):
tsaLeaf = target.Bytes
case strings.Contains(target.Name, "intermediate"):
tsaIntermed = append(tsaIntermed, target.Bytes)
default:
tsaRoot = target.Bytes
}
case RekorTarget:
tlinstance, err := pubkeyToTLogInstance(target.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse rekor key: %w", err)
}
tr.Tlogs = []*v1.TransparencyLogInstance{tlinstance}
case CTFETarget:
tlinstance, err := pubkeyToTLogInstance(target.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse ctlog key: %w", err)
}
tr.Ctlogs = []*v1.TransparencyLogInstance{tlinstance}
}
}
var fulcioAuthority, tsaAuthority *v1.CertificateAuthority
var err error

// concat the fulcio chain and process it into CertificateAuthority
fulcioAuthority, err = certChainToAuthority(concatCertChain(fulcioLeaf, fulcioIntermed, fulcioRoot))
if err != nil {
return nil, fmt.Errorf("failed to parse cert chain for Fulcio: %w", err)
}
tr.CertificateAuthorities = []*v1.CertificateAuthority{fulcioAuthority}

// concat the tsa chain and process it into CertificateAuthority
tsaAuthority, err = certChainToAuthority(concatCertChain(tsaLeaf, tsaIntermed, tsaRoot))
if err != nil {
return nil, fmt.Errorf("failed to parse cert chain for TSA: %w", err)
}
tr.TimestampAuthorities = append(tr.TimestampAuthorities, tsaAuthority)

marshaller := &protojson.MarshalOptions{
Indent: " ",
}
serialized, err := marshaller.Marshal(&tr)
if err != nil {
return nil, fmt.Errorf("failed to serialize trust root: %w", err)
}

return &TargetWithMetadata{
Name: "trusted_root.json",
Bytes: serialized,
}, nil
}

func concatCertChain(leaf []byte, intermediate [][]byte, root []byte) []byte {
var result []byte
result = append(result, leaf...)
result = append(result, byte('\n'))
for _, intermed := range intermediate {
result = append(result, intermed...)
result = append(result, byte('\n'))
}
result = append(result, root...)
return result
}

func pubkeyToTLogInstance(key []byte) (*v1.TransparencyLogInstance, error) {
logId := sha256.Sum256(key)
der, _ := pem.Decode(key)
keyDetails, err := getKeyDetails(der.Bytes)
if err != nil {
return nil, err
}

return &v1.TransparencyLogInstance{
BaseUrl: "",
HashAlgorithm: v1Common.HashAlgorithm_SHA2_256, // TODO: make it possible to change this value
PublicKey: &v1Common.PublicKey{
RawBytes: der.Bytes,
KeyDetails: keyDetails,
ValidFor: &v1Common.TimeRange{
Start: timestamppb.New(time.Now()),
},
},
LogId: &v1Common.LogId{
KeyId: logId[:],
},
}, nil
}

func getKeyDetails(key []byte) (v1Common.PublicKeyDetails, error) {
var k any
var err1, err2 error

k, err1 = x509.ParsePKCS1PublicKey(key)
if err1 != nil {
k, err2 = x509.ParsePKIXPublicKey(key)
if err2 != nil {
return 0, fmt.Errorf("Can't parse public key with PKCS1 or PKIX: %w, %w", err1, err2)
}
}

// borrowed from https://github.com/kommendorkapten/trtool/blob/main/cmd/trtool/app/common.go
switch v := k.(type) {
case *ecdsa.PublicKey:
if v.Curve == elliptic.P256() {
return v1Common.PublicKeyDetails_PKIX_ECDSA_P256_SHA_256, nil
}
if v.Curve == elliptic.P384() {
return v1Common.PublicKeyDetails_PKIX_ECDSA_P384_SHA_384, nil
}
if v.Curve == elliptic.P521() {
return v1Common.PublicKeyDetails_PKIX_ECDSA_P521_SHA_512, nil
}
return 0, errors.New("unsupported elliptic curve")
case *rsa.PublicKey:
/*
NOTE: It is not possible to recognize padding from just the public key alone;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just omit this commented out block, almost all Sigstore clients can't handle RSA-PSS keys.

we will just assume that the padding used is pkcs1v15
if padding == RSAPSS {
switch v.Size() * 8 {
case 2048:
return v1Common.PublicKeyDetails_PKIX_RSA_PSS_2048_SHA256, nil
case 3072:
return v1Common.PublicKeyDetails_PKIX_RSA_PSS_3072_SHA256, nil
case 4096:
return v1Common.PublicKeyDetails_PKIX_RSA_PSS_4096_SHA256, nil
default:
return 0, fmt.Errorf("unsupported public modulus %d", v.Size())
}
}
*/
switch v.Size() * 8 {
case 2048:
return v1Common.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256, nil
case 3072:
return v1Common.PublicKeyDetails_PKIX_RSA_PKCS1V15_3072_SHA256, nil
case 4096:
return v1Common.PublicKeyDetails_PKIX_RSA_PKCS1V15_4096_SHA256, nil
default:
return 0, fmt.Errorf("unsupported public modulus %d", v.Size())
}
case ed25519.PublicKey:
return v1Common.PublicKeyDetails_PKIX_ED25519, nil
default:
return 0, errors.New("unknown public key type")
}
}

func certChainToAuthority(certChainPem []byte) (*v1.CertificateAuthority, error) {
var cert *x509.Certificate
var err error
rest := certChainPem
certChain := v1Common.X509CertificateChain{Certificates: []*v1Common.X509Certificate{}}

// skip potential whitespace at end of file (8 is kinda random, but seems to work fine)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is necessary, I believe whitespace will be handled by pem.Decode. Would be good to test for that.

for len(rest) > 8 {
var derCert *pem.Block
derCert, rest = pem.Decode(rest)
cert, err = x509.ParseCertificate(derCert.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse Fulcio certificate: %w", err)
}
certChain.Certificates = append(certChain.Certificates, &v1Common.X509Certificate{RawBytes: cert.Raw})
}

// we end up using information from the last certificate, which is the root
uri := ""
if len(cert.URIs) > 0 {
uri = cert.URIs[0].String()
}
subject := v1Common.DistinguishedName{}
if len(cert.Subject.Organization) > 0 {
subject.Organization = cert.Subject.Organization[0]
subject.CommonName = cert.Subject.CommonName
}

authority := v1.CertificateAuthority{
Subject: &subject,
Uri: uri,
ValidFor: &v1Common.TimeRange{
Start: timestamppb.New(cert.NotBefore),
End: timestamppb.New(cert.NotAfter),
},
CertChain: &certChain,
}

return &authority, nil
}

func getTargetUsage(name string) string {
for _, knownTargetType := range []string{FulcioTarget, RekorTarget, CTFETarget, TSATarget} {
if strings.Contains(name, strings.ToLower(knownTargetType)) {
return knownTargetType
}
}

return UnknownTarget
}

func writeStagedTarget(dir, path string, data []byte) error {
path = filepath.Join(dir, "staged", "targets", path)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
Expand Down
Loading
Loading