Skip to content

Commit 6cde69d

Browse files
authored
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.
1 parent 61386fc commit 6cde69d

File tree

4 files changed

+146
-41
lines changed

4 files changed

+146
-41
lines changed

Diff for: biscuit.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,23 @@ var (
5353
UnsupportedAlgorithm = errors.New("biscuit: unsupported signature algorithm")
5454
)
5555

56-
func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block) (*Biscuit, error) {
57-
if rng == nil {
58-
rng = rand.Reader
56+
type biscuitOptions struct {
57+
rng io.Reader
58+
rootKeyID *uint32
59+
}
60+
61+
type biscuitOption interface {
62+
applyToBiscuit(*biscuitOptions) error
63+
}
64+
65+
func newBiscuit(root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block, opts ...biscuitOption) (*Biscuit, error) {
66+
options := biscuitOptions{
67+
rng: rand.Reader,
68+
}
69+
for _, opt := range opts {
70+
if err := opt.applyToBiscuit(&options); err != nil {
71+
return nil, err
72+
}
5973
}
6074

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

6781
symbols.Extend(authority.symbols)
6882

69-
nextPublicKey, nextPrivateKey, _ := ed25519.GenerateKey(rng)
83+
nextPublicKey, nextPrivateKey, _ := ed25519.GenerateKey(options.rng)
7084

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

104118
container := &pb.Biscuit{
119+
RootKeyId: options.rootKeyID,
105120
Authority: signedBlock,
106121
Proof: proof,
107122
}
@@ -113,6 +128,14 @@ func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTabl
113128
}, nil
114129
}
115130

131+
func New(rng io.Reader, root ed25519.PrivateKey, baseSymbols *datalog.SymbolTable, authority *Block) (*Biscuit, error) {
132+
var opts []biscuitOption
133+
if rng != nil {
134+
opts = []biscuitOption{WithRNG(rng)}
135+
}
136+
return newBiscuit(root, baseSymbols, authority, opts...)
137+
}
138+
116139
func (b *Biscuit) CreateBlock() BlockBuilder {
117140
return NewBlockBuilder(b.symbols.Clone())
118141
}
@@ -432,6 +455,10 @@ func (b *Biscuit) BlockCount() int {
432455
return len(b.container.Blocks)
433456
}
434457

458+
func (b *Biscuit) RootKeyID() *uint32 {
459+
return b.container.RootKeyId
460+
}
461+
435462
func (b *Biscuit) String() string {
436463
blocks := make([]string, len(b.blocks))
437464
for i, block := range b.blocks {
@@ -449,6 +476,7 @@ Biscuit {
449476
blocks,
450477
)
451478
}
479+
452480
func (b *Biscuit) Code() []string {
453481
blocks := make([]string, len(b.blocks))
454482
for i, block := range b.blocks {

Diff for: biscuit_test.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import (
1212

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

17-
builder := NewBuilder(privateRoot)
18+
builder := NewBuilder(
19+
privateRoot,
20+
WithRNG(rng),
21+
WithRootKeyID(rootKeyID))
1822

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

2933
b1, err := builder.Build()
3034
require.NoError(t, err)
35+
{
36+
keyID := b1.RootKeyID()
37+
require.NotNil(t, keyID, "root key ID present")
38+
require.EqualValues(t, rootKeyID, *keyID, "root key ID")
39+
}
3140

3241
b1ser, err := b1.Serialize()
3342
require.NoError(t, err)
3443
require.NotEmpty(t, b1ser)
3544

3645
b1deser, err := Unmarshal(b1ser)
3746
require.NoError(t, err)
47+
{
48+
keyID := b1deser.RootKeyID()
49+
require.NotNil(t, keyID, "root key ID present after round trip")
50+
require.EqualValues(t, rootKeyID, *keyID, "root key ID after round trip")
51+
}
3852

3953
block2 := b1deser.CreateBlock()
4054
block2.AddCheck(Check{
@@ -202,8 +216,8 @@ func TestBiscuitRules(t *testing.T) {
202216
require.NoError(t, err)
203217

204218
// b1 should allow alice & bob only
205-
//v, err := b1.Verify(publicRoot)
206-
//require.NoError(t, err)
219+
// v, err := b1.Verify(publicRoot)
220+
// require.NoError(t, err)
207221
verifyOwner(t, *b1, publicRoot, map[string]bool{"alice": true, "bob": true, "eve": false})
208222

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

237251
// b2 should now only allow alice
238-
//v, err = b2.Verify(publicRoot)
239-
//require.NoError(t, err)
252+
// v, err = b2.Verify(publicRoot)
253+
// require.NoError(t, err)
240254
verifyOwner(t, *b2, publicRoot, map[string]bool{"alice": true, "bob": false, "eve": false})
241255
}
242256

243257
func verifyOwner(t *testing.T, b Biscuit, publicRoot ed25519.PublicKey, owners map[string]bool) {
244-
245258
for user, valid := range owners {
246259
v, err := b.Authorizer(publicRoot)
247260
require.NoError(t, err)
@@ -318,7 +331,7 @@ func TestGenerateWorld(t *testing.T) {
318331
b, err := build.Build()
319332
require.NoError(t, err)
320333

321-
StringTable := (build.(*builder)).symbols
334+
StringTable := (build.(*builderOptions)).symbols
322335
world, err := b.generateWorld(defaultSymbolTable.Clone())
323336
require.NoError(t, err)
324337

Diff for: builder.go

+43-30
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package biscuit
22

33
import (
44
"crypto/ed25519"
5-
"crypto/rand"
65
"errors"
76
"io"
87

@@ -26,9 +25,10 @@ type Builder interface {
2625
Build() (*Biscuit, error)
2726
}
2827

29-
type builder struct {
30-
rng io.Reader
31-
root ed25519.PrivateKey
28+
type builderOptions struct {
29+
rng io.Reader
30+
rootKey ed25519.PrivateKey
31+
rootKeyID *uint32
3232

3333
symbolsStart int
3434
symbols *datalog.SymbolTable
@@ -38,38 +38,40 @@ type builder struct {
3838
context string
3939
}
4040

41-
type builderOption func(b *builder)
41+
type builderOption interface {
42+
applyToBuilder(b *builderOptions)
43+
}
4244

43-
func WithRandom(rng io.Reader) builderOption {
44-
return func(b *builder) {
45-
b.rng = rng
46-
}
45+
type symbolsOption struct {
46+
*datalog.SymbolTable
4747
}
4848

49+
func (o symbolsOption) applyToBuilder(b *builderOptions) {
50+
b.symbolsStart = o.Len()
51+
b.symbols = o.Clone()
52+
}
53+
54+
// WithSymbols supplies a symbol table to use when composing biscuits.
4955
func WithSymbols(symbols *datalog.SymbolTable) builderOption {
50-
return func(b *builder) {
51-
b.symbolsStart = symbols.Len()
52-
b.symbols = symbols.Clone()
53-
}
56+
return symbolsOption{symbols}
5457
}
5558

5659
func NewBuilder(root ed25519.PrivateKey, opts ...builderOption) Builder {
57-
b := &builder{
58-
rng: rand.Reader,
59-
root: root,
60+
b := &builderOptions{
61+
rootKey: root,
6062
symbols: defaultSymbolTable.Clone(),
6163
symbolsStart: defaultSymbolTable.Len(),
6264
facts: new(datalog.FactSet),
6365
}
6466

6567
for _, o := range opts {
66-
o(b)
68+
o.applyToBuilder(b)
6769
}
6870

6971
return b
7072
}
7173

72-
func (b *builder) AddBlock(block ParsedBlock) error {
74+
func (b *builderOptions) AddBlock(block ParsedBlock) error {
7375
for _, f := range block.Facts {
7476
if err := b.AddAuthorityFact(f); err != nil {
7577
return err
@@ -91,7 +93,7 @@ func (b *builder) AddBlock(block ParsedBlock) error {
9193
return nil
9294
}
9395

94-
func (b *builder) AddAuthorityFact(fact Fact) error {
96+
func (b *builderOptions) AddAuthorityFact(fact Fact) error {
9597
dlFact := fact.convert(b.symbols)
9698
if !b.facts.Insert(dlFact) {
9799
return ErrDuplicateFact
@@ -100,26 +102,37 @@ func (b *builder) AddAuthorityFact(fact Fact) error {
100102
return nil
101103
}
102104

103-
func (b *builder) AddAuthorityRule(rule Rule) error {
105+
func (b *builderOptions) AddAuthorityRule(rule Rule) error {
104106
dlRule := rule.convert(b.symbols)
105107
b.rules = append(b.rules, dlRule)
106108
return nil
107109
}
108110

109-
func (b *builder) AddAuthorityCheck(check Check) error {
111+
func (b *builderOptions) AddAuthorityCheck(check Check) error {
110112
b.checks = append(b.checks, check.convert(b.symbols))
111113
return nil
112114
}
113115

114-
func (b *builder) Build() (*Biscuit, error) {
115-
return New(b.rng, b.root, b.symbols, &Block{
116-
symbols: b.symbols.SplitOff(b.symbolsStart),
117-
facts: b.facts,
118-
rules: b.rules,
119-
checks: b.checks,
120-
context: b.context,
121-
version: MaxSchemaVersion,
122-
})
116+
func (b *builderOptions) Build() (*Biscuit, error) {
117+
opts := make([]biscuitOption, 0, 2)
118+
if v := b.rng; v != nil {
119+
opts = append(opts, WithRNG(b.rng))
120+
}
121+
if v := b.rootKeyID; v != nil {
122+
opts = append(opts, WithRootKeyID(*v))
123+
}
124+
return newBiscuit(
125+
b.rootKey,
126+
b.symbols,
127+
&Block{
128+
symbols: b.symbols.SplitOff(b.symbolsStart),
129+
facts: b.facts,
130+
rules: b.rules,
131+
checks: b.checks,
132+
context: b.context,
133+
version: MaxSchemaVersion,
134+
},
135+
opts...)
123136
}
124137

125138
type Unmarshaler struct {

Diff for: options.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package biscuit
2+
3+
import "io"
4+
5+
type compositionOption interface {
6+
builderOption
7+
biscuitOption
8+
}
9+
10+
type rngOption struct {
11+
io.Reader
12+
}
13+
14+
func (o rngOption) applyToBuilder(b *builderOptions) {
15+
if r := o.Reader; r != nil {
16+
b.rng = o
17+
}
18+
}
19+
20+
func (o rngOption) applyToBiscuit(b *biscuitOptions) error {
21+
if r := o.Reader; r != nil {
22+
b.rng = r
23+
}
24+
return nil
25+
}
26+
27+
// WithRNG supplies a random number generator as a byte stream from which to read when generating
28+
// key pairs with which to sign blocks within biscuits.
29+
func WithRNG(r io.Reader) compositionOption {
30+
return rngOption{r}
31+
}
32+
33+
type rootKeyIDOption uint32
34+
35+
func (o rootKeyIDOption) applyToBuilder(b *builderOptions) {
36+
id := uint32(o)
37+
b.rootKeyID = &id
38+
}
39+
40+
func (o rootKeyIDOption) applyToBiscuit(b *biscuitOptions) error {
41+
id := uint32(o)
42+
b.rootKeyID = &id
43+
return nil
44+
}
45+
46+
// WithRootKeyID specifies the identifier for the root key pair used to sign a biscuit's authority
47+
// block, allowing a consuming party to later select the corresponding public key to validate that
48+
// signature.
49+
func WithRootKeyID(id uint32) compositionOption {
50+
return rootKeyIDOption(id)
51+
}

0 commit comments

Comments
 (0)