Skip to content

Commit

Permalink
CT static API support (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCutter authored Jul 23, 2024
1 parent 0503173 commit e8e136f
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 177 deletions.
20 changes: 3 additions & 17 deletions api/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
)

Expand Down Expand Up @@ -66,23 +67,8 @@ type EntryBundle struct {
Entries [][]byte
}

// MarshalText implements encoding/TextMarshaller and writes out an EntryBundle
// instance as sequences of big-endian uint16 length-prefixed log entries,
// as specified by the tlog-tiles spec.
// TODO(#41): this _may_ need to be changed to support CT
func (t EntryBundle) MarshalText() ([]byte, error) {
r := &bytes.Buffer{}
sizeBs := make([]byte, 2)
for _, n := range t.Entries {
binary.BigEndian.PutUint16(sizeBs, uint16(len(n)))
if _, err := r.Write(sizeBs); err != nil {
return nil, err
}
if _, err := r.Write(n); err != nil {
return nil, err
}
}
return r.Bytes(), nil
func (t *EntryBundle) MarshalText() ([]byte, error) {
return nil, errors.New("unimplemented")
}

// UnmarshalText implements encoding/TextUnmarshaler and reads EntryBundles
Expand Down
23 changes: 12 additions & 11 deletions api/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
package api_test

import (
"bytes"
"crypto/rand"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
tessera "github.com/transparency-dev/trillian-tessera"
"github.com/transparency-dev/trillian-tessera/api"
)

Expand Down Expand Up @@ -80,27 +82,26 @@ func TestLeafBundle_MarshalTileRoundtrip(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("tile size %d", test.size), func(t *testing.T) {
tile := api.EntryBundle{Entries: make([][]byte, 0, test.size)}
bundleRaw := &bytes.Buffer{}
want := make([][]byte, test.size)
for i := 0; i < test.size; i++ {
// Fill in the leaf index
tile.Entries = append(tile.Entries, make([]byte, i*100))
if _, err := rand.Read(tile.Entries[i]); err != nil {
want[i] = make([]byte, i*100)
if _, err := rand.Read(want[i]); err != nil {
t.Error(err)
}
}

raw, err := tile.MarshalText()
if err != nil {
t.Fatalf("MarshalText() = %v", err)
_, _ = bundleRaw.Write(tessera.NewEntry(want[i]).MarshalBundleData(uint64(i)))
}

tile2 := api.EntryBundle{}
if err := tile2.UnmarshalText(raw); err != nil {
if err := tile2.UnmarshalText(bundleRaw.Bytes()); err != nil {
t.Fatalf("UnmarshalText() = %v", err)
}

if diff := cmp.Diff(tile, tile2); len(diff) != 0 {
t.Fatalf("Got tile with diff: %s", diff)
for i := 0; i < test.size; i++ {
if got, want := tile2.Entries[i], want[i]; !bytes.Equal(got, want) {
t.Errorf("%d: want %x, got %x", i, got, want)
}
}
})
}
Expand Down
58 changes: 58 additions & 0 deletions ct_only.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tessera

import (
"context"

"github.com/transparency-dev/trillian-tessera/ctonly"
)

// Storage described the expected functions from Tessera storage implementations.
type Storage interface {
// Add should duably assign an index to the provided Entry, and return it.
//
// Implementations MUST call MarshalBundleData method on the entry before persisting/integrating it.
Add(context.Context, *Entry) (uint64, error)
}

// NewCertificateTransparencySequencedWriter returns a function which knows how to add a CT-specific entry type to the log.
//
// This entry point MUST ONLY be used for CT logs participating in the CT ecosystem.
// It should not be used as the basis for any other/new transparency application as this protocol:
// a) embodies some techniques which are not considered to be best practice (it does this to retain backawards-compatibility with RFC6962)
// b) is not compatible with the https://c2sp.org/tlog-tiles API which we _very strongly_ encourage you to use instead.
//
// Returns the assigned index in the log, or an error.
func NewCertificateTransparencySequencedWriter(s Storage) func(context.Context, *ctonly.Entry) (uint64, error) {
return func(ctx context.Context, e *ctonly.Entry) (uint64, error) {
return s.Add(ctx, convertCTEntry(e))
}
}

// convertCTEntry returns an Entry struct which will do the right thing for CT Static API logs.
//
// This MUST NOT be used for any other purpose.
func convertCTEntry(e *ctonly.Entry) *Entry {
r := &Entry{}
r.internal.Identity = e.Identity()
r.marshalForBundle = func(idx uint64) []byte {
r.internal.LeafHash = e.MerkleLeafHash(idx)
r.internal.Data = e.LeafData(idx)
return r.internal.Data
}

return r
}
169 changes: 169 additions & 0 deletions ctonly/ct.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// Original source: https://github.com/FiloSottile/sunlight/blob/main/tile.go
//
// # Copyright 2023 The Sunlight Authors
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

// Package ctonly has support for CT Tiles API.
//
// This code should not be reused outside of CT.
// Most of this code came from Filipo's Sunlight implementation of https://c2sp.org/ct-static-api.
package ctonly

import (
"crypto/sha256"
"errors"

"github.com/transparency-dev/merkle/rfc6962"
"golang.org/x/crypto/cryptobyte"
)

// Entry represents a CT log entry.
type Entry struct {
Timestamp uint64
IsPrecert bool
Certificate []byte
Precertificate []byte
PrecertSigningCert []byte
IssuerKeyHash []byte
}

// LeafData returns the data which should be added to an entry bundle for this entry.
//
// Note that this will include data which IS NOT directly committed to by the entry's
// MerkleLeafHash.
func (c Entry) LeafData(idx uint64) []byte {
b := cryptobyte.NewBuilder([]byte{})
b.AddUint64(uint64(c.Timestamp))
if !c.IsPrecert {
b.AddUint16(0 /* entry_type = x509_entry */)
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.Certificate)
})
} else {
b.AddUint16(1 /* entry_type = precert_entry */)
b.AddBytes(c.IssuerKeyHash[:])
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.Certificate)
})
}
addExtensions(b, idx)
if c.IsPrecert {
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.Precertificate)
})
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.PrecertSigningCert)
})
}
return b.BytesOrPanic()
}

