From 7b44ee4c5fdca6ecef0dc4782049e5064f85d1da Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Fri, 6 Dec 2024 15:21:31 -0500 Subject: [PATCH] Allow selecting root public key by ID In order to more easily accommodate rotating of root private keys when issuing biscuits, allow consumers to choose which root public key to use when verifying the biscuit based on the key ID embedded within it at composition time, if any. Consumers can then accept biscuits signed with several root keys, learning to accept signatures from a rolling set of both older and newer keys. Introduce the "(*Biscuit).AuthorizerFor" method as an eventual replacement for the longstanding "(*Biscuit).Authorizer" method, along with with two new options for supplying either a single public key or a mapping from ID to public key (together with an optional default public key to use when the biscuit in question embeds no root key ID). Alternately, callers may supply a projection function that consumes an optional root key ID. --- biscuit.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++--- biscuit_test.go | 37 +++++++++++++++++--------- 2 files changed, 92 insertions(+), 16 deletions(-) diff --git a/biscuit.go b/biscuit.go index b5cf545..2f532e7 100644 --- a/biscuit.go +++ b/biscuit.go @@ -41,6 +41,9 @@ var ( ErrInvalidBlockRule = errors.New("biscuit: invalid block rule") // ErrEmptyKeys is returned when verifying a biscuit having no keys ErrEmptyKeys = errors.New("biscuit: empty keys") + // ErrNoPublicKeyAvailable is returned when no public root key is available to verify the + // signatures on a biscuit's blocks. + ErrNoPublicKeyAvailable = errors.New("biscuit: no public key available") // ErrUnknownPublicKey is returned when verifying a biscuit with the wrong public key ErrUnknownPublicKey = errors.New("biscuit: unknown public key") @@ -291,10 +294,42 @@ func (b *Biscuit) Seal(rng io.Reader) (*Biscuit, error) { }, nil } -// Checks the signature and creates an Authorizer -// The Authorizer can then test the authorizaion policies and -// accept or refuse the request -func (b *Biscuit) Authorizer(root ed25519.PublicKey, opts ...AuthorizerOption) (Authorizer, error) { +type ( + // A PublickKeyByIDProjection inspects an optional ID for a public key and returns the + // corresponding public key, if any. If it doesn't recognize the ID or can't find the public + // key, or no ID is supplied and there is no default public key available, it should return an + // error satisfying errors.Is(err, ErrNoPublicKeyAvailable). + PublickKeyByIDProjection func(*uint32) (ed25519.PublicKey, error) +) + +// WithSingularRootPublicKey supplies one public key to use as the root key with which to verify the +// signatures on a biscuit's blocks. +func WithSingularRootPublicKey(key ed25519.PublicKey) PublickKeyByIDProjection { + return func(*uint32) (ed25519.PublicKey, error) { + return key, nil + } +} + +// WithRootPublicKeys supplies a mapping to public keys from their corresponding IDs, used to select +// which public key to use to verify the signatures on a biscuit's blocks based on the key ID +// embedded within the biscuit when it was created. If the biscuit has no key ID available, this +// function selects the optional default key instead. If no public key is available—whether for the +// biscuit's embedded key ID or a default key when no such ID is present—it returns +// [ErrNoPublicKeyAvailable]. +func WithRootPublicKeys(keysByID map[uint32]ed25519.PublicKey, defaultKey *ed25519.PublicKey) PublickKeyByIDProjection { + return func(id *uint32) (ed25519.PublicKey, error) { + if id == nil { + if defaultKey != nil { + return *defaultKey, nil + } + } else if key, ok := keysByID[*id]; ok { + return key, nil + } + return nil, ErrNoPublicKeyAvailable + } +} + +func (b *Biscuit) authorizerFor(root ed25519.PublicKey, opts ...AuthorizerOption) (Authorizer, error) { currentKey := root // for now we only support Ed25519 @@ -377,6 +412,34 @@ func (b *Biscuit) Authorizer(root ed25519.PublicKey, opts ...AuthorizerOption) ( return NewVerifier(b, opts...) } +// AuthorizerFor selects from the supplied source a root public key to use to verify the signatures +// on the biscuit's blocks, returning an error satisfying errors.Is(err, ErrNoPublicKeyAvailable) if +// no such public key is available. If the signatures are valid, it creates an [Authorizer], which +// can then test the authorization policies and accept or refuse the request. +func (b *Biscuit) AuthorizerFor(keySource PublickKeyByIDProjection, opts ...AuthorizerOption) (Authorizer, error) { + if keySource == nil { + return nil, errors.New("root public key source must not be nil") + } + rootPublicKey, err := keySource(b.RootKeyID()) + if err != nil { + return nil, fmt.Errorf("choosing root public key: %w", err) + } + if len(rootPublicKey) == 0 { + return nil, ErrNoPublicKeyAvailable + } + return b.authorizerFor(rootPublicKey, opts...) +} + +// TODO: Add "Deprecated" note to the "(*Biscuit).Authorizer" method, recommending use of +// "(*Biscuit).AuthorizerFor" instead. Wait until after we release the module with the latter +// available, per https://go.dev/wiki/Deprecated. + +// Authorizer checks the signature and creates an [Authorizer]. The Authorizer can then test the +// authorizaion policies and accept or refuse the request. +func (b *Biscuit) Authorizer(root ed25519.PublicKey, opts ...AuthorizerOption) (Authorizer, error) { + return b.authorizerFor(root) +} + func (b *Biscuit) Checks() [][]datalog.Check { result := make([][]datalog.Check, 0, len(b.blocks)+1) result = append(result, b.authority.checks) diff --git a/biscuit_test.go b/biscuit_test.go index 0aae6d5..3edaf09 100644 --- a/biscuit_test.go +++ b/biscuit_test.go @@ -100,7 +100,7 @@ func TestBiscuit(t *testing.T) { b3deser, err := Unmarshal(b3ser) require.NoError(t, err) - v3, err := b3deser.Authorizer(publicRoot) + v3, err := b3deser.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) v3.AddFact(Fact{Predicate: Predicate{Name: "resource", IDs: []Term{String("/a/file1")}}}) @@ -108,14 +108,14 @@ func TestBiscuit(t *testing.T) { v3.AddPolicy(DefaultAllowPolicy) require.NoError(t, v3.Authorize()) - v3, err = b3deser.Authorizer(publicRoot) + v3, err = b3deser.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) v3.AddFact(Fact{Predicate: Predicate{Name: "resource", IDs: []Term{String("/a/file2")}}}) v3.AddFact(Fact{Predicate: Predicate{Name: "operation", IDs: []Term{String("read")}}}) v3.AddPolicy(DefaultAllowPolicy) require.Error(t, v3.Authorize()) - v3, err = b3deser.Authorizer(publicRoot) + v3, err = b3deser.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) v3.AddFact(Fact{Predicate: Predicate{Name: "resource", IDs: []Term{String("/a/file1")}}}) v3.AddFact(Fact{Predicate: Predicate{Name: "operation", IDs: []Term{String("write")}}}) @@ -176,7 +176,7 @@ func TestSealedBiscuit(t *testing.T) { b2deser, err := Unmarshal(b2ser) require.NoError(t, err) - _, err = b2deser.Authorizer(publicRoot) + _, err = b2deser.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) } @@ -260,7 +260,7 @@ func TestBiscuitRules(t *testing.T) { func verifyOwner(t *testing.T, b Biscuit, publicRoot ed25519.PublicKey, owners map[string]bool) { for user, valid := range owners { - v, err := b.Authorizer(publicRoot) + v, err := b.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) t.Run(fmt.Sprintf("verify owner %s", user), func(t *testing.T) { @@ -288,18 +288,31 @@ func verifyOwner(t *testing.T, b Biscuit, publicRoot ed25519.PublicKey, owners m func TestCheckRootKey(t *testing.T) { rng := rand.Reader + const rootKeyID = 123 publicRoot, privateRoot, _ := ed25519.GenerateKey(rng) - builder := NewBuilder(privateRoot) + builder := NewBuilder(privateRoot, WithRootKeyID(rootKeyID)) b, err := builder.Build() require.NoError(t, err) - _, err = b.Authorizer(publicRoot) + _, err = b.AuthorizerFor(WithRootPublicKeys(map[uint32]ed25519.PublicKey{ + rootKeyID: publicRoot, + }, nil)) require.NoError(t, err) + _, err = b.AuthorizerFor(WithRootPublicKeys(map[uint32]ed25519.PublicKey{ + rootKeyID + 1: publicRoot, + }, nil)) + require.ErrorIs(t, err, ErrNoPublicKeyAvailable) + + _, err = b.AuthorizerFor(WithRootPublicKeys(map[uint32]ed25519.PublicKey{ + rootKeyID: nil, + }, nil)) + require.ErrorIs(t, err, ErrNoPublicKeyAvailable) + publicNotRoot, _, _ := ed25519.GenerateKey(rng) - _, err = b.Authorizer(publicNotRoot) + _, err = b.AuthorizerFor(WithSingularRootPublicKey(publicNotRoot)) require.Equal(t, ErrInvalidSignature, err) } @@ -434,11 +447,11 @@ func TestBiscuitVerifyErrors(t *testing.T) { b, err := builder.Build() require.NoError(t, err) - _, err = b.Authorizer(publicRoot) + _, err = b.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) publicTest, _, _ := ed25519.GenerateKey(rng) - _, err = b.Authorizer(publicTest) + _, err = b.AuthorizerFor(WithSingularRootPublicKey(publicTest)) require.Error(t, err) } @@ -465,7 +478,7 @@ func TestBiscuitSha256Sum(t *testing.T) { b, err = b.Append(rng, root, blockBuilder.Build()) require.NoError(t, err) require.Equal(t, 1, b.BlockCount()) - +p h10, err := b.SHA256Sum(0) require.NoError(t, err) require.Equal(t, h0, h10) @@ -591,7 +604,7 @@ func TestInvalidRuleGeneration(t *testing.T) { require.NoError(t, err) t.Log(b.String()) - verifier, err := b.Authorizer(publicRoot) + verifier, err := b.AuthorizerFor(WithSingularRootPublicKey(publicRoot)) require.NoError(t, err) verifier.AddFact(Fact{Predicate: Predicate{