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 interface types for TimestampingAuthority and CertificateAuthority #300

Merged
merged 10 commits into from
Oct 22, 2024
64 changes: 64 additions & 0 deletions pkg/root/certificate_authority.go
codysoyland marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 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/x509"
"errors"
"time"
)

type CertificateAuthority interface {
Verify(cert *x509.Certificate, observerTimestamp time.Time) ([][]*x509.Certificate, error)
}

type FulcioCertificateAuthority struct {
Root *x509.Certificate
Intermediates []*x509.Certificate
ValidityPeriodStart time.Time
ValidityPeriodEnd time.Time
URI string
}

func (ca *FulcioCertificateAuthority) Verify(cert *x509.Certificate, observerTimestamp time.Time) ([][]*x509.Certificate, error) {
if !ca.ValidityPeriodStart.IsZero() && observerTimestamp.Before(ca.ValidityPeriodStart) {
return nil, errors.New("certificate is not valid yet")
}
if !ca.ValidityPeriodEnd.IsZero() && observerTimestamp.After(ca.ValidityPeriodEnd) {
return nil, errors.New("certificate is no longer valid")
}

rootCertPool := x509.NewCertPool()
rootCertPool.AddCert(ca.Root)
intermediateCertPool := x509.NewCertPool()
for _, cert := range ca.Intermediates {
intermediateCertPool.AddCert(cert)
}

// From spec:
// > ## Certificate
// > For a signature with a given certificate to be considered valid, it must have a timestamp while every certificate in the chain up to the root is valid (the so-called “hybrid model” of certificate verification per Braun et al. (2013)).

opts := x509.VerifyOptions{
CurrentTime: observerTimestamp,
Roots: rootCertPool,
Intermediates: intermediateCertPool,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageCodeSigning,
},
}

return cert.Verify(opts)
}
66 changes: 66 additions & 0 deletions pkg/root/timestamping_authority.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2024 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 (
"bytes"
"crypto/x509"
"errors"
"time"

tsaverification "github.com/sigstore/timestamp-authority/pkg/verification"
)

type Timestamp struct {
Time time.Time
URI string
}

type TimestampingAuthority interface {
Verify(signedTimestamp []byte, signatureBytes []byte) (*Timestamp, error)
}

type SigstoreTimestampingAuthority struct {
Root *x509.Certificate
Intermediates []*x509.Certificate
Leaf *x509.Certificate
ValidityPeriodStart time.Time
ValidityPeriodEnd time.Time
URI string
}

func (tsa *SigstoreTimestampingAuthority) Verify(signedTimestamp []byte, signatureBytes []byte) (*Timestamp, error) {
trustedRootVerificationOptions := tsaverification.VerifyOpts{
Roots: []*x509.Certificate{tsa.Root},
Intermediates: tsa.Intermediates,
TSACertificate: tsa.Leaf,
}

// Ensure timestamp responses are from trusted sources
timestamp, err := tsaverification.VerifyTimestampResponse(signedTimestamp, bytes.NewReader(signatureBytes), trustedRootVerificationOptions)
if err != nil {
return nil, err
}

if !tsa.ValidityPeriodStart.IsZero() && timestamp.Time.Before(tsa.ValidityPeriodStart) {
return nil, errors.New("timestamp is before the validity period start")
}
if !tsa.ValidityPeriodEnd.IsZero() && timestamp.Time.After(tsa.ValidityPeriodEnd) {
return nil, errors.New("timestamp is after the validity period end")
}

// All above verification successful, so return nil
return &Timestamp{Time: timestamp.Time, URI: tsa.URI}, nil
}
14 changes: 7 additions & 7 deletions pkg/root/trusted_material.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
)

