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 option to unify signed and log timestamps #51

Merged
merged 6 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion cmd/conformance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func main() {

// Check bundle and trusted root for Tlog information
if len(tr.TlogAuthorities()) > 0 && b.HasInclusionPromise() {
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1))
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
Copy link
Member

Choose a reason for hiding this comment

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

Minor: In trying to make sure I understand this pull request, I'm wondering if this should be verify.WithIntegratedTimestamps(1) instead.

Generally, I think we want people to use WithObserverTimestamps() and not have to reason about the specifics of WithIntegratedTimestamps() or WithSignedTimestamps().

But I thought what we were saying on #42 is that the conformance test will infer the verification policy from the trust root, and if the trust root has tlogs entries, we should rely on them both for inclusion and for establishing a timestamp?

I guess when inferring the policy from the trust root, there isn't a way to differentiate today. This isn't a problem really, but it does highlight the complexity of the problem space generally!

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 right, this should be WithIntegratedTimestamps since this is inferring a policy based on if a promise is present. I'll update in a follow up.

In order to infer WithObserverTimestamps, which would be more permissive, it would probably look something like a check for either bundled timestamps or SETs. We could do that here, but we can be more precise with WithSignedTimestamps and WithIntegratedTimestamps.

}

sev, err := verify.NewSignedEntityVerifier(tr, verifierConfig...)
Expand Down
12 changes: 12 additions & 0 deletions cmd/sigstore-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ var expectedOIDIssuer *string
var expectedSAN *string
var expectedSANRegex *string
var requireTSA *bool
var requireIntegratedTs *bool
var requireTimestamp *bool
var requireTlog *bool
var minBundleVersion *string
var onlineTlog *bool
Expand All @@ -57,6 +59,8 @@ func init() {
expectedSAN = flag.String("expectedSAN", "", "The expected identity in the signing certificate's SAN extension")
expectedSANRegex = flag.String("expectedSANRegex", "", "The expected identity in the signing certificate's SAN extension")
requireTSA = flag.Bool("requireTSA", false, "Require RFC 3161 signed timestamp")
requireIntegratedTs = flag.Bool("requireIntegratedTs", false, "Require log entry integrated timestamp")
requireTimestamp = flag.Bool("requireTimestamp", false, "Require either an RFC3161 signed timestamp or log entry integrated timestamp")
requireTlog = flag.Bool("requireTlog", true, "Require Artifact Transparency log entry (Rekor)")
minBundleVersion = flag.String("minBundleVersion", "", "Minimum acceptable bundle version (e.g. '0.1')")
onlineTlog = flag.Bool("onlineTlog", false, "Verify Artifact Transparency log entry online (Rekor)")
Expand Down Expand Up @@ -105,6 +109,14 @@ func run() error {
verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1))
}

if *requireIntegratedTs {
verifierConfig = append(verifierConfig, verify.WithIntegratedTimestamps(1))
}

if *requireTimestamp {
verifierConfig = append(verifierConfig, verify.WithObserverTimestamps(1))
}

if *requireTlog {
verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1))
}
Expand Down
1 change: 1 addition & 0 deletions examples/oci-image-verification/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func run() error {

verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1))

// TODO: Add flag for allowing observer timestamp once merged
if *requireTSA {
verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1))
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/verify/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestEnvelopeSubject(t *testing.T) {
entity, err := virtualSigstore.Attest("foo@example.com", "issuer", statement)
assert.NoError(t, err)

verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1))
verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithSignedTimestamps(1))
assert.NoError(t, err)

