Skip to content

Commit

Permalink
Allow associating an ID with a biscuit's root key (#151)
Browse files Browse the repository at this point in the history
In order to accommodate biscuit issuers with multiple key pairs in
use, whether concurrently or in an ongoing rotation cycle, biscuits
can record and expose an identifier for the root private key used to
sign its authority block. Allow issuers to associate such an
identifier with the private key when creating a new biscuit.

Introduce the option function "WithRootKeyID" to supply such an
identifier at composition time, and the "(*Biscuit).RootKeyID" method
to query this identifier later.
  • Loading branch information
seh authored Nov 13, 2024
1 parent 61386fc commit 6cde69d
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 41 deletions.
36 changes: 32 additions & 4 deletions biscuit.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,23 @@ var (
UnsupportedAlgorithm = errors.New("biscuit: unsupported signature algorithm")
)

func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block) (*Biscuit, error) {
if rng == nil {
rng = rand.Reader
type biscuitOptions struct {
rng io.Reader
rootKeyID *uint32
}

type biscuitOption interface {
applyToBiscuit(*biscuitOptions) error
}

func newBiscuit(root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block, opts ...biscuitOption) (*Biscuit, error) {
options := biscuitOptions{
rng: rand.Reader,
}
for _, opt := range opts {
if err := opt.applyToBiscuit(&options); err != nil {
return nil, err
}
}

symbols := baseSymbols.Clone()
Expand All @@ -66,7 +80,7 @@ func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTabl

symbols.Extend(authority.symbols)

nextPublicKey, nextPrivateKey, _ := ed25519.GenerateKey(rng)
nextPublicKey, nextPrivateKey, _ := ed25519.GenerateKey(options.rng)

protoAuthority, err := tokenBlockToProtoBlock(authority)
if err != nil {
Expand Down Expand Up @@ -102,6 +116,7 @@ func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTabl
}

container := &pb.Biscuit{
RootKeyId: options.rootKeyID,
Authority: signedBlock,
Proof: proof,
}
Expand All @@ -113,6 +128,14 @@ func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTabl
}, nil
}

func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block) (*Biscuit, error) {
var opts []biscuitOption
if rng != nil {
opts = []biscuitOption{WithRNG(rng)}
}
return newBiscuit(root, baseSymbols, authority, opts...)
}

func (b *Biscuit) CreateBlock() BlockBuilder {
return NewBlockBuilder(b.symbols.Clone())
}
Expand Down Expand Up @@ -432,6 +455,10 @@ func (b *Biscuit) BlockCount() int {
return len(b.container.Blocks)
}

func (b *Biscuit) RootKeyID() *uint32 {
return b.container.RootKeyId
}

