Skip to content

Commit af8a99f

Browse files
committed
Implement MTCLog AddTBS HTTP handler
1 parent bcf07ff commit af8a99f

5 files changed

Lines changed: 421 additions & 90 deletions

File tree

cmd/mtc/log/internal/entry/entry.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,47 @@ type EntryType uint16
4949
// MTCLogEntry represents leaf node as defined in
5050
// draft-ietf-plants-merkle-tree-certs section 5.2.1.
5151
type MTCLogEntry struct {
52-
Extensions []MTCLogEntryExtension
53-
Type EntryType // MTCLogEntryTypeNull, MTCLogEntryTypeTBSCert
54-
EntryData []byte // Raw DER bytes of TBSCertificateLogEntry if Type is TBSCert
52+
extensions []MTCLogEntryExtension
53+
entryType EntryType // MTCLogEntryTypeNull, MTCLogEntryTypeTBSCert
54+
entryData []byte // Raw DER bytes of TBSCertificateLogEntry if Type is TBSCert
55+
}
56+
57+
// New creates a new MTCLogEntry containing entryData and optional extensions.
58+
// If entryData is empty, Type is set to MTCLogEntryTypeNull.
59+
// Otherwise, Type is set to MTCLogEntryTypeTBSCert.
60+
func New(entryData []byte, extensions ...MTCLogEntryExtension) *MTCLogEntry {
61+
entryType := MTCLogEntryTypeTBSCert
62+
if len(entryData) == 0 {
63+
entryType = MTCLogEntryTypeNull
64+
}
65+
return &MTCLogEntry{
66+
extensions: extensions,
67+
entryType: entryType,
68+
entryData: entryData,
69+
}
70+
}
71+
72+
// Extensions returns the entry's extensions list.
73+
func (e *MTCLogEntry) Extensions() []MTCLogEntryExtension {
74+
return e.extensions
75+
}
76+
77+
// Type returns the entry's Type.
78+
func (e *MTCLogEntry) Type() EntryType {
79+
return e.entryType
80+
}
81+
82+
// EntryData returns the raw entry data payload.
83+
func (e *MTCLogEntry) EntryData() []byte {
84+
return e.entryData
5585
}
5686

