Skip to content
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
46 changes: 38 additions & 8 deletions cmd/mtc/log/internal/entry/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,47 @@ type EntryType uint16
// MTCLogEntry represents leaf node as defined in
// draft-ietf-plants-merkle-tree-certs section 5.2.1.
type MTCLogEntry struct {
Extensions []MTCLogEntryExtension
Type EntryType // MTCLogEntryTypeNull, MTCLogEntryTypeTBSCert
EntryData []byte // Raw DER bytes of TBSCertificateLogEntry if Type is TBSCert
extensions []MTCLogEntryExtension
entryType EntryType // MTCLogEntryTypeNull, MTCLogEntryTypeTBSCert
entryData []byte // Raw DER bytes of TBSCertificateLogEntry if Type is TBSCert
}

// New creates a new MTCLogEntry containing entryData and optional extensions.
// If entryData is empty, Type is set to MTCLogEntryTypeNull.
// Otherwise, Type is set to MTCLogEntryTypeTBSCert.
func New(entryData []byte, extensions ...MTCLogEntryExtension) *MTCLogEntry {
entryType := MTCLogEntryTypeTBSCert
if len(entryData) == 0 {
entryType = MTCLogEntryTypeNull
}
return &MTCLogEntry{
extensions: extensions,
entryType: entryType,
entryData: entryData,
}
}

// Extensions returns the entry's extensions list.
func (e *MTCLogEntry) Extensions() []MTCLogEntryExtension {
return e.extensions
}

// Type returns the entry's Type.
func (e *MTCLogEntry) Type() EntryType {
return e.entryType
}

// EntryData returns the raw entry data payload.
func (e *MTCLogEntry) EntryData() []byte {
return e.entryData
}