func (b *Biscuit) String() string {
blocks := make([]string, len(b.blocks))
for i, block := range b.blocks {
Expand All @@ -449,6 +476,7 @@ Biscuit {
blocks,
)
}

func (b *Biscuit) Code() []string {
blocks := make([]string, len(b.blocks))
for i, block := range b.blocks {
Expand Down
27 changes: 20 additions & 7 deletions biscuit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import (

func TestBiscuit(t *testing.T) {
rng := rand.Reader
const rootKeyID = 123
publicRoot, privateRoot, _ := ed25519.GenerateKey(rng)

builder := NewBuilder(privateRoot)
builder := NewBuilder(
privateRoot,
WithRNG(rng),
WithRootKeyID(rootKeyID))

builder.AddAuthorityFact(Fact{
Predicate: Predicate{Name: "right", IDs: []Term{String("/a/file1"), String("read")}},
Expand All @@ -28,13 +32,23 @@ func TestBiscuit(t *testing.T) {

b1, err := builder.Build()
require.NoError(t, err)
{
keyID := b1.RootKeyID()
require.NotNil(t, keyID, "root key ID present")
require.EqualValues(t, rootKeyID, *keyID, "root key ID")
}

b1ser, err := b1.Serialize()
require.NoError(t, err)
require.NotEmpty(t, b1ser)

b1deser, err := Unmarshal(b1ser)
require.NoError(t, err)
{
keyID := b1deser.RootKeyID()
require.NotNil(t, keyID, "root key ID present after round trip")
require.EqualValues(t, rootKeyID, *keyID, "root key ID after round trip")
}

block2 := b1deser.CreateBlock()
block2.AddCheck(Check{
Expand Down Expand Up @@ -202,8 +216,8 @@ func TestBiscuitRules(t *testing.T) {
require.NoError(t, err)

// b1 should allow alice & bob only
//v, err := b1.Verify(publicRoot)
//require.NoError(t, err)
// v, err := b1.Verify(publicRoot)
// require.NoError(t, err)
verifyOwner(t, *b1, publicRoot, map[string]bool{"alice": true, "bob": true, "eve": false})

block := b1.CreateBlock()
Expand Down Expand Up @@ -235,13 +249,12 @@ func TestBiscuitRules(t *testing.T) {
require.NoError(t, err)

// b2 should now only allow alice
//v, err = b2.Verify(publicRoot)
//require.NoError(t, err)
// v, err = b2.Verify(publicRoot)
// require.NoError(t, err)
verifyOwner(t, *b2, publicRoot, map[string]bool{"alice": true, "bob": false, "eve": false})
}

func verifyOwner(t *testing.T, b Biscuit, publicRoot ed25519.PublicKey, owners map[string]bool) {

for user, valid := range owners {
v, err := b.Authorizer(publicRoot)
require.NoError(t, err)
Expand Down Expand Up @@ -318,7 +331,7 @@ func TestGenerateWorld(t *testing.T) {
b, err := build.Build()
require.NoError(t, err)

StringTable := (build.(*builder)).symbols
StringTable := (build.(*builderOptions)).symbols
world, err := b.generateWorld(defaultSymbolTable.Clone())
require.NoError(t, err)

Expand Down
73 changes: 43 additions & 30 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package biscuit

import (
"crypto/ed25519"
"crypto/rand"
"errors"
"io"

Expand All @@ -26,9 +25,10 @@ type Builder interface {
Build() (*Biscuit, error)
}

type builder struct {
rng io.Reader
root ed25519.PrivateKey
type builderOptions struct {
rng io.Reader
rootKey ed25519.PrivateKey
rootKeyID *uint32

symbolsStart int
symbols *datalog.SymbolTable
Expand All @@ -38,38 +38,40 @@ type builder struct {
context string
}

type builderOption func(b *builder)
type builderOption interface {
applyToBuilder(b *builderOptions)
}

func WithRandom(rng io.Reader) builderOption {
return func(b *builder) {
b.rng = rng
}
type symbolsOption struct {
*datalog.SymbolTable
}

func (o symbolsOption) applyToBuilder(b *builderOptions) {
b.symbolsStart = o.Len()
b.symbols = o.Clone()
}

// WithSymbols supplies a symbol table to use when composing biscuits.
func WithSymbols(symbols *datalog.SymbolTable) builderOption {
return func(b *builder) {
b.symbolsStart = symbols.Len()
b.symbols = symbols.Clone()
}
return symbolsOption{symbols}
}

func NewBuilder(root ed25519.PrivateKey, opts ...builderOption) Builder {
b := &builder{
rng: rand.Reader,
root: root,
b := &builderOptions{
rootKey: root,
symbols: defaultSymbolTable.Clone(),
symbolsStart: defaultSymbolTable.Len(),
facts: new(datalog.FactSet),
}

for _, o := range opts {
o(b)
o.applyToBuilder(b)
}

return b
}

func (b *builder) AddBlock(block ParsedBlock) error {
func (b *builderOptions) AddBlock(block ParsedBlock) error {
for _, f := range block.Facts {
if err := b.AddAuthorityFact(f); err != nil {
return err
Expand All @@ -91,7 +93,7 @@ func (b *builder) AddBlock(block ParsedBlock) error {
return nil
}

func (b *builder) AddAuthorityFact(fact Fact) error {
func (b *builderOptions) AddAuthorityFact(fact Fact) error {
dlFact := fact.convert(b.symbols)
if !b.facts.Insert(dlFact) {
return ErrDuplicateFact
Expand All @@ -100,26 +102,37 @@ func (b *builder) AddAuthorityFact(fact Fact) error {
return nil
}

func (b *builder) AddAuthorityRule(rule Rule) error {
func (b *builderOptions) AddAuthorityRule(rule Rule) error {
dlRule := rule.convert(b.symbols)
b.rules = append(b.rules, dlRule)
return nil
}

func (b *builder) AddAuthorityCheck(check Check) error {
func (b *builderOptions) AddAuthorityCheck(check Check) error {
b.checks = append(b.checks, check.convert(b.symbols))
return nil
}

func (b *builder) Build() (*Biscuit, error) {
return New(b.rng, b.root, b.symbols, &Block{
symbols: b.symbols.SplitOff(b.symbolsStart),
facts: b.facts,
rules: b.rules,
checks: b.checks,
context: b.context,
version: MaxSchemaVersion,
})
func (b *builderOptions) Build() (*Biscuit, error) {
opts := make([]biscuitOption, 0, 2)
if v := b.rng; v != nil {
opts = append(opts, WithRNG(b.rng))
}
if v := b.rootKeyID; v != nil {
opts = append(opts, WithRootKeyID(*v))
}
return newBiscuit(
b.rootKey,
b.symbols,
&Block{
symbols: b.symbols.SplitOff(b.symbolsStart),
facts: b.facts,
rules: b.rules,
checks: b.checks,
context: b.context,
version: MaxSchemaVersion,
},
opts...)
}

type Unmarshaler struct {
Expand Down
51 changes: 51 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package biscuit

import "io"

type compositionOption interface {
builderOption
biscuitOption
}

type rngOption struct {
io.Reader
}

func (o rngOption) applyToBuilder(b *builderOptions) {
if r := o.Reader; r != nil {
b.rng = o
}
}

func (o rngOption) applyToBiscuit(b *biscuitOptions) error {
if r := o.Reader; r != nil {
b.rng = r
}
return nil
}

// WithRNG supplies a random number generator as a byte stream from which to read when generating
// key pairs with which to sign blocks within biscuits.
func WithRNG(r io.Reader) compositionOption {
return rngOption{r}
}

type rootKeyIDOption uint32

func (o rootKeyIDOption) applyToBuilder(b *builderOptions) {
id := uint32(o)
b.rootKeyID = &id
}

func (o rootKeyIDOption) applyToBiscuit(b *biscuitOptions) error {
id := uint32(o)
b.rootKeyID = &id
return nil
}

// WithRootKeyID specifies the identifier for the root key pair used to sign a biscuit's authority
// block, allowing a consuming party to later select the corresponding public key to validate that
// signature.
func WithRootKeyID(id uint32) compositionOption {
return rootKeyIDOption(id)
}

0 comments on commit 6cde69d

Please sign in to comment.