5787
// Marshal encodes the MTCLogEntry into TLS Presentation bytes.
5888
//
5989
// Returns an error if the extensions are not specs compliant, or if the
6090
// resulting bytes do not fit in a t-log leaf.
6191
func (e *MTCLogEntry) Marshal() ([]byte, error) {
62-
if e.Type == MTCLogEntryTypeNull && len(e.EntryData) > 0 {
92+
if e.entryType == MTCLogEntryTypeNull && len(e.entryData) > 0 {
6393
return nil, errors.New("null entry must have empty EntryData")
6494
}
6595
// struct {} Empty;
@@ -90,9 +120,9 @@ func (e *MTCLogEntry) Marshal() ([]byte, error) {
90120
// "The extensions list MUST appear in ascending order by extension_type and
91121
// MUST NOT contain two extensions with the same extension_type."
92122
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
93-
for i, ext := range e.Extensions {
123+
for i, ext := range e.extensions {
94124
if i > 0 {
95-
prevType := e.Extensions[i-1].Type
125+
prevType := e.extensions[i-1].Type
96126
if ext.Type == prevType {
97127
child.SetError(fmt.Errorf("duplicate extension type %d", ext.Type))
98128
return
@@ -110,10 +140,10 @@ func (e *MTCLogEntry) Marshal() ([]byte, error) {
110140
}
111141
})
112142

113-
b.AddUint16(uint16(e.Type))
143+
b.AddUint16(uint16(e.entryType))
114144

115145
// EntryData fills up the rest of the structure, no size prefix needed.
116-
b.AddBytes(e.EntryData)
146+
b.AddBytes(e.entryData)
117147

118148
res, err := b.Bytes()
119149
if err != nil {

cmd/mtc/log/internal/entry/entry_test.go

Lines changed: 61 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"bytes"
1919
"errors"
2020
"fmt"
21+
"slices"
2122
"testing"
2223

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

36-
e.Extensions = nil
37+
var extensions []MTCLogEntryExtension
3738
for !extListStr.Empty() {
3839
var ext MTCLogEntryExtension
3940
if !extListStr.ReadUint16((*uint16)(&ext.Type)) {
@@ -44,55 +45,55 @@ func (e *MTCLogEntry) unmarshal(data []byte) error {
4445
return fmt.Errorf("failed to read extension length")
4546
}
4647
ext.Data = append([]byte(nil), extDataStr...)
47-
if n := len(e.Extensions); n > 0 {
48-
if ext.Type < e.Extensions[n-1].Type {
49-
return fmt.Errorf("mtc: entry extensions out of order (type %d after %d)", ext.Type, e.Extensions[n-1].Type)
48+
if n := len(extensions); n > 0 {
49+
if ext.Type < extensions[n-1].Type {
50+
return fmt.Errorf("mtc: entry extensions out of order (type %d after %d)", ext.Type, extensions[n-1].Type)
5051
}
51-
if ext.Type == e.Extensions[n-1].Type {
52+
if ext.Type == extensions[n-1].Type {
5253
return fmt.Errorf("mtc: duplicate entry extension type %d", ext.Type)
5354
}
5455
}
55-
e.Extensions = append(e.Extensions, ext)
56+
extensions = append(extensions, ext)
5657
}
5758

58-
if !s.ReadUint16((*uint16)(&e.Type)) {
59+
var entryType EntryType
60+
if !s.ReadUint16((*uint16)(&entryType)) {
5961
return errors.New("missing entry type")
6062
}
6163

62-
if e.Type != MTCLogEntryTypeNull && e.Type != MTCLogEntryTypeTBSCert {
63-
return fmt.Errorf("unknown or unsupported log entry type %d", e.Type)
64+
if entryType != MTCLogEntryTypeNull && entryType != MTCLogEntryTypeTBSCert {
65+
return fmt.Errorf("unknown or unsupported log entry type %d", entryType)
6466
}
6567

66-
if e.Type == MTCLogEntryTypeNull && !s.Empty() {
68+
if entryType == MTCLogEntryTypeNull && !s.Empty() {
6769
return fmt.Errorf("null entry must have empty data")
6870
}
6971

70-
e.EntryData = append([]byte(nil), s...)
72+
*e = MTCLogEntry{
73+
extensions: extensions,
74+
entryType: entryType,
75+
entryData: slices.Clone(s),
76+
}
7177
return nil
7278
}
7379

7480
func TestMTCLogEntry_RoundTrip(t *testing.T) {
7581
tests := []struct {
7682
name string
77-
entry MTCLogEntry
83+
entry *MTCLogEntry
7884
}{
7985
{
80-
name: "null entry no extensions",
81-
entry: MTCLogEntry{
82-
Type: MTCLogEntryTypeNull,
83-
},
86+
name: "null entry no extensions",
87+
entry: New(nil),
8488
},
8589
{
8690
name: "tbs cert entry with sorted extensions",
87-
entry: MTCLogEntry{
88-
Type: MTCLogEntryTypeTBSCert,
89-
EntryData: []byte("fake-der-octets"),
90-
Extensions: []MTCLogEntryExtension{
91-
{Type: 1, Data: []byte("ext-1-data")},
92-
{Type: 5, Data: []byte("ext-5-data")},
93-
{Type: 10, Data: []byte("")},
94-
},
95-
},
91+
entry: New(
92+
[]byte("fake-der-octets"),
93+
MTCLogEntryExtension{Type: 1, Data: []byte("ext-1-data")},
94+
MTCLogEntryExtension{Type: 5, Data: []byte("ext-5-data")},
95+
MTCLogEntryExtension{Type: 10, Data: []byte("")},
96+
),
9697
},
9798
}
9899

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

111-
if got.Type != tc.entry.Type {
112-
t.Errorf("Type = %d, want %d", got.Type, tc.entry.Type)
112+
if got.Type() != tc.entry.Type() {
113+
t.Errorf("Type() = %d, want %d", got.Type(), tc.entry.Type())
113114
}
114-
if !bytes.Equal(got.EntryData, tc.entry.EntryData) {
115-
t.Errorf("EntryData = %x, want %x", got.EntryData, tc.entry.EntryData)
115+
if !bytes.Equal(got.EntryData(), tc.entry.EntryData()) {
116+
t.Errorf("EntryData() = %x, want %x", got.EntryData(), tc.entry.EntryData())
116117
}
117118

118-
if len(got.Extensions) != len(tc.entry.Extensions) {
119-
t.Fatalf("len(Extensions) = %d, want %d", len(got.Extensions), len(tc.entry.Extensions))
119+
gotExts := got.Extensions()
120+
wantExts := tc.entry.Extensions()
121+
if len(gotExts) != len(wantExts) {
122+
t.Fatalf("len(Extensions) = %d, want %d", len(gotExts), len(wantExts))
120123
}
121-
for i := range got.Extensions {
122-
if got.Extensions[i].Type != tc.entry.Extensions[i].Type || !bytes.Equal(got.Extensions[i].Data, tc.entry.Extensions[i].Data) {
123-
t.Errorf("Extension[%d] = %+v, want %+v", i, got.Extensions[i], tc.entry.Extensions[i])
124+
for i := range gotExts {
125+
if gotExts[i].Type != wantExts[i].Type || !bytes.Equal(gotExts[i].Data, wantExts[i].Data) {
126+
t.Errorf("Extension[%d] = %+v, want %+v", i, gotExts[i], wantExts[i])
124127
}
125128
}
126129
})
127130
}
128131
}
129132

130133
func TestMTCLogEntry_MarshalErrors(t *testing.T) {
134+
nullWithData := New([]byte("unexpected-data"))
135+
nullWithData.entryType = MTCLogEntryTypeNull
136+
131137
tests := []struct {
132138
name string
133-
entry MTCLogEntry
139+
entry *MTCLogEntry
134140
wantErr bool
135141
}{
136142
{
137-
name: "trailing data on null entry",
138-
entry: MTCLogEntry{
139-
Type: MTCLogEntryTypeNull,
140-
EntryData: []byte("unexpected-data"),
141-
},
143+
name: "trailing data on null entry",
144+
entry: nullWithData,
142145
wantErr: true,
143146
},
144147
{
145148
name: "duplicate extensions with same data",
146-
entry: MTCLogEntry{
147-
Type: MTCLogEntryTypeTBSCert,
148-
Extensions: []MTCLogEntryExtension{
149-
{Type: 2, Data: []byte("data-a")},
150-
{Type: 2, Data: []byte("data-a")},
151-
},
152-
},
149+
entry: New(
150+
[]byte("fake-der"),
151+
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
152+
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
153+
),
153154
wantErr: true,
154155
},
155156
{
156157
name: "duplicate extensions with different data",
157-
entry: MTCLogEntry{
158-
Type: MTCLogEntryTypeTBSCert,
159-
Extensions: []MTCLogEntryExtension{
160-
{Type: 2, Data: []byte("data-a")},
161-
{Type: 2, Data: []byte("data-b")},
162-
},
163-
},
158+
entry: New(
159+
[]byte("fake-der"),
160+
MTCLogEntryExtension{Type: 2, Data: []byte("data-a")},
161+
MTCLogEntryExtension{Type: 2, Data: []byte("data-b")},
162+
),
164163
wantErr: true,
165164
},
166165
{
167166
name: "unsorted extensions",
168-
entry: MTCLogEntry{
169-
Type: MTCLogEntryTypeTBSCert,
170-
Extensions: []MTCLogEntryExtension{
171-
{Type: 5, Data: []byte("data-5")},
172-
{Type: 1, Data: []byte("data-1")},
173-
},
174-
},
167+
entry: New(
168+
[]byte("fake-der"),
169+
MTCLogEntryExtension{Type: 5, Data: []byte("data-5")},
170+
MTCLogEntryExtension{Type: 1, Data: []byte("data-1")},
171+
),
175172
wantErr: true,
176173
},
177174
{
178-
name: "entry size exceeds tile limit",
179-
entry: MTCLogEntry{
180-
Type: MTCLogEntryTypeTBSCert,
181-
EntryData: make([]byte, MaxMTCLogEntrySize),
182-
},
175+
name: "entry size exceeds tile limit",
176+
entry: New(make([]byte, MaxMTCLogEntrySize)),
183177
wantErr: true,
184178
},
185179
}
@@ -210,7 +204,7 @@ func TestMTCLogEntry_UnmarshalErrors(t *testing.T) {
210204
{
211205
name: "trailing data on null entry",
212206
mutate: func(b []byte) []byte {
213-
e := MTCLogEntry{Type: MTCLogEntryTypeNull}
207+
e := New(nil)
214208
data, _ := e.Marshal()
215209
return append(data, 0x00)
216210
},

cmd/mtc/log/internal/handler/handlers.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,27 @@ package handler
1616

1717
import (
1818
"context"
19+
"encoding/json"
20+
"fmt"
1921
"log/slog"
2022
"net/http"
2123

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

25-
type addTBS func(context.Context, log.TBSCertificateLogEntry) (uint64, log.MTCProof, error)
27+
const (
28+
// maxAddTBSRequestBodyBytes is the maximum allowed HTTP request body size
29+
// (128 KiB) for JSON submissions. This accommodates base64 encoding and JSON
30+
// formatting overhead for certificates up to 64 KiB binary size.
31+
maxAddTBSRequestBodyBytes = 128 << 10
32+
)
33+
34+
type addTBS func(context.Context, log.TBSCertificateLogEntry) (*log.AddTBSRsp, error)
2635

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

42-
// addTBSHandler returns a handler which logs a DER-encoded TBSCertificateLogEntry.
43-
func addTBSHandler(add addTBS) http.HandlerFunc {
44-
return func(w http.ResponseWriter, r *http.Request) {
45-
// TODO: parse request
46-
// TODO: write response
47-
if _, _, err := add(r.Context(), log.TBSCertificateLogEntry{}); err != nil {
48-
slog.ErrorContext(r.Context(), "Failed to add entry to MTC log", slog.Any("error", err))
51+
// addTBSHandler returns a handler which logs a TBSCertificateLogEntry.
52+
//
53+
// This handler:
54+
//
55+
// - Accepts JSON-Encoded TBSCertificateLogentry, serializes it in
56+
// DER format, encapsulates it in a TLS encoded MTCLogEntry, and logs it
57+
// using the argument add function.
58+
// - Returns an AddTBSRsp JSON payload containing an index and an MTCProof.
59+
func addTBSHandler(add addTBS) http.Handler {
60+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61+
defer func() {
62+
if err := r.Body.Close(); err != nil {
63+
slog.ErrorContext(r.Context(), "resp.Body.Close()", slog.Any("error", err))
64+
}
65+
}()
66+
67+
var entry log.TBSCertificateLogEntry
68+
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
69+
slog.WarnContext(r.Context(), "rejection: malformed JSON submission", slog.Any("error", err))
70+
http.Error(w, "Invalid TBSCertificateLogEntry JSON payload", http.StatusBadRequest)
71+
return
4972
}
50-
http.Error(w, "not implemented", http.StatusNotImplemented)
51-
}
73+
if err := entry.Validate(); err != nil {
74+
slog.WarnContext(r.Context(), "rejection: invalid TBSCertificateLogEntry fields", slog.Any("error", err))
75+
http.Error(w, fmt.Sprintf("Invalid TBSCertificateLogEntry: %v", err.Error()), http.StatusBadRequest)
76+
return
77+
}
78+
79+
rsp, err := add(r.Context(), entry)
80+
if err != nil {
81+
slog.ErrorContext(r.Context(), "failed to add entry to MTC log", slog.Any("error", err))
82+
http.Error(w, "Could not add entry to log", http.StatusInternalServerError)
83+
return
84+
}
85+
86+
w.Header().Set("Content-Type", "application/json")
87+
w.WriteHeader(http.StatusCreated)
88+
if err := json.NewEncoder(w).Encode(rsp); err != nil {
89+
slog.ErrorContext(r.Context(), "failed to write response", slog.Any("error", err))
90+
}
91+
})
5292
}

0 commit comments

Comments
 (0)