type TrustedMaterial interface {
TimestampingAuthorities() []CertificateAuthority
TimestampingAuthorities() []TimestampingAuthority
FulcioCertificateAuthorities() []CertificateAuthority
RekorLogs() map[string]*TransparencyLog
CTLogs() map[string]*TransparencyLog
Expand All @@ -31,8 +31,8 @@ type TrustedMaterial interface {

type BaseTrustedMaterial struct{}

func (b *BaseTrustedMaterial) TimestampingAuthorities() []CertificateAuthority {
return []CertificateAuthority{}
func (b *BaseTrustedMaterial) TimestampingAuthorities() []TimestampingAuthority {
return []TimestampingAuthority{}
}

func (b *BaseTrustedMaterial) FulcioCertificateAuthorities() []CertificateAuthority {
Expand Down Expand Up @@ -67,12 +67,12 @@ func (tmc TrustedMaterialCollection) PublicKeyVerifier(keyID string) (TimeConstr
return nil, fmt.Errorf("public key verifier not found for keyID: %s", keyID)
}

func (tmc TrustedMaterialCollection) TimestampingAuthorities() []CertificateAuthority {
var certAuthorities []CertificateAuthority
func (tmc TrustedMaterialCollection) TimestampingAuthorities() []TimestampingAuthority {
var timestampingAuthorities []TimestampingAuthority
for _, tm := range tmc {
certAuthorities = append(certAuthorities, tm.TimestampingAuthorities()...)
timestampingAuthorities = append(timestampingAuthorities, tm.TimestampingAuthorities()...)
}
return certAuthorities
return timestampingAuthorities
}

func (tmc TrustedMaterialCollection) FulcioCertificateAuthorities() []CertificateAuthority {
Expand Down
102 changes: 73 additions & 29 deletions pkg/root/trusted_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,9 @@ type TrustedRoot struct {
BaseTrustedMaterial
trustedRoot *prototrustroot.TrustedRoot
rekorLogs map[string]*TransparencyLog
fulcioCertAuthorities []CertificateAuthority
certificateAuthorities []CertificateAuthority
ctLogs map[string]*TransparencyLog
timestampingAuthorities []CertificateAuthority
}

type CertificateAuthority struct {
Root *x509.Certificate
Intermediates []*x509.Certificate
Leaf *x509.Certificate
ValidityPeriodStart time.Time
ValidityPeriodEnd time.Time
URI string
timestampingAuthorities []TimestampingAuthority
}

type TransparencyLog struct {
Expand All @@ -66,12 +57,12 @@ type TransparencyLog struct {
SignatureHashFunc crypto.Hash
}

func (tr *TrustedRoot) TimestampingAuthorities() []CertificateAuthority {
func (tr *TrustedRoot) TimestampingAuthorities() []TimestampingAuthority {
return tr.timestampingAuthorities
}

func (tr *TrustedRoot) FulcioCertificateAuthorities() []CertificateAuthority {
return tr.fulcioCertAuthorities
return tr.certificateAuthorities
}

func (tr *TrustedRoot) RekorLogs() map[string]*TransparencyLog {
Expand Down Expand Up @@ -102,12 +93,12 @@ func NewTrustedRootFromProtobuf(protobufTrustedRoot *prototrustroot.TrustedRoot)
return nil, err
}

trustedRoot.fulcioCertAuthorities, err = ParseCertificateAuthorities(protobufTrustedRoot.GetCertificateAuthorities())
trustedRoot.certificateAuthorities, err = ParseCertificateAuthorities(protobufTrustedRoot.GetCertificateAuthorities())
if err != nil {
return nil, err
}

trustedRoot.timestampingAuthorities, err = ParseCertificateAuthorities(protobufTrustedRoot.GetTimestampAuthorities())
trustedRoot.timestampingAuthorities, err = ParseTimestampingAuthorities(protobufTrustedRoot.GetTimestampAuthorities())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -232,12 +223,12 @@ func ParseCertificateAuthorities(certAuthorities []*prototrustroot.CertificateAu
if err != nil {
return nil, err
}
certificateAuthorities[i] = *certificateAuthority
certificateAuthorities[i] = certificateAuthority
}
return certificateAuthorities, nil
}

func ParseCertificateAuthority(certAuthority *prototrustroot.CertificateAuthority) (certificateAuthority *CertificateAuthority, err error) {
func ParseCertificateAuthority(certAuthority *prototrustroot.CertificateAuthority) (*FulcioCertificateAuthority, error) {
if certAuthority == nil {
return nil, fmt.Errorf("CertificateAuthority is nil")
}
Expand All @@ -250,20 +241,17 @@ func ParseCertificateAuthority(certAuthority *prototrustroot.CertificateAuthorit
return nil, fmt.Errorf("CertificateAuthority cert chain is empty")
}

certificateAuthority = &CertificateAuthority{
certificateAuthority := &FulcioCertificateAuthority{
URI: certAuthority.Uri,
}
for i, cert := range certChain.GetCertificates() {
parsedCert, err := x509.ParseCertificate(cert.RawBytes)
if err != nil {
return nil, err
}
switch {
case i == 0 && !parsedCert.IsCA:
certificateAuthority.Leaf = parsedCert
case i < chainLen-1:
if i < chainLen-1 {
certificateAuthority.Intermediates = append(certificateAuthority.Intermediates, parsedCert)
case i == chainLen-1:
} else {
certificateAuthority.Root = parsedCert
}
}
Expand All @@ -281,12 +269,68 @@ func ParseCertificateAuthority(certAuthority *prototrustroot.CertificateAuthorit

certificateAuthority.URI = certAuthority.Uri

// TODO: Should we inspect/enforce ca.Subject?
// TODO: Handle validity period (ca.ValidFor)

return certificateAuthority, nil
}

func ParseTimestampingAuthorities(certAuthorities []*prototrustroot.CertificateAuthority) (timestampingAuthorities []TimestampingAuthority, err error) {
timestampingAuthorities = make([]TimestampingAuthority, len(certAuthorities))
for i, certAuthority := range certAuthorities {
timestampingAuthority, err := ParseTimestampingAuthority(certAuthority)
if err != nil {
return nil, err
}
timestampingAuthorities[i] = timestampingAuthority
}
return timestampingAuthorities, nil
}

func ParseTimestampingAuthority(certAuthority *prototrustroot.CertificateAuthority) (TimestampingAuthority, error) {
if certAuthority == nil {
return nil, fmt.Errorf("CertificateAuthority is nil")
}
certChain := certAuthority.GetCertChain()
if certChain == nil {
return nil, fmt.Errorf("CertificateAuthority missing cert chain")
}
chainLen := len(certChain.GetCertificates())
if chainLen < 1 {
return nil, fmt.Errorf("CertificateAuthority cert chain is empty")
}

timestampingAuthority := &SigstoreTimestampingAuthority{
URI: certAuthority.Uri,
}
for i, cert := range certChain.GetCertificates() {
parsedCert, err := x509.ParseCertificate(cert.RawBytes)
if err != nil {
return nil, err
}
switch {
case i == 0 && !parsedCert.IsCA:
timestampingAuthority.Leaf = parsedCert
case i < chainLen-1:
timestampingAuthority.Intermediates = append(timestampingAuthority.Intermediates, parsedCert)
case i == chainLen-1:
timestampingAuthority.Root = parsedCert
}
}
validFor := certAuthority.GetValidFor()
if validFor != nil {
start := validFor.GetStart()
if start != nil {
timestampingAuthority.ValidityPeriodStart = start.AsTime()
}
end := validFor.GetEnd()
if end != nil {
timestampingAuthority.ValidityPeriodEnd = end.AsTime()
}
}

timestampingAuthority.URI = certAuthority.Uri

return timestampingAuthority, nil
}

func NewTrustedRootFromPath(path string) (*TrustedRoot, error) {
trustedrootJSON, err := os.ReadFile(path)
if err != nil {
Expand Down Expand Up @@ -322,14 +366,14 @@ func NewTrustedRootProtobuf(rootJSON []byte) (*prototrustroot.TrustedRoot, error
func NewTrustedRoot(mediaType string,
certificateAuthorities []CertificateAuthority,
certificateTransparencyLogs map[string]*TransparencyLog,
timestampAuthorities []CertificateAuthority,
timestampAuthorities []TimestampingAuthority,
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,
certificateAuthorities: certificateAuthorities,
ctLogs: certificateTransparencyLogs,
timestampingAuthorities: timestampAuthorities,
rekorLogs: transparencyLogs,
Expand Down Expand Up @@ -430,7 +474,7 @@ func NewLiveTrustedRoot(opts *tuf.Options) (*LiveTrustedRoot, error) {
return ltr, nil
}

func (l *LiveTrustedRoot) TimestampingAuthorities() []CertificateAuthority {
func (l *LiveTrustedRoot) TimestampingAuthorities() []TimestampingAuthority {
l.mu.RLock()
defer l.mu.RUnlock()
return l.TrustedRoot.TimestampingAuthorities()
Expand Down
Loading