_, err = verifier.Verify(entity, SkipArtifactAndIdentitiesPolicy)
Expand Down Expand Up @@ -98,7 +98,7 @@ func TestSignatureVerifierMessageSignature(t *testing.T) {
entity, err := virtualSigstore.Sign("foofighters@example.com", "issuer", []byte(artifact))
assert.NoError(t, err)

verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1))
verifier, err := verify.NewSignedEntityVerifier(virtualSigstore, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
assert.NoError(t, err)

result, err := verifier.Verify(entity, verify.NewPolicy(verify.WithArtifact(bytes.NewBufferString(artifact)), verify.WithoutIdentitiesUnsafe()))
Expand Down
149 changes: 124 additions & 25 deletions pkg/verify/signed_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,40 @@ type SignedEntityVerifier struct {
}

type VerifierConfig struct { // nolint: revive
performOnlineVerification bool
weExpectSignedTimestamps bool
signedTimestampThreshold int
weExpectTlogEntries bool
tlogEntriesThreshold int
weExpectSCTs bool
ctlogEntriesThreshold int
// performOnlineVerification queries logs during verification.
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
// Default is offline
performOnlineVerification bool
// weExpectSignedTimestamps requires RFC3161 timestamps to verify
// short-lived certificates
weExpectSignedTimestamps bool
// signedTimestampThreshold is the minimum number of verified
// RFC3161 timestamps in a bundle
signedTimestampThreshold int
// requireIntegratedTimestamps requires log entry integrated timestamps to
// verify short-lived certificates
requireIntegratedTimestamps bool
// integratedTimeThreshold is the minimum number of log entry
// integrated timestamps in a bundle
integratedTimeThreshold int
// requireObserverTimestamps requires RFC3161 timestamps and/or log
// integrated timestamps to verify short-lived certificates
requireObserverTimestamps bool
// observerTimestampThreshold is the minimum number of verified
// RFC3161 timestamps and/or log integrated timestamps in a bundle
observerTimestampThreshold int
// weExpectTlogEntries requires log inclusion proofs in a bundle
weExpectTlogEntries bool
// tlogEntriesThreshold is the minimum number of verified inclusion
// proofs in a bundle
tlogEntriesThreshold int
// weExpectSCTs requires SCTs in Fulcio certificates
weExpectSCTs bool
// ctlogEntriesTreshold is the minimum number of verified SCTs in
// a Fulcio certificate
ctlogEntriesThreshold int
// weDoNotExpectAnyObserverTimestamps uses the certificate's lifetime
// rather than a provided signed or log timestamp. Most workflows will
// not use this option
weDoNotExpectAnyObserverTimestamps bool
}

Expand Down Expand Up @@ -104,10 +131,25 @@ func WithSignedTimestamps(threshold int) VerifierOption {
}
}

// WithObserverTimestamps configures the SignedEntityVerifier to expect
// timestamps from either an RFC3161 timestamp authority or a log's
// SignedEntryTimestamp. These are verified using the TrustedMaterial's
// TSACertificateAuthorities() or TlogAuthorities(), and used to verify
// the Fulcio certificate.
func WithObserverTimestamps(threshold int) VerifierOption {
return func(c *VerifierConfig) error {
if threshold < 1 {
return errors.New("observer timestamp threshold must be at least 1")
}
c.requireObserverTimestamps = true
c.observerTimestampThreshold = threshold
return nil
}
}

// WithTransparencyLog configures the SignedEntityVerifier to expect
// Transparency Log entries, verify them using the TrustedMaterial's
// TlogAuthorities(), and, if it exists, use the resulting Inclusion timestamp(s)
// to verify the Fulcio certificate.
// Transparency Log inclusion proofs or SignedEntryTimestamps, verifying them
// using the TrustedMaterial's TlogAuthorities().
func WithTransparencyLog(threshold int) VerifierOption {
return func(c *VerifierConfig) error {
if threshold < 1 {
Expand All @@ -119,6 +161,17 @@ func WithTransparencyLog(threshold int) VerifierOption {
}
}

// WithIntegratedTimestamps configures the SignedEntityVerifier to
// expect log entry integrated timestamps from either SignedEntryTimestamps
// or live log lookups.
func WithIntegratedTimestamps(threshold int) VerifierOption {
return func(c *VerifierConfig) error {
c.requireIntegratedTimestamps = true
c.integratedTimeThreshold = threshold
return nil
}
}

// WithSignedCertificateTimestamps configures the SignedEntityVerifier to
// expect the Fulcio certificate to have a SignedCertificateTimestamp, and
// verify it using the TrustedMaterial's CTLogAuthorities().
Expand Down Expand Up @@ -150,8 +203,9 @@ func WithoutAnyObserverTimestampsInsecure() VerifierOption {
}

func (c *VerifierConfig) Validate() error {
if !c.weExpectSignedTimestamps && !c.weExpectTlogEntries && !c.weDoNotExpectAnyObserverTimestamps {
return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one, or both, of WithSignedTimestamps() or WithTransparencyLog()")
if !c.requireObserverTimestamps && !c.weExpectSignedTimestamps && !c.requireIntegratedTimestamps && !c.weDoNotExpectAnyObserverTimestamps {
return errors.New("when initializing a new SignedEntityVerifier, you must specify at least one of " +
"WithObserverTimestamps(), WithSignedTimestamps(), WithIntegratedTimestamps(), or WithoutAnyObserverTimestampsInsecure()")
}

return nil
Expand Down Expand Up @@ -416,11 +470,16 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V
return nil, fmt.Errorf("failed to build policy: %w", err)
}

// Let's go by the spec: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit#heading=h.msyyz1cr5bcs
// Let's go by the spec: https://docs.google.com/document/d/1kbhK2qyPPk8SLavHzYSDM8-Ueul9_oxIMVFuWMWKz0E/edit#heading=h.g11ovq2s1jxh
// > ## Transparency Log Entry
verifiedTlogTimestamps, err := v.VerifyTransparencyLogInclusion(entity)
if err != nil {
return nil, fmt.Errorf("failed to verify log inclusion: %w", err)
}

// > ## Establishing a Time for the Signature
// > First, establish a time for the signature. This timestamp is required to validate the certificate chain, so this step comes first.

verifiedTimestamps, err := v.VerifyObserverTimestamps(entity)
verifiedTimestamps, err := v.VerifyObserverTimestamps(entity, verifiedTlogTimestamps)
if err != nil {
return nil, fmt.Errorf("failed to verify timestamps: %w", err)
}
Expand Down Expand Up @@ -547,35 +606,75 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V
return result, nil
}

// VerifyObserverTimestamps verifies TlogEntries and SignedTimestamps, if we
// expect them, and returns a slice of verified results, which embed the actual
// time.Time value. This value can then be used to verify certificates, if any.
// VerifyTransparencyLogInclusion verifies TlogEntries if expected. Optionally returns
// a list of verified timestamps from the log integrated timestamps when verifying
// with observer timestamps.
// TODO: Return a different verification result for logs specifically (also for #48)
func (v *SignedEntityVerifier) VerifyTransparencyLogInclusion(entity SignedEntity) ([]TimestampVerificationResult, error) {
verifiedTimestamps := []TimestampVerificationResult{}

if v.config.weExpectTlogEntries {
// log timestamps should be verified if with WithIntegratedTimestamps or WithObserverTimestamps is used
verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold,
v.config.requireIntegratedTimestamps || v.config.requireObserverTimestamps, v.config.performOnlineVerification)
if err != nil {
return nil, err
}

for _, vts := range verifiedTlogTimestamps {
verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "Tlog", URI: "TODO", Timestamp: vts})
}
}

