Skip to content

Commit 0a4b400

Browse files
authored
entry: add MTCLogEntry, TBSCertificateLogEntry (#8867)
Implements: - MTCLogEntry - FromX509 - TBS - BundleReader - BundleWriter Note: going the other way and generating an X.509 certificate from a TBSCertificateLogEntry is not yet implemented.
1 parent e49356d commit 0a4b400

2 files changed

Lines changed: 699 additions & 0 deletions

File tree

trees/entry/entry.go

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
// Package entry defines types related to TBSCertificateLogEntry and MTCLogEntry from
2+
// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-log-entries,
3+
// and the entry bundle encoding from https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#log-entries
4+
//
5+
// The concepts fit together like so:
6+
//
7+
// Entry bundles contain MTCLogEntry. MTCLogEntry contains (typically) TBSCertificateLogEntry.
8+
//
9+
// TBSCertificateLogEntry is part of the X.509 layer. It will be used to build certificates
10+
// (TODO: implement TBSCertificateLogEntry.ToX509).
11+
//
12+
// MTCLogEntry is part of the Merkle tree layer and is what gets hashed. It provides type switching
13+
// (needed for null_entry) and extensibility.
14+
//
15+
// Entry bundles are part of the tile storage layer. They provide a simple length-prefixed framing so that
16+
// MTCLogEntries can be concatenated unambiguously.
17+
//
18+
// Note that neither TBSCertificateLogEntry nor MTCLogEntry carries its own length, since they will
19+
// always be wrapped or converted into a format that has length information: an entry bundle or an
20+
// X.509 certificate.
21+
package entry
22+
23+
import (
24+
"bytes"
25+
"crypto"
26+
"fmt"
27+
"io"
28+
29+
"golang.org/x/crypto/cryptobyte"
30+
"golang.org/x/crypto/cryptobyte/asn1"
31+
)
32+
33+
const typeNullEntry = 0
34+
const typeTBSCertEntry = 1
35+
36+
// MTCLogEntry implements the corresponding structure from
37+
// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-log-entries
38+
//
39+
// struct {
40+
// MTCLogEntryExtension extensions<0..2^16-1>;
41+
// MTCLogEntryType type;
42+
// select (type) {
43+
// case null_entry: Empty;
44+
// case tbs_cert_entry: opaque tbs_cert_entry_data[N];
45+
// /* May be extended with future types. */
46+
// }
47+
// } MTCLogEntry;
48+
//
49+
// The zero value represents a null_entry.
50+
type MTCLogEntry struct {
51+
extensions []byte
52+
typ uint16
53+
value []byte
54+
}
55+
56+
// TBS returns the TBSCertificateLogEntry bytes if Type is tbs_cert_entry, or nil otherwise.
57+
func (mtcle *MTCLogEntry) TBS() []byte {
58+
if mtcle.typ == typeTBSCertEntry {
59+
return mtcle.value
60+
}
61+
return nil
62+
}
63+
64+
// Marshal returns the encoding of its receiver.
65+
//
66+
// Rejects unknown MTCLogEntryTypes. Rejects non-empty MTCLogEntry.extensions.
67+
func (mtcle *MTCLogEntry) Marshal() ([]byte, error) {
68+
var builder cryptobyte.Builder
69+
// Extensions are always empty in this implementation.
70+
if len(mtcle.extensions) != 0 {
71+
builder.SetError(fmt.Errorf("extensions not supported"))
72+
}
73+
builder.AddUint16(0)
74+
builder.AddUint16(mtcle.typ)
75+
switch mtcle.typ {
76+
case typeTBSCertEntry:
77+
// We don't encode a length prefix for Value. Per the spec:
78+
// opaque tbs_cert_entry_data[N];
79+
// ...
80+
// When type is tbs_cert_entry, N is the number of bytes needed to
81+
// consume the rest of the input.
82+
//
83+
// In other words, per TLS presentation syntax (https://datatracker.ietf.org/doc/html/rfc8446#section-3.4),
84+
// this is a fixed-length vector of size N, where N is known externally.
85+
builder.AddBytes(mtcle.value)
86+
case typeNullEntry:
87+
if len(mtcle.value) != 0 {
88+
return nil, fmt.Errorf("non-empty value for null_entry MTCLogEntry")
89+
}
90+
// Append nothing; the encoding of the null entry is Empty.
91+
default:
92+
return nil, fmt.Errorf("unknown MTCLogEntryType %d", mtcle.typ)
93+
}
94+
return builder.Bytes()
95+
}
96+
97+
// unmarshalMTCLE parses a MTCLogEntry and returns it.
98+
//
99+
// Rejects unknown MTCLogEntryType.
100+
func unmarshalMTCLE(input []byte) (*MTCLogEntry, error) {
101+
val := cryptobyte.String(input)
102+
103+
var extensions cryptobyte.String
104+
if !val.ReadUint16LengthPrefixed(&extensions) {
105+
return nil, fmt.Errorf("malformed extensions")
106+
}
107+
108+
var typ uint16
109+
if !val.ReadUint16(&typ) {
110+
return nil, fmt.Errorf("malformed type")
111+
}
112+
113+
switch typ {
114+
case typeTBSCertEntry:
115+
case typeNullEntry:
116+
if len(val) > 0 {
117+
return nil, fmt.Errorf("null_entry with non-empty value")
118+
}
119+
default:
120+
return nil, fmt.Errorf("unknown MTCLogEntryType %d", typ)
121+
}
122+
123+
// Per the spec, value is not length-prefixed. It's a fixed-length vector, where
124+
// the length is known externally. So it just consists of the rest of the bytes.
125+
return &MTCLogEntry{
126+
extensions: []byte(extensions),
127+
typ: typ,
128+
value: []byte(val),
129+
}, nil
130+
}
131+
132+
// FromX509 takes a DER-encoded X.509 certificate and transforms it into a TBSCertificateLogEntry,
133+
// then returns a MTCLogEntry wrapping that TBSCertificateLogEntry.
134+
func FromX509(in []byte, hash crypto.Hash) (*MTCLogEntry, error) {
135+
var inner cryptobyte.String
136+
input := cryptobyte.String(in)
137+
138+
// https://datatracker.ietf.org/doc/html/rfc5280#page-116
139+
//
140+
// Certificate ::= SEQUENCE {
141+
// tbsCertificate TBSCertificate,
142+
// ...
143+
//
144+
// TBSCertificate ::= SEQUENCE {
145+
// version [0] Version DEFAULT v1,
146+
// serialNumber CertificateSerialNumber,
147+
// ...
148+
if !input.ReadASN1(&inner, asn1.SEQUENCE) {
149+
return nil, fmt.Errorf("failed to read outer sequence")
150+
}
151+
if !input.Empty() {
152+
return nil, fmt.Errorf("extra bytes at end")
153+
}
154+
155+
var tbsCertificate cryptobyte.String
156+
if !inner.ReadASN1(&tbsCertificate, asn1.SEQUENCE) {
157+
return nil, fmt.Errorf("failed to read tbsCertificate")
158+
}
159+
160+
// https://datatracker.ietf.org/doc/html/rfc5280#page-117
161+
// TBSCertificate ::= SEQUENCE {
162+
// version [0] Version DEFAULT v1,
163+
// serialNumber CertificateSerialNumber,
164+
// signature AlgorithmIdentifier,
165+
// issuer Name,
166+
// validity Validity,
167+
// subject Name,
168+
// subjectPublicKeyInfo SubjectPublicKeyInfo,
169+
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
170+
// -- If present, version MUST be v2 or v3
171+
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
172+
// -- If present, version MUST be v2 or v3
173+
// extensions [3] Extensions OPTIONAL
174+
// -- If present, version MUST be v3 -- }
175+
//
176+
// https://ietf-plants-wg.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html#name-log-entries
177+
var version cryptobyte.String
178+
if !tbsCertificate.ReadASN1(&version, asn1.Tag(0).Constructed().ContextSpecific()) {
179+
return nil, fmt.Errorf("failed to read version")
180+
}
181+
// Version should always be v3, which is represented as an ASN.1 INTEGER 2.
182+
// That's tag 2, length 1, value 2.
183+
if !bytes.Equal(version, []byte{2, 1, 2}) {
184+
return nil, fmt.Errorf("invalid X.509 version")
185+
}
186+
var fields []cryptobyte.String
187+
for i := range 5 {
188+
var fieldElement cryptobyte.String
189+
var fieldTag asn1.Tag
190+
191+
if !tbsCertificate.ReadAnyASN1Element(&fieldElement, &fieldTag) {
192+
return nil, fmt.Errorf("failed to read field")
193+
}
194+
195+
switch i {
196+
case 2, 3, 4: // issuer, validity, subject from the TBSCertificate.
197+
fields = append(fields, fieldElement)
198+
}
199+
}
200+
201+
// Read and transform SubjectPublicKeyInfo from the input.
202+
//
203+
// It gets written as two fields in the output:
204+
// subjectPublicKeyAlgorithm AlgorithmIdentifier{PUBLIC-KEY,
205+
// {PublicKeyAlgorithms}},
206+
// subjectPublicKeyInfoHash OCTET STRING,
207+
//
208+
// Use ReadASN1Element, not ReadASN1, so spki contains the tag and
209+
// length bytes, which should be included in the hash.
210+
var spki cryptobyte.String
211+
if !tbsCertificate.ReadASN1Element(&spki, asn1.SEQUENCE) {
212+
return nil, fmt.Errorf("malformed subjectPublicKeyInfo")
213+
}
214+
215+
h := hash.New()
216+
h.Write(spki)
217+
spkiHash := h.Sum(nil)
218+
219+
// Remove the tag and length from subjectPublicKeyInfo and then parse
220+
// subjectPublicKeyAlgorithm.
221+
var spkiInner cryptobyte.String
222+
if !spki.ReadASN1(&spkiInner, asn1.SEQUENCE) {
223+
return nil, fmt.Errorf("malformed subjectPublicKeyInfo")
224+
}
225+
var algID cryptobyte.String
226+
if !spkiInner.ReadASN1Element(&algID, asn1.SEQUENCE) {
227+
return nil, fmt.Errorf("malformed algorithmIdentifier")
228+
}
229+
230+
// Read the extensions.
231+
//
232+
// Note that we've ignored issuerUniqueID and subjectUniqueID, which are OPTIONAL and
233+
// forbidden by the BRs. Since those fields have encoding instructions ([1] and [2]),
234+
// if by some chance they are present we will error when trying to read extensions,
235+
// which has an encoding instruction of [3].
236+
var extensions cryptobyte.String
237+
extensionsTag := asn1.Tag(3).Constructed().ContextSpecific()
238+
if !tbsCertificate.ReadASN1Element(&extensions, extensionsTag) {
239+
return nil, fmt.Errorf("error reading extensions")
240+
}
241+
242+
if !tbsCertificate.Empty() {
243+
return nil, fmt.Errorf("extra bytes at end")
244+
}
245+
246+
// TBSCertificateLogEntry ::= SEQUENCE {
247+
// version [0] EXPLICIT Version DEFAULT v1,
248+
// issuer Name,
249+
// validity Validity,
250+
// subject Name,
251+
// subjectPublicKeyAlgorithm AlgorithmIdentifier{PUBLIC-KEY,
252+
// {PublicKeyAlgorithms}},
253+
// subjectPublicKeyInfoHash OCTET STRING,
254+
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
255+
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
256+
// extensions [3] EXPLICIT Extensions{{CertExtensions}}
257+
// OPTIONAL
258+
// }
259+
//
260+
// TBSCertificateLogEntry, relative to TBSCertificate, lacks `serialNumber`
261+
// and `signature`, and encodes subjectPublicKeyInfo as its hash.
262+
var builder cryptobyte.Builder
263+
264+
// version
265+
builder.AddASN1(asn1.Tag(0).Constructed().ContextSpecific(), func(child *cryptobyte.Builder) {
266+
child.AddASN1Int64(2)
267+
})
268+
269+
// issuer, validity, subject
270+
for _, f := range fields {
271+
// The fields were read with ReadASN1Element so they still include
272+
// their tag and length. Add them straight to the builder.
273+
builder.AddBytes(f)
274+
}
275+
// subjectPublicKeyAlgorithm
276+
builder.AddBytes(algID)
277+
// subjectPublicKeyInfoHash
278+
builder.AddASN1OctetString(spkiHash)
279+
// extensions
280+
builder.AddBytes(extensions)
281+
282+
tbsCertificateLogEntryBytes, err := builder.Bytes()
283+
if err != nil {
284+
return nil, err
285+
}
286+
287+
return &MTCLogEntry{
288+
typ: typeTBSCertEntry,
289+
value: tbsCertificateLogEntryBytes,
290+
}, nil
291+
}
292+
293+
// BundleBuilder appends a sequence of MTCLogEntry to a buffer as an entry bundle.
294+
type BundleBuilder struct {
295+
builder cryptobyte.Builder
296+
}
297+
298+
// NewBundleBuilder returns a BundleBuilder that appends to the given buffer. Like
299+
// cryptobyte.Builder, the slice will be reallocated if its capacity is exceeded.
300+
// Use Bytes to get the final buffer.
301+
func NewBundleBuilder(buf []byte) *BundleBuilder {
302+
return &BundleBuilder{*cryptobyte.NewBuilder(buf)}
303+
}
304+
305+
// Bytes returns the bundle's bytes.
306+
func (b *BundleBuilder) Bytes() ([]byte, error) {
307+
return b.builder.Bytes()
308+
}
309+
310+
// Add appends a single MTCLogEntry, with its length prefix, to the builder.
311+
func (b *BundleBuilder) Add(mtcLogEntry *MTCLogEntry) {
312+
out, err := mtcLogEntry.Marshal()
313+
if err != nil {
314+
b.builder.SetError(err)
315+
return
316+
}
317+
318+
b.builder.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
319+
child.AddBytes(out)
320+
})
321+
}
322+
323+
// BundleReader reads records of MTCLogEntry from the underlying buffer in the
324+
// entry bundle format.
325+
type BundleReader struct {
326+
reader cryptobyte.String
327+
}
328+
329+
// NewBundleReader returns a new BundleReader.
330+
func NewBundleReader(buf []byte) *BundleReader {
331+
return &BundleReader{cryptobyte.String(buf)}
332+
}
333+
334+
// ReadEntry reads the bytes of a single entry.
335+
//
336+
// Returns the parsed MTCLogEntry as well as its bytes, both of which
337+
// reference the same memory as the original buffer.
338+
//
339+
// Returns MTCLogEntry{}, nil, io.EOF when there is no more to read.
340+
func (br *BundleReader) ReadEntry() (*MTCLogEntry, []byte, error) {
341+
if br.reader.Empty() {
342+
return nil, nil, io.EOF
343+
}
344+
var body cryptobyte.String
345+
if !br.reader.ReadUint16LengthPrefixed(&body) {
346+
return nil, nil, fmt.Errorf("malformed length")
347+
}
348+
349+
mtcle, err := unmarshalMTCLE(body)
350+
if err != nil {
351+
return nil, nil, err
352+
}
353+
354+
return mtcle, body, nil
355+
}

0 commit comments

Comments
 (0)