-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial biscuit implementation
- Loading branch information
Showing
15 changed files
with
1,909 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
Oops, something went wrong.