blob: 5f860b81073dcae424299473af11d7371d44c619 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package signing signs syncbase updates using public key signatures, and
// allows these signatures to be checked on other nodes.
//
// The functionality is geared specifically towards syncbase synchronization
// because it is designed to allow a signature to remain valid during its
// propagation across the syncgroup once it has been accepted by at least one
// member of a syncgroup, even if the original key or its blessings are
// invalidated in the meantime.
//
// There are three types of participant:
// - an "author", which creates an update, and signs it with Sign().
// - one or more "validators", each of which receives a change directly from
// the author, and applies Check() to validate it.
// - zero or more "checkers', each of whom receives a change from a validator
// or another checker, and applied Check() to check it.
//
// A validator checks the signature and blessings provided by the author, and
// then appends its own signature, vouching for the fact that the author's
// signature was good at the time the validator saw it.
//
// A checker checks the signatures of both the author and validator but uses
// weaker checks for signature validity than a validator. In particular, it
// uses a significant grace period for key expiry so that a change admitted to
// the syncgroup by a validator has an opportunity to propagate to all the
// nodes in the syncgroup if the keys or blessings are revoked after the change
// is admitted, but before it is fully propagated. The intent is that the
// grace period be chosen to be greater than the diameter of the syncgroup
// (measured in time). One way to ensure that is to insist that members sync
// with a central server at least every T time units, and make the grace period
// be 2T. The central server may sign the data anew to allow new members to pick
// it up.
//
// The model is further complicated by performance concerns. An update written
// to syncbase might be quite small (perhaps tens of bytes) but:
// a) a public key signature or verification can take on the order of a
// millisecond. (Currently, ECDSA signing might a little under 1ms and
// verification just over 2ms on a workstation. A checker performs two such
// verifications.)
// b) unmarshalling even a simple Blessings object can take milliseconds. (!)
// c) marshalling a public key can take 10us.
// d) a Blessings object is of the order of a kilobyte of more, which may
// represent substantial space overhead if duplicated.
//
// Because of (a), we wish to batch syncbase updates, so that a single
// signature check applies to several updates. Thus the Data in a
// DataWithSignature is a vector of Item, rather than a single Item.
//
// However, we will not always wish to put all updates in the same batch. For
// example, an author and a validator might share two different syncgroups with
// different memberships. In such a case, the author might keep the batches
// for one syncgoup separate from batches for the other syncgroup, even though
// the author blessings and validator identities are the same for all the
// batches. Thus, because of (b,c,d), it's worth decoupling the author's
// Blessings data and the validator's key data separately from the signed
// batches itself, so that the blessings and validator data can be processed
// once, even though several batches of updates are being sent. A
// ValidationCache is used to hold this data separately, and allow it to be
// sent just once, rather than once per signature.
//
// Lastly, imagine that the author sends a batch of 10 updates to a validator,
// and the validator then syncs with a checker that is permitted to see only
// half of the updates; perhaps ACLs prevent if from seeing the others. This
// requires that the signature on the batch remain valid even if some of the
// updates in the batch are removed. This is accomplished via the Item type,
// which is a VDL union type that contains either the bytes of the marshalled
// form of the update, or (if the update must not be sent) the SHA-256 hash of
// the data (which can be computed with SumByteVectorWithLength()).
package signing
import "bytes"
import "crypto/sha256"
import "encoding/binary"
import "hash"
import "time"
import "v.io/x/ref/services/syncbase/signing/krl"
import "v.io/v23/context"
import "v.io/v23/security"
import "v.io/v23/verror"
const pkgPath = "v.io/x/ref/services/syncbase/signing"
// These are among the errors may be returned by Check(), and indicate that the
// operation should be retried when new data has been added to the
// ValidationCache. The errors are public to make it easier for the client to
// test for them.
var (
ErrNeedAuthorBlessingsAndValidatorDataForHash = verror.Register(
pkgPath+".ErrNeedAuthorBlessingsAndValidatorDataForHash",
verror.RetryRefetch,
"{1:}{2:} The ValidationCache contains neither the author blessings nor the validator data{:_}")
ErrNeedAuthorBlessingsForHash = verror.Register(
pkgPath+".ErrNeedAuthorBlessingsForHash",
verror.RetryRefetch,
"{1:}{2:} The ValidationCache does not contain the author blessings{:_}")
ErrNeedValidatorDataForHash = verror.Register(
pkgPath+".ErrNeedValidatorDataForHash",
verror.RetryRefetch,
"{1:}{2:} The ValidationCache does not contain the validator data{:_}")
)
// These errors are less likely to be tested for, and so are not exported.
var (
errAuthorKeyIsRevoked = verror.Register(
pkgPath+".errAuthorKeyIsRevoked",
verror.NoRetry,
"{1:}{2:} The author key has been revoked{:_}")
errBadAuthorSignature = verror.Register(
pkgPath+".errBadAuthorSignature",
verror.NoRetry,
"{1:}{2:} Author signature verification failed{:_}")
errBadValidatorSignature = verror.Register(
pkgPath+".errBadValidatorSignature",
verror.NoRetry,
"{1:}{2:} Validator signature verification failed{:_}")
errAuthorBlessingsHaveNoValidNames = verror.Register(
pkgPath+".errAuthorBlessingsHaveNoValidNames",
verror.NoRetry,
"{1:}{2:} Author Blessings have no valid names{:_}")
errMayNotValidateOwnSignature = verror.Register(
pkgPath+".errMayNotValidateOwnSignature",
verror.NoRetry,
"{1:}{2:} Author may not validate its own signature{:_}")
errSenderIsNotAuthor = verror.Register(
pkgPath+".errSenderIsNotAuthor",
verror.NoRetry,
"{1:}{2:} Author is not sender of RPC; will not validate{:_}")
errValidatesWrongNames = verror.Register(
pkgPath+".errValidatesWrongNames",
verror.NoRetry,
"{1:}{2:} The validated names are not a subset of the names sent by the checker{:_}")
errValidatorIsSigner = verror.Register(
pkgPath+".errValidatorIsSigner",
verror.NoRetry,
"{1:}{2:} The signature was validated by its author; treating as invalid{:_}")
errValidatorKeyIsRevoked = verror.Register(
pkgPath+".errValidatorKeyIsRevoked",
verror.NoRetry,
"{1:}{2:} The validator key is revoked{:_}")
)
// --------------------------------------------
// SignData() uses authorPrincipal to sign data using blessings (which must be
// associated with the authorPrincipal). A pointer to a newly constructed
// DataWithSignature with IsValidated==false is returned. Ensures that the
// blessings are stored in *cache. Typically, "authorPrincipal" is obtained from
// v23.GetPrincipal(ctx).
//
// If a recipient of the result *d complains that it does not understand the
// hash d.BlessingHash, the signer should present it with
// blessingsData.MarshalledBlessings, which will allow the recipient to
// construct the Blessings. The Blessings are transmitted out of line because
// they are large, and may be reused for multiple signatures.
func SignData(ctx *context.T, cache *ValidationCache, authorPrincipal security.Principal,
blessings security.Blessings, data []Item) (d *DataWithSignature, blessingsData *BlessingsData, err error) {
d = new(DataWithSignature)
d.Data = data
d.BlessingsHash, blessingsData, err = cache.AddBlessings(ctx, blessings)
if err == nil {
d.AuthorSigned, err = authorPrincipal.Sign(d.authorSignatureHash())
}
return d, blessingsData, err
}
// hashByteVectorWithLength() calls hasher.Write() on a representation of
// len(b), followed by the contents of b.
func hashByteVectorWithLength(hasher hash.Hash, b []byte) {
var length [8]byte
binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
hasher.Write(length[:])
hasher.Write(b)
}
// SumByteVectorWithLength() returns a SHA-256 hash of
// len(b), followed by the contents of b.
func SumByteVectorWithLength(b []byte) []byte {
hasher := sha256.New()
var length [8]byte
binary.LittleEndian.PutUint64(length[:], uint64(len(b)))
hasher.Write(length[:])
hasher.Write(b)
return hasher.Sum(nil)[:]
}
// authorSignatureHash() returns the hash that the author should sign.
func (d *DataWithSignature) authorSignatureHash() []byte {
hasher := sha256.New()
var length [8]byte
binary.LittleEndian.PutUint64(length[:], uint64(len(d.Data)))
hasher.Write(length[:])
for i := range d.Data {
if data, gotData := d.Data[i].(ItemData); gotData {
hasher.Write(SumByteVectorWithLength(data.Value))
} else if hash, gotHash := d.Data[i].(ItemHash); gotHash {
hasher.Write(hash.Value)
} else {
// d.Data is neither a Data nor a Hash. This shouldn't
// happen unless the mashalled data is somehow
// corrupted. The signature will not match unless the
// original author of the data was seeing the same.
hasher.Write([]byte("no data"))
}
}
hashByteVectorWithLength(hasher, d.BlessingsHash)
return hasher.Sum(nil)[:]
}
// validatorSignatureHash() returns the hash that the validator should sign,
// given the hash that the author signed.
func (d *DataWithSignature) validatorSignatureHash(authorSignatureHash []byte) []byte {
var buffer [32]byte
var buf []byte = buffer[:]
if len(d.AuthorSigned.Hash) > len(buf) {
buf = make([]byte, len(d.AuthorSigned.Hash))
}
hasher := sha256.New()
hashByteVectorWithLength(hasher, authorSignatureHash)
hashByteVectorWithLength(hasher, d.AuthorSigned.Purpose)
hashByteVectorWithLength(hasher, buf[:copy(buf, d.AuthorSigned.Hash)])
hashByteVectorWithLength(hasher, d.AuthorSigned.R)
hashByteVectorWithLength(hasher, d.AuthorSigned.S)
hashByteVectorWithLength(hasher, d.ValidatorDataHash)
return hasher.Sum(nil)[:]
}
// Check() verifies the signature(s) on *d:
//
// If d.IsValidated==false, checks that:
// 1. the author's signature is available in *cache.
// 2. the author's signature over its blessings and the data is
// cyprotgraphically valid.
// 3. security.SigningBlessingNames() yields a non-empty list of names when
// applied to the author's blessings.
// 4. the author's public key is not known to be revoked.
// 5. the local's public key (call.LocalPrincipal().PublicKey()) is not known
// to be revoked.
// 6. the author's public key is the public key of the RPC caller.
// 7. the author's public key and the local public key differ.
// If checks pass and there are no other errors:
// - records the list of names found in check (3) in the ValidationData
// - adds a validation signature using the local public key (which is now the
// validator)
// - sets d.IsValidated
// - returns the list of names found in check (3), and a nil error.
// Otherwise returns a nil list of names and a non-nil error.
//
// If d.Validated==true, checks that:
// 1. the author's signature and the validator data are available in *cache.
// 2. the author's signature over its blessings and the data is
// cyprotgraphically valid.
// 8. the list of names stored in the ValidatorData by the validator is
// non-empty.
// 9. the author's public key and the validator's public key differ.
// 10. the list of names stored in the ValidatorData by the validator is a
// subset of the list of names that the author's blessings could have
// represented.
// 11. the author's public key is not known to be revoked more than
// gracePeriod ago.
// 12. the validator's public key is not known to be revoked more than
// gracePeriod ago.
// 13. the validator's signature is cryptographically valid.
// If checks pass and there are no other errors:
// - returns the list of names in the validator's data, and a nil error.
// Otherwise returns a nil list of names and a non-nil error.
func (d *DataWithSignature) Check(ctx *context.T, cache *ValidationCache, call security.Call,
krl *krl.KRL, gracePeriod time.Duration) (names []string, err error) {
// Verify that we have the Blessings and ValidatorData.
var authorBlessingsData *BlessingsData = cache.LookupBlessingsData(ctx, d.BlessingsHash)
var validatorData *ValidatorData
if d.IsValidated {
validatorData = cache.LookupValidatorData(ctx, d.ValidatorDataHash)
}
if authorBlessingsData == nil || (validatorData == nil && d.IsValidated) { // Check (1).
if authorBlessingsData == nil && (validatorData == nil && d.IsValidated) {
err = verror.New(ErrNeedAuthorBlessingsAndValidatorDataForHash, ctx)
} else if authorBlessingsData == nil {
err = verror.New(ErrNeedAuthorBlessingsForHash, ctx)
} else {
err = verror.New(ErrNeedValidatorDataForHash, ctx)
}
}
// Check the author signature.
var authorSignatureHash []byte
if err == nil {
authorSignatureHash = d.authorSignatureHash()
if !d.AuthorSigned.Verify(authorBlessingsData.UnmarshalledBlessings.PublicKey(), authorSignatureHash) { // Check (2).
err = verror.New(errBadAuthorSignature, ctx)
}
}
// Check or create the validator signature.
now := time.Now()
if err != nil {
// err already set
} else if !d.IsValidated {
// Not yet validated, so this run will attempt to validate.
var validatedNames []string
var localKeyMarshalled []byte
var senderKeyMarshalled []byte
validatedNames, _ = security.SigningBlessingNames(ctx, call.LocalPrincipal(),
authorBlessingsData.UnmarshalledBlessings)
if len(validatedNames) == 0 { // Check (3).
err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
} else if localKeyMarshalled, err = call.LocalPrincipal().PublicKey().MarshalBinary(); err != nil {
// err already set
} else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now) { // Check (4).
err = verror.New(errAuthorKeyIsRevoked, ctx)
} else if krl.RevocationTime(localKeyMarshalled).Before(now) { // Check (5).
err = verror.New(errValidatorKeyIsRevoked, ctx)
} else if senderKeyMarshalled, err = call.RemoteBlessings().PublicKey().MarshalBinary(); err != nil {
// err already set
} else if !bytes.Equal(senderKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (6).
err = verror.New(errSenderIsNotAuthor, ctx)
} else if bytes.Equal(localKeyMarshalled, authorBlessingsData.MarshalledPublicKey) { // Check (7).
err = verror.New(errMayNotValidateOwnSignature, ctx)
} else {
// Local principal is different from author, so can validate.
validatorData = &ValidatorData{
Names: validatedNames,
PublicKey: call.LocalPrincipal().PublicKey(),
MarshalledPublicKey: localKeyMarshalled,
}
d.ValidatorDataHash = cache.AddValidatorData(ctx, validatorData)
d.ValidatorSigned, err = call.LocalPrincipal().Sign(d.validatorSignatureHash(authorSignatureHash))
d.IsValidated = (err == nil)
}
} else { // Data already validated; check the validator siganture.
if len(validatorData.Names) == 0 { // Check (8).
err = verror.New(errAuthorBlessingsHaveNoValidNames, ctx)
} else if bytes.Equal(validatorData.MarshalledPublicKey, authorBlessingsData.MarshalledPublicKey) { // Check (9).
err = verror.New(errValidatorIsSigner, ctx)
} else if !authorBlessingsData.UnmarshalledBlessings.CouldHaveNames(validatorData.Names) { // Check (10).
err = verror.New(errValidatesWrongNames, ctx)
} else if krl.RevocationTime(authorBlessingsData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (11).
err = verror.New(errAuthorKeyIsRevoked, ctx)
} else if krl.RevocationTime(validatorData.MarshalledPublicKey).Before(now.Add(-gracePeriod)) { // Check (12).
err = verror.New(errValidatorKeyIsRevoked, ctx)
} else if !d.ValidatorSigned.Verify(validatorData.PublicKey, d.validatorSignatureHash(authorSignatureHash)) { // Check (13).
err = verror.New(errBadValidatorSignature, ctx)
} // else success.
}
// If there were no errors, return the list of names from the validator.
if err == nil {
names = make([]string, len(validatorData.Names))
copy(names, validatorData.Names)
}
return names, err
}