From 1662c2599b58ff25685f34e1f2f4d4972e215716 Mon Sep 17 00:00:00 2001 From: Robin Bryce Date: Wed, 30 Oct 2024 15:22:18 +0000 Subject: [PATCH] fix: seal format default and bagged consitency check corner case * fix: test helpers now default to producing v1 seals, but the version was not set. * fix: the legacy consistency proof check accounts properly for the case where mmrsizeA == mmrsizeB --- massifs/massifcontextverified.go | 66 ++++++++++++++++------ massifs/massifcontextverifiedv0.go | 89 ++++++++++++++++++++++++++++++ massifs/testcommitter.go | 1 + mmr/verifyconsistencybagged.go | 19 +++---- 4 files changed, 147 insertions(+), 28 deletions(-) create mode 100644 massifs/massifcontextverifiedv0.go diff --git a/massifs/massifcontextverified.go b/massifs/massifcontextverified.go index b672737..a81aff8 100644 --- a/massifs/massifcontextverified.go +++ b/massifs/massifcontextverified.go @@ -155,9 +155,7 @@ func (mc *MassifContext) verifyContext( ctx context.Context, options ReaderOptions, ) (*VerifiedContext, error) { - var ok bool var err error - var peaksB [][]byte // This checks that any un-committed data is consistent with the latest seal available for the massif @@ -171,6 +169,23 @@ func (mc *MassifContext) verifyContext( return nil, err } + if state.Version == int(MMRStateVersion1) { + return mc.verifyContextV1(msg, state, options) + } + return mc.verifyContextV0(msg, state, options) +} + +func (mc *MassifContext) verifyContextV1( + msg *cose.CoseSign1Message, state MMRState, options ReaderOptions, +) (*VerifiedContext, error) { + var ok bool + var err error + var peaksB [][]byte + + if state.Version != int(MMRStateVersion1) { + return nil, fmt.Errorf("unsupported MMR state version %d", state.Version) + } + // 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. @@ -179,21 +194,9 @@ func (mc *MassifContext) verifyContext( return nil, err } - // NOTICE: The verification uses the public key that is provided on the - // message. If the caller wants to ensure the massif is signed by the - // expected key then they must obtain a copy of the public key from a source - // they trust and supply it as an option. - pubKeyProvider := cose.NewCWTPublicKeyProvider(msg) - - if options.trustedSealerPubKey != nil { - var remotePub crypto.PublicKey - remotePub, _, err = pubKeyProvider.PublicKey() - if err != nil { - return nil, err - } - if !options.trustedSealerPubKey.Equal(remotePub) { - return nil, ErrRemoteSealKeyMatchFailed - } + pubKeyProvider, err := mc.sealPublicKeyProvider(msg, options) + if err != nil { + return nil, err } // Ensure the peaks we read from the store are the ones that were signed. @@ -218,7 +221,7 @@ func (mc *MassifContext) verifyContext( err, mc.Start.MassifIndex, mc.TenantIdentity) } if !ok { - // We don't expect false without error, but we + // We don't expect false without error. return nil, fmt.Errorf("%w: failed to verify accumulator state massif %d for tenant %s", mmr.ErrConsistencyCheck, mc.Start.MassifIndex, mc.TenantIdentity) } @@ -252,3 +255,30 @@ func (mc *MassifContext) verifyContext( ConsistentRoots: peaksB, }, nil } + +func (mc *MassifContext) sealPublicKeyProvider( + msg *cose.CoseSign1Message, options ReaderOptions, +) (*cose.CWTPublicKeyProvider, error) { + + var err error + + // NOTICE: The verification uses the public key that is provided on the + // message. If the caller wants to ensure the massif is signed by the + // expected key then they must obtain a copy of the public key from a source + // they trust and supply it as an option. + pubKeyProvider := cose.NewCWTPublicKeyProvider(msg) + if options.trustedSealerPubKey == nil { + return pubKeyProvider, nil + } + + var remotePub crypto.PublicKey + remotePub, _, err = pubKeyProvider.PublicKey() + if err != nil { + return nil, err + } + if !options.trustedSealerPubKey.Equal(remotePub) { + return nil, ErrRemoteSealKeyMatchFailed + } + return pubKeyProvider, nil +} + diff --git a/massifs/massifcontextverifiedv0.go b/massifs/massifcontextverifiedv0.go new file mode 100644 index 0000000..060150a --- /dev/null +++ b/massifs/massifcontextverifiedv0.go @@ -0,0 +1,89 @@ +package massifs + +import ( + "crypto/sha256" + "fmt" + + "github.com/datatrails/go-datatrails-common/cose" + "github.com/datatrails/go-datatrails-merklelog/mmr" +) + +// verifyContextV0 reads and verifies the V0 seal, which will remain important +// for replicas for a while. +func (mc *MassifContext) verifyContextV0( + msg *cose.CoseSign1Message, state MMRState, options ReaderOptions, +) (*VerifiedContext, error) { + + var ok bool + var err error + var rootB []byte + + if state.Version != int(MMRStateVersion0) { + return nil, fmt.Errorf("unsupported MMR state version %d", state.Version) + } + + state.LegacySealRoot, err = mmr.GetRoot(state.MMRSize, mc, sha256.New()) + pubKeyProvider, err := mc.sealPublicKeyProvider(msg, options) + if err != nil { + return nil, err + } + + err = VerifySignedCheckPoint( + *options.codec, pubKeyProvider, msg, state, nil, + ) + if err != nil { + return nil, fmt.Errorf( + "%w: failed to verify seal for massif %d for tenant %s: %v", + ErrSealVerifyFailed, mc.Start.MassifIndex, mc.TenantIdentity, err) + } + cp, err := mmr.IndexConsistencyProofBagged( + state.MMRSize, mc.RangeCount(), mc, sha256.New()) + if err != nil { + return nil, fmt.Errorf( + "%w: error creating bagged consistency proof from %d for massif %d, for tenant %s", + err, state.MMRSize, mc.Start.MassifIndex, mc.TenantIdentity) + } + + ok, rootB, err = mmr.CheckConsistencyBagged(mc, sha256.New(), cp, state.LegacySealRoot) + if err != nil { + return nil, fmt.Errorf( + "%w: error verifying bagged consistency proof from %d for massif %d, for tenant %s", + err, state.MMRSize, mc.Start.MassifIndex, mc.TenantIdentity) + } + if !ok { + // We don't expect false without error, but we + return nil, fmt.Errorf("%w: failed to verify bagged state for massif %d for tenant %s", + mmr.ErrConsistencyCheck, mc.Start.MassifIndex, mc.TenantIdentity) + } + // If the caller has provided a trusted base state, also verify against + // that. Typically this is used for 3d party verification, the 3rd party has + // saved a previously verified state in a local store, and they want to + // check the remote log is consistent with the log portion they have locally + // before replicating the new data. + if options.trustedBaseState != nil { + + cp, err := mmr.IndexConsistencyProofBagged( + state.MMRSize, mc.RangeCount(), mc, sha256.New()) + if err != nil { + return nil, fmt.Errorf( + "%w: error checking consistency with trusted base state from %d for tenant %s", + err, options.trustedBaseState.MMRSize, mc.TenantIdentity) + } + + ok, _, err = mmr.CheckConsistencyBagged(mc, sha256.New(), cp, options.trustedBaseState.LegacySealRoot) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf( + "%w: the accumulator produced for the trusted base state doesn't match the root produced for the seal state fetched from the log", + mmr.ErrConsistencyCheck) + } + } + return &VerifiedContext{ + MassifContext: *mc, + Sign1Message: *msg, + MMRState: state, + ConsistentRoots: [][]byte{rootB}, + }, nil +} diff --git a/massifs/testcommitter.go b/massifs/testcommitter.go index 236e717..2800e90 100644 --- a/massifs/testcommitter.go +++ b/massifs/testcommitter.go @@ -94,6 +94,7 @@ func (c *TestMinimalCommitter) ContextCommitted(ctx context.Context, tenantIdent } state := MMRState{ + Version: int(MMRStateVersion1), MMRSize: mmrSize, Peaks: peaks, Timestamp: time.Now().UnixMilli(), diff --git a/mmr/verifyconsistencybagged.go b/mmr/verifyconsistencybagged.go index 56659b1..a1657f7 100644 --- a/mmr/verifyconsistencybagged.go +++ b/mmr/verifyconsistencybagged.go @@ -22,26 +22,25 @@ func VerifyConsistencyBagged( hasher hash.Hash, peakHashesA [][]byte, proof ConsistencyProof, rootA []byte, rootB []byte) bool { - // A zero length path not valid, even in the case where the mmr's are - // identical (root a == root b) - if len(proof.PathBagged) == 0 { + // There must be something to prove + if len(peakHashesA) == 0 { return false } - // There must be something to prove - if len(peakHashesA) == 0 { + // Check the peakHashesA, which will have been retrieved from the updated + // log, recreate rootA. rootA should have come from a previous Merkle + // Signed Root. + if !bytes.Equal(HashPeaksRHS(hasher, peakHashesA), rootA) { return false } // Catch the case where mmr b is exactly mmr a - if bytes.Equal(rootA, rootB) { + if len(proof.PathBagged) == 0 && bytes.Equal(rootA, rootB) { return true } - // Check the peakHashesA, which will have been retrieved from the updated - // log, recreate rootA. rootA should have come from a previous Merkle - // Signed Root. - if !bytes.Equal(HashPeaksRHS(hasher, peakHashesA), rootA) { + // Otherwise, s zero length path not valid + if len(proof.PathBagged) == 0 { return false }