From b9bce7ce15f4a5359df35be6a666ab3e2307454b Mon Sep 17 00:00:00 2001 From: Luke Valenta Date: Wed, 26 Feb 2025 15:23:19 -0500 Subject: [PATCH] Add NotAfter field to AssertionRequest - Allow an initial notAfter flag to be specified when creating an assertion request. This can only serve to shorten the lifetime of the assertion, so may have limited value. - Add the minimum of (initial not_after, batch not_after, umbilical not_after) to a queued assertion. Later, this may be added to the abridged assertion and proof. --- ca/ca.go | 40 +++++++++++++++++++++++++------- cmd/mtc/main.go | 59 +++++++++++++++++++++++++---------------------- mtc.go | 11 ++++++++- umbilical/x509.go | 16 ++++++------- 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/ca/ca.go b/ca/ca.go index e452852..8ac2d02 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -125,7 +125,22 @@ func (h *Handle) QueueMultiple(it func(yield func(ar mtc.AssertionRequest) error defer w.Close() bw := bufio.NewWriter(w) + nextBatch := mtc.Batch{ + Number: h.params.ActiveBatches(time.Now()).End + 1, + CA: &h.params, + } + batchStart, batchEnd := nextBatch.ValidityInterval() + if err := it(func(ar mtc.AssertionRequest) error { + // Check that the assertion matches the checksum. + err = ar.Check() + if err != nil { + return err + } + notAfter := ar.NotAfter + if notAfter.IsZero() || batchEnd.Before(notAfter) { + notAfter = batchEnd + } switch h.params.EvidencePolicy { case mtc.EmptyEvidencePolicyType: case mtc.UmbilicalEvidencePolicyType: @@ -145,16 +160,12 @@ func (h *Handle) QueueMultiple(it func(yield func(ar mtc.AssertionRequest) error } } if chain == nil { - return fmt.Errorf("missing x509 chain evidence") + return errors.New("missing x509 chain evidence") } - // TODO validate based on the min of the certificate 'not_after' and the - // next batch's expiration, and add the timestamp to the queued assertion. - // https://github.com/davidben/merkle-tree-certs/pull/92 - batch := mtc.Batch{ - CA: &h.params, - Number: h.params.ActiveBatches(time.Now()).End + 1, + if notAfter.IsZero() || chain[0].NotAfter.Before(notAfter) { + notAfter = chain[0].NotAfter } - _, err = umbilical.CheckAssertionValidForX509(ar.Assertion, batch, chain, h.umbilicalRoots, h.revocationChecker) + _, err = umbilical.CheckAssertionValidForX509(ar.Assertion, batchStart, notAfter, chain, h.umbilicalRoots, h.revocationChecker) if err != nil { return err } @@ -162,6 +173,12 @@ func (h *Handle) QueueMultiple(it func(yield func(ar mtc.AssertionRequest) error return fmt.Errorf("unknown evidence policy: %d", h.params.EvidencePolicy) } + if notAfter != ar.NotAfter { + ar.NotAfter = notAfter + // Recompute the checksum. + ar.Checksum = nil + } + buf, err := ar.MarshalBinary() if err != nil { return err @@ -1009,6 +1026,13 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error { if !empty { err = h.WalkQueue(func(ar mtc.AssertionRequest) error { + // Skip assertions that are already expired. + if start, _ := batch.ValidityInterval(); ar.NotAfter.Before(start) { + return nil + } + + // TODO add not_after to abridged assertion and proof + // https://github.com/davidben/merkle-tree-certs/pull/92 aa := ar.Assertion.Abridge() buf, err := aa.MarshalBinary() if err != nil { diff --git a/cmd/mtc/main.go b/cmd/mtc/main.go index 17fb2ce..c21c9c7 100644 --- a/cmd/mtc/main.go +++ b/cmd/mtc/main.go @@ -96,7 +96,11 @@ func assertionRequestFlags(inFile bool) []cli.Flag { Category: "Assertion", Usage: "Only proceed if assertion matches checksum", }, - + &cli.StringFlag{ + Name: "not_after", + Category: "Assertion", + Usage: "An initial not_after value for the assertion request in RFC3339 format, which can be used to shorten an assertion's lifetime", + }, &cli.StringFlag{ Name: "from-x509-pem", Category: "Assertion", @@ -142,6 +146,7 @@ func assertionRequestFromFlags(cc *cli.Context) (*mtc.AssertionRequest, error) { func assertionRequestFromFlagsUnchecked(cc *cli.Context) (*mtc.AssertionRequest, error) { var ( checksum []byte + notAfter time.Time err error ) @@ -152,6 +157,13 @@ func assertionRequestFromFlagsUnchecked(cc *cli.Context) (*mtc.AssertionRequest, } } + if cc.String("not_after") != "" { + notAfter, err = time.Parse(time.RFC3339, cc.String("not_after")) + if err != nil { + return nil, fmt.Errorf("parsing not_after: %w", err) + } + } + assertionPath := cc.String("in-file") if assertionPath != "" { assertionBuf, err := os.ReadFile(assertionPath) @@ -353,6 +365,7 @@ func assertionRequestFromFlagsUnchecked(cc *cli.Context) (*mtc.AssertionRequest, Assertion: a, Evidence: el, Checksum: checksum, + NotAfter: notAfter, }, nil } @@ -483,31 +496,8 @@ func handleCaShowQueue(cc *cli.Context) error { err = h.WalkQueue(func(ar mtc.AssertionRequest) error { count++ - a := ar.Assertion - cs := a.Claims - subj := a.Subject w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) - fmt.Fprintf(w, "checksum\t%x\n", ar.Checksum) - fmt.Fprintf(w, "subject_type\t%s\n", subj.Type()) - switch subj := subj.(type) { - case *mtc.TLSSubject: - asubj := subj.Abridge().(*mtc.AbridgedTLSSubject) - fmt.Fprintf(w, "signature_scheme\t%s\n", asubj.SignatureScheme) - fmt.Fprintf(w, "public_key_hash\t%x\n", asubj.PublicKeyHash[:]) - } - if len(cs.DNS) != 0 { - fmt.Fprintf(w, "dns\t%s\n", cs.DNS) - } - if len(cs.DNSWildcard) != 0 { - fmt.Fprintf(w, "dns_wildcard\t%s\n", cs.DNSWildcard) - } - if len(cs.IPv4) != 0 { - fmt.Fprintf(w, "ip4\t%s\n", cs.IPv4) - } - if len(cs.IPv6) != 0 { - fmt.Fprintf(w, "ip6\t%s\n", cs.IPv6) - } - err = writeEvidenceList(w, ar.Evidence) + err = writeAssertionRequest(w, ar) if err != nil { return err } @@ -715,6 +705,21 @@ func handleInspectTree(cc *cli.Context) error { return nil } +func writeAssertionRequest(w *tabwriter.Writer, ar mtc.AssertionRequest) error { + fmt.Fprintf(w, "checksum\t%x\n", ar.Checksum) + if ar.NotAfter.IsZero() { + fmt.Fprintf(w, "not_after\tunset\n") + } else { + fmt.Fprintf(w, "not_after\t%v\n", ar.NotAfter.UTC()) + } + writeAssertion(w, ar.Assertion) + err := writeEvidenceList(w, ar.Evidence) + if err != nil { + return err + } + return nil +} + func writeAssertion(w *tabwriter.Writer, a mtc.Assertion) { aa := a.Abridge() cs := aa.Claims @@ -849,9 +854,7 @@ func handleInspectAssertionRequest(cc *cli.Context) error { } w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) - fmt.Fprintf(w, "checksum\t%x\n", ar.Checksum) - writeAssertion(w, ar.Assertion) - err = writeEvidenceList(w, ar.Evidence) + err = writeAssertionRequest(w, ar) if err != nil { return err } diff --git a/mtc.go b/mtc.go index c0e940b..ad1d831 100644 --- a/mtc.go +++ b/mtc.go @@ -136,6 +136,7 @@ type AssertionRequest struct { Checksum []byte Assertion Assertion Evidence EvidenceList + NotAfter time.Time } // Copy of tls.SignatureScheme to prevent cycling dependencies @@ -957,7 +958,8 @@ func (e UnknownEvidence) Info() []byte { return e.info } func (ar *AssertionRequest) UnmarshalBinary(data []byte) error { var ( - s cryptobyte.String = cryptobyte.String(data) + notAfter uint64 + s cryptobyte.String = cryptobyte.String(data) ) ar.Checksum = make([]byte, sha256.Size) if !s.CopyBytes(ar.Checksum) { @@ -974,6 +976,11 @@ func (ar *AssertionRequest) UnmarshalBinary(data []byte) error { return err } + if !s.ReadUint64(¬After) { + return ErrTruncated + } + ar.NotAfter = time.Unix(int64(notAfter), 0) + if !s.Empty() { return ErrExtraBytes } @@ -1001,6 +1008,8 @@ func (ar *AssertionRequest) marshalAndCheckAssertionRequest() ([]byte, error) { } b.AddBytes(buf) + b.AddUint64(uint64(ar.NotAfter.Unix())) + checksumBytes, err := b.Bytes() if err != nil { return nil, err diff --git a/umbilical/x509.go b/umbilical/x509.go index a25b6cb..7e04303 100644 --- a/umbilical/x509.go +++ b/umbilical/x509.go @@ -3,6 +3,8 @@ package umbilical import ( + "time" + "github.com/bwesterb/mtc" "github.com/bwesterb/mtc/umbilical/revocation" @@ -87,9 +89,9 @@ func GetChainFromTLSServer(addr string) (chain []*x509.Certificate, err error) { return } -// Checks whether the given assertion (to be) issued in the given batch -// is consistent with the given X.509 certificate chain and -// trusted roots. The assertion is allowed to cover less than the certificate: +// Checks whether the given assertion (to be) issued is consistent with +// the given X.509 certificate chain and accepted roots for the given validity +// interval. The assertion is allowed to cover less than the certificate: // eg, only example.com where the certificate covers some.example.com too. // // On the other hand, we are more strict than is perhaps required. For @@ -102,7 +104,7 @@ func GetChainFromTLSServer(addr string) (chain []*x509.Certificate, err error) { // revocation of intermediates. // // If consistent, returns one or more verified chains. -func CheckAssertionValidForX509(a mtc.Assertion, batch mtc.Batch, +func CheckAssertionValidForX509(a mtc.Assertion, start, end time.Time, chain []*x509.Certificate, roots *x509.CertPool, rc *revocation.Checker) ( [][]*x509.Certificate, error) { if len(chain) == 0 { @@ -168,9 +170,7 @@ func CheckAssertionValidForX509(a mtc.Assertion, batch mtc.Batch, return nil, fmt.Errorf("Subjects don't match") } - // Verify chain at the start of the batch's validity period - start, end := batch.ValidityInterval() - + // Verify chain at the start of the validity period opts := x509.VerifyOptions{ Roots: roots, Intermediates: x509.NewCertPool(), @@ -187,7 +187,7 @@ func CheckAssertionValidForX509(a mtc.Assertion, batch mtc.Batch, var ret [][]*x509.Certificate var errs []error - // Verify each chain at the end of the batch's validity period + // Verify each chain at the end of the validity period for _, candidateChain := range chains { opts = x509.VerifyOptions{ Roots: x509.NewCertPool(),