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 the ability to contruct TrustRoot from targets #247

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
35 changes: 34 additions & 1 deletion pkg/root/trusted_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type CertificateAuthority struct {
Leaf *x509.Certificate
ValidityPeriodStart time.Time
ValidityPeriodEnd time.Time
URI string
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI @cmurphy, about the URI being added for TSAs

}

type TransparencyLog struct {
Expand Down Expand Up @@ -81,6 +82,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())
Expand Down Expand Up @@ -240,7 +250,9 @@ func ParseCertificateAuthority(certAuthority *prototrustroot.CertificateAuthorit
return nil, fmt.Errorf("CertificateAuthority cert chain is empty")
}

certificateAuthority = &CertificateAuthority{}
certificateAuthority = &CertificateAuthority{
URI: certAuthority.Uri,
}
for i, cert := range certChain.GetCertificates() {
parsedCert, err := x509.ParseCertificate(cert.RawBytes)
if err != nil {
Expand Down Expand Up @@ -302,6 +314,27 @@ func NewTrustedRootProtobuf(rootJSON []byte) (*prototrustroot.TrustedRoot, error
return pbTrustedRoot, nil
}

// NewTrustedRoot initializes a TrustedRoot object from a mediaType string, list of Fulcio
// certificate authorities, list of timestamp authorities and maps of ctlogs and rekor
// transparency log instances.
func NewTrustedRoot(mediaType string,
certificateAuthorities []CertificateAuthority,
certificateTransparencyLogs map[string]*TransparencyLog,
timestampAuthorities []CertificateAuthority,
transparencyLogs map[string]*TransparencyLog) (*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{
fulcioCertAuthorities: certificateAuthorities,
ctLogs: certificateTransparencyLogs,
timestampingAuthorities: timestampAuthorities,
rekorLogs: transparencyLogs,
}
return tr, nil
}

// FetchTrustedRoot fetches the Sigstore trusted root from TUF and returns it.
func FetchTrustedRoot() (*TrustedRoot, error) {
return FetchTrustedRootWithOptions(tuf.DefaultOptions())
Expand Down
221 changes: 221 additions & 0 deletions pkg/root/trusted_root_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// 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/x509"
"fmt"
"sort"
"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"
)

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)
// ensure stable sorting of the slice
Copy link
Member

Choose a reason for hiding this comment

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

Very nice!

sort.Slice(tr.trustedRoot.Tlogs, func(i, j int) bool {
iTime := time.Unix(0, 0)
jTime := time.Unix(0, 0)

if tr.trustedRoot.Tlogs[i].PublicKey.ValidFor.Start != nil {
iTime = tr.trustedRoot.Tlogs[i].PublicKey.ValidFor.Start.AsTime()
}
if tr.trustedRoot.Tlogs[j].PublicKey.ValidFor.Start != nil {
iTime = tr.trustedRoot.Tlogs[j].PublicKey.ValidFor.Start.AsTime()
}
return iTime.Before(jTime)
})
}

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)
// ensure stable sorting of the slice
sort.Slice(tr.trustedRoot.Ctlogs, func(i, j int) bool {
iTime := time.Unix(0, 0)
jTime := time.Unix(0, 0)

if tr.trustedRoot.Ctlogs[i].PublicKey.ValidFor.Start != nil {
iTime = tr.trustedRoot.Ctlogs[i].PublicKey.ValidFor.Start.AsTime()
}
if tr.trustedRoot.Ctlogs[j].PublicKey.ValidFor.Start != nil {
iTime = tr.trustedRoot.Ctlogs[j].PublicKey.ValidFor.Start.AsTime()
}
return iTime.Before(jTime)
})
}

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)
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to sort the certificate authorities too (and TSAs) similar to what you did for the tlogs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, you're right, I forgot to do that. I'll submit a fix for it as well.

}

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) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func certificateAuthorityToProtobufCA(ca CertificateAuthority) (*prototrustroot.CertificateAuthority, error) {
func certificateAuthorityToProtobufCA(ca *CertificateAuthority) (*prototrustroot.CertificateAuthority, error) {

Same here on receiving via value or pointer.

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: ca.URI,
Subject: &protocommon.DistinguishedName{
Organization: org,
CommonName: ca.Root.Subject.CommonName,
},
ValidFor: &protocommon.TimeRange{
Start: timestamppb.New(ca.ValidityPeriodStart),
},
CertChain: &protocommon.X509CertificateChain{
Certificates: allCerts,
},
}

if !ca.ValidityPeriodEnd.IsZero() {
caProto.ValidFor.End = timestamppb.New(ca.ValidityPeriodEnd)
}

return &caProto, nil
}

func transparencyLogToProtobufTL(tl TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {
Copy link
Member

@kommendorkapten kommendorkapten Aug 21, 2024

Choose a reason for hiding this comment

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

Suggested change
func transparencyLogToProtobufTL(tl TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {
func transparencyLogToProtobufTL(tl *TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {

Any reason not to receive this via a pointer to avoid excessive copying?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right, I missed that. I will fix it.

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, tl.ValidityPeriodEnd)
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, start time.Time, end time.Time) (*protocommon.PublicKey, error) {
pkd := protocommon.PublicKey{
ValidFor: &protocommon.TimeRange{
Start: timestamppb.New(start),
},
}

if !end.IsZero() {
pkd.ValidFor.End = timestamppb.New(end)
}

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
}
20 changes: 20 additions & 0 deletions pkg/root/trusted_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"encoding/pem"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -165,3 +167,21 @@ func TestTrustedMaterialCollectionRSA(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, verifier, verifier2)
}

func TestFromJSONToJSON(t *testing.T) {
trustedrootJSON, err := os.ReadFile("../../examples/trusted-root-public-good.json")
assert.NoError(t, err)

trustedRoot, err := NewTrustedRootFromJSON(trustedrootJSON)
assert.NoError(t, err)

jsonBytes, err := json.Marshal(trustedRoot)
assert.NoError(t, err)

// Protobuf JSON serialization intentionally strips second fraction from time, if
// the fraction is 0. We do the same to the expected result:
// https://github.com/golang/protobuf/blob/b7697bb698b1c56643249ef6179c7cae1478881d/jsonpb/encode.go#L207
trJSONTrimmedTime := strings.ReplaceAll(string(trustedrootJSON), ".000Z\"", "Z\"")

assert.JSONEq(t, trJSONTrimmedTime, string(jsonBytes))
}
Loading