Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions cookbook/signedbiscuit/biscuit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package signedbiscuit

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"fmt"
"time"

"github.com/flynn/biscuit-go"
"github.com/flynn/biscuit-go/sig"
)

type Metadata struct {
ClientID string
UserID string
UserEmail string
UserGroups []string
IssueTime time.Time
}

type UserKeyPair struct {
Public []byte
Private []byte
}

func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) {
privKeyBytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return nil, fmt.Errorf("failed to marshal ecdsa privkey: %v", err)
}
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal ecdsa pubkey: %v", err)
}
return &UserKeyPair{
Private: privKeyBytes,
Public: pubKeyBytes,
}, nil
}

// WithSignableFacts returns a biscuit which will only verify after being
// signed with the private key matching the given userPubkey.
func WithSignableFacts(b biscuit.Builder, audience string, audienceKey crypto.Signer, userPublicKey []byte, expireTime time.Time, m *Metadata) (biscuit.Builder, error) {
builder := &signedBiscuitBuilder{
Builder: b,
}

if err := builder.withAudienceSignature(audience, audienceKey); err != nil {
return nil, err
}

if err := builder.withUserToSignFact(userPublicKey); err != nil {
return nil, err
}

if err := builder.withExpire(expireTime); err != nil {
return nil, err
}

if err := builder.withMetadata(m); err != nil {
return nil, err
}

return builder.Builder, nil
}

// Sign append a user signature on the given token and return it.
// The UserKeyPair key format to provide depends on the signature algorithm:
// - for ECDSA_P256_SHA256, the private key must be encoded in SEC 1, ASN.1 DER form,
// and the public key in PKIX, ASN.1 DER form.
func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, error) {
b, err := biscuit.Unmarshal(token)
if err != nil {
return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err)
}

v, err := b.Verify(rootPubKey)
if err != nil {
return nil, fmt.Errorf("biscuit: failed to verify: %w", err)
}
verifier := &signedBiscuitVerifier{
Verifier: v,
}

toSignData, err := verifier.getUserToSignData(userKey.Public)
if err != nil {
return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err)
}

if err := verifier.ensureNotAlreadyUserSigned(toSignData.DataID, userKey.Public); err != nil {
return nil, fmt.Errorf("biscuit: previous signature check failed: %w", err)
}

tokenHash, err := b.SHA256Sum(b.BlockCount())
if err != nil {
return nil, err
}

signData, err := userSign(tokenHash, userKey, toSignData)
if err != nil {
return nil, fmt.Errorf("biscuit: signature failed: %w", err)
}

builder := &signedBiscuitBlockBuilder{
BlockBuilder: b.CreateBlock(),
}
if err := builder.withUserSignature(signData); err != nil {
return nil, fmt.Errorf("biscuit: failed to create signature block: %w", err)
}

clientKey := sig.GenerateKeypair(rand.Reader)
b, err = b.Append(rand.Reader, clientKey, builder.Build())
if err != nil {
return nil, fmt.Errorf("biscuit: failed to append signature block: %w", err)
}

return b.Serialize()
}

type UserSignatureMetadata struct {
*Metadata
UserSignatureNonce []byte
UserSignatureTimestamp time.Time
}

// WithSignatureVerification prepares the given verifier in order to verify the audience and user signatures.
// The user signature metadata are returned to the caller to handle the anti replay checks, but they shouldn't be used
// before having called verifier.Verify()
func WithSignatureVerification(v biscuit.Verifier, audience string, audienceKey *ecdsa.PublicKey) (biscuit.Verifier, *UserSignatureMetadata, error) {
verifier := &signedBiscuitVerifier{
Verifier: v,
}

audienceVerificationData, err := verifier.getAudienceVerificationData(audience)
if err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err)
}

if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to verify audience signature: %w", err)
}
if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err)
}

userVerificationData, err := verifier.getUserVerificationData()
if err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err)
}

signatureBlockID, err := v.Biscuit().GetBlockID(biscuit.Fact{Predicate: biscuit.Predicate{
Name: "signature",
IDs: []biscuit.Atom{
userVerificationData.DataID,
userVerificationData.UserPubKey,
userVerificationData.Signature,
userVerificationData.Nonce,
userVerificationData.Timestamp,
},
}})
if err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to retrieve signature blockID: %w", err)
}

signedTokenHash, err := v.Biscuit().SHA256Sum(signatureBlockID - 1)
if err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err)
}

if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to verify user signature: %w", err)
}
if err := verifier.withValidatedUserSignature(userVerificationData); err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err)
}

if err := verifier.withCurrentTime(time.Now()); err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to add current time: %w", err)
}

metas, err := verifier.getMetadata()
if err != nil {
return nil, nil, fmt.Errorf("biscuit: failed to get metadata: %v", err)
}
return v, &UserSignatureMetadata{
Metadata: metas,
UserSignatureNonce: userVerificationData.Nonce,
UserSignatureTimestamp: time.Time(userVerificationData.Timestamp),
}, nil
}
88 changes: 88 additions & 0 deletions cookbook/signedbiscuit/biscuit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package signedbiscuit

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
"time"

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

func TestBiscuit(t *testing.T) {
rootKey := sig.GenerateKeypair(rand.Reader)
audience := "http://random.audience.url"

audienceKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

userKey := generateUserKeyPair(t)
metas := &Metadata{
ClientID: "abcd",
UserEmail: "1234@example.com",
UserID: "1234",
UserGroups: []string{"grp1", "grp2"},
IssueTime: time.Now(),
}

builder := biscuit.NewBuilder(rootKey)

builder, err = WithSignableFacts(builder, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas)
require.NoError(t, err)

b, err := builder.Build()
require.NoError(t, err)
signableBiscuit, err := b.Serialize()
require.NoError(t, err)
t.Logf("signable biscuit size: %d", len(signableBiscuit))

t.Run("happy path", func(t *testing.T) {
signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey)
require.NoError(t, err)
t.Logf("signed biscuit size: %d", len(signedBiscuit))

b, err := biscuit.Unmarshal(signedBiscuit)
require.NoError(t, err)
verifier, err := b.Verify(rootKey.Public())
require.NoError(t, err)

verifier, res, err := WithSignatureVerification(verifier, audience, audienceKey.Public().(*ecdsa.PublicKey))
require.NoError(t, verifier.Verify())

require.NoError(t, err)
require.Equal(t, metas.ClientID, res.ClientID)
require.Equal(t, metas.UserID, res.UserID)
require.Equal(t, metas.UserEmail, res.UserEmail)
require.Equal(t, metas.UserGroups, res.UserGroups)
require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second)
require.NotEmpty(t, res.UserSignatureNonce)
require.NotEmpty(t, res.UserSignatureTimestamp)
})

t.Run("user sign with wrong key", func(t *testing.T) {
_, err := Sign(signableBiscuit, rootKey.Public(), generateUserKeyPair(t))
require.Error(t, err)
})

t.Run("verify wrong audience", func(t *testing.T) {
signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey)
require.NoError(t, err)

b, err := biscuit.Unmarshal(signedBiscuit)
require.NoError(t, err)
verifier, err := b.Verify(rootKey.Public())
require.NoError(t, err)

_, _, err = WithSignatureVerification(verifier, "http://another.audience.url", audienceKey.Public().(*ecdsa.PublicKey))
require.Error(t, err)

wrongAudienceKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

_, _, err = WithSignatureVerification(verifier, audience, wrongAudienceKey.Public().(*ecdsa.PublicKey))
require.Error(t, err)
})
}
Loading