diff --git a/cookbook/signedbiscuit/biscuit.go b/cookbook/signedbiscuit/biscuit.go new file mode 100644 index 0000000..3f0c508 --- /dev/null +++ b/cookbook/signedbiscuit/biscuit.go @@ -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 +} diff --git a/cookbook/signedbiscuit/biscuit_test.go b/cookbook/signedbiscuit/biscuit_test.go new file mode 100644 index 0000000..fbf51d5 --- /dev/null +++ b/cookbook/signedbiscuit/biscuit_test.go @@ -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) + }) +} 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..31302fa --- /dev/null +++ b/cookbook/signedbiscuit/wrapper.go @@ -0,0 +1,501 @@ +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 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 *signedBiscuitBuilder) 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.Caveat{Queries: []biscuit.Rule{{ + Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable("dataID")}}, + Body: []biscuit.Predicate{ + {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 + } + + 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 *signedBiscuitBuilder) 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.Caveat{Queries: []biscuit.Rule{{ + 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("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 + } + + return nil +} + +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), + }, + }}) +} + +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{ + {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable("now")}}, + }, + Constraints: []biscuit.Constraint{{ + Name: biscuit.Variable("now"), + Checker: biscuit.DateComparisonChecker{ + Comparison: datalog.DateComparisonBefore, + Date: biscuit.Date(exp), + }, + }}, + }}}); err != nil { + return err + } + + return nil +} + +type signedBiscuitBlockBuilder struct { + biscuit.BlockBuilder +} + +func (b *signedBiscuitBlockBuilder) 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 signedBiscuitVerifier struct { + biscuit.Verifier +} + +func (v *signedBiscuitVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { + toSign, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_sign", + 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("dataID"), + biscuit.Variable("alg"), + biscuit.Bytes(userPubKey), + }, + }, { + Name: "data", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("dataID"), + biscuit.Variable("pubkey"), + }, + }, + }, + }) + 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 *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{ + {Name: "signature", IDs: []biscuit.Atom{ + dataID, + userPubKey, + biscuit.Variable("signature"), + biscuit.Variable("signerNonce"), + biscuit.Variable("signerTimestamp"), + }}, + }, + }) + if err != nil { + return err + } + if len(alreadySigned) != 0 { + return ErrAlreadySigned + } + + return nil +} + +func (v *signedBiscuitVerifier) getUserVerificationData() (*userVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_validate", + IDs: []biscuit.Atom{ + 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("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 { + 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 *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}, + }}) + + return nil +} + +func (v *signedBiscuitVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "audience_to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable("challenge"), + biscuit.Variable("signature"), + }}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Symbol(audience), + biscuit.Variable("challenge"), + biscuit.Variable("signature"), + }}, + }, + }) + 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 *signedBiscuitVerifier) getMetadata() (*Metadata, error) { + metaFacts, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.Variable("clientID"), + biscuit.Variable("userID"), + biscuit.Variable("userEmail"), + biscuit.Variable("userGroups"), + biscuit.Variable("issueTime"), + }}, + Body: []biscuit.Predicate{ + {Name: "metadata", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable("clientID"), + biscuit.Variable("userID"), + biscuit.Variable("userEmail"), + biscuit.Variable("userGroups"), + biscuit.Variable("issueTime"), + }}, + }, + }) + 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") + } + 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), + UserGroups: groups, + IssueTime: time.Time(issueTime), + }, nil +} + +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}, + }}) + + return nil +} + +func (v *signedBiscuitVerifier) 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 +} 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) }