diff --git a/ca/ca.go b/ca/ca.go index 7df4d1f..d08c750 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -21,11 +21,8 @@ import ( "golang.org/x/crypto/cryptobyte" ) -const csLen = 32 - var ( - ErrChecksumInvalid = errors.New("Invalid checksum") - ErrClosed = errors.New("Handle is closed") + ErrClosed = errors.New("Handle is closed") ) type NewOpts struct { @@ -50,76 +47,12 @@ type Handle struct { indices map[uint32]*Index aas map[uint32]*os.File + evs map[uint32]*os.File trees map[uint32]*Tree batchNumbersCache []uint32 // cache for existing batches } -type QueuedAssertion struct { - Checksum []byte - Assertion mtc.Assertion -} - -func (a *QueuedAssertion) UnmarshalBinary(data []byte) error { - var ( - s cryptobyte.String = cryptobyte.String(data) - checksum [csLen]byte - ) - if !s.CopyBytes(checksum[:]) { - return mtc.ErrTruncated - } - - a.Checksum = make([]byte, csLen) - copy(a.Checksum, checksum[:]) - - checksum2 := sha256.Sum256([]byte(s)) - if !bytes.Equal(checksum2[:], checksum[:]) { - return ErrChecksumInvalid - } - - if err := a.Assertion.UnmarshalBinary([]byte(s)); err != nil { - return err - } - - return nil -} - -func (a *QueuedAssertion) marshalAndCheckAssertion() ([]byte, error) { - buf, err := a.Assertion.MarshalBinary() - if err != nil { - return nil, err - } - - checksum2 := sha256.Sum256([]byte(buf)) - if a.Checksum == nil { - a.Checksum = checksum2[:] - } else if !bytes.Equal(checksum2[:], a.Checksum) { - return nil, ErrChecksumInvalid - } - - return buf, nil -} - -// If set, checks whether the Checksum is correct. If not set, sets the -// Checksum to the correct value. -func (a *QueuedAssertion) Check() error { - _, err := a.marshalAndCheckAssertion() - return err -} - -func (a *QueuedAssertion) MarshalBinary() ([]byte, error) { - var b cryptobyte.Builder - - buf, err := a.marshalAndCheckAssertion() - if err != nil { - return nil, err - } - b.AddBytes(a.Checksum) - b.AddBytes(buf) - - return b.Bytes() -} - func (ca *Handle) Params() mtc.CAParams { return ca.params } @@ -137,6 +70,10 @@ func (ca *Handle) Close() error { r.Close() } + for _, r := range ca.evs { + r.Close() + } + for _, t := range ca.trees { t.Close() } @@ -165,7 +102,7 @@ func (h *Handle) dropQueue() error { // // For each entry, if checksum is not nil, makes sure the assertion // matches the checksum -func (h *Handle) QueueMultiple(it func(yield func(qa QueuedAssertion) error) error) error { +func (h *Handle) QueueMultiple(it func(yield func(ar mtc.AssertionRequest) error) error) error { if h.closed { return ErrClosed } @@ -177,8 +114,8 @@ func (h *Handle) QueueMultiple(it func(yield func(qa QueuedAssertion) error) err defer w.Close() bw := bufio.NewWriter(w) - if err := it(func(qa QueuedAssertion) error { - buf, err := qa.MarshalBinary() + if err := it(func(ar mtc.AssertionRequest) error { + buf, err := ar.MarshalBinary() if err != nil { return err } @@ -208,14 +145,9 @@ func (h *Handle) QueueMultiple(it func(yield func(qa QueuedAssertion) error) err // Queue assertion for publication. // // If checksum is not nil, makes sure assertion matches the checksum. -func (h *Handle) Queue(a mtc.Assertion, checksum []byte) error { - return h.QueueMultiple(func(yield func(qa QueuedAssertion) error) error { - return yield( - QueuedAssertion{ - Checksum: checksum, - Assertion: a, - }, - ) +func (h *Handle) Queue(ar mtc.AssertionRequest) error { + return h.QueueMultiple(func(yield func(ar mtc.AssertionRequest) error) error { + return yield(ar) }) } @@ -227,6 +159,7 @@ func Open(path string) (*Handle, error) { path: path, indices: make(map[uint32]*Index), aas: make(map[uint32]*os.File), + evs: make(map[uint32]*os.File), trees: make(map[uint32]*Tree), } if err := h.lock(); err != nil { @@ -282,6 +215,10 @@ func (h Handle) aaPath(number uint32) string { return gopath.Join(h.batchPath(number), "abridged-assertions") } +func (h Handle) evPath(number uint32) string { + return gopath.Join(h.batchPath(number), "evidence") +} + func (h Handle) batchPath(number uint32) string { return gopath.Join(h.batchesPath(), fmt.Sprintf("%d", number)) } @@ -387,7 +324,7 @@ func (h *Handle) listBatchRange() (mtc.BatchRange, error) { } // Calls f on each assertion queued to be published. -func (h *Handle) WalkQueue(f func(QueuedAssertion) error) error { +func (h *Handle) WalkQueue(f func(mtc.AssertionRequest) error) error { r, err := os.OpenFile(h.queuePath(), os.O_RDONLY, 0) if err != nil { return fmt.Errorf("Opening queue: %w", err) @@ -400,7 +337,7 @@ func (h *Handle) WalkQueue(f func(QueuedAssertion) error) error { var ( prefix [2]byte aLen uint16 - qa QueuedAssertion + ar mtc.AssertionRequest ) _, err := io.ReadFull(br, prefix[:]) if err == io.EOF { @@ -418,12 +355,12 @@ func (h *Handle) WalkQueue(f func(QueuedAssertion) error) error { return fmt.Errorf("Reading queue: %w", err) } - err = qa.UnmarshalBinary(buf) + err = ar.UnmarshalBinary(buf) if err != nil { return fmt.Errorf("Parsing queue: %w", err) } - err = f(qa) + err = f(ar) if err != nil { return err } @@ -489,6 +426,14 @@ func (h *Handle) closeBatch(batch uint32) error { delete(h.aas, batch) } + if r, ok := h.evs[batch]; ok { + err := r.Close() + if err != nil { + return fmt.Errorf("closing evidence for %d: %w", batch, err) + } + delete(h.evs, batch) + } + if r, ok := h.trees[batch]; ok { err := r.Close() if err != nil { @@ -545,10 +490,25 @@ func (ca *Handle) aaFileFor(batch uint32) (*os.File, error) { return r, nil } +// Returns file handle to evidence file for the given batch. +func (ca *Handle) evFileFor(batch uint32) (*os.File, error) { + if r, ok := ca.evs[batch]; ok { + return r, nil + } + + r, err := os.Open(ca.evPath(batch)) + if err != nil { + return nil, err + } + + return r, nil +} + type keySearchResult struct { Batch uint32 SequenceNumber uint64 Offset uint64 + EvidenceOffset uint64 } // Returns the certificate for an issued assertion @@ -589,7 +549,46 @@ func (ca *Handle) CertificateFor(a mtc.Assertion) (*mtc.BikeshedCertificate, err }, nil } -// Search for AbridgedAssertions's batch/seqno/offset by key. +var errShortCircuit = errors.New("short circuit") + +// Returns the evidence for an issued assertion +func (ca *Handle) EvidenceFor(a mtc.Assertion) (*mtc.Evidence, error) { + aa := a.Abridge() + var key [mtc.HashLen]byte + err := aa.Key(key[:]) + if err != nil { + return nil, err + } + res, err := ca.aaByKey(key[:]) + if err != nil { + return nil, fmt.Errorf("searching by key: %w", err) + } + + if res == nil { + return nil, fmt.Errorf("no assertion with key %x on record", key) + } + + var ev *mtc.Evidence + evFile, err := ca.evFileFor(res.Batch) + if err != nil { + return nil, err + } + _, err = evFile.Seek(int64(res.EvidenceOffset), 0) + if err != nil { + return nil, err + } + err = mtc.UnmarshalEvidenceEntries(evFile, func(_ int, ev2 *mtc.Evidence) error { + ev = ev2 + return errShortCircuit + }) + if err != errShortCircuit { + return nil, err + } + + return ev, nil +} + +// Search for AbridgedAssertions's batch/seqno/offset/evidence_offset by key. func (ca *Handle) aaByKey(key []byte) (*keySearchResult, error) { batches, err := ca.listBatchRange() if err != nil { @@ -600,7 +599,7 @@ func (ca *Handle) aaByKey(key []byte) (*keySearchResult, error) { return nil, nil } - for batch := batches.End - 1; batch <= batches.End; batch-- { + for batch := batches.End - 1; batch >= batches.Begin && batch <= batches.End; batch-- { res, err := ca.aaByKeyIn(batch, key) if err != nil { return nil, fmt.Errorf("Searching in batch %d: %w", batch, err) @@ -610,6 +609,7 @@ func (ca *Handle) aaByKey(key []byte) (*keySearchResult, error) { Batch: batch, SequenceNumber: res.SequenceNumber, Offset: res.Offset, + EvidenceOffset: res.EvidenceOffset, }, nil } } @@ -760,6 +760,7 @@ func (h *Handle) issueBatch(number uint32, empty bool) error { "tree", "signed-validity-window", "abridged-assertions", + "evidence", "index", }, ) @@ -884,7 +885,7 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error { prevHeads = w.ValidityWindow.TreeHeads } - // Read queue and write abridged-assertions + // Read queue and write abridged-assertions and evidence aasPath := gopath.Join(dir, "abridged-assertions") aasW, err := os.Create(aasPath) if err != nil { @@ -893,23 +894,48 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error { defer aasW.Close() aasBW := bufio.NewWriter(aasW) + evPath := gopath.Join(dir, "evidence") + evW, err := os.Create(evPath) + if err != nil { + return fmt.Errorf("creating %s: %w", evPath, err) + } + defer evW.Close() + evBW := bufio.NewWriter(evW) + if !empty { - err = h.WalkQueue(func(qa QueuedAssertion) error { - aa := qa.Assertion.Abridge() + err = h.WalkQueue(func(ar mtc.AssertionRequest) error { + aa := ar.Assertion.Abridge() buf, err := aa.MarshalBinary() if err != nil { - return fmt.Errorf("Marshalling assertion %x: %w", qa.Checksum, err) + return fmt.Errorf("Marshalling assertion %x: %w", ar.Checksum, err) } _, err = aasBW.Write(buf) if err != nil { return fmt.Errorf( "Writing assertion %x to %s: %w", - qa.Checksum, + ar.Checksum, aasPath, err, ) } + + ev := ar.Evidence + buf, err = ev.MarshalBinary() + if err != nil { + return fmt.Errorf("Marshalling evidence %x: %w", ar.Checksum, err) + } + + _, err = evBW.Write(buf) + if err != nil { + return fmt.Errorf( + "Writing evidence %x to %s: %w", + ar.Checksum, + evPath, + err, + ) + } + return nil }) if err != nil { @@ -932,6 +958,21 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error { } defer aasR.Close() + err = evBW.Flush() + if err != nil { + return fmt.Errorf("flushing %s: %w", evPath, err) + } + + err = evW.Close() + if err != nil { + return fmt.Errorf("closing %s: %w", evPath, err) + } + evR, err := os.OpenFile(evPath, os.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("opening %s: %w", evPath, err) + } + defer evR.Close() + // Compute tree tree, err := batch.ComputeTree(bufio.NewReader(aasR)) if err != nil { @@ -970,7 +1011,7 @@ func (h *Handle) issueBatchTo(dir string, batch mtc.Batch, empty bool) error { defer indexW.Close() - err = ComputeIndex(aasR, indexW) + err = ComputeIndex(aasR, evR, indexW) if err != nil { return fmt.Errorf("computing %s to start: %w", indexPath, err) } diff --git a/ca/index.go b/ca/index.go index 85a2aef..db6e643 100644 --- a/ca/index.go +++ b/ca/index.go @@ -1,17 +1,18 @@ package ca -// Functions to work with the batches' index file into abridged-assertions. +// Functions to work with the batches' index file into abridged-assertions and evidence. // -// The index file consists of 48 byte entries, sorted by key. +// The index file consists of 56 byte entries, sorted by key. // -// +--------------+--------------+---------------+ -// | 32-byte key | uint64 seqno | uint64 offset | -// +--------------+--------------+---------------+ +// +--------------+--------------+---------------+------------------------+ +// | 32-byte key | uint64 seqno | uint64 offset | uint64 evidence_offset | +// +--------------+--------------+---------------+------------------------+ // // Each entry corresponds to an AbridgedAssertion. The key is its key, -// the seqno is the sequence number within the abridged-assertions list, and -// the offset is the byte-offset in the abridged-assertions file. -// offset and seqno are encoded big endian. +// the seqno is the sequence number within the abridged-assertions list, +// the offset is the byte-offset in the abridged-assertions file, and +// the evidence_offset is the byte-offset in the evidence file. +// offset, evidence_offset, and seqno are encoded big endian. // // This allows quick lookups by key using interpolation search. // @@ -40,6 +41,7 @@ type Index struct { type IndexSearchResult struct { SequenceNumber uint64 Offset uint64 + EvidenceOffset uint64 } // Opens an index @@ -138,6 +140,7 @@ func (h *Index) Search(hash []byte) (*IndexSearchResult, error) { ss := cryptobyte.String(val[:]) ss.ReadUint64(&ret.SequenceNumber) ss.ReadUint64(&ret.Offset) + ss.ReadUint64(&ret.EvidenceOffset) return &ret, nil case -1: // tmp < needle a.Set(&tmp) @@ -154,19 +157,21 @@ func (h *Index) Search(hash []byte) (*IndexSearchResult, error) { } type indexEntry struct { - key [mtc.HashLen]byte - seqno uint64 - offset uint64 + key [mtc.HashLen]byte + seqno uint64 + offset uint64 + evidenceOffset uint64 } -// Reads a stream of AbridgedAssertions from r, and writes the index to w. -func ComputeIndex(r io.Reader, w io.Writer) error { +// Reads a streams of AbridgedAssertions Evidence from aaReader and evReader, +// and writes the index to w. +func ComputeIndex(aaReader, evReader io.Reader, w io.Writer) error { // First compute keys seqno := uint64(0) entries := []indexEntry{} var key [mtc.HashLen]byte - err := mtc.UnmarshalAbridgedAssertions(r, func(offset int, + err := mtc.UnmarshalAbridgedAssertions(aaReader, func(offset int, aa *mtc.AbridgedAssertion) error { err := aa.Key(key[:]) if err != nil { @@ -181,6 +186,13 @@ func ComputeIndex(r io.Reader, w io.Writer) error { return nil }) + seqno = uint64(0) + err = mtc.UnmarshalEvidenceEntries(evReader, func(offset int, _ *mtc.Evidence) error { + entries[seqno].evidenceOffset = uint64(offset) + seqno++ + return nil + }) + if err != nil { return fmt.Errorf("computing keys: %w", err) } @@ -204,6 +216,7 @@ func ComputeIndex(r io.Reader, w io.Writer) error { b.AddBytes(entry.key[:]) b.AddUint64(entry.seqno) b.AddUint64(entry.offset) + b.AddUint64(entry.evidenceOffset) buf, _ := b.Bytes() _, err = bw.Write(buf) diff --git a/cmd/mtc/main.go b/cmd/mtc/main.go index 88b38d2..2d1df44 100644 --- a/cmd/mtc/main.go +++ b/cmd/mtc/main.go @@ -46,9 +46,10 @@ func writeToFileOrStdout(path string, buf []byte) error { return nil } -// Flags used to create or specify an assertion. Used in `mtc ca queue'. +// Flags used to create or specify an assertion request. +// Used in `mtc ca queue' and 'mtc ca cert'. // Includes the in-file flag, if inFile is true. -func assertionFlags(inFile bool) []cli.Flag { +func assertionRequestFlags(inFile bool) []cli.Flag { ret := []cli.Flag{ &cli.StringSliceFlag{ Name: "dns", @@ -110,7 +111,7 @@ func assertionFlags(inFile bool) []cli.Flag { Name: "in-file", Category: "Assertion", Aliases: []string{"i"}, - Usage: "Read assertion from the given file", + Usage: "Read assertion request from the given file", }, ) } @@ -118,21 +119,21 @@ func assertionFlags(inFile bool) []cli.Flag { return ret } -func assertionFromFlags(cc *cli.Context) (*ca.QueuedAssertion, error) { - qa, err := assertionFromFlagsUnchecked(cc) +func assertionRequestFromFlags(cc *cli.Context) (*mtc.AssertionRequest, error) { + ar, err := assertionRequestFromFlagsUnchecked(cc) if err != nil { return nil, err } - err = qa.Check() + err = ar.Check() if err != nil { return nil, err } - return qa, nil + return ar, nil } -func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) { +func assertionRequestFromFlagsUnchecked(cc *cli.Context) (*mtc.AssertionRequest, error) { var ( checksum []byte err error @@ -150,7 +151,7 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) { assertionBuf, err := os.ReadFile(assertionPath) if err != nil { return nil, fmt.Errorf( - "reading assertion %s: %w", + "reading assertion request %s: %w", assertionPath, err, ) @@ -174,24 +175,21 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) { } } - var a mtc.Assertion - err = a.UnmarshalBinary(assertionBuf) + var ar mtc.AssertionRequest + err = ar.UnmarshalBinary(assertionBuf) if err != nil { return nil, fmt.Errorf( - "parsing assertion %s: %w", + "parsing assertion request %s: %w", assertionPath, err, ) } - - return &ca.QueuedAssertion{ - Assertion: a, - Checksum: checksum, - }, nil + return &ar, nil } var ( a mtc.Assertion + e mtc.Evidence scheme mtc.SignatureScheme ) @@ -251,6 +249,9 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) { if err != nil { return nil, fmt.Errorf("from-x509: %s", err) } + + e.Type = mtc.X509ChainEvidenceType + e.Info = mtc.X509ChainEvidenceInfo(certs) } // Setting any claim will overwrite those suggested by the @@ -339,14 +340,15 @@ func assertionFromFlagsUnchecked(cc *cli.Context) (*ca.QueuedAssertion, error) { a.Subject = subj } - return &ca.QueuedAssertion{ + return &mtc.AssertionRequest{ Assertion: a, + Evidence: e, Checksum: checksum, }, nil } func handleCaQueue(cc *cli.Context) error { - qa, err := assertionFromFlags(cc) + ar, err := assertionRequestFromFlags(cc) if err != nil { return err } @@ -357,17 +359,17 @@ func handleCaQueue(cc *cli.Context) error { } defer h.Close() - return h.QueueMultiple(func(yield func(qa ca.QueuedAssertion) error) error { + return h.QueueMultiple(func(yield func(ar mtc.AssertionRequest) error) error { for i := 0; i < cc.Int("debug-repeat"); i++ { - qa2 := *qa + ar2 := *ar if cc.Bool("debug-vary") { - qa2.Checksum = nil - qa2.Assertion.Claims.DNS = append( - qa2.Assertion.Claims.DNS, + ar2.Checksum = nil + ar2.Assertion.Claims.DNS = append( + ar2.Assertion.Claims.DNS, fmt.Sprintf("%d.example.com", i), ) } - if err := yield(qa2); err != nil { + if err := yield(ar2); err != nil { return err } } @@ -375,13 +377,13 @@ func handleCaQueue(cc *cli.Context) error { }) } -func handleNewAssertion(cc *cli.Context) error { - qa, err := assertionFromFlags(cc) +func handleNewAssertionRequest(cc *cli.Context) error { + ar, err := assertionRequestFromFlags(cc) if err != nil { return err } - buf, err := qa.Assertion.MarshalBinary() + buf, err := ar.MarshalBinary() if err != nil { return err } @@ -390,8 +392,6 @@ func handleNewAssertion(cc *cli.Context) error { return err } - fmt.Fprintf(os.Stderr, "checksum: %x\n", qa.Checksum) - return nil } @@ -412,12 +412,12 @@ func handleCaCert(cc *cli.Context) error { } defer h.Close() - qa, err := assertionFromFlags(cc) + ar, err := assertionRequestFromFlags(cc) if err != nil { return err } - cert, err := h.CertificateFor(qa.Assertion) + cert, err := h.CertificateFor(ar.Assertion) if err != nil { return err } @@ -434,6 +434,35 @@ func handleCaCert(cc *cli.Context) error { return nil } +func handleCaEvidence(cc *cli.Context) error { + h, err := ca.Open(cc.String("ca-path")) + if err != nil { + return err + } + defer h.Close() + + ar, err := assertionRequestFromFlags(cc) + if err != nil { + return err + } + + ev, err := h.EvidenceFor(ar.Assertion) + if err != nil { + return err + } + + buf, err := ev.MarshalBinary() + if err != nil { + return err + } + + if err := writeToFileOrStdout(cc.String("out-file"), buf); err != nil { + return err + } + + return nil +} + func handleCaShowQueue(cc *cli.Context) error { h, err := ca.Open(cc.String("ca-path")) if err != nil { @@ -443,13 +472,13 @@ func handleCaShowQueue(cc *cli.Context) error { count := 0 - err = h.WalkQueue(func(qa ca.QueuedAssertion) error { + err = h.WalkQueue(func(ar mtc.AssertionRequest) error { count++ - a := qa.Assertion + 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", qa.Checksum) + 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: @@ -469,6 +498,7 @@ func handleCaShowQueue(cc *cli.Context) error { if len(cs.IPv6) != 0 { fmt.Fprintf(w, "ip6\t%s\n", cs.IPv6) } + writeEvidence(w, ar.Evidence) w.Flush() fmt.Printf("\n") return nil @@ -476,7 +506,7 @@ func handleCaShowQueue(cc *cli.Context) error { if err != nil { return err } - fmt.Printf("Total number of assertions in queue: %d\n", count) + fmt.Printf("Total number of assertion requests in queue: %d\n", count) return nil } @@ -614,9 +644,10 @@ func handleInspectIndex(cc *cli.Context) error { } var ( - key []byte - seqno uint64 - offset uint64 + key []byte + seqno uint64 + offset uint64 + evidenceOffset uint64 ) s := cryptobyte.String(buf) @@ -624,11 +655,11 @@ func handleInspectIndex(cc *cli.Context) error { total := 0 fmt.Printf("%64s %7s %7s\n", "key", "seqno", "offset") for !s.Empty() { - if !s.ReadBytes(&key, 32) || !s.ReadUint64(&seqno) || !s.ReadUint64(&offset) { + if !s.ReadBytes(&key, 32) || !s.ReadUint64(&seqno) || !s.ReadUint64(&offset) || !s.ReadUint64(&evidenceOffset) { return errors.New("truncated") } - fmt.Printf("%x %7d %7d\n", key, seqno, offset) + fmt.Printf("%x %7d %7d %7d\n", key, seqno, offset, evidenceOffset) total++ } @@ -681,6 +712,28 @@ func writeAssertion(w *tabwriter.Writer, a mtc.Assertion) { } } +func writeEvidence(w *tabwriter.Writer, e mtc.Evidence) { + + fmt.Fprintf(w, "evidence\t") + switch e.Type { + case mtc.EmptyEvidenceType: + fmt.Fprintf(w, "empty\n") + case mtc.X509ChainEvidenceType: + fmt.Fprintf(w, "x509_chain\n") + for i, cert := range e.Info.(mtc.X509ChainEvidenceInfo) { + fmt.Fprintf(w, " certificate\t%d\n", i) + fmt.Fprintf(w, " subject\t%s\n", cert.Subject.String()) + fmt.Fprintf(w, " issuer\t%s\n", cert.Issuer.String()) + fmt.Fprintf(w, " serial_no\t%x\n", cert.SerialNumber) + fmt.Fprintf(w, " not_before\t%s\n", cert.NotBefore) + fmt.Fprintf(w, " not_after\t%s\n", cert.NotAfter) + } + default: + fmt.Fprintf(w, "unknown\n") + fmt.Fprintf(w, " raw\t%x", e.Info.(mtc.UnknownEvidenceInfo)) + } +} + func handleInspectCert(cc *cli.Context) error { buf, err := inspectGetBuf(cc) if err != nil { @@ -752,24 +805,52 @@ func handleInspectCert(cc *cli.Context) error { return nil } -func handleInspectAssertion(cc *cli.Context) error { +func handleInspectAssertionRequest(cc *cli.Context) error { buf, err := inspectGetBuf(cc) if err != nil { return err } - var a mtc.Assertion - err = a.UnmarshalBinary(buf) + var ar mtc.AssertionRequest + err = ar.UnmarshalBinary(buf) if err != nil { return err } w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) - writeAssertion(w, a) + fmt.Fprintf(w, "checksum\t%x\n", ar.Checksum) + writeAssertion(w, ar.Assertion) + writeEvidence(w, ar.Evidence) w.Flush() return nil } +func handleInspectEvidence(cc *cli.Context) error { + + r, err := inspectGetReader(cc) + if err != nil { + return err + } + defer r.Close() + + count := 0 + err = mtc.UnmarshalEvidenceEntries( + bufio.NewReader(r), + func(_ int, e *mtc.Evidence) error { + count++ + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + writeEvidence(w, *e) + w.Flush() + return nil + }, + ) + if err != nil { + return err + } + fmt.Printf("Total number of evidence entries: %d\n", count) + return nil +} + func handleInspectAbridgedAssertions(cc *cli.Context) error { r, err := inspectGetReader(cc) if err != nil { @@ -912,7 +993,7 @@ func main() { Usage: "queue assertion for issuance", Action: handleCaQueue, Flags: append( - assertionFlags(true), + assertionRequestFlags(true), &cli.IntFlag{ Name: "debug-repeat", Category: "Debug", @@ -931,10 +1012,23 @@ func main() { Usage: "creates certificate for an issued assertion", Action: handleCaCert, Flags: append( - assertionFlags(true), + assertionRequestFlags(true), + &cli.StringFlag{ + Name: "out-file", + Usage: "path to write cert to", + Aliases: []string{"o"}, + }, + ), + }, + { + Name: "evidence", + Usage: "fetches evidence for an issued assertion", + Action: handleCaEvidence, + Flags: append( + assertionRequestFlags(true), &cli.StringFlag{ Name: "out-file", - Usage: "path to write assertion to", + Usage: "path to write evidence to", Aliases: []string{"o"}, }, ), @@ -974,9 +1068,15 @@ func main() { ArgsUsage: "[path]", }, { - Name: "assertion", - Usage: "parses an assertion", - Action: handleInspectAssertion, + Name: "assertion-request", + Usage: "parses an assertion request", + Action: handleInspectAssertionRequest, + ArgsUsage: "[path]", + }, + { + Name: "evidence", + Usage: "parses batch's evidence file", + Action: handleInspectEvidence, ArgsUsage: "[path]", }, { @@ -1007,14 +1107,14 @@ func main() { }, }, { - Name: "new-assertion", - Usage: "creates a new assertion", - Action: handleNewAssertion, + Name: "new-assertion-request", + Usage: "creates a new assertion request", + Action: handleNewAssertionRequest, Flags: append( - assertionFlags(false), + assertionRequestFlags(false), &cli.StringFlag{ Name: "out-file", - Usage: "path to write assertion to", + Usage: "path to write assertion request to", Aliases: []string{"o"}, }, ), diff --git a/go.mod b/go.mod index 3503818..1419683 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( golang.org/x/exp v0.0.0-20240119083558-1b970713d09a ) -require go.etcd.io/bbolt v1.4.0 // indirect +require go.etcd.io/bbolt v1.4.0 require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 719a345..3d41e81 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,16 @@ github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7q github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -20,3 +26,5 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http/server.go b/http/server.go index 87face3..986b027 100644 --- a/http/server.go +++ b/http/server.go @@ -2,7 +2,6 @@ package http import ( "context" - "encoding/hex" "errors" "fmt" "io" @@ -45,21 +44,10 @@ func (s *Server) Shutdown(ctx context.Context) error { return s.server.Shutdown(ctx) } -func assertionFromRequestUnchecked(r *http.Request) (*ca.QueuedAssertion, error) { - +func assertionFromRequestUnchecked(r *http.Request) (*mtc.AssertionRequest, error) { var ( - checksum []byte - err error + ar mtc.AssertionRequest ) - - checksumParam := r.URL.Query().Get("checksum") - if checksumParam != "" { - checksum, err = hex.DecodeString(checksumParam) - if err != nil { - return nil, err - } - } - var a mtc.Assertion switch r.Method { case http.MethodPost: body, err := io.ReadAll(r.Body) @@ -67,33 +55,28 @@ func assertionFromRequestUnchecked(r *http.Request) (*ca.QueuedAssertion, error) return nil, err } defer r.Body.Close() - - err = a.UnmarshalBinary(body) + err = ar.UnmarshalBinary(body) if err != nil { return nil, err } + return &ar, nil default: return nil, fmt.Errorf("unsupported HTTP method: %v", r.Method) } - - return &ca.QueuedAssertion{ - Assertion: a, - Checksum: checksum, - }, nil } -func assertionFromRequest(r *http.Request) (*ca.QueuedAssertion, error) { - qa, err := assertionFromRequestUnchecked(r) +func assertionFromRequest(r *http.Request) (*mtc.AssertionRequest, error) { + ar, err := assertionFromRequestUnchecked(r) if err != nil { return nil, err } - err = qa.Check() + err = ar.Check() if err != nil { return nil, err } - return qa, nil + return ar, nil } func handleCaQueue(path string) func(w http.ResponseWriter, r *http.Request) { @@ -104,13 +87,13 @@ func handleCaQueue(path string) func(w http.ResponseWriter, r *http.Request) { return } defer h.Close() - qa, err := assertionFromRequest(r) + a, err := assertionFromRequest(r) if err != nil { http.Error(w, "invalid assertion", http.StatusBadRequest) return } - err = h.Queue(qa.Assertion, qa.Checksum) + err = h.Queue(*a) if err != nil { http.Error(w, "failed to queue assertion", http.StatusInternalServerError) return @@ -127,13 +110,13 @@ func handleCaCert(path string) func(w http.ResponseWriter, r *http.Request) { return } defer h.Close() - qa, err := assertionFromRequest(r) + a, err := assertionFromRequest(r) if err != nil { http.Error(w, "invalid assertion", http.StatusBadRequest) return } - cert, err := h.CertificateFor(qa.Assertion) + cert, err := h.CertificateFor(a.Assertion) if err != nil { http.Error(w, "failed to get certificate for assertion", http.StatusBadRequest) return diff --git a/mtc.go b/mtc.go index fcfba30..45df3b9 100644 --- a/mtc.go +++ b/mtc.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto" "crypto/sha256" + "crypto/x509" "errors" "fmt" "io" @@ -53,6 +54,24 @@ type Claims struct { Unknown []UnknownClaim } +type EvidenceType uint16 + +const ( + EmptyEvidenceType EvidenceType = iota + X509ChainEvidenceType +) + +type Evidence struct { + Type EvidenceType + Info EvidenceInfo +} + +type EvidenceInfo any + +type X509ChainEvidenceInfo []*x509.Certificate + +type UnknownEvidenceInfo []byte + // Represents a claim we do not how to interpret. type UnknownClaim struct { Type ClaimType @@ -100,6 +119,12 @@ type AbridgedAssertion struct { Claims Claims } +type AssertionRequest struct { + Checksum []byte + Assertion Assertion + Evidence Evidence +} + // Copy of tls.SignatureScheme to prevent cycling dependencies type SignatureScheme uint16 @@ -899,6 +924,87 @@ func (a *AbridgedAssertion) unmarshal(s *cryptobyte.String) error { return nil } +func (ar *AssertionRequest) UnmarshalBinary(data []byte) error { + var ( + s cryptobyte.String = cryptobyte.String(data) + ) + ar.Checksum = make([]byte, sha256.Size) + if !s.CopyBytes(ar.Checksum) { + return ErrTruncated + } + + err := ar.Assertion.unmarshal(&s) + if err != nil { + return err + } + + err = ar.Evidence.unmarshal(&s) + if err != nil { + return err + } + + if !s.Empty() { + return ErrExtraBytes + } + + err = ar.Check() + if err != nil { + return err + } + + return nil +} + +func (ar *AssertionRequest) marshalAndCheckAssertionRequest() ([]byte, error) { + var b cryptobyte.Builder + + buf, err := ar.Assertion.MarshalBinary() + if err != nil { + return nil, err + } + b.AddBytes(buf) + + buf, err = ar.Evidence.MarshalBinary() + if err != nil { + return nil, err + } + b.AddBytes(buf) + + checksumBytes, err := b.Bytes() + if err != nil { + return nil, err + } + checksum2 := sha256.Sum256(checksumBytes) + if ar.Checksum == nil { + ar.Checksum = checksum2[:] + } else if !bytes.Equal(checksum2[:], ar.Checksum) { + return nil, ErrChecksumInvalid + } + + return b.Bytes() +} + +// If set, checks whether the Checksum is correct. If not set, sets the +// Checksum to the correct value. +func (ar *AssertionRequest) Check() error { + // TODO validate evidence + _, err := ar.marshalAndCheckAssertionRequest() + return err +} + +func (ar *AssertionRequest) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + + buf, err := ar.marshalAndCheckAssertionRequest() + if err != nil { + return nil, err + } + b.AddBytes(ar.Checksum) + b.AddBytes(buf) + + return b.Bytes() +} + func (t *Tree) LeafCount() uint64 { return t.nLeaves } @@ -1468,7 +1574,7 @@ func (c *Claims) MarshalBinary() ([]byte, error) { return nil, err } - for i := 0; i < len(c.Unknown); i++ { + for i := range len(c.Unknown) { claim := c.Unknown[i] if i == 0 { if claim.Type <= Ipv6ClaimType { @@ -1489,6 +1595,84 @@ func (c *Claims) MarshalBinary() ([]byte, error) { return b.Bytes() } +func (e *Evidence) UnmarshalBinary(data []byte) error { + *e = Evidence{} + s := cryptobyte.String(data) + err := e.unmarshal(&s) + if err != nil { + return err + } + if !s.Empty() { + return ErrExtraBytes + } + return nil +} + +func (e *Evidence) unmarshal(s *cryptobyte.String) error { + + var ( + evidenceInfo cryptobyte.String + evidenceType EvidenceType + ) + + if !s.ReadUint16((*uint16)(&evidenceType)) || + !s.ReadUint24LengthPrefixed(&evidenceInfo) { + return ErrTruncated + } + e.Type = evidenceType + switch e.Type { + case EmptyEvidenceType: + if !evidenceInfo.Empty() { + return errors.New("non-empty info for empty evidence") + } + case X509ChainEvidenceType: + chain, err := x509.ParseCertificates([]byte(evidenceInfo)) + if err != nil { + return errors.New("failed to parse X.509 chain") + } + e.Info = X509ChainEvidenceInfo(chain) + default: + unknownInfo := make([]byte, len(evidenceInfo)) + evidenceInfo.CopyBytes(unknownInfo) + e.Info = UnknownEvidenceInfo(unknownInfo) + } + + return nil +} + +func (e *Evidence) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + + b.AddUint16(uint16(e.Type)) + b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { + switch e.Type { + case EmptyEvidenceType: + case X509ChainEvidenceType: + for _, cert := range e.Info.(X509ChainEvidenceInfo) { + b.AddBytes(cert.Raw) + } + default: + b.AddBytes(e.Info.(UnknownEvidenceInfo)) + } + }) + + return b.Bytes() +} + +// Unmarshals Evidence entries from r and calls f for each, with +// the offset in the stream as first argument, and the Evidence +// as second argument. +// +// Returns early on error. +func UnmarshalEvidenceEntries(r io.Reader, + f func(int, *Evidence) error) error { + return unmarshal(r, f) +} + +func (e *Evidence) maxSize() int { + return (65535+2)*2 + 2 +} + func NewMerkleTreeProof(batch *Batch, index uint64, path []byte) *MerkleTreeProof { return &MerkleTreeProof{ anchor: batch.Anchor(), diff --git a/utils.go b/utils.go index 185bd53..5e74d22 100644 --- a/utils.go +++ b/utils.go @@ -16,6 +16,10 @@ var ( // ErrExtraBytes is a parsing error returned when there are extraneous // bytes at the end of, or within, the data. ErrExtraBytes = errors.New("Unexpected extra (internal) bytes") + + // ErrChecksumInvalid is an error returned when a checksum does not + // match the corresponding data. + ErrChecksumInvalid = errors.New("Invalid checksum") ) type unmarshaler interface {