return verifiedTimestamps, nil
}

// VerifyObserverTimestamps verifies RFC3161 signed timestamps, and verifies
// that timestamp thresholds are met with log entry integrated timestamps,
// signed timestamps, or a combination of both. The returned timestamps
// can be used to verify short-lived certificates.
// logTimestamps may be populated with verified log entry integrated timestamps
// In order to be verifiable, a SignedEntity must have at least one verified
// "observer timestamp".
func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity) ([]TimestampVerificationResult, error) {
func (v *SignedEntityVerifier) VerifyObserverTimestamps(entity SignedEntity, logTimestamps []TimestampVerificationResult) ([]TimestampVerificationResult, error) {
verifiedTimestamps := []TimestampVerificationResult{}

// From spec:
// > … if verification or timestamp parsing fails, the Verifier MUST abort
if v.config.weExpectSignedTimestamps {
verifiedSignedTimestamps, err := VerifyTimestampAuthority(entity, v.trustedMaterial, v.config.signedTimestampThreshold)
verifiedSignedTimestamps, err := VerifyTimestampAuthorityWithThreshold(entity, v.trustedMaterial, v.config.signedTimestampThreshold)
if err != nil {
return nil, err
}

for _, vts := range verifiedSignedTimestamps {
verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "TimestampAuthority", URI: "TODO", Timestamp: vts})
}
}

if v.config.weExpectTlogEntries {
verifiedTlogTimestamps, err := VerifyArtifactTransparencyLog(entity, v.trustedMaterial, v.config.tlogEntriesThreshold, v.config.performOnlineVerification)
if v.config.requireIntegratedTimestamps {
if len(logTimestamps) < v.config.integratedTimeThreshold {
return nil, fmt.Errorf("threshold not met for verified log entry integrated timestamps: %d < %d", len(logTimestamps), v.config.integratedTimeThreshold)
}
verifiedTimestamps = append(verifiedTimestamps, logTimestamps...)
}

if v.config.requireObserverTimestamps {
verifiedSignedTimestamps, err := VerifyTimestampAuthority(entity, v.trustedMaterial)
if err != nil {
return nil, err
}

for _, vts := range verifiedTlogTimestamps {
verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "Tlog", URI: "TODO", Timestamp: vts})
// check threshold for both RFC3161 and log timestamps
tsCount := len(verifiedSignedTimestamps) + len(logTimestamps)
if tsCount < v.config.observerTimestampThreshold {
return nil, fmt.Errorf("threshold not met for verified signed & log entry integrated timestamps: %d < %d",
tsCount, v.config.observerTimestampThreshold)
}

// append all timestamps
verifiedTimestamps = append(verifiedTimestamps, logTimestamps...)
for _, vts := range verifiedSignedTimestamps {
verifiedTimestamps = append(verifiedTimestamps, TimestampVerificationResult{Type: "TimestampAuthority", URI: "TODO", Timestamp: vts})
}
}

Expand Down
Loading