Skip to content

Commit

Permalink
Initial biscuit implementation (#5)
Browse files Browse the repository at this point in the history
Initial biscuit implementation
  • Loading branch information
daeMOn63 authored Aug 19, 2020
1 parent 2bf20ec commit a0282d7
Show file tree
Hide file tree
Showing 15 changed files with 1,909 additions and 5 deletions.
236 changes: 236 additions & 0 deletions biscuit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package biscuit

import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"io"

"github.com/flynn/biscuit-go/datalog"
"github.com/flynn/biscuit-go/pb"
"github.com/flynn/biscuit-go/sig"
"google.golang.org/protobuf/proto"
)

// Biscuit represents a valid Biscuit token
// It contains multiple `Block` elements, the associated symbol table,
// and a serialized version of this data
type Biscuit struct {
authority *Block
blocks []*Block
symbols *datalog.SymbolTable
container *pb.Biscuit
}

var (
// ErrSymbolTableOverlap is returned when multiple blocks declare the same symbols
ErrSymbolTableOverlap = errors.New("biscuit: symbol table overlap")
// ErrInvalidAuthorityIndex occurs when an authority block index is not 0
ErrInvalidAuthorityIndex = errors.New("biscuit: invalid authority index")
// ErrInvalidAuthorityFact occurs when an authority fact is an ambient fact
ErrInvalidAuthorityFact = errors.New("biscuit: invalid authority fact")
// ErrInvalidBlockFact occurs when a block fact provides an authority or ambient fact
ErrInvalidBlockFact = errors.New("biscuit: invalid block fact")
// ErrInvalidBlockRule occurs when a block rule generate an authority or ambient fact
ErrInvalidBlockRule = errors.New("biscuit: invalid block rule")
// ErrEmptyKeys is returned when verifying a biscuit having no keys
ErrEmptyKeys = errors.New("biscuit: empty keys")
// ErrUnknownPublicKey is returned when verifying a biscuit with the wrong public key
ErrUnknownPublicKey = errors.New("biscuit: unknown public key")
)

func New(rng io.Reader, root sig.Keypair, symbols *datalog.SymbolTable, authority *Block) (*Biscuit, error) {
if rng == nil {
rng = rand.Reader
}

if !symbols.IsDisjoint(authority.symbols) {
return nil, ErrSymbolTableOverlap
}

if authority.index != 0 {
return nil, ErrInvalidAuthorityIndex
}

symbols.Extend(authority.symbols)

pbAuthority, err := proto.Marshal(tokenBlockToProtoBlock(authority))
if err != nil {
return nil, err
}

ts := &sig.TokenSignature{}
ts.Sign(rng, root, pbAuthority)

container := &pb.Biscuit{
Authority: pbAuthority,
Keys: [][]byte{root.Public().Bytes()},
Signature: tokenSignatureToProtoSignature(ts),
}

return &Biscuit{
authority: authority,
symbols: symbols,
container: container,
}, nil
}

func (b *Biscuit) CreateBlock() BlockBuilder {
return NewBlockBuilder(uint32(len(b.blocks)+1), b.symbols.Clone())
}

func (b *Biscuit) Append(rng io.Reader, keypair sig.Keypair, block *Block) (*Biscuit, error) {
if b.container == nil {
return nil, errors.New("biscuit: append failed, token is sealed")
}

if !b.symbols.IsDisjoint(block.symbols) {
return nil, ErrSymbolTableOverlap
}

if int(block.index) != len(b.blocks)+1 {
return nil, ErrInvalidBlockIndex
}

// clone biscuit fields and append new block
authority := new(Block)
*authority = *b.authority

blocks := make([]*Block, len(b.blocks)+1)
for i, oldBlock := range b.blocks {
blocks[i] = new(Block)
*blocks[i] = *oldBlock
}
blocks[len(b.blocks)] = block

symbols := b.symbols.Clone()
symbols.Extend(block.symbols)

// serialize and sign the new block
pbBlock, err := proto.Marshal(tokenBlockToProtoBlock(block))
if err != nil {
return nil, err
}

ts, err := protoSignatureToTokenSignature(b.container.Signature)
if err != nil {
return nil, err
}
ts.Sign(rng, keypair, pbBlock)

// clone container and append new marshalled block and public key
container := &pb.Biscuit{
Authority: append([]byte{}, b.container.Authority...),
Blocks: append([][]byte{}, b.container.Blocks...),
Keys: append([][]byte{}, b.container.Keys...),
Signature: tokenSignatureToProtoSignature(ts),
}

container.Blocks = append(container.Blocks, pbBlock)
container.Keys = append(container.Keys, keypair.Public().Bytes())

return &Biscuit{
authority: authority,
blocks: blocks,
symbols: symbols,
container: container,
}, nil
}

func (b *Biscuit) Verify(root sig.PublicKey) (Verifier, error) {
if err := b.checkRootKey(root); err != nil {
return nil, err
}

return NewVerifier(b)
}

func (b *Biscuit) Caveats() [][]datalog.Caveat {
result := make([][]datalog.Caveat, 0, len(b.blocks)+1)
result = append(result, b.authority.caveats)
for _, block := range b.blocks {
result = append(result, block.caveats)
}
return result
}

