diff --git a/README.md b/README.md index 171794d82..fb973d2b5 100644 --- a/README.md +++ b/README.md @@ -363,14 +363,14 @@ in all the namespaces, which is not great, so we'll work around that by having another step where we manually copy the secrets to `tuf-system` namespace so that we can create a proper tuf root that `cosign` can use. -There are two steps in the process, first, copy ctlog, fulcio, and rekor +There are two steps in the process, first, copy ctlog, fulcio, rekor and TSA public secrets into the `tuf-system` namespace, followed by a construction of a tuf root from those pieces of information. In addition to that, we'll need to have a tuf web server that serves the root information so that tools like `cosign` can validate the roots of trust. For that, we need to copy the following secrets (namespace/secret) with the -keys in the secrets into the`tuf-system` namespace so that the job there has +keys in the secrets into the `tuf-system` namespace so that the job there has enough information to construct the tuf root: * fulcio-system/fulcio-pub-key @@ -383,6 +383,20 @@ enough information to construct the tuf root: * tsa-system/tsa-cert-chain - cert-chain - Holds the certificate chain for TimeStamp Authority +Certificate chains for fulcio and TSA can either be provided in a single file +or in individual files. When providing as individual files, the following +file naming scheme has to be followed: + +* `_root.crt.pem`, e.g. `tsa_root.crt.pem` +* `_intermediate_0.crt.pem`, e.g. `tsa_intermediate_0.crt.pem` +* `_intermediate_1.crt.pem`, e.g. `tsa_intermediate_1.crt.pem` +* (more intermediates, but at most 10 intermediate certificates altogether) +* `_leaf.crt.pem`, e.g. `tsa_leaf.crt.pem` + +Intermediate certificates, if provided, must be ordered correctly: +`intermediate_0` is signed by `root`, `intermediate_1` is signed by +`intermediate_0` etc. + Once we have all that information in one place, we can construct a tuf root out of it that can be used by tools like `cosign` and `policy-controller`. diff --git a/cmd/tuf/server/main.go b/cmd/tuf/server/main.go index edc1dbf6f..2ea1972ad 100644 --- a/cmd/tuf/server/main.go +++ b/cmd/tuf/server/main.go @@ -50,8 +50,10 @@ var ( // 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") // Name of the "secret" where we create one entry per key JSON definition as generated by TUF, e.g. "root.json", "timestamp.json", ... - keysSecretName = flag.String("keyssecret", "", "Name of the secret to create for generated keys (keys won't be stored unless this is provided)") - noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") + keysSecretName = flag.String("keyssecret", "", "Name of the secret to create for generated keys (keys won't be stored unless this is provided)") + 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. This will be deprecated and removed in the future.") + trustedRoot = flag.Bool("trusted-root", true, "Generate and serve trusted_root.json") ) func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) { @@ -126,7 +128,7 @@ func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysS } // 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 { return fmt.Errorf("failed to create repo: %v", err) } @@ -196,6 +198,10 @@ func main() { ctx := signals.NewContext() + if *metadataTargets { + logging.FromContext(ctx).Warnf("Serving individual TUF targets with custom Sigstore metadata will be deprecated and removed in the future.") + } + serve := false init := false overwrite := true diff --git a/go.mod b/go.mod index 2f4badc55..6634999a3 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,9 @@ require ( github.com/sigstore/fulcio v1.6.3 github.com/sigstore/rekor v1.3.6 github.com/sigstore/sigstore v1.8.8 + github.com/sigstore/sigstore-go v0.6.1-0.20240821212051-2198ac32dd94 github.com/sigstore/timestamp-authority v1.2.2 + github.com/stretchr/testify v1.9.0 github.com/theupdateframework/go-tuf v0.7.0 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 go.uber.org/zap v1.27.0 @@ -224,6 +226,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -252,6 +255,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/thales-e-security/pool v0.0.2 // indirect + github.com/theupdateframework/go-tuf/v2 v2.0.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect github.com/transparency-dev/merkle v0.0.2 // indirect diff --git a/go.sum b/go.sum index cacc5c170..ee4fa2f20 100644 --- a/go.sum +++ b/go.sum @@ -1198,6 +1198,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q= +github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -1471,8 +1473,8 @@ 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.8 h1:B6ZQPBKK7Z7tO3bjLNnlCMG+H66tO4E/+qAphX8T/hg= github.com/sigstore/sigstore v1.8.8/go.mod h1:GW0GgJSCTBJY3fUOuGDHeFWcD++c4G8Y9K015pwcpDI= -github.com/sigstore/sigstore-go v0.5.1 h1:5IhKvtjlQBeLnjKkzMELNG4tIBf+xXQkDzhLV77+/8Y= -github.com/sigstore/sigstore-go v0.5.1/go.mod h1:TuOfV7THHqiDaUHuJ5+QN23RP/YoKmsbwJpY+aaYPN0= +github.com/sigstore/sigstore-go v0.6.1-0.20240821212051-2198ac32dd94 h1:MoT4su5n2fVgwoXWPpXeHCvtY48BkxcONsySq1rHMiw= +github.com/sigstore/sigstore-go v0.6.1-0.20240821212051-2198ac32dd94/go.mod h1:+RyopI/FJDE6z5WVs2sQ2nkc+zsxxByDmbp8a4HoxbA= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 h1:2zHmUvaYCwV6LVeTo+OAkTm8ykOGzA9uFlAjwDPAUWM= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8/go.mod h1:OEhheBplZinUsm7W9BupafztVZV3ldkAxEHbpAeC0Pk= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 h1:RKk4Z+qMaLORUdT7zntwMqKiYAej1VQlCswg0S7xNSY= diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index e0675f99f..5fae5e816 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -16,9 +16,19 @@ package repo import ( "archive/tar" + "bytes" "compress/gzip" "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -26,13 +36,28 @@ import ( "os" "path" "path/filepath" + "sort" "strings" "time" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/theupdateframework/go-tuf" "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,7 @@ func CreateRepoWithMetadata(ctx context.Context, targets []TargetWithMetadata) ( return local, dir, nil } -// CreateRepo creates and initializes a TUF repo for Sigstore by adding +// 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 +152,266 @@ 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) } +// 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: true}) +} + +func constructTrustedRoot(targets []TargetWithMetadata) (*TargetWithMetadata, error) { + var fulcioRoot, tsaLeaf, tsaRoot []byte + var fulcioIntermed, tsaIntermed [][]byte + rekorKeys := map[string]*root.TransparencyLog{} + ctlogKeys := map[string]*root.TransparencyLog{} + now := time.Now() + + // 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 { + // no leaf for Fulcio certificate, the leaf is the code signing cert + 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, id, err := pubkeyToTransparencyLogInstance(target.Bytes, now) + if err != nil { + return nil, fmt.Errorf("failed to parse rekor key: %w", err) + } + rekorKeys[id] = tlinstance + case CTFETarget: + tlinstance, id, err := pubkeyToTransparencyLogInstance(target.Bytes, now) + if err != nil { + return nil, fmt.Errorf("failed to parse ctlog key: %w", err) + } + ctlogKeys[id] = tlinstance + } + } + + fulcioChainPem := concatCertChain([]byte{}, fulcioIntermed, fulcioRoot) + fulcioAuthorities := []root.CertificateAuthority{} + if len(fulcioChainPem) > 0 { + fulcioAuthority, err := certChainToCertificateAuthority(fulcioChainPem) + if err != nil { + return nil, fmt.Errorf("failed to parse cert chain for Fulcio: %w", err) + } + fulcioAuthorities = append(fulcioAuthorities, *fulcioAuthority) + } + + tsaChainPem := concatCertChain(tsaLeaf, tsaIntermed, tsaRoot) + tsaAuthorities := []root.CertificateAuthority{} + if len(tsaChainPem) > 0 { + tsaAuthority, err := certChainToCertificateAuthority(tsaChainPem) + if err != nil { + return nil, fmt.Errorf("failed to parse cert chain for TSA: %w", err) + } + tsaAuthorities = append(tsaAuthorities, *tsaAuthority) + } + + tr, err := root.NewTrustedRoot( + root.TrustedRootMediaType01, + fulcioAuthorities, + ctlogKeys, + tsaAuthorities, + rekorKeys, + ) + if err != nil { + return nil, fmt.Errorf("failed to create TrustedRoot: %w", err) + } + serialized, err := json.Marshal(tr) + if err != nil { + return nil, fmt.Errorf("failed to serialize TrustedRoot to JSON: %w", err) + } + + return &TargetWithMetadata{ + Name: "trusted_root.json", + Bytes: serialized, + }, nil +} + +func pubkeyToTransparencyLogInstance(keyBytes []byte, tm time.Time) (*root.TransparencyLog, string, error) { + logID := sha256.Sum256(keyBytes) + der, _ := pem.Decode(keyBytes) + key, keyDetails, err := getKeyWithDetails(der.Bytes) + if err != nil { + return nil, "", err + } + + return &root.TransparencyLog{ + BaseURL: "", + ID: logID[:], + ValidityPeriodStart: tm, + HashFunc: crypto.SHA256, // we can't get this from the keyBytes, assume SHA256 + PublicKey: key, + SignatureHashFunc: keyDetails, + }, hex.EncodeToString(logID[:]), nil +} + +func getKeyWithDetails(key []byte) (crypto.PublicKey, crypto.Hash, error) { + var k any + var hashFunc crypto.Hash + var err1, err2 error + + k, err1 = x509.ParsePKCS1PublicKey(key) + if err1 != nil { + k, err2 = x509.ParsePKIXPublicKey(key) + if err2 != nil { + return 0, 0, fmt.Errorf("can't parse public key with PKCS1 or PKIX: %w, %w", err1, err2) + } + } + + switch v := k.(type) { + case *ecdsa.PublicKey: + switch v.Curve { + case elliptic.P256(): + hashFunc = crypto.SHA256 + case elliptic.P384(): + hashFunc = crypto.SHA384 + case elliptic.P521(): + hashFunc = crypto.SHA512 + default: + return 0, 0, fmt.Errorf("unsupported elliptic curve %T", v.Curve) + } + case *rsa.PublicKey: + switch v.Size() * 8 { + case 2048, 3072, 4096: + hashFunc = crypto.SHA256 + default: + return 0, 0, fmt.Errorf("unsupported public modulus %d", v.Size()) + } + case ed25519.PublicKey: + hashFunc = crypto.SHA512 + default: + return 0, 0, errors.New("unknown public key type") + } + + return k, hashFunc, nil +} + +func certChainToCertificateAuthority(certChainPem []byte) (*root.CertificateAuthority, error) { + var cert *x509.Certificate + var err error + rest := bytes.TrimSpace(certChainPem) + certChain := []*x509.Certificate{} + + for len(rest) > 0 { + var derCert *pem.Block + derCert, rest = pem.Decode(rest) + rest = bytes.TrimSpace(rest) + if derCert == nil { + return nil, fmt.Errorf("input is left, but it is not a certificate: %+v", rest) + } + cert, err = x509.ParseCertificate(derCert.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + certChain = append(certChain, cert) + } + if len(certChain) == 0 { + return nil, fmt.Errorf("no certificates found in input") + } + + ca := root.CertificateAuthority{} + + for i, cert := range certChain { + switch { + case i == 0 && !cert.IsCA: + ca.Leaf = cert + case i < len(certChain)-1: + ca.Intermediates = append(ca.Intermediates, cert) + case i == len(certChain)-1: + ca.Root = cert + } + } + + ca.ValidityPeriodStart = certChain[0].NotBefore + ca.ValidityPeriodEnd = certChain[0].NotAfter + + return &ca, nil +} + +func concatCertChain(leaf []byte, intermediate [][]byte, root []byte) []byte { + var result []byte + if len(leaf) > 0 { + // for Fulcio, the leaf will always be empty, don't necessarily append an empty newline + 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 getTargetUsage(name string) string { + for _, knownTargetType := range []string{FulcioTarget, RekorTarget, CTFETarget, TSATarget} { + if strings.Contains(strings.ToLower(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 f9612bbfd..5f763ff3e 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -17,9 +17,14 @@ package repo import ( "bytes" "context" + "fmt" "os" "path/filepath" + "regexp" "testing" + + "github.com/sigstore/scaffolding/pkg/certs" + "github.com/stretchr/testify/require" ) const ( @@ -56,6 +61,125 @@ c70LfiFo//8/QsvyjLIUtEWHTkGeuf4PpbYXr5qpJ6tWhG2MARxdeg8CAwEAAQ== MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzf N6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA== -----END PUBLIC KEY-----` + + tsaCertChain = `-----BEGIN CERTIFICATE----- +MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMw +MjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRp +YXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMM +R2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9 +FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/ +BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0j +BBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9 +HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEA +g+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe +-----END CERTIFICATE----- +-----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----- +-----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-----` + + trJSONValidForStart = "2024-08-26T12:03:30.241272895Z" + trJSON = `{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzfN6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2024-08-26T12:03:30.241272895Z" + } + }, + "logId": { + "keyId": "xBzny6gmou42sCYrHOzNuGqi1s2cMxcCEq1wrKF9XDs=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "RedHat", + "commonName": "testcert" + }, + "certChain": { + "certificates": [ + { + "rawBytes": "MIICNzCCAd2gAwIBAgITPLBoBQhl1hqFND9S+SGWbfzaRTAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNlcnQwHhcNMjEwMzEyMjMyNDQ5WhcNMzEwMjI4MjMyNDQ5WjBoMQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQRn+Alyof6xP3GQClSwgV0NFuYYEwmKP/WLWr/LwB6LUYzt5v49RlqG83KuaJSpeOj7G7MVABdpIZYWwqAiZV3o2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUT8Jwm6JuVb0dsiuHUROiHOOVHVkwHwYDVR0jBBgwFoAUT8Jwm6JuVb0dsiuHUROiHOOVHVkwCgYIKoZIzj0EAwIDSAAwRQIhAJkNZmP6sKA+8EebRXFkBa9DPjacBpTcOljJotvKidRhAiAuNrIazKEw2G4dw8x1z6EYk9G+7fJP5m93bjm/JfMBtA==" + } + ] + }, + "validFor": { + "start": "2021-03-12T23:24:49Z", + "end": "2031-02-28T23:24:49Z" + } + } + ], + "ctlogs": [ + { + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu1Ah4n2P8JGt92Qg86FdR8f1pou43yndggMuRCX0JB+bLn1rUFRAKQVd+xnnd4PXJLLdml8ZohCr0lhBuMxZ7zBzt0T98kblUCxBgABPNpWIkTgacyC8MlIYY/yBSuDWAJOA5IKi4Hh9nI+Mmb/FXgbOz5a5mZx8w7pMiTMu0+Rd9cPzRkUZDQfZsLONr6PwmyCAIL1oK80fevxKZPME0UV8bFPWnRxeVaFr5ddd/DOenV8H6SPyr4ODbSOItpl53y6Az0m3FTIUf8cSsyR7dfE4zpA3M4djjtoKDNFRsTjU2RWVQW9XMaxzznGVGhLEwkC+sYjR5NQvH5iiRvV18q+CGQqNX2+WWM3SPuty3nc86RBNR0FOgSQA0TL2OAs6bJNmfzcwZxAKYbj7/88tj6qrjLaQtFTbBm2a7+TAQfs3UTiQi00zEDYqeSj2WQvacNm1dWEAyx0QNLHiKGTn4TShGj8LUoGyjJ26Y6VPsotvCoj8jM0eaN8Pc9/AYywVI+QktjaPZa7KGH3XJHJkTIQQRcUxOtDstKpcriAefDs8jjL5ju9t5J3qEvgzmclNJKRnla4p3maM0vk+8cC7EXMV4P1zuCwr3akaHFJo5Y0aFhKsnHqTc70LfiFo//8/QsvyjLIUtEWHTkGeuf4PpbYXr5qpJ6tWhG2MARxdeg8CAwEAAQ==", + "keyDetails": "PKIX_RSA_PKCS1V15_4096_SHA256", + "validFor": { + "start": "2024-08-26T12:03:30.241272895Z" + } + }, + "logId": { + "keyId": "G3CTL21UG8/5ygV+/WVy/pvB8nUiZGOEnMVKIEDzPxY=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "GitHub, Inc.", + "commonName": "Internal Services Root" + }, + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB3DCCAWKgAwIBAgIUchkNsH36Xa04b1LqIc+qr9DVecMwCgYIKoZIzj0EAwMwMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMB4XDTIzMDQxNDAwMDAwMFoXDTI0MDQxMzAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgVGltZXN0YW1waW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUD5ZNbSqYMd6r8qpOOEX9ibGnZT9GsuXOhr/f8U9FJugBGExKYp40OULS0erjZW7xV9xV52NnJf5OeDq4e5ZKqNWMFQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUaW1RudOgVt0leqY0WKYbuPr47wAwCgYIKoZIzj0EAwMDaAAwZQIwbUH9HvD4ejCZJOWQnqAlkqURllvu9M8+VqLbiRK+zSfZCZwsiljRn8MQQRSkXEE5AjEAg+VxqtojfVfu8DhzzhCx9GKETbJHb19iV72mMKUbDAFmzZ6bQ8b54Zb8tidy5aWe" + }, + { + "rawBytes": "MIICEDCCAZWgAwIBAgIUX8ZO5QXP7vN4dMQ5e9sU3nub8OgwCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTI4MDQxMjAwMDAwMFowMjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRkwFwYDVQQDExBUU0EgaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEvMLY/dTVbvIJYANAuszEwJnQE1llftynyMKIMhh48HmqbVr5ygybzsLRLVKbBWOdZ21aeJz+gZiytZetqcyF9WlER5NEMf6JV7ZNojQpxHq4RHGoGSceQv/qvTiZxEDKo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaW1RudOgVt0leqY0WKYbuPr47wAwHwYDVR0jBBgwFoAU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaQAwZgIxAK1B185ygCrIYFlIs3GjswjnwSMG6LY8woLVdakKDZxVa8f8cqMs1DhcxJ0+09w95QIxAO+tBzZk7vjUJ9iJgD4R6ZWTxQWKqNm74jO99o+o9sv4FI/SZTZTFyMn0IJEHdNmyA==" + }, + { + "rawBytes": "MIIB9DCCAXqgAwIBAgIUa/JAkdUjK4JUwsqtaiRJGWhqLSowCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MB4XDTIzMDQxNDAwMDAwMFoXDTMzMDQxMTAwMDAwMFowODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZJbnRlcm5hbCBTZXJ2aWNlcyBSb290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf9jFAXxz4kx68AHRMOkFBhflDcMTvzaXz4x/FCcXjJ/1qEKon/qPIGnaURskDtyNbNDOpeJTDDFqt48iMPrnzpx6IZwqemfUJN4xBEZfza+pYt/iyod+9tZr20RRWSv/o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQU9NYYlobnAG4c0/qjxyH/lq/wz+QwCgYIKoZIzj0EAwMDaAAwZQIxALZLZ8BgRXzKxLMMN9VIlO+e4hrBnNBgF7tz7Hnrowv2NetZErIACKFymBlvWDvtMAIwZO+ki6ssQ1bsZo98O8mEAf2NZ7iiCgDDU0Vwjeco6zyeh0zBTs9/7gV6AHNQ53xD" + } + ] + }, + "validFor": { + "start": "2023-04-14T00:00:00Z", + "end": "2024-04-13T00:00:00Z" + } + } + ] +}` ) func TestCreateRepo(t *testing.T) { @@ -132,3 +256,36 @@ func TestCompressUncompressFS(t *testing.T) { t.Errorf("Roundtripped rekor differs:\n%s\n%s", rekorPublicKey, string(rtRekor)) } } + +func TestConstructTrustedRoot(t *testing.T) { + tsaCerts, err := certs.SplitCertChain([]byte(tsaCertChain), "tsa") + if err != nil { + t.Fatalf("Failed to split TSA cert chain: %v", err) + } + + targets := []TargetWithMetadata{ + {Name: "fulcio.crt.pem", Bytes: []byte(fulcioRootCert)}, + {Name: "ctfe.pub", Bytes: []byte(ctlogPublicKey)}, + {Name: "rekor.pub", Bytes: []byte(rekorPublicKey)}, + } + + for k, v := range tsaCerts { + targets = append(targets, TargetWithMetadata{Name: k, Bytes: v}) + } + + tr, err := constructTrustedRoot(targets) + if err != nil { + t.Fatalf("Failed to construct trusted root: %v", err) + } + if tr.Name != "trusted_root.json" { + t.Fatalf("Wrong name for trusted root target. Expected 'trusted_root.json', got '%s'", tr.Name) + } + actualTr := string(tr.Bytes) + + // the "validFor" for tlog/ctlog keys will be current time; let's replace them + // to be the time from the expected sample + re := regexp.MustCompile(`"validFor"\s*:\s*\{\s*"start"\s*:\s*"[^"]+"\s*\}`) + actualTr = re.ReplaceAllString(actualTr, fmt.Sprintf(`"validFor": {"start": "%s"}`, trJSONValidForStart)) + + require.JSONEq(t, trJSON, actualTr) +}