Skip to content

Commit

Permalink
Add the ability to contruct TrustRoot from targets (#247)
Browse files Browse the repository at this point in the history
* Add the ability to contruct TrustRoot from targets

Signed-off-by: Slavek Kabrda <[email protected]>

* Address review, fix linting issues

Signed-off-by: Slavek Kabrda <[email protected]>

* Fix test by stripping second fraction from input

Signed-off-by: Slavek Kabrda <[email protected]>

* Add an example, fix a public key reading error case

Signed-off-by: Slavek Kabrda <[email protected]>

* Address review

Signed-off-by: Slavek Kabrda <[email protected]>

* Address review, pull out sorting functionality in separate functions

Signed-off-by: Slavek Kabrda <[email protected]>

---------

Signed-off-by: Slavek Kabrda <[email protected]>
  • Loading branch information
bkabrda authored Aug 21, 2024
1 parent f9235ff commit 2198ac3
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 1 deletion.
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
}

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
235 changes: 235 additions & 0 deletions pkg/root/trusted_root_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// 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
sortTlogSlice(tr.trustedRoot.Tlogs)

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
sortTlogSlice(tr.trustedRoot.Ctlogs)

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)
}
// ensure stable sorting of the slice
sortCASlice(tr.trustedRoot.CertificateAuthorities)

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)
}
// ensure stable sorting of the slice
sortCASlice(tr.trustedRoot.TimestampAuthorities)

return nil
}

func sortCASlice(slc []*prototrustroot.CertificateAuthority) {
sort.Slice(slc, func(i, j int) bool {
iTime := time.Unix(0, 0)
jTime := time.Unix(0, 0)

if slc[i].ValidFor.Start != nil {
iTime = slc[i].ValidFor.Start.AsTime()
}
if slc[j].ValidFor.Start != nil {
jTime = slc[j].ValidFor.Start.AsTime()
}

return iTime.Before(jTime)
})
}

func sortTlogSlice(slc []*prototrustroot.TransparencyLogInstance) {
sort.Slice(slc, func(i, j int) bool {
iTime := time.Unix(0, 0)
jTime := time.Unix(0, 0)

if slc[i].PublicKey.ValidFor.Start != nil {
iTime = slc[i].PublicKey.ValidFor.Start.AsTime()
}
if slc[j].PublicKey.ValidFor.Start != nil {
jTime = slc[j].PublicKey.ValidFor.Start.AsTime()
}

return iTime.Before(jTime)
})
}

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: 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) {
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))
}

0 comments on commit 2198ac3

Please sign in to comment.