diff --git a/pkg/root/trusted_root.go b/pkg/root/trusted_root.go index 3101d1f6..f2f42cf7 100644 --- a/pkg/root/trusted_root.go +++ b/pkg/root/trusted_root.go @@ -17,6 +17,7 @@ package root import ( "crypto" "crypto/ecdsa" + "crypto/rsa" "crypto/x509" "encoding/hex" "fmt" @@ -78,6 +79,15 @@ func (tr *TrustedRoot) CTLogs() map[string]*TransparencyLog { return tr.ctLogs } +func (tr *TrustedRoot) MarshalJSON() ([]byte, error) { + err := tr.constructProtoTrustRoot() + if err != nil { + return nil, fmt.Errorf("failed constructing protobuf TrustRoot representation: %w", err) + } + + return protojson.Marshal(tr.trustedRoot) +} + func NewTrustedRootFromProtobuf(protobufTrustedRoot *prototrustroot.TrustedRoot) (trustedRoot *TrustedRoot, err error) { if protobufTrustedRoot.GetMediaType() != TrustedRootMediaType01 { return nil, fmt.Errorf("unsupported TrustedRoot media type: %s", protobufTrustedRoot.GetMediaType()) @@ -154,6 +164,25 @@ func ParseTransparencyLogs(tlogs []*prototrustroot.TransparencyLogInstance) (tra PublicKey: ecKey, SignatureHashFunc: crypto.SHA256, } + case protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256, + protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_3072_SHA256, + protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_4096_SHA256: + key, err := x509.ParsePKIXPublicKey(tlog.GetPublicKey().GetRawBytes()) + if err != nil { + return nil, err + } + var rsaKey *rsa.PublicKey + var ok bool + if rsaKey, ok = key.(*rsa.PublicKey); !ok { + return nil, fmt.Errorf("tlog public key is not RSA") + } + transparencyLogs[encodedKeyID] = &TransparencyLog{ + BaseURL: tlog.GetBaseUrl(), + ID: tlog.GetLogId().GetKeyId(), + HashFunc: hashFunc, + PublicKey: rsaKey, + SignatureHashFunc: crypto.SHA256, + } // This key format is deprecated, but currently in use for Sigstore staging instance case protocommon.PublicKeyDetails_PKCS1_RSA_PKCS1V5: //nolint:staticcheck key, err := x509.ParsePKCS1PublicKey(tlog.GetPublicKey().GetRawBytes()) diff --git a/pkg/root/trusted_root_create.go b/pkg/root/trusted_root_create.go new file mode 100644 index 00000000..3d1946fc --- /dev/null +++ b/pkg/root/trusted_root_create.go @@ -0,0 +1,378 @@ +// Copyright 2023 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 root + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "time" + + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + prototrustroot "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + FulcioTarget = "Fulcio" + RekorTarget = "Rekor" + CTFETarget = "CTFE" + TSATarget = "TSA" +) + +type RawTrustedRootTarget interface { + GetType() string + GetBytes() []byte +} + +type BaseTrustedRootTarget struct { + Type string + Bytes []byte +} + +func (b *BaseTrustedRootTarget) GetType() string { + return b.Type +} + +func (b *BaseTrustedRootTarget) GetBytes() []byte { + return b.Bytes +} + +// NewTrustedRootFromTargets initializes a TrustedRoot object from a mediaType string and +// a slice of targets. These targets are expected to be PEM-encoded public keys/certificate chains. +// This method of constructing the TrustedRoot has some shortcomings which can be aided by manually +// adjusting the TrustedRoot object after instantiation: +// +// - publicKey instances for tlogs/ctlogs will have validFor.start set to Time.now() and no validFor.end. +// - Merkle Tree hash function is hardcoded to SHA256, as this is not derivable from the public key. +// - Each certificate chain is expected to be given as a single target, where there is a newline +// between individual certificates. It is expected that the certificate chain is ordered (root last). +func NewTrustedRootFromTargets(mediaType string, targets []RawTrustedRootTarget) (*TrustedRoot, error) { + // document that we assume 1 cert chain per target and with certs already ordered from leaf to root + if mediaType != TrustedRootMediaType01 { + return nil, fmt.Errorf("unsupported TrustedRoot media type: %s", TrustedRootMediaType01) + } + tr := &TrustedRoot{ + ctLogs: make(map[string]*TransparencyLog), + rekorLogs: make(map[string]*TransparencyLog), + } + now := time.Now() + + var fulcioCertChains, tsaCertChains [][]byte + + for _, target := range targets { + switch target.GetType() { + case FulcioTarget: + fulcioCertChains = append(fulcioCertChains, target.GetBytes()) + case TSATarget: + tsaCertChains = append(tsaCertChains, target.GetBytes()) + case RekorTarget: + tlInstance, keyId, err := pubkeyToTransparencyLogInstance(target.GetBytes(), now) + if err != nil { + return nil, fmt.Errorf("failed to parse rekor key: %w", err) + } + tr.rekorLogs[keyId] = tlInstance + case CTFETarget: + tlInstance, keyId, err := pubkeyToTransparencyLogInstance(target.GetBytes(), now) + if err != nil { + return nil, fmt.Errorf("failed to parse ctlog key: %w", err) + } + tr.ctLogs[keyId] = tlInstance + } + } + + for _, fulcioCertChain := range fulcioCertChains { + fulcioCA, err := certsToAuthority(fulcioCertChain) + if err != nil { + return nil, fmt.Errorf("failed to parse Fulcio certificate chain: %w", err) + } + tr.fulcioCertAuthorities = append(tr.fulcioCertAuthorities, *fulcioCA) + } + + for _, tsaCertChain := range tsaCertChains { + tsaCA, err := certsToAuthority(tsaCertChain) + if err != nil { + return nil, fmt.Errorf("failed to parse TSA certificate chain: %w", err) + } + tr.timestampingAuthorities = append(tr.timestampingAuthorities, *tsaCA) + } + + return tr, nil +} + +func pubkeyToTransparencyLogInstance(keyBytes []byte, tm time.Time) (*TransparencyLog, string, error) { + logId := sha256.Sum256(keyBytes) + der, _ := pem.Decode(keyBytes) + key, keyDetails, err := getKeyWithDetails(der.Bytes) + if err != nil { + return nil, "", err + } + + return &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()) + } + default: + return 0, 0, errors.New("unknown public key type") + } + + return k, hashFunc, nil +} + +func certsToAuthority(certChainPem []byte) (*CertificateAuthority, error) { + var cert *x509.Certificate + var err error + rest := certChainPem + certChain := []*x509.Certificate{} + + // 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) + 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 := 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 + } + } + + // TODO: should this rather be the innermost cert? + ca.ValidityPeriodStart = ca.Root.NotBefore + ca.ValidityPeriodEnd = ca.Root.NotAfter + + return &ca, nil +} + +func (tr *TrustedRoot) constructProtoTrustRoot() error { + tr.trustedRoot = &prototrustroot.TrustedRoot{} + tr.trustedRoot.MediaType = TrustedRootMediaType01 + + for logId, transparencyLog := range tr.rekorLogs { + tlProto, err := transparencyLogToProtobufTL(*transparencyLog) + if err != nil { + return fmt.Errorf("failed converting rekor log %s to protobuf: %w", logId, err) + } + tr.trustedRoot.Tlogs = append(tr.trustedRoot.Tlogs, tlProto) + } + + for logId, ctLog := range tr.ctLogs { + ctProto, err := transparencyLogToProtobufTL(*ctLog) + if err != nil { + return fmt.Errorf("failed converting ctlog %s to protobuf: %w", logId, err) + } + tr.trustedRoot.Ctlogs = append(tr.trustedRoot.Ctlogs, ctProto) + } + + for _, ca := range tr.fulcioCertAuthorities { + caProto, err := certificateAuthorityToProtobufCA(ca) + if err != nil { + return fmt.Errorf("failed converting fulcio cert chain to protobuf: %w", err) + } + tr.trustedRoot.CertificateAuthorities = append(tr.trustedRoot.CertificateAuthorities, caProto) + } + + for _, ca := range tr.timestampingAuthorities { + caProto, err := certificateAuthorityToProtobufCA(ca) + if err != nil { + return fmt.Errorf("failed converting TSA cert chain to protobuf: %w", err) + } + tr.trustedRoot.TimestampAuthorities = append(tr.trustedRoot.TimestampAuthorities, caProto) + } + + return nil +} + +func certificateAuthorityToProtobufCA(ca CertificateAuthority) (*prototrustroot.CertificateAuthority, error) { + org := "" + if len(ca.Root.Subject.Organization) > 0 { + org = ca.Root.Subject.Organization[0] + } + var allCerts []*protocommon.X509Certificate + if ca.Leaf != nil { + allCerts = append(allCerts, &protocommon.X509Certificate{RawBytes: ca.Leaf.Raw}) + } + for _, intermed := range ca.Intermediates { + allCerts = append(allCerts, &protocommon.X509Certificate{RawBytes: intermed.Raw}) + } + if ca.Root == nil { + return nil, fmt.Errorf("root certificate is nil") + } + allCerts = append(allCerts, &protocommon.X509Certificate{RawBytes: ca.Root.Raw}) + + caProto := prototrustroot.CertificateAuthority{ + Uri: "", + Subject: &protocommon.DistinguishedName{ + Organization: org, + CommonName: ca.Root.Subject.CommonName, + }, + ValidFor: &protocommon.TimeRange{ + Start: timestamppb.New(ca.ValidityPeriodStart), + End: timestamppb.New(ca.ValidityPeriodEnd), + }, + CertChain: &protocommon.X509CertificateChain{ + Certificates: allCerts, + }, + } + + return &caProto, nil +} + +func transparencyLogToProtobufTL(tl TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) { + hashAlgo, err := hashAlgorithmToProtobufHashAlgorithm(tl.HashFunc) + if err != nil { + return nil, fmt.Errorf("failed converting hash algorithm to protobuf: %w", err) + } + publicKey, err := publicKeyToProtobufPublicKey(tl.PublicKey, tl.ValidityPeriodStart) + if err != nil { + return nil, fmt.Errorf("failed converting public key to protobuf: %w", err) + } + trProto := prototrustroot.TransparencyLogInstance{ + BaseUrl: tl.BaseURL, + HashAlgorithm: hashAlgo, + PublicKey: publicKey, + LogId: &protocommon.LogId{ + KeyId: tl.ID, + }, + } + + return &trProto, nil +} + +func hashAlgorithmToProtobufHashAlgorithm(hashAlgorithm crypto.Hash) (protocommon.HashAlgorithm, error) { + switch hashAlgorithm { + case crypto.SHA256: + return protocommon.HashAlgorithm_SHA2_256, nil + case crypto.SHA384: + return protocommon.HashAlgorithm_SHA2_384, nil + case crypto.SHA512: + return protocommon.HashAlgorithm_SHA2_512, nil + case crypto.SHA3_256: + return protocommon.HashAlgorithm_SHA3_256, nil + case crypto.SHA3_384: + return protocommon.HashAlgorithm_SHA3_384, nil + default: + return 0, fmt.Errorf("unsupported hash algorithm for Merkle tree: %v", hashAlgorithm) + } +} + +func publicKeyToProtobufPublicKey(publicKey crypto.PublicKey, tm time.Time) (*protocommon.PublicKey, error) { + pkd := protocommon.PublicKey{ + ValidFor: &protocommon.TimeRange{ + Start: timestamppb.New(tm), + }, + } + + rawBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed marshalling public key: %w", err) + } + pkd.RawBytes = rawBytes + + switch p := publicKey.(type) { + case *ecdsa.PublicKey: + switch p.Curve { + case elliptic.P256(): + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_ECDSA_P256_SHA_256 + case elliptic.P384(): + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_ECDSA_P384_SHA_384 + case elliptic.P521(): + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_ECDSA_P521_SHA_512 + default: + return nil, fmt.Errorf("unsupported curve for ecdsa key: %T", p.Curve) + } + case *rsa.PublicKey: + switch p.Size() * 8 { + case 2048: + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256 + case 3072: + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_3072_SHA256 + case 4096: + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_RSA_PKCS1V15_4096_SHA256 + default: + return nil, fmt.Errorf("unsupported public modulus for RSA key: %d", p.Size()) + } + case *ed25519.PublicKey: + pkd.KeyDetails = protocommon.PublicKeyDetails_PKIX_ED25519 + default: + return nil, fmt.Errorf("unknown public key type: %T", p) + } + + return &pkd, nil +} diff --git a/pkg/root/trusted_root_create_test.go b/pkg/root/trusted_root_create_test.go new file mode 100644 index 00000000..24a095ff --- /dev/null +++ b/pkg/root/trusted_root_create_test.go @@ -0,0 +1,217 @@ +// Copyright 2023 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 root + +import ( + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + fulcioRootCert = `-----BEGIN CERTIFICATE----- +MIICNzCCAd2gAwIBAgITPLBoBQhl1hqFND9S+SGWbfzaRTAKBggqhkjOPQQDAjBo +MQswCQYDVQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlw +cGVuaGFtMQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMI +dGVzdGNlcnQwHhcNMjEwMzEyMjMyNDQ5WhcNMzEwMjI4MjMyNDQ5WjBoMQswCQYD +VQQGEwJVSzESMBAGA1UECBMJV2lsdHNoaXJlMRMwEQYDVQQHEwpDaGlwcGVuaGFt +MQ8wDQYDVQQKEwZSZWRIYXQxDDAKBgNVBAsTA0NUTzERMA8GA1UEAxMIdGVzdGNl +cnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQRn+Alyof6xP3GQClSwgV0NFuY +YEwmKP/WLWr/LwB6LUYzt5v49RlqG83KuaJSpeOj7G7MVABdpIZYWwqAiZV3o2Yw +ZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU +T8Jwm6JuVb0dsiuHUROiHOOVHVkwHwYDVR0jBBgwFoAUT8Jwm6JuVb0dsiuHUROi +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 +MlIYY/yBSuDWAJOA5IKi4Hh9nI+Mmb/FXgbOz5a5mZx8w7pMiTMu0+Rd9cPzRkUZ +DQfZsLONr6PwmyCAIL1oK80fevxKZPME0UV8bFPWnRxeVaFr5ddd/DOenV8H6SPy +r4ODbSOItpl53y6Az0m3FTIUf8cSsyR7dfE4zpA3M4djjtoKDNFRsTjU2RWVQW9X +MaxzznGVGhLEwkC+sYjR5NQvH5iiRvV18q+CGQqNX2+WWM3SPuty3nc86RBNR0FO +gSQA0TL2OAs6bJNmfzcwZxAKYbj7/88tj6qrjLaQtFTbBm2a7+TAQfs3UTiQi00z +EDYqeSj2WQvacNm1dWEAyx0QNLHiKGTn4TShGj8LUoGyjJ26Y6VPsotvCoj8jM0e +aN8Pc9/AYywVI+QktjaPZa7KGH3XJHJkTIQQRcUxOtDstKpcriAefDs8jjL5ju9t +5J3qEvgzmclNJKRnla4p3maM0vk+8cC7EXMV4P1zuCwr3akaHFJo5Y0aFhKsnHqT +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==` + ctlogKeyID = `G3CTL21UG8/5ygV+/WVy/pvB8nUiZGOEnMVKIEDzPxY=` + ctlogMapKey = `1b70932f6d541bcff9ca057efd6572fe9bc1f275226463849cc54a2040f33f16` + + rekorPublicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzf +N6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA== +-----END PUBLIC KEY-----` + + rekorPublicKeyBase64DER = `MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEF6j2sTItLcs0wKoOpMzI+9lJmCzfN6mY2prOeaBRV2dnsJzC94hOxkM5pSp9nbAK1TBOI45fOOPsH2rSR++HrA==` + rekorKeyID = `xBzny6gmou42sCYrHOzNuGqi1s2cMxcCEq1wrKF9XDs=` + rekorMapKey = `c41ce7cba826a2ee36b0262b1ceccdb86aa2d6cd9c33170212ad70aca17d5c3b` + + 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 TestConstructTrustedRootJSON(t *testing.T) { + var targets []RawTrustedRootTarget + + var tsaCertChain []byte = []byte{} + tsaCertChain = append(tsaCertChain, []byte(tsaLeafCert)...) + tsaCertChain = append(tsaCertChain, byte('\n')) + tsaCertChain = append(tsaCertChain, []byte(tsaIntermedCert)...) + tsaCertChain = append(tsaCertChain, byte('\n')) + tsaCertChain = append(tsaCertChain, []byte(tsaRootCert)...) + + targets = append(targets, &BaseTrustedRootTarget{ + Type: TSATarget, + Bytes: tsaCertChain, + }) + targets = append(targets, &BaseTrustedRootTarget{ + Type: FulcioTarget, + Bytes: []byte(fulcioRootCert), + }) + targets = append(targets, &BaseTrustedRootTarget{ + Type: CTFETarget, + Bytes: []byte(ctlogPublicKey), + }) + targets = append(targets, &BaseTrustedRootTarget{ + Type: RekorTarget, + Bytes: []byte(rekorPublicKey), + }) + + // create and serialize the TrustedRoot + tr, err := NewTrustedRootFromTargets(TrustedRootMediaType01, targets) + require.NoError(t, err) + js, err := json.Marshal(tr) + require.NoError(t, err) + + // now try loading it again + fromJS, err := NewTrustedRootFromJSON(js) + require.NoError(t, err) + + // verify media type + require.Equal(t, TrustedRootMediaType01, fromJS.trustedRoot.MediaType) + + // verify tlogs + rekorKeyDER, _ := pem.Decode([]byte(rekorPublicKey)) + rekorKey, _ := x509.ParsePKIXPublicKey(rekorKeyDER.Bytes) + decodedRekorID, _ := base64.StdEncoding.DecodeString(rekorKeyID) + require.Equal(t, map[string]*TransparencyLog{ + rekorMapKey: { + ID: decodedRekorID, + HashFunc: crypto.SHA256, + ValidityPeriodStart: fromJS.rekorLogs[rekorMapKey].ValidityPeriodStart, + PublicKey: rekorKey, + SignatureHashFunc: crypto.SHA256, + }, + }, fromJS.rekorLogs) + + // verify ctlogs + ctlogKeyDER, _ := pem.Decode([]byte(ctlogPublicKey)) + ctlogKey, _ := x509.ParsePKCS1PublicKey(ctlogKeyDER.Bytes) + decodedCtlogID, _ := base64.StdEncoding.DecodeString(ctlogKeyID) + require.Equal(t, map[string]*TransparencyLog{ + ctlogMapKey: { + ID: decodedCtlogID, + HashFunc: crypto.SHA256, + ValidityPeriodStart: fromJS.ctLogs[ctlogMapKey].ValidityPeriodStart, + PublicKey: ctlogKey, + SignatureHashFunc: crypto.SHA256, + }, + }, fromJS.ctLogs) + + // verify fulcio certs + fulcioCertDER, _ := pem.Decode([]byte(fulcioRootCert)) + parsedFulcioCert, _ := x509.ParseCertificate(fulcioCertDER.Bytes) + require.Equal(t, []CertificateAuthority{ + { + Root: parsedFulcioCert, + ValidityPeriodStart: parsedFulcioCert.NotBefore, + ValidityPeriodEnd: parsedFulcioCert.NotAfter, + }, + }, fromJS.fulcioCertAuthorities) + + // verify tsa certs + tsaLeafCertDER, _ := pem.Decode([]byte(tsaLeafCert)) + parsedTSALeafCert, _ := x509.ParseCertificate(tsaLeafCertDER.Bytes) + tsaIntermedCertDER, _ := pem.Decode([]byte(tsaIntermedCert)) + parsedTSAIntermedCert, _ := x509.ParseCertificate(tsaIntermedCertDER.Bytes) + tsaRootCertDER, _ := pem.Decode([]byte(tsaRootCert)) + parsedTSARootCert, _ := x509.ParseCertificate(tsaRootCertDER.Bytes) + require.Equal(t, []CertificateAuthority{ + { + Root: parsedTSARootCert, + Intermediates: []*x509.Certificate{parsedTSAIntermedCert}, + Leaf: parsedTSALeafCert, + ValidityPeriodStart: parsedTSARootCert.NotBefore, + ValidityPeriodEnd: parsedTSARootCert.NotAfter, + }, + }, fromJS.timestampingAuthorities) +}