// MerkleLeafHash returns the RFC6962 leaf hash for this entry.
//
// Note that we embed an SCT extension which captures the index of the entry in the log according to
// the mechanism specified in https://c2sp.org/ct-static-api.
func (c Entry) MerkleLeafHash(leafIndex uint64) []byte {
b := &cryptobyte.Builder{}
b.AddUint8(0 /* version = v1 */)
b.AddUint8(0 /* leaf_type = timestamped_entry */)
b.AddUint64(uint64(c.Timestamp))
if !c.IsPrecert {
b.AddUint16(0 /* entry_type = x509_entry */)
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.Certificate)
})
} else {
b.AddUint16(1 /* entry_type = precert_entry */)
b.AddBytes(c.IssuerKeyHash[:])
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.Certificate)
})
}
addExtensions(b, leafIndex)
return rfc6962.DefaultHasher.HashLeaf(b.BytesOrPanic())
}

func (c Entry) Identity() []byte {
var r [sha256.Size]byte
if c.IsPrecert {
r = sha256.Sum256(c.Precertificate)
} else {
r = sha256.Sum256(c.Certificate)
}
return r[:]
}

func addExtensions(b *cryptobyte.Builder, leafIndex uint64) {
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
ext, err := extensions{LeafIndex: leafIndex}.Marshal()
if err != nil {
b.SetError(err)
return
}
b.AddBytes(ext)
})
}

// extensions is the CTExtensions field of SignedCertificateTimestamp and
// TimestampedEntry, according to c2sp.org/static-ct-api.
type extensions struct {
LeafIndex uint64
}

func (c extensions) Marshal() ([]byte, error) {
// enum {
// leaf_index(0), (255)
// } ExtensionType;
//
// struct {
// ExtensionType extension_type;
// opaque extension_data<0..2^16-1>;
// } Extension;
//
// Extension CTExtensions<0..2^16-1>;
//
// uint8 uint40[5];
// uint40 LeafIndex;

b := &cryptobyte.Builder{}
b.AddUint8(0 /* extension_type = leaf_index */)
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
if c.LeafIndex >= 1<<40 {
b.SetError(errors.New("leaf_index out of range"))
return
}
addUint40(b, uint64(c.LeafIndex))
})
return b.Bytes()
}

// addUint40 appends a big-endian, 40-bit value to the byte string.
func addUint40(b *cryptobyte.Builder, v uint64) {
b.AddBytes([]byte{byte(v >> 32), byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
Loading

0 comments on commit e8e136f

Please sign in to comment.