Skip to content

Commit

Permalink
Pre-signed receipts attached as seal headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Robin Bryce committed Oct 27, 2024
1 parent c91e8eb commit 463a188
Show file tree
Hide file tree
Showing 35 changed files with 1,247 additions and 285 deletions.
1 change: 1 addition & 0 deletions massifs/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/fxamacker/cbor v1.5.1
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions massifs/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg=
github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
Expand Down
2 changes: 1 addition & 1 deletion massifs/localmassifreader.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (r *LocalReader) ReplaceVerifiedContext(
// Note: ensure that the root is *never* written to disc or available in the
// cached copy of the seal, so that it always has to be recomputed.
state := vc.MMRState
state.Rootx = nil
state.LegacySealRoot = nil
state.Peaks = nil

return r.cache.ReplaceSeal(sealFilename, vc.Start.MassifIndex, &SealedState{
Expand Down
5 changes: 3 additions & 2 deletions massifs/massifcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (mc *MassifContext) CopyPeakStack() map[uint64]int {
// with how GetRoot accesses the store. The default configuration works only for
// how leaf addition accesses the stack.
func (mc *MassifContext) CreatePeakStackMap() error {
mc.peakStackMap = PeakStackMap(mc.Start.MassifHeight, mc.Start.FirstIndex+1)
mc.peakStackMap = PeakStackMap(mc.Start.MassifHeight, mc.Start.FirstIndex)
if mc.peakStackMap == nil {
return fmt.Errorf("invalid massif height or first index in start record")
}
Expand Down Expand Up @@ -438,6 +438,7 @@ func (mc *MassifContext) CheckConsistency(
return nil, ErrStateRootMissing
}

// Note: this can never be 0, because we always create a new massif with at least one node
mmrSizeCurrent := mc.RangeCount()

if mmrSizeCurrent < baseState.MMRSize {
Expand Down Expand Up @@ -633,4 +634,4 @@ func (mc MassifContext) RangeCount() uint64 {
// added.
func (mc MassifContext) LastLeafMMRIndex() uint64 {
return RangeLastLeafIndex(mc.Start.FirstIndex, mc.Start.MassifHeight)
}
}
4 changes: 2 additions & 2 deletions massifs/massifcontextverified.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func (mc *MassifContext) verifyContext(
// get the peaks from the local store, we are checking the store against the
// latest additions. as we verify the signature below, any changes to the
// store will be caught.
state.Peaks, err = mmr.PeakHashes(mc, state.MMRSize)
state.Peaks, err = mmr.PeakHashes(mc, state.MMRSize-1)
if err != nil {
return nil, err
}
Expand All @@ -200,7 +200,7 @@ func (mc *MassifContext) verifyContext(
// Otherwise we can get caught out by the store tampered after the seal was
// created. Of course the seal itself could have been replaced, but at that
// point the only defense is an indpendent replica.
err = VerifySignedRoot(
err = VerifySignedCheckPoint(
*options.codec, pubKeyProvider, msg, state, nil,
)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion massifs/massifpeakstack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ func TestPeakStack_Height4Massif2to3Size63(t *testing.T) {
assert.Equal(t, mc3.peakStackMap[iPeakNode30], iStack30)
assert.Equal(t, mc3.peakStackMap[iPeakNode45], iStack45)

proof, err := mmr.IndexProofBagged(mmrSizeB, &mc3, sha256.New(), iPeakNode30)
proof, err := mmr.InclusionProofBagged(mmrSizeB, &mc3, sha256.New(), iPeakNode30)
require.NoError(t, err)

peakHash, err := mc3.Get(iPeakNode30)
Expand Down
234 changes: 234 additions & 0 deletions massifs/mmriver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package massifs

import (
"bytes"
"context"
"crypto/sha256"
"fmt"

"github.com/datatrails/go-datatrails-common/azblob"
commoncbor "github.com/datatrails/go-datatrails-common/cbor"
"github.com/fxamacker/cbor/v2"

commoncose "github.com/datatrails/go-datatrails-common/cose"
"github.com/datatrails/go-datatrails-common/logger"
"github.com/datatrails/go-datatrails-merklelog/mmr"
)

// MMRIVER COSE Receipts to accompany our COSE MMRIVER seals

type MMRiverInclusionProof struct {
Index uint64 `cbor:"1,keyasint"`
InclusionPath [][]byte `cbor:"2,keyasint"`
}

type MMRiverConsistencyProof struct {
TreeSize1 uint64 `cbor:"1,keyasint"`
TreeSize2 uint64 `cbor:"2,keyasint"`
ConsistencyPaths [][]byte `cbor:"3,keyasint"`
RightPeaks [][]byte `cbor:"4,keyasint"`
}

type MMRiverVerifiableProofs struct {
InclusionProofs []MMRiverInclusionProof `cbor:"-1,keyasint,omitempty"`
ConsistencyProofs []MMRiverConsistencyProof `cbor:"-2,keyasint,omitempty"`
}

// MMRiverInclusionProofHeader provides for encoding, and defered decoding, of
// COSE_Sign1 message headers for MMRIVER receipts
type MMRiverVerifiableProofsHeader struct {
VerifiableProofs MMRiverVerifiableProofs `cbor:"396,keyasint"`
}

/*
func SetMMRiverInclusionProofsHeader(
msg *commoncose.CoseSign1Message, massif MassifReader, mmrSize, mmrIndex uint64) error {
msg.Headers.Unprotected[VDSCoseReceiptProofsTag] = proofs
}*/

// VerifySignedInclusionReceipts verifies a signed COSE receipt encoded according to the MMRIVER VDS
// on success the produced root is returned.
// Signature verification failure is not an error, but the returned root will be nil and the result will be false.
// All other unexpected issues are returned as errors, with a false result and nil root.
// Note that MMRIVER receipts allow for multiple inclusion proofs to be attached to the receipt.
// This function returns true only if ALL receipts verify
func VerifySignedInclusionReceipts(
ctx context.Context,
receipt *commoncose.CoseSign1Message,
candidates [][]byte,
) (bool, []byte, error) {

var err error

// ignore any existing payload
receipt.Payload = nil

// We must return false if there are no candidates
if len(candidates) == 0 {
return false, nil, fmt.Errorf("no candidates provided")
}

var header MMRiverVerifiableProofsHeader
err = cbor.Unmarshal(receipt.Headers.RawUnprotected, &header)
if err != nil {
return false, nil, fmt.Errorf("MMRIVER receipt proofs malformed")
}
verifiableProofs := header.VerifiableProofs
if len(verifiableProofs.InclusionProofs) == 0 {
return false, nil, fmt.Errorf("MMRIVER receipt inclusion proofs not present")
}

// permit *fewer* candidates than proofs, but not more
if len(candidates) > len(verifiableProofs.InclusionProofs) {
return false, nil, fmt.Errorf("MMRIVER receipt more candidates than proofs")
}

var proof MMRiverInclusionProof

proof = verifiableProofs.InclusionProofs[0]
receipt.Payload = mmr.IncludedRoot(
sha256.New(),
proof.Index, candidates[0],
proof.InclusionPath)

err = receipt.VerifyWithCWTPublicKey(nil)
if err != nil {
return false, nil, fmt.Errorf(
"MMRIVER receipt VERIFY FAILED for: mmrIndex %d, candidate %d, err %v", proof.Index, 0, err)
}
// verify the first proof then just compare the produced roots

for i := 1; i < len(verifiableProofs.InclusionProofs); i++ {

proof = verifiableProofs.InclusionProofs[i]
proven := mmr.IncludedRoot(sha256.New(), proof.Index, candidates[i], proof.InclusionPath)
if bytes.Compare(receipt.Payload, proven) != 0 {
return false, nil, fmt.Errorf(
"MMRIVER receipt VERIFY FAILED for: mmrIndex %d, candidate %d, err %v", proof.Index, i, err)
}
}
return true, receipt.Payload, nil
}

// VerifySignedInclusionReceipt verifies a reciept comprised of a single inclusion proof
// If there are 0 or more than 1 candidates, the result will be false and an error will be returned
func VerifySignedInclusionReceipt(
ctx context.Context,
receipt *commoncose.CoseSign1Message,
candidate []byte,
) (bool, []byte, error) {

ok, root, err := VerifySignedInclusionReceipts(ctx, receipt, [][]byte{candidate})
if err != nil {
return false, nil, err
}
if !ok {
return false, nil, nil
}
return true, root, nil
}

type ReceiptBuilder struct {
log logger.Logger
massifReader MassifReader
cborCodec commoncbor.CBORCodec
sealReader SignedRootReader

massifHeight uint8
}

// newReceiptBuilder creates a new receiptBuilder configured with all the necessary readers and information required to build a receipt
// Note that errors are logged assuming the calling context is retrieving a receipt,
// and that all returned errors are StatusErrors that can be returned to the client or nil
func NewReceiptBuilder(log logger.Logger, reader azblob.Reader, massifHeight uint8) (ReceiptBuilder, error) {

var err error

b := ReceiptBuilder{
log: log,
massifHeight: massifHeight,
}

b.massifReader = NewMassifReader(log, reader)
if b.cborCodec, err = NewRootSignerCodec(); err != nil {
return ReceiptBuilder{}, err
}
b.sealReader = NewSignedRootReader(log, reader, b.cborCodec)
b.massifHeight = massifHeight

return b, nil
}

func (b *ReceiptBuilder) BuildReceipt(
ctx context.Context, tenantIdentity string, mmrIndex uint64,
) (*commoncose.CoseSign1Message, error) {

log := b.log.FromContext(ctx)
defer log.Close()

massifIndex := MassifIndexFromMMRIndex(b.massifHeight, mmrIndex)

// Get the seal with the latest peak for this event
massif, err := b.massifReader.GetMassif(ctx, tenantIdentity, massifIndex)
if err != nil {
return nil, fmt.Errorf(
"%w: failed to read massif %d for %s", err, massifIndex, tenantIdentity)
}

sealContext := LogBlobContext{
BlobPath: TenantMassifSignedRootPath(tenantIdentity, uint32(massifIndex)),
}

msg, state, err := b.sealReader.ReadLogicalContext(ctx, sealContext)
if err != nil {
return nil, fmt.Errorf("failed to read seal: %s, %v", sealContext.BlobPath, err)
}

proof, err := mmr.InclusionProof(&massif, state.MMRSize, mmrIndex)
if err != nil {
return nil, fmt.Errorf(
"failed to generating inclusion proof: %d in MMR(%d), %v", mmrIndex, state.MMRSize, err)
}

peakIndex := mmr.PeakIndex(mmr.LeafCount(state.MMRSize), len(proof))

// NOTE: The old-accumulator compatibility property, from
// https://eprint.iacr.org/2015/718.pdf, along with the COSE protected &
// unprotected buckets, is why we can just pre sign the receipts.
// As long as the receipt consumer is convinced of the logs consistency (not split view),
// it does not matter which accumulator state the receipt is signed against.

var peaksHeader MMRStateReceipts
err = cbor.Unmarshal(msg.Headers.RawUnprotected, &peaksHeader)
if err != nil {
return nil, fmt.Errorf(
"%w: failed decoding peaks header: for tenant %s, seal %d", err, tenantIdentity, massifIndex)
}
if peakIndex >= len(peaksHeader.PeakReceipts) {
return nil, fmt.Errorf(
"%w: peaks header containes to few peak receipts: for tenant %s, seal %d", err, tenantIdentity, massifIndex)
}

// This is an array of marshaled COSE_Sign1's
receiptMsg := peaksHeader.PeakReceipts[peakIndex]
signed, err := commoncose.NewCoseSign1MessageFromCBOR(
receiptMsg, commoncose.WithDecOptions(CheckpointDecOptions()))
if err != nil {
return nil, fmt.Errorf(
"%w: failed to decode pre-signed receipt for: %d in MMR(%d)",
err, mmrIndex, state.MMRSize)
}

// signed.Headers.RawProtected = nil
signed.Headers.RawUnprotected = nil

verifiableProofs := MMRiverVerifiableProofs{
InclusionProofs: []MMRiverInclusionProof{{
Index: mmrIndex,
InclusionPath: proof}},
}

signed.Headers.Unprotected[VDSCoseReceiptProofsTag] = verifiableProofs

return signed, nil
}
8 changes: 4 additions & 4 deletions massifs/peakstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "github.com/datatrails/go-datatrails-merklelog/mmr"

// PeakStackMap builds a map from mmr indices to peak stack entries
// massifHeight is the 1 based height (not the height index)
func PeakStackMap(massifHeight uint8, mmrSize uint64) map[uint64]int {
func PeakStackMap(massifHeight uint8, mmrIndex uint64) map[uint64]int {

if massifHeight == 0 {
return nil
Expand All @@ -15,12 +15,12 @@ func PeakStackMap(massifHeight uint8, mmrSize uint64) map[uint64]int {
// XXX:TODO there is likely a more efficient way to do this using
// PeaksBitmap or a variation of it, but this isn't a terribly hot path.
stackMap := map[uint64]int{}
iPeaks := mmr.PosPeaks(mmrSize)
iPeaks := mmr.Peaks(mmrIndex)
for i, ip := range iPeaks {
if mmr.PosHeight(ip) < uint64(massifHeight-1) {
if mmr.IndexHeight(ip) < uint64(massifHeight-1) {
continue
}
stackMap[ip-1] = i
stackMap[ip] = i
}

return stackMap
Expand Down
14 changes: 7 additions & 7 deletions massifs/peakstack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
func TestPeakStackMap(t *testing.T) {
type args struct {
massifHeight uint8
mmrSize uint64
mmrIndex uint64
}
tests := []struct {
name string
Expand All @@ -17,25 +17,25 @@ func TestPeakStackMap(t *testing.T) {
}{
// Note that the mmrSize used here, is also the FirstLeaf + 1 of the
// massif containing the peak stack.
{"massifpeakstack_test:0", args{2, 1}, map[uint64]int{}},
{"massifpeakstack_test:1", args{2, 4}, map[uint64]int{
{"massifpeakstack_test:0", args{2, 0}, map[uint64]int{}},
{"massifpeakstack_test:1", args{2, 3}, map[uint64]int{
2: 0,
}},
{"massifpeakstack_test:2", args{2, 7}, map[uint64]int{
{"massifpeakstack_test:2", args{2, 6}, map[uint64]int{
6: 0,
}},

{"massifpeakstack_test:3", args{2, 10}, map[uint64]int{
{"massifpeakstack_test:3", args{2, 9}, map[uint64]int{
6: 0,
9: 1,
}},
{"massifpeakstack_test:4", args{2, 15}, map[uint64]int{
{"massifpeakstack_test:4", args{2, 14}, map[uint64]int{
14: 0,
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := PeakStackMap(tt.args.massifHeight, tt.args.mmrSize); !reflect.DeepEqual(got, tt.want) {
if got := PeakStackMap(tt.args.massifHeight, tt.args.mmrIndex); !reflect.DeepEqual(got, tt.want) {
t.Errorf("PeakStackMap() = %v, want %v", got, tt.want)
}
})
Expand Down
Loading

0 comments on commit 463a188

Please sign in to comment.