// Marshal encodes the MTCLogEntry into TLS Presentation bytes.
//
// Returns an error if the extensions are not specs compliant, or if the
// resulting bytes do not fit in a t-log leaf.
func (e *MTCLogEntry) Marshal() ([]byte, error) {
if e.Type == MTCLogEntryTypeNull && len(e.EntryData) > 0 {
if e.entryType == MTCLogEntryTypeNull && len(e.entryData) > 0 {
return nil, errors.New("null entry must have empty EntryData")
}
// struct {} Empty;
Expand Down Expand Up @@ -90,9 +120,9 @@ func (e *MTCLogEntry) Marshal() ([]byte, error) {
// "The extensions list MUST appear in ascending order by extension_type and
// MUST NOT contain two extensions with the same extension_type."
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
for i, ext := range e.Extensions {
for i, ext := range e.extensions {
if i > 0 {
prevType := e.Extensions[i-1].Type
prevType := e.extensions[i-1].Type
if ext.Type == prevType {
child.SetError(fmt.Errorf("duplicate extension type %d", ext.Type))
return
Expand All @@ -110,10 +140,10 @@ func (e *MTCLogEntry) Marshal() ([]byte, error) {
}
})

b.AddUint16(uint16(e.Type))
b.AddUint16(uint16(e.entryType))

// EntryData fills up the rest of the structure, no size prefix needed.
b.AddBytes(e.EntryData)
b.AddBytes(e.entryData)

res, err := b.Bytes()
if err != nil {
Expand Down
128 changes: 61 additions & 67 deletions cmd/mtc/log/internal/entry/entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"errors"
"fmt"
"slices"
"testing"

"golang.org/x/crypto/cryptobyte"
Expand All @@ -33,7 +34,7 @@ func (e *MTCLogEntry) unmarshal(data []byte) error {
return errors.New("malformed extension list length")
}

e.Extensions = nil
var extensions []MTCLogEntryExtension
for !extListStr.Empty() {
var ext MTCLogEntryExtension
if !extListStr.ReadUint16((*uint16)(&ext.Type)) {
Expand All @@ -44,55 +45,55 @@ func (e *MTCLogEntry) unmarshal(data []byte) error {
return fmt.Errorf("failed to read extension length")
}
ext.Data = append([]byte(nil), extDataStr...)
if n := len(e.Extensions); n > 0 {
if ext.Type < e.Extensions[n-1].Type {
return fmt.Errorf("mtc: entry extensions out of order (type %d after %d)", ext.Type, e.Extensions[n-1].Type)
if n := len(extensions); n > 0 {
if ext.Type < extensions[n-1].Type {
return fmt.Errorf("mtc: entry extensions out of order (type %d after %d)", ext.Type, extensions[n-1].Type)
}
if ext.Type == e.Extensions[n-1].Type {
if ext.Type == extensions[n-1].Type {
return fmt.Errorf("mtc: duplicate entry extension type %d", ext.Type)
}
}
e.Extensions = append(e.Extensions, ext)
extensions = append(extensions, ext)
}

if !s.ReadUint16((*uint16)(&e.Type)) {
var entryType EntryType
if !s.ReadUint16((*uint16)(&entryType)) {
return errors.New("missing entry type")
}

if e.Type != MTCLogEntryTypeNull && e.Type != MTCLogEntryTypeTBSCert {
return fmt.Errorf("unknown or unsupported log entry type %d", e.Type)
if entryType != MTCLogEntryTypeNull && entryType != MTCLogEntryTypeTBSCert {
return fmt.Errorf("unknown or unsupported log entry type %d", entryType)
}

if e.Type == MTCLogEntryTypeNull && !s.Empty() {
if entryType == MTCLogEntryTypeNull && !s.Empty() {
return fmt.Errorf("null entry must have empty data")
}

e.EntryData = append([]byte(nil), s...)
*e = MTCLogEntry{
extensions: extensions,
entryType: entryType,
entryData: slices.Clone(s),
}
return nil
}

func TestMTCLogEntry_RoundTrip(t *testing.T) {
tests := []struct {
name string
entry MTCLogEntry
entry *MTCLogEntry
}{
{
name: "null entry no extensions",
entry: MTCLogEntry{
Type: MTCLogEntryTypeNull,
},
name: "null entry no extensions",
entry: New(nil),
},
{
name: "tbs cert entry with sorted extensions",
entry: MTCLogEntry{
Type: MTCLogEntryTypeTBSCert,
EntryData: []byte("fake-der-octets"),
Extensions: []MTCLogEntryExtension{
{Type: 1, Data: []byte("ext-1-data")},
{Type: 5, Data: []byte("ext-5-data")},
{Type: 10, Data: []byte("")},
},
},
entry: New(
[]byte("fake-der-octets"),
MTCLogEntryExtension{Type: 1, Data: []byte("ext-1-data")},
MTCLogEntryExtension{Type: 5, Data: []byte("ext-5-data")},
MTCLogEntryExtension{Type: 10, Data: []byte("")},
),
},
}

Expand All @@ -108,78 +109,71 @@ func TestMTCLogEntry_RoundTrip(t *testing.T) {
t.Fatalf("unmarshal() unexpected error: %v", err)
}

if got.Type != tc.entry.Type {
t.Errorf("Type = %d, want %d", got.Type, tc.entry.Type)
if got.Type() != tc.entry.Type() {
t.Errorf("Type() = %d, want %d", got.Type(), tc.entry.Type())
}
if !bytes.Equal(got.EntryData, tc.entry.EntryData) {
t.Errorf("EntryData = %x, want %x", got.EntryData, tc.entry.EntryData)
if !bytes.Equal(got.EntryData(), tc.entry.EntryData()) {
t.Errorf("EntryData() = %x, want %x", got.EntryData(), tc.entry.EntryData())
}

if len(got.Extensions) != len(tc.entry.Extensions) {
t.Fatalf("len(Extensions) = %d, want %d", len(got.Extensions), len(tc.entry.Extensions))
gotExts := got.Extensions()
wantExts := tc.entry.Extensions()
if len(gotExts) != len(wantExts) {
t.Fatalf("len(Extensions) = %d, want %d", len(gotExts), len(wantExts))
}
for i := range got.Extensions {
if got.Extensions[i].Type != tc.entry.Extensions[i].Type || !bytes.Equal(got.Extensions[i].Data, tc.entry.Extensions[i].Data) {
t.Errorf("Extension[%d] = %+v, want %+v", i, got.Extensions[i], tc.entry.Extensions[i])
for i := range gotExts {
if gotExts[i].Type != wantExts[i].Type || !bytes.Equal(gotExts[i].Data, wantExts[i].Data) {
t.Errorf("Extension[%d] = %+v, want %+v", i, gotExts[i], wantExts[i])
}
}
})
}
}

func TestMTCLogEntry_MarshalErrors(t *testing.T) {
nullWithData := New([]byte("unexpected-data"))
nullWithData.entryType = MTCLogEntryTypeNull

tests := []struct {
name string
entry MTCLogEntry
entry *MTCLogEntry
wantErr bool
}{
{
name: "trailing data on null entry",
entry: MTCLogEntry{
Type: MTCLogEntryTypeNull,
EntryData: []byte("unexpected-data"),
},
name: "trailing data on null entry",
entry: nullWithData,
wantErr: true,
},
{
name: "duplicate extensions with same data",
entry: MTCLogEntry{
Type: MTCLogEntryTypeTBSCert,
Extensions: []MTCLogEntryExtension{
{Type: 2, Data: []byte("data-a")},
{Type: 2, Data: []byte("data-a")},
},
},
entry: New(
[]byte("fake-der"),
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
),
wantErr: true,
},
{
name: "duplicate extensions with different data",
entry: MTCLogEntry{
Type: MTCLogEntryTypeTBSCert,
Extensions: []MTCLogEntryExtension{
{Type: 2, Data: []byte("data-a")},
{Type: 2, Data: []byte("data-b")},
},
},
entry: New(
[]byte("fake-der"),
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
MTCLogEntryExtension{Type: 2, Data: []byte("data-b")},
),
wantErr: true,
},
{
name: "unsorted extensions",
entry: MTCLogEntry{
Type: MTCLogEntryTypeTBSCert,
Extensions: []MTCLogEntryExtension{
{Type: 5, Data: []byte("data-5")},
{Type: 1, Data: []byte("data-1")},
},
},
entry: New(
[]byte("fake-der"),
MTCLogEntryExtension{Type: 5, Data: []byte("data-5")},
MTCLogEntryExtension{Type: 1, Data: []byte("data-1")},
),
wantErr: true,
},
{
name: "entry size exceeds tile limit",
entry: MTCLogEntry{
Type: MTCLogEntryTypeTBSCert,
EntryData: make([]byte, MaxMTCLogEntrySize),
},
name: "entry size exceeds tile limit",
entry: New(make([]byte, MaxMTCLogEntrySize)),
wantErr: true,
},
}
Expand Down Expand Up @@ -210,7 +204,7 @@ func TestMTCLogEntry_UnmarshalErrors(t *testing.T) {
{
name: "trailing data on null entry",
mutate: func(b []byte) []byte {
e := MTCLogEntry{Type: MTCLogEntryTypeNull}
e := New(nil)
data, _ := e.Marshal()
return append(data, 0x00)
},
Expand Down
62 changes: 51 additions & 11 deletions cmd/mtc/log/internal/handler/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@ package handler

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"

"github.com/transparency-dev/tessera/cmd/mtc/log"
)

type addTBS func(context.Context, log.TBSCertificateLogEntry) (uint64, log.MTCProof, error)
const (
// maxAddTBSRequestBodyBytes is the maximum allowed HTTP request body size
// (128 KiB) for JSON submissions. This accommodates base64 encoding and JSON
// formatting overhead for certificates up to 64 KiB binary size.
maxAddTBSRequestBodyBytes = 128 << 10
)

type addTBS func(context.Context, log.TBSCertificateLogEntry) (*log.AddTBSRsp, error)

// New returns a new http.Handler for the mtc-tlog service.
func New(mtcLog *log.MTCLog) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("POST /add-tbs", addTBSHandler(mtcLog.AddTBS))
mux.Handle("POST /add-tbs", http.MaxBytesHandler(addTBSHandler(mtcLog.AddTBS), maxAddTBSRequestBodyBytes))
mux.HandleFunc("GET /proof-to-landmark", func(w http.ResponseWriter, r *http.Request) {
// TODO parse request
// TODO write response
Expand All @@ -39,14 +48,45 @@ func New(mtcLog *log.MTCLog) http.Handler {
return mux
}

// addTBSHandler returns a handler which logs a DER-encoded TBSCertificateLogEntry.
func addTBSHandler(add addTBS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: parse request
// TODO: write response
if _, _, err := add(r.Context(), log.TBSCertificateLogEntry{}); err != nil {
slog.ErrorContext(r.Context(), "Failed to add entry to MTC log", slog.Any("error", err))
// addTBSHandler returns a handler which logs a TBSCertificateLogEntry.
//
// This handler:
//
// - Accepts JSON-Encoded TBSCertificateLogentry, serializes it in
// DER format, encapsulates it in a TLS encoded MTCLogEntry, and logs it
// using the argument add function.
// - Returns an AddTBSRsp JSON payload containing an index and an MTCProof.
func addTBSHandler(add addTBS) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := r.Body.Close(); err != nil {
slog.ErrorContext(r.Context(), "resp.Body.Close()", slog.Any("error", err))
}
}()

var entry log.TBSCertificateLogEntry
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
slog.WarnContext(r.Context(), "rejection: malformed JSON submission", slog.Any("error", err))
http.Error(w, "Invalid TBSCertificateLogEntry JSON payload", http.StatusBadRequest)
return
}
http.Error(w, "not implemented", http.StatusNotImplemented)
}
if err := entry.Validate(); err != nil {
slog.WarnContext(r.Context(), "rejection: invalid TBSCertificateLogEntry fields", slog.Any("error", err))
http.Error(w, fmt.Sprintf("Invalid TBSCertificateLogEntry: %v", err.Error()), http.StatusBadRequest)
return
}

rsp, err := add(r.Context(), entry)
if err != nil {
slog.ErrorContext(r.Context(), "failed to add entry to MTC log", slog.Any("error", err))
http.Error(w, "Could not add entry to log", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rsp); err != nil {
slog.ErrorContext(r.Context(), "failed to write response", slog.Any("error", err))
}
})
}
Loading
Loading