Skip to content

Commit

Permalink
add signed biscuit cookbook
Browse files Browse the repository at this point in the history
  • Loading branch information
daeMOn63 committed Oct 15, 2020
1 parent 9ea2c86 commit 15d7d35
Show file tree
Hide file tree
Showing 6 changed files with 1,092 additions and 2 deletions.
194 changes: 194 additions & 0 deletions cookbook/signedbiscuit/biscuit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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
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
}

// GenerateSignable returns a biscuit which will only verify after being
// signed with the private key matching the given userPubkey.
func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey crypto.Signer, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) {
builder := &hubauthBuilder{
biscuit.NewBuilder(rand.Reader, rootKey),
}

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
}

b, err := builder.Build()
if err != nil {
return nil, err
}

return b.Serialize()
}

// 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 := &hubauthVerifier{
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 := &hubauthBlockBuilder{
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 VerifiedMetadata struct {
*Metadata
UserSignatureNonce []byte
UserSignatureTimestamp time.Time
}

// Verify will verify the biscuit, the included audience and user signature, and return an error
// when anything is invalid.
func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *ecdsa.PublicKey) (*VerifiedMetadata, 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 := &hubauthVerifier{v}

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

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

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

// TODO: improve biscuit API to allow retrieve the block index the signature is at
// so that we can still append other blocks if needed. Right now the signature MUST BE the last block.
signedTokenHash, err := b.SHA256Sum(b.BlockCount() - 1)
if err != nil {
return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err)
}

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

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

if err := verifier.Verify(); err != nil {
return nil, fmt.Errorf("biscuit: failed to verify: %w", err)
}

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

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

"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: "[email protected]",
UserID: "1234",
IssueTime: time.Now(),
}
signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas)
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))

res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey.Public().(*ecdsa.PublicKey))
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.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)

_, err = Verify(signedBiscuit, rootKey.Public(), "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 = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey.Public().(*ecdsa.PublicKey))
require.Error(t, err)
})
}
Loading

0 comments on commit 15d7d35

Please sign in to comment.