Skip to content

Commit

Permalink
Allow selecting root public key by ID (#154)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
seh authored Jan 13, 2025
1 parent e51c1c2 commit d4b5e2c
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 16 deletions.
71 changes: 67 additions & 4 deletions biscuit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 25 additions & 12 deletions biscuit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,22 @@ 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")}}})
v3.AddFact(Fact{Predicate: Predicate{Name: "operation", IDs: []Term{String("read")}}})
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")}}})
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand Down

0 comments on commit d4b5e2c

Please sign in to comment.