Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow selecting root public key by ID #154

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -96,22 +96,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 @@ -172,7 +172,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 @@ -256,7 +256,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 @@ -284,18 +284,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 @@ -430,11 +443,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 @@ -461,7 +474,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 @@ -587,7 +600,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
Loading