diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index d562bfac4..532a9ad52 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -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) { @@ -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) } diff --git a/go.mod b/go.mod index b8ec6bdf6..d1266c16c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a3a829c94..42d6b2fb1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index c866dc64a..a0ec67f32 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -18,7 +18,14 @@ 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" @@ -26,13 +33,31 @@ import ( "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 { @@ -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 @@ -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) { + 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"): + 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; + 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) + 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 { diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index 9a078d488..21d701656 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -17,9 +17,13 @@ package repo import ( "bytes" "context" + "encoding/base64" "os" "path/filepath" "testing" + + v1 "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1" + "google.golang.org/protobuf/encoding/protojson" ) const ( @@ -38,6 +42,8 @@ HOOVHVkwCgYIKoZIzj0EAwIDSAAwRQIhAJkNZmP6sKA+8EebRXFkBa9DPjacBpTc OljJotvKidRhAiAuNrIazKEw2G4dw8x1z6EYk9G+7fJP5m93bjm/JfMBtA== -----END CERTIFICATE-----` + fulcioRootCertBase64DER = `MIICNzCCAd2gAwIBAgITPLBoBQhl1hqFND9S+SGWbfzaRTAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNlcnQwHhcNMjEwMzEyMjMyNDQ5WhcNMzEwMjI4MjMyNDQ5WjBoMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQRn+Alyof6xP3GQClSwgV0NFuYYEwmKP/WLWr/LwB6LUYzt5v49RlqG83KuaJSpeOj7G7MVABdpIZYWwqAiZV3o2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUT8Jwm6JuVb0dsiuHUROiHOOVHVkwHwYDVR0jBBgwFoAUT8Jwm6JuVb0dsiuHUROiHOOVHVkwCgYIKoZIzj0EAwIDSAAwRQIhAJkNZmP6sKA+8EebRXFkBa9DPjacBpTcOljJotvKidRhAiAuNrIazKEw2G4dw8x1z6EYk9G+7fJP5m93bjm/JfMBtA==` + ctlogPublicKey = `-----BEGIN RSA PUBLIC KEY----- MIICCgKCAgEAu1Ah4n2P8JGt92Qg86FdR8f1pou43yndggMuRCX0JB+bLn1rUFRA KQVd+xnnd4PXJLLdml8ZohCr0lhBuMxZ7zBzt0T98kblUCxBgABPNpWIkTgacyC8 @@ -52,17 +58,72 @@ aN8Pc9/AYywVI+QktjaPZa7KGH3XJHJkTIQQRcUxOtDstKpcriAefDs8jjL5ju9t c70LfiFo//8/QsvyjLIUtEWHTkGeuf4PpbYXr5qpJ6tWhG2MARxdeg8CAwEAAQ== -----END RSA PUBLIC KEY-----` + ctlogPublicKeyBase64DER = `MIICCgKCAgEAu1Ah4n2P8JGt92Qg86FdR8f1pou43yndggMuRCX0JB+bLn1rUFRAKQVd+xnnd4PXJLLdml8ZohCr0lhBuMxZ7zBzt0T98kblUCxBgABPNpWIkTgacyC8MlIYY/yBSuDWAJOA5IKi4Hh9nI+Mmb/FXgbOz5a5mZx8w7pMiTMu0+Rd9cPzRkUZDQfZsLONr6PwmyCAIL1oK80fevxKZPME0UV8bFPWnRxeVaFr5ddd/DOenV8H6SPyr4ODbSOItpl53y6Az0m3FTIUf8cSsyR7dfE4zpA3M4djjtoKDNFRsTjU2RWVQW9XMaxzznGVGhLEwkC+sYjR5NQvH5iiRvV18q+CGQqNX2+WWM3SPuty3nc86RBNR0FOgSQA0TL2OAs6bJNmfzcwZxAKYbj7/88tj6qrjLaQtFTbBm2a7+TAQfs3UTiQi00zEDYqeSj2WQvacNm1dWEAyx0QNLHiKGTn4TShGj8LUoGyjJ26Y6VPsotvCoj8jM0eaN8Pc9/AYywVI+QktjaPZa7KGH3XJHJkTIQQRcUxOtDstKpcriAefDs8jjL5ju9t5J3qEvgzmclNJKRnla4p3maM0vk+8cC7EXMV4P1zuCwr3akaHFJo5Y0aFhKsnHqTc70LfiFo//8/QsvyjLIUtEWHTkGeuf4PpbYXr5qpJ6tWhG2MARxdeg8CAwEAAQ==` + rekorPublicKey = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzf N6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA== -----END PUBLIC KEY-----` + + rekorPublicKeyBase64DER = `MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzfN6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA==` + + tsaLeafCert = `-----BEGIN CERTIFICATE----- +MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMw +MjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRp +YXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMM +R2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9 +FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/ +BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0j +BBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9 +HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEA +g+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe +-----END CERTIFICATE-----` + + tsaLeafCertBase64DER = `MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe` + + tsaIntermedCert = `-----BEGIN CERTIFICATE----- +MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMw +ODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2 +aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMG +A1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKI +Mhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZN +ojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T +AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYD +VR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIx +AK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w9 +5QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJE +HdNmyA== +-----END CERTIFICATE-----` + + tsaIntermedCertBase64DER = `MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==` + + tsaRootCert = `-----BEGIN CERTIFICATE----- +MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMw +ODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2 +aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMG +A1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBS +b290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMT +vzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwq +emfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYw +EgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/w +z+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz +7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDD +U0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD +-----END CERTIFICATE-----` + + tsaRootCertBase64DER = `MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD` ) func TestCreateRepo(t *testing.T) { files := map[string][]byte{ - "fulcio_v1.crt.pem": []byte(fulcioRootCert), - "ctfe.pub": []byte(ctlogPublicKey), - "rekor.pub": []byte(rekorPublicKey), + "fulcio_v1.crt.pem": []byte(fulcioRootCert), + "ctfe.pub": []byte(ctlogPublicKey), + "rekor.pub": []byte(rekorPublicKey), + "tsa_leaf.crt.pem": []byte(tsaLeafCert), + "tsa_intermediate_1.crt.pem": []byte(tsaIntermedCert), + "tsa_root.crt.pem": []byte(tsaRootCert), } repo, dir, err := CreateRepo(context.Background(), files) if err != nil { @@ -76,6 +137,64 @@ func TestCreateRepo(t *testing.T) { t.Logf("Got repo meta as: %+v", meta) } +func TestCreateRepoWithOptions(t *testing.T) { + files := map[string][]byte{ + "fulcio_v1.crt.pem": []byte(fulcioRootCert), + "ctfe.pub": []byte(ctlogPublicKey), + "rekor.pub": []byte(rekorPublicKey), + "tsa_leaf.crt.pem": []byte(tsaLeafCert), + "tsa_intermediate_1.crt.pem": []byte(tsaIntermedCert), + "tsa_root.crt.pem": []byte(tsaRootCert), + } + + repo, dir, err := CreateRepoWithOptions(context.Background(), files, CreateRepoOptions{AddMetadataTargets: true, AddTrustedRoot: true}) + if err != nil { + t.Fatalf("Failed to CreateRepoWithOptions: %s", err) + } + defer os.RemoveAll(dir) + meta, err := repo.GetMeta() + if err != nil { + t.Errorf("Failed to GetMeta: %s", err) + } + t.Logf("Got repo meta as: %+v", meta) + trustedRootContent, err := os.ReadFile(filepath.Join(dir, "repository", "targets", "trusted_root.json")) + if err != nil { + t.Errorf("Failed to read trusted_root.json: %s", err) + } + t.Logf("Trusted root content: %s", trustedRootContent) + + tr := v1.TrustedRoot{} + err = protojson.Unmarshal(trustedRootContent, &tr) + if err != nil { + t.Errorf("Failed to parse trusted_root.json: %s", err) + } + + if tr.MediaType != "application/vnd.dev.sigstore.trustedroot+json;version=0.1" { + t.Errorf("Wrong trusted_root.json mediaType %s", tr.MediaType) + } + if base64.StdEncoding.EncodeToString(tr.CertificateAuthorities[0].CertChain.Certificates[0].RawBytes) != fulcioRootCertBase64DER { + t.Errorf("Wrong fulcio certificate RawBytes") + } + + if base64.StdEncoding.EncodeToString(tr.TimestampAuthorities[0].CertChain.Certificates[0].RawBytes) != tsaLeafCertBase64DER { + t.Errorf("Wrong TSA leaf certificate RawBytes") + } + if base64.StdEncoding.EncodeToString(tr.TimestampAuthorities[0].CertChain.Certificates[1].RawBytes) != tsaIntermedCertBase64DER { + t.Errorf("Wrong TSA intermediate certificate RawBytes") + } + if base64.StdEncoding.EncodeToString(tr.TimestampAuthorities[0].CertChain.Certificates[2].RawBytes) != tsaRootCertBase64DER { + t.Errorf("Wrong TSA root certificate RawBytes") + } + + if base64.StdEncoding.EncodeToString(tr.Tlogs[0].PublicKey.RawBytes) != rekorPublicKeyBase64DER { + t.Errorf("Wrong tlog (rekor) pubkey RawBytes") + } + + if base64.StdEncoding.EncodeToString(tr.Ctlogs[0].PublicKey.RawBytes) != ctlogPublicKeyBase64DER { + t.Errorf("Wrong tlog (rekor) pubkey RawBytes") + } +} + func TestCompressUncompressFS(t *testing.T) { files := map[string][]byte{ "fulcio_v1.crt.pem": []byte(fulcioRootCert),