func (b *Biscuit) Serialize() ([]byte, error) {
return proto.Marshal(b.container)
}

func (b *Biscuit) String() string {
blocks := make([]string, len(b.blocks))
for i, block := range b.blocks {
blocks[i] = block.String(b.symbols)
}

return fmt.Sprintf(`
Biscuit {
symbols: %+q
authority: %s
blocks: %v
}`,
*b.symbols,
b.authority.String(b.symbols),
blocks,
)
}

func (b *Biscuit) checkRootKey(root sig.PublicKey) error {
if len(b.container.Keys) == 0 {
return ErrEmptyKeys
}
if !bytes.Equal(b.container.Keys[0], root.Bytes()) {
return ErrUnknownPublicKey
}

return nil
}

func (b *Biscuit) generateWorld(symbols *datalog.SymbolTable) (*datalog.World, error) {
world := datalog.NewWorld()

idAuthority := symbols.Sym("authority")
if idAuthority == nil {
return nil, errors.New("biscuit: failed to generate world, missing 'authority' symbol in symbol table")
}
idAmbient := symbols.Sym("ambient")
if idAmbient == nil {
return nil, errors.New("biscuit: failed to generate world, missing 'ambient' symbol in symbol table")
}

for _, fact := range *b.authority.facts {
if len(fact.Predicate.IDs) == 0 || fact.Predicate.IDs[0] == idAmbient {
return nil, ErrInvalidAuthorityFact
}

world.AddFact(fact)
}

for _, rule := range b.authority.rules {
world.AddRule(rule)
}

for _, block := range b.blocks {
for _, fact := range *block.facts {
if len(fact.Predicate.IDs) == 0 || fact.Predicate.IDs[0] == idAuthority || fact.Predicate.IDs[0] == idAmbient {
return nil, ErrInvalidBlockFact
}
world.AddFact(fact)
}

for _, rule := range block.rules {
if len(rule.Head.IDs) == 0 || rule.Head.IDs[0] == idAuthority || rule.Head.IDs[0] == idAmbient {
return nil, ErrInvalidBlockRule
}
world.AddRule(rule)
}
}

if err := world.Run(); err != nil {
return nil, err
}

return world, nil
}
101 changes: 101 additions & 0 deletions biscuit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package biscuit

import (
"crypto/rand"
"testing"

"github.com/flynn/biscuit-go/sig"
"github.com/stretchr/testify/require"
)

func TestBiscuit(t *testing.T) {
rng := rand.Reader
root := sig.GenerateKeypair(rng)

builder := NewBuilder(rng, root)

builder.AddAuthorityFact(Fact{
Predicate: Predicate{Name: "right", IDs: []Atom{Symbol("authority"), String("/a/file1"), Symbol("read")}},
})
builder.AddAuthorityFact(Fact{
Predicate: Predicate{Name: "right", IDs: []Atom{Symbol("authority"), String("/a/file1"), Symbol("write")}},
})
builder.AddAuthorityFact(Fact{
Predicate: Predicate{Name: "right", IDs: []Atom{Symbol("authority"), String("/a/file2"), Symbol("read")}},
})

b1, err := builder.Build()
require.NoError(t, err)

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

b1deser, err := Unmarshal(b1ser)
require.NoError(t, err)

block2 := b1deser.CreateBlock()
block2.AddCaveat(Caveat{
Queries: []Rule{
{
Head: Predicate{Name: "caveat", IDs: []Atom{Variable(0)}},
Body: []Predicate{
{Name: "resource", IDs: []Atom{Symbol("ambient"), Variable(0)}},
{Name: "operation", IDs: []Atom{Symbol("ambient"), Symbol("read")}},
{Name: "right", IDs: []Atom{Symbol("authority"), Variable(0), Symbol("read")}},
},
},
},
})

keypair2 := sig.GenerateKeypair(rng)
b2, err := b1deser.Append(rng, keypair2, block2.Build())
require.NoError(t, err)

b2ser, err := b2.Serialize()
require.NoError(t, err)
require.NotEmpty(t, b2ser)

b2deser, err := Unmarshal(b2ser)
require.NoError(t, err)

block3 := b2deser.CreateBlock()
block3.AddCaveat(Caveat{
Queries: []Rule{
{
Head: Predicate{Name: "caveat2", IDs: []Atom{String("/a/file1")}},
Body: []Predicate{
{Name: "resource", IDs: []Atom{Symbol("ambient"), String("/a/file1")}},
},
},
},
})

keypair3 := sig.GenerateKeypair(rng)
b3, err := b2deser.Append(rng, keypair3, block3.Build())
require.NoError(t, err)

b3ser, err := b3.Serialize()
require.NoError(t, err)
require.NotEmpty(t, b3ser)

b3deser, err := Unmarshal(b3ser)
require.NoError(t, err)

v3, err := b3deser.Verify(root.Public())
require.NoError(t, err)

v3.AddOperation("read")
v3.AddResource("/a/file1")
require.NoError(t, v3.Verify())

v3.Reset()
v3.AddOperation("read")
v3.AddResource("/a/file2")
require.Error(t, v3.Verify())

v3.Reset()
v3.AddOperation("write")
v3.AddResource("/a/file1")
require.Error(t, v3.Verify())
}
Loading

0 comments on commit a0282d7

Please sign in to comment.