From 6e5639fc41d658e08ee430b2b9deec51e6cc5711 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 15 Oct 2020 09:56:11 +0200 Subject: [PATCH 1/7] add signed biscuit cookbook --- cookbook/signedbiscuit/biscuit.go | 194 +++++++++++ cookbook/signedbiscuit/biscuit_test.go | 65 ++++ cookbook/signedbiscuit/signature.go | 176 ++++++++++ cookbook/signedbiscuit/signature_test.go | 230 ++++++++++++ cookbook/signedbiscuit/wrapper.go | 426 +++++++++++++++++++++++ 5 files changed, 1091 insertions(+) create mode 100644 cookbook/signedbiscuit/biscuit.go create mode 100644 cookbook/signedbiscuit/biscuit_test.go create mode 100644 cookbook/signedbiscuit/signature.go create mode 100644 cookbook/signedbiscuit/signature_test.go create mode 100644 cookbook/signedbiscuit/wrapper.go diff --git a/cookbook/signedbiscuit/biscuit.go b/cookbook/signedbiscuit/biscuit.go new file mode 100644 index 0000000..94907d2 --- /dev/null +++ b/cookbook/signedbiscuit/biscuit.go @@ -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 +} diff --git a/cookbook/signedbiscuit/biscuit_test.go b/cookbook/signedbiscuit/biscuit_test.go new file mode 100644 index 0000000..3a6f37c --- /dev/null +++ b/cookbook/signedbiscuit/biscuit_test.go @@ -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: "1234@example.com", + 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) + }) +} diff --git a/cookbook/signedbiscuit/signature.go b/cookbook/signedbiscuit/signature.go new file mode 100644 index 0000000..ce24711 --- /dev/null +++ b/cookbook/signedbiscuit/signature.go @@ -0,0 +1,176 @@ +package signedbiscuit + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + "time" + + "github.com/flynn/biscuit-go" +) + +var ( + ErrUnsupportedSignatureAlg = errors.New("unsupported signature algorithm") + ErrInvalidSignature = errors.New("invalid signature") +) + +type SignatureAlg biscuit.Symbol + +const ( + ECDSA_P256_SHA256 SignatureAlg = "ECDSA_P256_SHA256" +) + +type userToSignData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes +} + +type userSignatureData struct { + DataID biscuit.Integer + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +type userVerificationData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { + if len(tokenHash) == 0 { + return nil, errors.New("invalid tokenHash") + } + + signerTimestamp := time.Now() + signerNonce := make([]byte, nonceSize) + if _, err := rand.Read(signerNonce); err != nil { + return nil, err + } + + var dataToSign []byte + dataToSign = append(dataToSign, toSignData.Data...) + dataToSign = append(dataToSign, tokenHash...) + dataToSign = append(dataToSign, signerNonce...) + dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) + + var signedData biscuit.Bytes + switch SignatureAlg(toSignData.Alg) { + case ECDSA_P256_SHA256: + privKey, err := x509.ParseECPrivateKey(userKey.Private) + if err != nil { + return nil, err + } + hash := sha256.Sum256(dataToSign) + signedData, err = ecdsa.SignASN1(rand.Reader, privKey, hash[:]) + if err != nil { + return nil, err + } + default: + return nil, ErrUnsupportedSignatureAlg + } + + return &userSignatureData{ + DataID: toSignData.DataID, + Nonce: signerNonce, + Signature: signedData, + Timestamp: biscuit.Date(signerTimestamp), + UserPubKey: userKey.Public, + }, nil +} + +func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) error { + var signedData []byte + signedData = append(signedData, data.Data...) + signedData = append(signedData, signedTokenHash...) + signedData = append(signedData, data.Nonce...) + signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) + + switch SignatureAlg(data.Alg) { + case ECDSA_P256_SHA256: + pk, err := x509.ParsePKIXPublicKey(data.UserPubKey) + if err != nil { + return err + } + pubkey, ok := pk.(*ecdsa.PublicKey) + if !ok { + return errors.New("invalid pubkey, not an *ecdsa.PublicKey") + } + + hash := sha256.Sum256(signedData) + if !ecdsa.VerifyASN1(pubkey, hash[:], data.Signature) { + return ErrInvalidSignature + } + return nil + default: + return ErrUnsupportedSignatureAlg + } +} + +type audienceVerificationData struct { + Audience biscuit.Symbol + Challenge biscuit.Bytes + Signature biscuit.Bytes +} + +func audienceSign(audience string, audienceKey crypto.Signer) (*audienceVerificationData, error) { + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return nil, err + } + + signedData := append(signStaticCtx, challenge...) + signedData = append(signedData, []byte(audience)...) + signedHash := sha256.Sum256(signedData) + signature, err := audienceKey.Sign(rand.Reader, signedHash[:], crypto.SHA256) + if err != nil { + return nil, err + } + + return &audienceVerificationData{ + Audience: biscuit.Symbol(audience), + Challenge: challenge, + Signature: signature, + }, nil +} + +func verifyAudienceSignature(audiencePubkey *ecdsa.PublicKey, data *audienceVerificationData) error { + signedData := append(signStaticCtx, data.Challenge...) + signedData = append(signedData, []byte(data.Audience)...) + hash := sha256.Sum256(signedData) + + if !ecdsa.VerifyASN1(audiencePubkey, hash[:], data.Signature) { + return errors.New("invalid signature") + } + return nil +} + +func validatePKIXP256PublicKey(pubkey []byte) error { + key, err := x509.ParsePKIXPublicKey(pubkey) + if err != nil { + return fmt.Errorf("failed to parse PKIX, ASN.1 DER public key: %v", err) + } + + ecKey, ok := key.(*ecdsa.PublicKey) + if !ok { + return errors.New("public key is not an *ecdsa.PublicKey") + } + + if ecKey.Curve != elliptic.P256() { + return fmt.Errorf("publickey is on wrong curve, expected P256") + } + + return nil +} diff --git a/cookbook/signedbiscuit/signature_test.go b/cookbook/signedbiscuit/signature_test.go new file mode 100644 index 0000000..b9ac490 --- /dev/null +++ b/cookbook/signedbiscuit/signature_test.go @@ -0,0 +1,230 @@ +package signedbiscuit + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "testing" + "time" + + "github.com/flynn/biscuit-go" + "github.com/stretchr/testify/require" +) + +func TestUserSignVerify(t *testing.T) { + tokenHash := make([]byte, 32) + _, err := rand.Read(tokenHash) + require.NoError(t, err) + + challenge := make([]byte, challengeSize) + _, err = rand.Read(challenge) + require.NoError(t, err) + + userKey := generateUserKeyPair(t) + + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + } + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + require.NotEmpty(t, signedData.Signature) + require.Equal(t, biscuit.Integer(1), signedData.DataID) + require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) + + require.Len(t, signedData.Nonce, nonceSize) + zeroNonce := make([]byte, nonceSize) + require.NotEqual(t, biscuit.Bytes(zeroNonce), signedData.Nonce) + + require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) + + require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ + DataID: toSignData.DataID, + Alg: toSignData.Alg, + Data: toSignData.Data, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + UserPubKey: signedData.UserPubKey, + })) +} + +func TestUserSignFail(t *testing.T) { + validTokenHash := make([]byte, 32) + _, err := rand.Read(validTokenHash) + require.NoError(t, err) + + validChallenge := make([]byte, challengeSize) + _, err = rand.Read(validChallenge) + require.NoError(t, err) + + invalidPrivateKey := &UserKeyPair{ + Private: make([]byte, 32), + } + + testCases := []struct { + desc string + tokenHash []byte + userKey *UserKeyPair + data *userToSignData + expectedErr error + }{ + { + desc: "empty tokenHash", + tokenHash: []byte{}, + }, + { + desc: "unsupported alg", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: "unsupported", + }, + expectedErr: ErrUnsupportedSignatureAlg, + }, + { + desc: "wrong private key encoding", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + }, + userKey: invalidPrivateKey, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + _, err := userSign(testCase.tokenHash, testCase.userKey, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func TestVerifyUserSignatureFail(t *testing.T) { + tokenHash := []byte("token hash") + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + } + + userKey := generateUserKeyPair(t) + invalidKey := generateUserKeyPair(t) + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(t, err) + wrongKeyKind, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey) + require.NoError(t, err) + + testCases := []struct { + desc string + tokenHash []byte + data *userVerificationData + expectedErr error + }{ + { + desc: "unsupported alg", + expectedErr: ErrUnsupportedSignatureAlg, + data: &userVerificationData{ + Alg: "unknown", + }, + }, + { + desc: "invalid pubkey encoding", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: make([]byte, 32), + }, + }, + { + desc: "invalid pubkey kind", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: wrongKeyKind, + }, + }, + { + desc: "wrong pubkey", + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: invalidKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + }, + }, + { + desc: "tampered token hash", + expectedErr: ErrInvalidSignature, + tokenHash: []byte("wrong"), + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + }, + }, + { + desc: "tampered nonce", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: []byte("another nonce"), + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + }, + }, + { + desc: "tampered timestamp", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + err := verifyUserSignature(testCase.tokenHash, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func generateUserKeyPair(t *testing.T) *UserKeyPair { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + kp, err := NewECDSAKeyPair(priv) + require.NoError(t, err) + return kp +} diff --git a/cookbook/signedbiscuit/wrapper.go b/cookbook/signedbiscuit/wrapper.go new file mode 100644 index 0000000..bcead2c --- /dev/null +++ b/cookbook/signedbiscuit/wrapper.go @@ -0,0 +1,426 @@ +package signedbiscuit + +import ( + "bytes" + "crypto" + "crypto/rand" + "errors" + "fmt" + "time" + + "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/datalog" +) + +var ( + ErrAlreadySigned = errors.New("already signed") + ErrInvalidToSignDataPrefix = errors.New("invalid to_sign data prefix") +) + +var ( + signStaticCtx = []byte("biscuit-pop-v0") + challengeSize = 16 + nonceSize = 16 +) + +type hubauthBuilder struct { + biscuit.Builder +} + +// withUserToSignFact add an authority should_sign fact and associated data to the biscuit +// with an authority caveat requiring the verifier to provide a valid_signature fact. +// the verifier is responsible of ensuring that a valid signature exists over the data. +func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { + dataID := biscuit.Integer(0) + + if err := validatePKIXP256PublicKey(userPubkey); err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "should_sign", + IDs: []biscuit.Atom{ + dataID, + biscuit.Symbol(ECDSA_P256_SHA256), + biscuit.Bytes(userPubkey), + }, + }}); err != nil { + return err + } + + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "data", + IDs: []biscuit.Atom{ + dataID, + biscuit.Bytes(append(signStaticCtx, challenge...)), + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +// withAudienceSignature add an authority audience_signature fact, containing a challenge and +// a matching signature using the audience key. +// the verifier is responsible of providing a valid_audience_signature fact, after +// verifying the signature using the audience pubkey. +func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey crypto.Signer) error { + if len(audience) == 0 { + return errors.New("audience is required") + } + + data, err := audienceSign(audience, audienceKey) + if err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "audience_signature", + IDs: []biscuit.Atom{ + data.Audience, + data.Challenge, + data.Signature, + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +func (b *hubauthBuilder) withMetadata(m *Metadata) error { + return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.String(m.ClientID), + biscuit.String(m.UserID), + biscuit.String(m.UserEmail), + biscuit.Date(m.IssueTime), + }, + }}) +} + +func (b *hubauthBuilder) withExpire(exp time.Time) error { + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, + }, + Constraints: []biscuit.Constraint{{ + Name: biscuit.Variable(0), + Checker: biscuit.DateComparisonChecker{ + Comparison: datalog.DateComparisonBefore, + Date: biscuit.Date(exp), + }, + }}, + }); err != nil { + return err + } + + return nil +} + +type hubauthBlockBuilder struct { + biscuit.BlockBuilder +} + +func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { + return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "signature", + IDs: []biscuit.Atom{ + sigData.DataID, + sigData.UserPubKey, + sigData.Signature, + sigData.Nonce, + sigData.Timestamp, + }, + }}) +} + +type hubauthVerifier struct { + biscuit.Verifier +} + +func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { + toSign, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_sign", + IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, + }, + Body: []biscuit.Predicate{ + { + Name: "should_sign", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(1), + biscuit.Bytes(userPubKey), + }, + }, { + Name: "data", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(2), + }, + }, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toSign), 1; g != w { + return nil, fmt.Errorf("invalid to_sign fact count, got %d, want %d", g, w) + } + + toSignFact := toSign[0] + if g, w := len(toSignFact.IDs), 3; g != w { + return nil, fmt.Errorf("invalid to_sign fact, got %d atoms, want %d", g, w) + } + + sigData := &userToSignData{} + var ok bool + sigData.DataID, ok = toSign[0].IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_sign atom: dataID") + } + sigData.Alg, ok = toSign[0].IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_sign atom: alg") + } + sigData.Data, ok = toSign[0].IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_sign atom: data") + } + + if !bytes.HasPrefix(sigData.Data, signStaticCtx) { + return nil, ErrInvalidToSignDataPrefix + } + + return sigData, nil +} + +func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { + alreadySigned, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, + }, + }) + if err != nil { + return err + } + if len(alreadySigned) != 0 { + return ErrAlreadySigned + } + + return nil +} + +func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // dataID + biscuit.Variable(1), // alg + biscuit.Variable(2), // pubkey + biscuit.Variable(3), // data + biscuit.Variable(4), // signature + biscuit.Variable(5), // signerNonce + biscuit.Variable(6), // signerTimestamp + }}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, + {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 7; g != w { + return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) + } + + toVerify := &userVerificationData{} + var ok bool + toVerify.DataID, ok = toValidateFact.IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_validate atom: dataID") + } + toVerify.Alg, ok = toValidateFact.IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_validate atom: alg") + } + toVerify.UserPubKey, ok = toValidateFact.IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: userPubKey") + } + toVerify.Data, ok = toValidateFact.IDs[3].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: data") + } + toVerify.Signature, ok = toValidateFact.IDs[4].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: signature") + } + toVerify.Nonce, ok = toValidateFact.IDs[5].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: nonce") + } + toVerify.Timestamp, ok = toValidateFact.IDs[6].(biscuit.Date) + if !ok { + return nil, errors.New("invalid to_validate atom: timestamp") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, + }}) + + return nil +} + +func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "audience_to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // challenge + biscuit.Variable(1), // signature + }}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 2; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact atom count, got %d, want %d", g, w) + } + + toVerify := &audienceVerificationData{Audience: biscuit.Symbol(audience)} + var ok bool + toVerify.Challenge, ok = toValidateFact.IDs[0].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: challenge") + } + toVerify.Signature, ok = toValidateFact.IDs[1].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: signature") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) getMetadata() (*Metadata, error) { + metaFacts, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // clientID + biscuit.Variable(1), // userID + biscuit.Variable(2), // userEmail + biscuit.Variable(3), // issueTime + }}, + Body: []biscuit.Predicate{ + {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(metaFacts), 1; g != w { + return nil, fmt.Errorf("invalid metadata fact count, got %d, want %d", g, w) + } + + metaFact := metaFacts[0] + + clientID, ok := metaFact.IDs[0].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: clientID") + } + userID, ok := metaFact.IDs[1].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userID") + } + userEmail, ok := metaFact.IDs[2].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userEmail") + } + issueTime, ok := metaFact.IDs[3].(biscuit.Date) + if !ok { + return nil, errors.New("invalid metadata atom: issueTime") + } + return &Metadata{ + ClientID: string(clientID), + UserID: string(userID), + UserEmail: string(userEmail), + IssueTime: time.Time(issueTime), + }, nil +} + +func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_audience_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, + }}) + + return nil +} + +func (v *hubauthVerifier) withCurrentTime(t time.Time) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "current_time", + IDs: []biscuit.Atom{ + biscuit.Symbol("ambient"), + biscuit.Date(t), + }, + }}) + + return nil +} From ba54acd8b743d7d0c5da233ddebdae683ab22af1 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 5 Nov 2020 11:29:37 +0100 Subject: [PATCH 2/7] Improve biscuit signature cookbook interface Signature cookbook now exposes more generic endpoints, allowing to integrate more easily in real use cases context. The endpoints now works on biscuit.Builder or biscuit.Verifier instead of raw biscuits, so the signature flow can be chained with others Verifier now exposes a SHA256Sum proxy to the biscuit one to avoid an extra biscuit parameter requirement on signature verifcation. Verifier PrintWorld now run the world on call to ease debugging. This ensure rules have generated their facts before printing. --- cookbook/signedbiscuit/biscuit.go | 75 +++++++++++++------------- cookbook/signedbiscuit/biscuit_test.go | 29 ++++++++-- verifier.go | 7 +++ 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/cookbook/signedbiscuit/biscuit.go b/cookbook/signedbiscuit/biscuit.go index 94907d2..59e0fde 100644 --- a/cookbook/signedbiscuit/biscuit.go +++ b/cookbook/signedbiscuit/biscuit.go @@ -39,11 +39,11 @@ func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) { }, nil } -// GenerateSignable returns a biscuit which will only verify after being +// WithSignableFacts 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) { +func WithSignableFacts(b biscuit.Builder, audience string, audienceKey crypto.Signer, userPublicKey []byte, expireTime time.Time, m *Metadata) (biscuit.Builder, error) { builder := &hubauthBuilder{ - biscuit.NewBuilder(rand.Reader, rootKey), + Builder: b, } if err := builder.withAudienceSignature(audience, audienceKey); err != nil { @@ -62,12 +62,7 @@ func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey crypto.S return nil, err } - b, err := builder.Build() - if err != nil { - return nil, err - } - - return b.Serialize() + return builder.Builder, nil } // Sign append a user signature on the given token and return it. @@ -123,70 +118,72 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, return b.Serialize() } -type VerifiedMetadata struct { +type UserSignatureMetadata 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) +// 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 := &hubauthVerifier{ + Verifier: v, } - verifier := &hubauthVerifier{v} audienceVerificationData, err := verifier.getAudienceVerificationData(audience) if err != nil { - return nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) + return nil, 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) + return nil, 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) + return nil, 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) + return nil, nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) + } + + signatureBlockID, err := v.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) } - // 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) + signedTokenHash, err := v.SHA256Sum(signatureBlockID - 1) if err != nil { - return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) + return nil, 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) + return nil, 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) + return nil, 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) + return nil, nil, fmt.Errorf("biscuit: failed to add current time: %w", err) } metas, err := verifier.getMetadata() if err != nil { - return nil, fmt.Errorf("biscuit: failed to get metadata: %v", err) + return nil, nil, fmt.Errorf("biscuit: failed to get metadata: %v", err) } - return &VerifiedMetadata{ + return v, &UserSignatureMetadata{ Metadata: metas, UserSignatureNonce: userVerificationData.Nonce, UserSignatureTimestamp: time.Time(userVerificationData.Timestamp), diff --git a/cookbook/signedbiscuit/biscuit_test.go b/cookbook/signedbiscuit/biscuit_test.go index 3a6f37c..b75af16 100644 --- a/cookbook/signedbiscuit/biscuit_test.go +++ b/cookbook/signedbiscuit/biscuit_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/flynn/biscuit-go" "github.com/flynn/biscuit-go/sig" "github.com/stretchr/testify/require" ) @@ -25,7 +26,15 @@ func TestBiscuit(t *testing.T) { UserID: "1234", IssueTime: time.Now(), } - signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) + + builder := biscuit.NewBuilder(rand.Reader, 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)) @@ -34,7 +43,14 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) t.Logf("signed biscuit size: %d", len(signedBiscuit)) - res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey.Public().(*ecdsa.PublicKey)) + 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) @@ -53,13 +69,18 @@ func TestBiscuit(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)) + 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 = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey.Public().(*ecdsa.PublicKey)) + _, _, err = WithSignatureVerification(verifier, audience, wrongAudienceKey.Public().(*ecdsa.PublicKey)) require.Error(t, err) }) } diff --git a/verifier.go b/verifier.go index a4ce531..1480782 100644 --- a/verifier.go +++ b/verifier.go @@ -143,11 +143,18 @@ func (v *verifier) Biscuit() *Biscuit { return v.biscuit } +// SHA256Sum proxy to the SHA256Sum of the underlying verified biscuit +func (v *verifier) SHA256Sum(count int) ([]byte, error) { + return v.biscuit.SHA256Sum(count) +} + func (v *verifier) PrintWorld() string { debug := datalog.SymbolDebugger{ SymbolTable: v.symbols, } + v.world.Run() // ensure rules have generated their facts + return debug.World(v.world) } From a669ae2026a85187607db37b50a52e20c18d3355 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Fri, 20 Nov 2020 11:15:56 +0100 Subject: [PATCH 3/7] fix AddAuthorityCaveat after rebase --- cookbook/signedbiscuit/wrapper.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cookbook/signedbiscuit/wrapper.go b/cookbook/signedbiscuit/wrapper.go index bcead2c..e59cb84 100644 --- a/cookbook/signedbiscuit/wrapper.go +++ b/cookbook/signedbiscuit/wrapper.go @@ -63,13 +63,13 @@ func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { return err } - if err := b.AddAuthorityCaveat(biscuit.Rule{ + if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, Body: []biscuit.Predicate{ {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, }, - }); err != nil { + }}}); err != nil { return err } @@ -101,13 +101,13 @@ func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey cryp return err } - if err := b.AddAuthorityCaveat(biscuit.Rule{ + if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, Body: []biscuit.Predicate{ {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, }, - }); err != nil { + }}}); err != nil { return err } @@ -127,7 +127,7 @@ func (b *hubauthBuilder) withMetadata(m *Metadata) error { } func (b *hubauthBuilder) withExpire(exp time.Time) error { - if err := b.AddAuthorityCaveat(biscuit.Rule{ + if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, Body: []biscuit.Predicate{ {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, @@ -139,7 +139,7 @@ func (b *hubauthBuilder) withExpire(exp time.Time) error { Date: biscuit.Date(exp), }, }}, - }); err != nil { + }}}); err != nil { return err } From 2f6bf71756adb9fffc1702daa9913ccf405715ef Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Tue, 24 Nov 2020 10:28:58 +0100 Subject: [PATCH 4/7] fix after master rebase --- cookbook/signedbiscuit/biscuit.go | 4 ++-- cookbook/signedbiscuit/biscuit_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cookbook/signedbiscuit/biscuit.go b/cookbook/signedbiscuit/biscuit.go index 59e0fde..52eff19 100644 --- a/cookbook/signedbiscuit/biscuit.go +++ b/cookbook/signedbiscuit/biscuit.go @@ -149,7 +149,7 @@ func WithSignatureVerification(v biscuit.Verifier, audience string, audienceKey return nil, nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) } - signatureBlockID, err := v.GetBlockID(biscuit.Fact{Predicate: biscuit.Predicate{ + signatureBlockID, err := v.Biscuit().GetBlockID(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "signature", IDs: []biscuit.Atom{ userVerificationData.DataID, @@ -163,7 +163,7 @@ func WithSignatureVerification(v biscuit.Verifier, audience string, audienceKey return nil, nil, fmt.Errorf("biscuit: failed to retrieve signature blockID: %w", err) } - signedTokenHash, err := v.SHA256Sum(signatureBlockID - 1) + signedTokenHash, err := v.Biscuit().SHA256Sum(signatureBlockID - 1) if err != nil { return nil, nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) } diff --git a/cookbook/signedbiscuit/biscuit_test.go b/cookbook/signedbiscuit/biscuit_test.go index b75af16..917378e 100644 --- a/cookbook/signedbiscuit/biscuit_test.go +++ b/cookbook/signedbiscuit/biscuit_test.go @@ -27,7 +27,7 @@ func TestBiscuit(t *testing.T) { IssueTime: time.Now(), } - builder := biscuit.NewBuilder(rand.Reader, rootKey) + builder := biscuit.NewBuilder(rootKey) builder, err = WithSignableFacts(builder, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) require.NoError(t, err) From 036e25ba275345809b278efe22ab64b3d85cfd48 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Fri, 4 Dec 2020 17:43:05 +0100 Subject: [PATCH 5/7] fix signature cookbook to use named variables --- cookbook/signedbiscuit/wrapper.go | 119 +++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/cookbook/signedbiscuit/wrapper.go b/cookbook/signedbiscuit/wrapper.go index e59cb84..c4e39cd 100644 --- a/cookbook/signedbiscuit/wrapper.go +++ b/cookbook/signedbiscuit/wrapper.go @@ -64,10 +64,20 @@ func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { } if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ - Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable("dataID")}}, Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "should_sign", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("dataID"), + biscuit.Variable("alg"), + biscuit.Variable("pubkey"), + }}, + {Name: "valid_signature", IDs: []biscuit.Atom{ + biscuit.Symbol("ambient"), + biscuit.Variable("dataID"), + biscuit.Variable("alg"), + biscuit.Variable("pubkey"), + }}, }, }}}); err != nil { return err @@ -102,10 +112,19 @@ func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey cryp } if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ - Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable("audience")}}, Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, + {Name: "audience_signature", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("audience"), + biscuit.Variable("challenge"), + biscuit.Variable("signature"), + }}, + {Name: "valid_audience_signature", IDs: []biscuit.Atom{ + biscuit.Symbol("ambient"), + biscuit.Variable("audience"), + biscuit.Variable("signature"), + }}, }, }}}); err != nil { return err @@ -128,12 +147,12 @@ func (b *hubauthBuilder) withMetadata(m *Metadata) error { func (b *hubauthBuilder) withExpire(exp time.Time) error { if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ - Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable("now")}}, Body: []biscuit.Predicate{ - {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, + {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable("now")}}, }, Constraints: []biscuit.Constraint{{ - Name: biscuit.Variable(0), + Name: biscuit.Variable("now"), Checker: biscuit.DateComparisonChecker{ Comparison: datalog.DateComparisonBefore, Date: biscuit.Date(exp), @@ -171,21 +190,21 @@ func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSi toSign, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "to_sign", - IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, + IDs: []biscuit.Atom{biscuit.Variable("dataID"), biscuit.Variable("alg"), biscuit.Variable("pubkey")}, }, Body: []biscuit.Predicate{ { Name: "should_sign", IDs: []biscuit.Atom{ biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(1), + biscuit.Variable("dataID"), + biscuit.Variable("alg"), biscuit.Bytes(userPubKey), }, }, { Name: "data", IDs: []biscuit.Atom{ biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(2), + biscuit.Variable("dataID"), + biscuit.Variable("pubkey"), }, }, }, @@ -227,9 +246,15 @@ func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSi func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { alreadySigned, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable("signerTimestamp")}}, Body: []biscuit.Predicate{ - {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, + {Name: "signature", IDs: []biscuit.Atom{ + dataID, + userPubKey, + biscuit.Variable("signature"), + biscuit.Variable("signerNonce"), + biscuit.Variable("signerTimestamp"), + }}, }, }) if err != nil { @@ -247,18 +272,33 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro Head: biscuit.Predicate{ Name: "to_validate", IDs: []biscuit.Atom{ - biscuit.Variable(0), // dataID - biscuit.Variable(1), // alg - biscuit.Variable(2), // pubkey - biscuit.Variable(3), // data - biscuit.Variable(4), // signature - biscuit.Variable(5), // signerNonce - biscuit.Variable(6), // signerTimestamp + biscuit.Variable("dataID"), + biscuit.Variable("alg"), + biscuit.Variable("pubkey"), + biscuit.Variable("data"), + biscuit.Variable("signature"), + biscuit.Variable("signerNonce"), + biscuit.Variable("signerTimestamp"), }}, Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, - {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, + {Name: "should_sign", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("dataID"), + biscuit.Variable("alg"), + biscuit.Variable("pubkey"), + }}, + {Name: "data", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("dataID"), + biscuit.Variable("data"), + }}, + {Name: "signature", IDs: []biscuit.Atom{ + biscuit.Variable("dataID"), + biscuit.Variable("pubkey"), + biscuit.Variable("signature"), + biscuit.Variable("signerNonce"), + biscuit.Variable("signerTimestamp"), + }}, }, }) if err != nil { @@ -322,11 +362,16 @@ func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienc Head: biscuit.Predicate{ Name: "audience_to_validate", IDs: []biscuit.Atom{ - biscuit.Variable(0), // challenge - biscuit.Variable(1), // signature + biscuit.Variable("challenge"), + biscuit.Variable("signature"), }}, Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, + {Name: "audience_signature", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Symbol(audience), + biscuit.Variable("challenge"), + biscuit.Variable("signature"), + }}, }, }) if err != nil { @@ -361,13 +406,19 @@ func (v *hubauthVerifier) getMetadata() (*Metadata, error) { Head: biscuit.Predicate{ Name: "metadata", IDs: []biscuit.Atom{ - biscuit.Variable(0), // clientID - biscuit.Variable(1), // userID - biscuit.Variable(2), // userEmail - biscuit.Variable(3), // issueTime + biscuit.Variable("clientID"), + biscuit.Variable("userID"), + biscuit.Variable("userEmail"), + biscuit.Variable("issueTime"), }}, Body: []biscuit.Predicate{ - {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, + {Name: "metadata", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("clientID"), + biscuit.Variable("userID"), + biscuit.Variable("userEmail"), + biscuit.Variable("issueTime"), + }}, }, }) if err != nil { From f2e50c1a45f477e75219db6ee21352ae7d132723 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Fri, 11 Dec 2020 14:26:37 +0100 Subject: [PATCH 6/7] rename signedBiscuit helpers --- cookbook/signedbiscuit/biscuit.go | 17 ++++++++-------- cookbook/signedbiscuit/wrapper.go | 32 +++++++++++++++---------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/cookbook/signedbiscuit/biscuit.go b/cookbook/signedbiscuit/biscuit.go index 52eff19..3f0c508 100644 --- a/cookbook/signedbiscuit/biscuit.go +++ b/cookbook/signedbiscuit/biscuit.go @@ -13,10 +13,11 @@ import ( ) type Metadata struct { - ClientID string - UserID string - UserEmail string - IssueTime time.Time + ClientID string + UserID string + UserEmail string + UserGroups []string + IssueTime time.Time } type UserKeyPair struct { @@ -42,7 +43,7 @@ func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) { // 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 := &hubauthBuilder{ + builder := &signedBiscuitBuilder{ Builder: b, } @@ -79,7 +80,7 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, if err != nil { return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } - verifier := &hubauthVerifier{ + verifier := &signedBiscuitVerifier{ Verifier: v, } @@ -102,7 +103,7 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, return nil, fmt.Errorf("biscuit: signature failed: %w", err) } - builder := &hubauthBlockBuilder{ + builder := &signedBiscuitBlockBuilder{ BlockBuilder: b.CreateBlock(), } if err := builder.withUserSignature(signData); err != nil { @@ -128,7 +129,7 @@ type UserSignatureMetadata struct { // 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 := &hubauthVerifier{ + verifier := &signedBiscuitVerifier{ Verifier: v, } diff --git a/cookbook/signedbiscuit/wrapper.go b/cookbook/signedbiscuit/wrapper.go index c4e39cd..127a8a6 100644 --- a/cookbook/signedbiscuit/wrapper.go +++ b/cookbook/signedbiscuit/wrapper.go @@ -23,14 +23,14 @@ var ( nonceSize = 16 ) -type hubauthBuilder struct { +type signedBiscuitBuilder struct { biscuit.Builder } // withUserToSignFact add an authority should_sign fact and associated data to the biscuit // with an authority caveat requiring the verifier to provide a valid_signature fact. // the verifier is responsible of ensuring that a valid signature exists over the data. -func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { +func (b *signedBiscuitBuilder) withUserToSignFact(userPubkey []byte) error { dataID := biscuit.Integer(0) if err := validatePKIXP256PublicKey(userPubkey); err != nil { @@ -90,7 +90,7 @@ func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { // a matching signature using the audience key. // the verifier is responsible of providing a valid_audience_signature fact, after // verifying the signature using the audience pubkey. -func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey crypto.Signer) error { +func (b *signedBiscuitBuilder) withAudienceSignature(audience string, audienceKey crypto.Signer) error { if len(audience) == 0 { return errors.New("audience is required") } @@ -133,7 +133,7 @@ func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey cryp return nil } -func (b *hubauthBuilder) withMetadata(m *Metadata) error { +func (b *signedBiscuitBuilder) withMetadata(m *Metadata) error { return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "metadata", IDs: []biscuit.Atom{ @@ -145,7 +145,7 @@ func (b *hubauthBuilder) withMetadata(m *Metadata) error { }}) } -func (b *hubauthBuilder) withExpire(exp time.Time) error { +func (b *signedBiscuitBuilder) withExpire(exp time.Time) error { if err := b.AddAuthorityCaveat(biscuit.Caveat{Queries: []biscuit.Rule{{ Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable("now")}}, Body: []biscuit.Predicate{ @@ -165,11 +165,11 @@ func (b *hubauthBuilder) withExpire(exp time.Time) error { return nil } -type hubauthBlockBuilder struct { +type signedBiscuitBlockBuilder struct { biscuit.BlockBuilder } -func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { +func (b *signedBiscuitBlockBuilder) withUserSignature(sigData *userSignatureData) error { return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "signature", IDs: []biscuit.Atom{ @@ -182,11 +182,11 @@ func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) erro }}) } -type hubauthVerifier struct { +type signedBiscuitVerifier struct { biscuit.Verifier } -func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { +func (v *signedBiscuitVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { toSign, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "to_sign", @@ -244,7 +244,7 @@ func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSi return sigData, nil } -func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { +func (v *signedBiscuitVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { alreadySigned, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable("signerTimestamp")}}, Body: []biscuit.Predicate{ @@ -267,7 +267,7 @@ func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, use return nil } -func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { +func (v *signedBiscuitVerifier) getUserVerificationData() (*userVerificationData, error) { toValidate, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "to_validate", @@ -348,7 +348,7 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro return toVerify, nil } -func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { +func (v *signedBiscuitVerifier) withValidatedUserSignature(data *userVerificationData) error { v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, @@ -357,7 +357,7 @@ func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) return nil } -func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { +func (v *signedBiscuitVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { toValidate, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "audience_to_validate", @@ -401,7 +401,7 @@ func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienc return toVerify, nil } -func (v *hubauthVerifier) getMetadata() (*Metadata, error) { +func (v *signedBiscuitVerifier) getMetadata() (*Metadata, error) { metaFacts, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "metadata", @@ -455,7 +455,7 @@ func (v *hubauthVerifier) getMetadata() (*Metadata, error) { }, nil } -func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { +func (v *signedBiscuitVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, @@ -464,7 +464,7 @@ func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificat return nil } -func (v *hubauthVerifier) withCurrentTime(t time.Time) error { +func (v *signedBiscuitVerifier) withCurrentTime(t time.Time) error { v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "current_time", IDs: []biscuit.Atom{ From 6b93dcf10af3009355f43526a751f4ec8fa2edc0 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Fri, 11 Dec 2020 14:50:00 +0100 Subject: [PATCH 7/7] add user groups as signed biscuit metadata --- cookbook/signedbiscuit/biscuit_test.go | 10 +++++--- cookbook/signedbiscuit/wrapper.go | 34 ++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cookbook/signedbiscuit/biscuit_test.go b/cookbook/signedbiscuit/biscuit_test.go index 917378e..fbf51d5 100644 --- a/cookbook/signedbiscuit/biscuit_test.go +++ b/cookbook/signedbiscuit/biscuit_test.go @@ -21,10 +21,11 @@ func TestBiscuit(t *testing.T) { userKey := generateUserKeyPair(t) metas := &Metadata{ - ClientID: "abcd", - UserEmail: "1234@example.com", - UserID: "1234", - IssueTime: time.Now(), + ClientID: "abcd", + UserEmail: "1234@example.com", + UserID: "1234", + UserGroups: []string{"grp1", "grp2"}, + IssueTime: time.Now(), } builder := biscuit.NewBuilder(rootKey) @@ -55,6 +56,7 @@ func TestBiscuit(t *testing.T) { 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) diff --git a/cookbook/signedbiscuit/wrapper.go b/cookbook/signedbiscuit/wrapper.go index 127a8a6..31302fa 100644 --- a/cookbook/signedbiscuit/wrapper.go +++ b/cookbook/signedbiscuit/wrapper.go @@ -134,12 +134,19 @@ func (b *signedBiscuitBuilder) withAudienceSignature(audience string, audienceKe } func (b *signedBiscuitBuilder) withMetadata(m *Metadata) error { + + userGroups := make(biscuit.Set, 0, len(m.UserGroups)) + for _, g := range m.UserGroups { + userGroups = append(userGroups, biscuit.String(g)) + } + return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "metadata", IDs: []biscuit.Atom{ biscuit.String(m.ClientID), biscuit.String(m.UserID), biscuit.String(m.UserEmail), + userGroups, biscuit.Date(m.IssueTime), }, }}) @@ -409,6 +416,7 @@ func (v *signedBiscuitVerifier) getMetadata() (*Metadata, error) { biscuit.Variable("clientID"), biscuit.Variable("userID"), biscuit.Variable("userEmail"), + biscuit.Variable("userGroups"), biscuit.Variable("issueTime"), }}, Body: []biscuit.Predicate{ @@ -417,6 +425,7 @@ func (v *signedBiscuitVerifier) getMetadata() (*Metadata, error) { biscuit.Variable("clientID"), biscuit.Variable("userID"), biscuit.Variable("userEmail"), + biscuit.Variable("userGroups"), biscuit.Variable("issueTime"), }}, }, @@ -443,15 +452,30 @@ func (v *signedBiscuitVerifier) getMetadata() (*Metadata, error) { if !ok { return nil, errors.New("invalid metadata atom: userEmail") } - issueTime, ok := metaFact.IDs[3].(biscuit.Date) + userGroups, ok := metaFact.IDs[3].(biscuit.Set) + if !ok { + return nil, errors.New("invalid metadata atom: userGroups") + } + issueTime, ok := metaFact.IDs[4].(biscuit.Date) if !ok { return nil, errors.New("invalid metadata atom: issueTime") } + + groups := make([]string, 0, len(userGroups)) + for _, g := range userGroups { + grpStr, ok := g.(biscuit.String) + if !ok { + return nil, fmt.Errorf("invalid set atom: got %T, want biscuit.String", g) + } + groups = append(groups, string(grpStr)) + } + return &Metadata{ - ClientID: string(clientID), - UserID: string(userID), - UserEmail: string(userEmail), - IssueTime: time.Time(issueTime), + ClientID: string(clientID), + UserID: string(userID), + UserEmail: string(userEmail), + UserGroups: groups, + IssueTime: time.Time(issueTime), }, nil }