From 6cde69d95a929e020c088b6256e14419dc976bab Mon Sep 17 00:00:00 2001 From: "Steven E. Harris" Date: Wed, 13 Nov 2024 08:56:31 -0500 Subject: [PATCH] Allow associating an ID with a biscuit's root key (#151) 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. --- biscuit.go | 36 +++++++++++++++++++++--- biscuit_test.go | 27 +++++++++++++----- builder.go | 73 +++++++++++++++++++++++++++++-------------------- options.go | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 41 deletions(-) create mode 100644 options.go diff --git a/biscuit.go b/biscuit.go index ecc5bd7..4112bda 100644 --- a/biscuit.go +++ b/biscuit.go @@ -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() @@ -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 { @@ -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, } @@ -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()) } @@ -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 { @@ -449,6 +476,7 @@ Biscuit { blocks, ) } + func (b *Biscuit) Code() []string { blocks := make([]string, len(b.blocks)) for i, block := range b.blocks { diff --git a/biscuit_test.go b/biscuit_test.go index 667e2f5..f099a6b 100644 --- a/biscuit_test.go +++ b/biscuit_test.go @@ -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")}}, @@ -28,6 +32,11 @@ 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) @@ -35,6 +44,11 @@ func TestBiscuit(t *testing.T) { 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{ @@ -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() @@ -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) @@ -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) diff --git a/builder.go b/builder.go index 599fa68..ff8641d 100644 --- a/builder.go +++ b/builder.go @@ -2,7 +2,6 @@ package biscuit import ( "crypto/ed25519" - "crypto/rand" "errors" "io" @@ -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 @@ -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 @@ -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 @@ -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 { diff --git a/options.go b/options.go new file mode 100644 index 0000000..0f75655 --- /dev/null +++ b/options.go @@ -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) +}