Commit 0a0cb766 authored by Loïck Bonniot's avatar Loïck Bonniot

[p] Handle case in mails

parent 9cba231d
Pipeline #2193 passed with stage
......@@ -5,13 +5,11 @@ import (
"regexp"
"time"
"github.com/spf13/viper"
"dfss/dfssc/common"
"dfss/dfssc/security"
pb "dfss/dfssp/api"
"dfss/net"
"github.com/spf13/viper"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
......@@ -27,6 +25,8 @@ type RegisterManager struct {
bits int
}
var mailRegex = regexp.MustCompile(`.+@.+\..+`)
// NewRegisterManager return a new Register Manager to register a user
func NewRegisterManager(passphrase, country, organization, unit, mail string, bits int, v *viper.Viper) (*RegisterManager, error) {
m := &RegisterManager{v, passphrase, country, organization, unit, mail, bits}
......@@ -44,8 +44,7 @@ func NewRegisterManager(passphrase, country, organization, unit, mail string, bi
// Check the validity of the provided email, passphrase and bits
func (m *RegisterManager) checkValidParams() error {
re, _ := regexp.Compile(`.+@.+\..+`)
if b := re.MatchString(m.mail); !b {
if b := mailRegex.MatchString(m.mail); !b {
return errors.New("Provided mail is not valid")
}
......
......@@ -2,6 +2,7 @@ package contract_test // Using another package to avoid import cycles
import (
"fmt"
"log"
"os"
"path/filepath"
"testing"
......@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
viper.Set("verbose", true)
srv := server.GetServer()
go func() { _ = net.Listen("localhost:9090", srv) }()
go func() { log.Fatal(net.Listen("localhost:9090", srv)) }()
// Run
code := m.Run()
......@@ -142,7 +143,7 @@ func TestGetWaitingForUser(t *testing.T) {
c1.Ready = false
c2 := entities.NewContract()
c2.AddSigner(nil, "mail1", []byte{})
c2.AddSigner(nil, "mAil1", []byte{})
c2.AddSigner(&knownID, "mail2", []byte{0x12})
c2.Ready = false
......
......@@ -4,6 +4,7 @@ package contract
import (
"crypto/sha512"
"log"
"strings"
"time"
"dfss/dfssp/api"
......@@ -62,7 +63,6 @@ func (c *Builder) Execute() *api.ErrorCode {
// checkInput checks that a PostContractRequest is well-formed
func (c *Builder) checkInput() *api.ErrorCode {
if len(c.in.Signer) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Expecting at least one signer"}
}
......@@ -76,18 +76,23 @@ func (c *Builder) checkInput() *api.ErrorCode {
}
return nil
}
// fetchSigners fetches authenticated users for this contract from the DB
func (c *Builder) fetchSigners() error {
var users []entities.User
// Convert emails to case-tolerant emails
var conditions []bson.RegEx
for _, s := range c.in.Signer {
conditions = append(conditions, bson.RegEx{Pattern: "^" + s + "$", Options: "i"})
}
// Fetch users where email is part of the signers slice in request
// and authentication is valid
err := c.m.Get("users").FindAll(bson.M{
"expiration": bson.M{"$gt": time.Now()},
"email": bson.M{"$in": c.in.Signer},
"email": bson.M{"$in": conditions},
}, &users)
if err != nil {
return err
......@@ -96,8 +101,9 @@ func (c *Builder) fetchSigners() error {
// Locate missing users
for _, s := range c.in.Signer {
found := false
lowerEmail := strings.ToLower(s)
for _, u := range users {
if s == u.Email {
if lowerEmail == strings.ToLower(u.Email) {
found = true
break
}
......
......@@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"time"
......@@ -88,7 +89,7 @@ func TestAddContract(t *testing.T) {
errorCode, err := client.PostContract(context.Background(), &api.PostContractRequest{
Hash: defaultHash[:],
Filename: "ContractFilename",
Signer: []string{user1.Email, user2.Email},
Signer: []string{strings.ToUpper(user1.Email), user2.Email},
Comment: "ContractComment",
})
assert.Equal(t, nil, err)
......
......@@ -89,7 +89,7 @@ func (r *ContractRepository) GetWaitingForUser(email string) ([]Contract, error)
"ready": false,
"signers": bson.M{
"$elemMatch": bson.M{
"email": email,
"email": bson.M{"$regex": bson.RegEx{Pattern: "^" + email + "$", Options: "i"}},
"hash": []byte{},
}},
}, &res)
......
package user
import (
"crypto/rsa"
"crypto/x509"
"errors"
"log"
"strings"
"time"
"dfss/auth"
"dfss/dfssp/api"
"dfss/dfssp/authority"
"dfss/dfssp/contract"
"dfss/dfssp/entities"
"dfss/mgdb"
"github.com/spf13/viper"
"gopkg.in/mgo.v2/bson"
)
// Check if the authentication request was made in time
func checkTokenTimeout(user *entities.User) error {
now := time.Now().UTC()
bad := now.After(user.Registration.Add(maxRegistrationDelay))
if bad {
return errors.New("Registration request is too old, please register again")
}
return nil
}
// Gerenate the user's certificate and certificate hash according to the specified parameters
//
// This function should only be called AFTER checking the AuthRequest for validity
func generateUserCert(csr string, parent *x509.Certificate, key *rsa.PrivateKey) ([]byte, []byte, error) {
x509csr, err := auth.PEMToCertificateRequest([]byte(csr))
if err != nil {
return nil, nil, err
}
cert, err := auth.GetCertificate(viper.GetInt("validity"), auth.GenerateUID(), x509csr, parent, key)
if err != nil {
return nil, nil, err
}
c, _ := auth.PEMToCertificate(cert)
certHash := auth.GetCertificateHash(c)
return cert, certHash, nil
}
// Auth checks if the authentication request is valid, and if so,
// generate the certificate and certificate hash for the user, and
// updates the user's entry in the database
//
// If there is already an entry in the database with the same email,
// and that this entry already has a certificate and certificate hash,
// evaluates the request as invalid
//
// The user's ConnectionInfo field is NOT handled here
// This data should be gathered upon beginning the signing sequence
func Auth(pid *authority.PlatformID, manager *mgdb.MongoManager, in *api.AuthRequest) (*api.RegisteredUser, error) {
// Check the request validity
err := checkAuthRequest(in)
if err != nil {
return nil, err
}
// Find the user in the database (last created)
var user entities.User
err = manager.Get("users").Collection.Find(bson.M{
"email": bson.M{"$eq": in.Email},
}).Sort("-registration").One(&user)
if err != nil {
return nil, err
}
// If the user already has a certificate and certificate hash in the database, does nothing
if user.Certificate != "" || len(user.CertHash) != 0 {
return nil, errors.New("User is already registered")
}
// Check if the delta between now and the moment the user was created (ie the moment he sent the register request) is in bound
err = checkTokenTimeout(&user)
if err != nil {
return nil, err
}
// Check if the token is correct
if in.Token != user.RegToken {
return nil, errors.New("Token mismatch")
}
// Generate the certificates and hash
cert, certHash, err := generateUserCert(user.Csr, pid.RootCA, pid.Pkey)
if err != nil {
return nil, err
}
user.Certificate = string(cert)
user.CertHash = certHash
user.Expiration = time.Now().AddDate(0, 0, viper.GetInt("validity"))
// Updating the database
ok, err := manager.Get("users").UpdateByID(user)
if !ok {
return nil, err
}
// Update missed contracts in background
go launchMissedContracts(manager, &user)
// Returning the RegisteredUser message
return &api.RegisteredUser{ClientCert: user.Certificate}, nil
}
func launchMissedContracts(manager *mgdb.MongoManager, user *entities.User) {
repository := entities.NewContractRepository(manager.Get("contracts"))
contracts, err := repository.GetWaitingForUser(user.Email)
if err != nil {
log.Println("Cannot get missed contracts for user", user.Email+":", err)
}
lowerEmail := strings.ToLower(user.Email)
for _, c := range contracts {
c.Ready = true
for i := range c.Signers {
if strings.ToLower(c.Signers[i].Email) == lowerEmail {
c.Signers[i].Hash = user.CertHash
c.Signers[i].UserID = user.ID
}
if len(c.Signers[i].Hash) == 0 {
c.Ready = false
}
}
// Update contract in database
_, err = repository.Collection.UpdateByID(c)
if err != nil {
log.Println("Cannot update missed contract", c.ID, "for user", user.Email+":", err)
}
if c.Ready {
// Send required mails
builder := contract.NewContractBuilder(manager, nil)
builder.Contract = &c
builder.SendNewContractMail()
}
}
}
package user_test
import (
"dfss/dfssp/api"
"testing"
"time"
"dfss/dfssp/entities"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestAuthUserNotFound(t *testing.T) {
mail := "wrong@wrong.wrong"
token := "wrong"
client := clientTest(t, ValidServ)
request := &api.AuthRequest{Email: mail, Token: token}
msg, err := client.Auth(context.Background(), request)
assert.Equal(t, (*api.RegisteredUser)(nil), msg)
if err == nil {
t.Fatal("The request user should not have been found in the database")
}
}
func TestAuthTwice(t *testing.T) {
email := "email"
token := "token"
user := entities.NewUser()
user.Email = email
user.RegToken = token
user.Csr = string(csr)
user.Certificate = "foo"
user.CertHash = []byte{0xaa}
_, err = repository.Collection.Insert(*user)
if err != nil {
t.Fatal(err)
}
// User is already registered
client := clientTest(t, ValidServ)
request := &api.AuthRequest{Email: email, Token: token}
msg, err := client.Auth(context.Background(), request)
assert.Equal(t, msg, (*api.RegisteredUser)(nil))
if err == nil {
t.Fatal("The user should have been evaluated as already registered")
}
}
func TestWrongAuthRequestContext(t *testing.T) {
mail := "right@right.right"
token := "right"
user := entities.NewUser()
user.Email = mail
user.RegToken = token
user.Registration = time.Now().UTC().Add(time.Hour * -48)
_, err := repository.Collection.Insert(*user)
if err != nil {
t.Fatal(err)
}
client := clientTest(t, ValidServ)
request := &api.AuthRequest{Email: mail, Token: "foo"}
// Token timeout
msg, err := client.Auth(context.Background(), request)
assert.Equal(t, (*api.RegisteredUser)(nil), msg)
if err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
// Token mismatch
user.Registration = time.Now().UTC()
_, err = repository.Collection.UpdateByID(*user)
if err != nil {
t.Fatal(err)
}
msg, err = client.Auth(context.Background(), request)
assert.Equal(t, (*api.RegisteredUser)(nil), msg)
if err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
res := entities.User{}
err = repository.Collection.FindByID(*user, &res)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, res.Certificate, "")
assert.Equal(t, res.CertHash, []byte{})
// Invalid certificate request (none here)
request.Token = token
msg, err = client.Auth(context.Background(), request)
assert.Equal(t, (*api.RegisteredUser)(nil), msg)
if err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
err = repository.Collection.FindByID(*user, &res)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, res.Certificate, "")
assert.Equal(t, res.CertHash, []byte{})
}
......@@ -3,17 +3,14 @@ package user
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"log"
"regexp"
"time"
"dfss/auth"
"dfss/dfssp/api"
"dfss/dfssp/authority"
"dfss/dfssp/contract"
"dfss/dfssp/entities"
"dfss/dfssp/templates"
"dfss/mgdb"
......@@ -21,12 +18,21 @@ import (
"gopkg.in/mgo.v2/bson"
)
var (
mailRegex = regexp.MustCompile(`.+@.+\..+`)
maxRegistrationDelay = 24 * time.Hour
)
// Check if the registration request has usable fields
func checkRegisterRequest(in *api.RegisterRequest) *api.ErrorCode {
if len(in.Email) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid email length"}
}
if b := mailRegex.MatchString(in.Email); !b {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid mail"}
}
if len(in.Request) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid request length"}
}
......@@ -97,10 +103,14 @@ func Register(manager *mgdb.MongoManager, in *api.RegisterRequest) (*api.ErrorCo
}
token := fmt.Sprintf("%x", b)
// If there is already an entry with the same mail, do nothing
// If there is already an entry with the same mail (case-insensitive), do nothing.
var res []entities.User
err = manager.Get("users").FindAll(bson.M{
"email": bson.M{"$eq": in.Email},
"$or": []bson.M{
bson.M{"expiration": bson.M{"$gt": time.Now()}}, // authentified
bson.M{"registration": bson.M{"$gt": time.Now().Add(-1 * maxRegistrationDelay)}}, // authentifying
},
"email": bson.M{"$regex": bson.RegEx{Pattern: "^" + in.Email + "$", Options: "i"}},
}, &res)
if len(res) != 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "An entry already exists with the same mail"}, nil
......@@ -140,137 +150,3 @@ func checkAuthRequest(in *api.AuthRequest) error {
return nil
}
// Check if the authentication request was made in time
func checkTokenTimeout(user *entities.User) error {
now := time.Now().UTC()
bad := now.After(user.Registration.Add(time.Hour * 24))
if bad {
return errors.New("Registration request is over 24 hours old")
}
return nil
}
// Gerenate the user's certificate and certificate hash according to the specified parameters
//
// This function should only be called AFTER checking the AuthRequest for validity
func generateUserCert(csr string, parent *x509.Certificate, key *rsa.PrivateKey) ([]byte, []byte, error) {
x509csr, err := auth.PEMToCertificateRequest([]byte(csr))
if err != nil {
return nil, nil, err
}
cert, err := auth.GetCertificate(viper.GetInt("validity"), auth.GenerateUID(), x509csr, parent, key)
if err != nil {
return nil, nil, err
}
c, _ := auth.PEMToCertificate(cert)
certHash := auth.GetCertificateHash(c)
return cert, certHash, nil
}
// Auth checks if the authentication request is valid, and if so,
// generate the certificate and certificate hash for the user, and
// updates the user's entry in the database
//
// If there is already an entry in the database with the same email,
// and that this entry already has a certificate and certificate hash,
// evaluates the request as invalid
//
// The user's ConnectionInfo field is NOT handled here
// This data should be gathered upon beginning the signing sequence
func Auth(pid *authority.PlatformID, manager *mgdb.MongoManager, in *api.AuthRequest) (*api.RegisteredUser, error) {
// Check the request validity
err := checkAuthRequest(in)
if err != nil {
return nil, err
}
// Find the user in the database
var user entities.User
err = manager.Get("users").Collection.Find(bson.M{
"email": bson.M{"$eq": in.Email},
}).One(&user)
if err != nil {
return nil, err
}
// If the user already has a certificate and certificate hash in the database, does nothing
if user.Certificate != "" || len(user.CertHash) != 0 {
return nil, errors.New("User is already registered")
}
// Check if the delta between now and the moment the user was created (ie the moment he sent the register request) is in bound of 24h
err = checkTokenTimeout(&user)
if err != nil {
return nil, err
}
// Check if the token is correct
if in.Token != user.RegToken {
return nil, errors.New("Token mismatch")
}
// Generate the certificates and hash
cert, certHash, err := generateUserCert(user.Csr, pid.RootCA, pid.Pkey)
if err != nil {
return nil, err
}
user.Certificate = string(cert)
user.CertHash = certHash
user.Expiration = time.Now().AddDate(0, 0, viper.GetInt("validity"))
// Updating the database
ok, err := manager.Get("users").UpdateByID(user)
if !ok {
return nil, err
}
// Update missed contracts in background
go launchMissedContracts(manager, &user)
// Returning the RegisteredUser message
return &api.RegisteredUser{ClientCert: user.Certificate}, nil
}
func launchMissedContracts(manager *mgdb.MongoManager, user *entities.User) {
repository := entities.NewContractRepository(manager.Get("contracts"))
contracts, err := repository.GetWaitingForUser(user.Email)
if err != nil {
log.Println("Cannot get missed contracts for user", user.Email+":", err)
}
for _, c := range contracts {
c.Ready = true
for i := range c.Signers {
if c.Signers[i].Email == user.Email {
c.Signers[i].Hash = user.CertHash
c.Signers[i].UserID = user.ID
}
if len(c.Signers[i].Hash) == 0 {
c.Ready = false
}
}
// Update contract in database
_, err = repository.Collection.UpdateByID(c)
if err != nil {
log.Println("Cannot update missed contract", c.ID, "for user", user.Email+":", err)
}
if c.Ready {
// Send required mails
builder := contract.NewContractBuilder(manager, nil)
builder.Contract = &c
builder.SendNewContractMail()
}
}
}
......@@ -2,26 +2,24 @@ package user_test
import (
"dfss/dfssp/api"
"dfss/net"
"testing"
"time"
"dfss/dfssp/entities"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"testing"
)
const (
// ValidServ is a host/port adress to a platform server with bad setup
ValidServ = "localhost:9090"
// InvalidServ is a host/port adress to a platform server with bad setup
InvalidServ = "localhost:9091"
)
func TestSimpleRegister(t *testing.T) {
client := clientTest(t, ValidServ)
func clientTest(t *testing.T, hostPort string) api.PlatformClient {
conn, err := net.Connect(hostPort, nil, nil, rootCA, nil)
if err != nil {
t.Fatal("Unable to connect: ", err)
request := &api.RegisterRequest{
Email: "simple@simple.simple",
Request: string(csr),
}
return api.NewPlatformClient(conn)
errCode, err := client.Register(context.Background(), request)
assert.Nil(t, err)
assert.Equal(t, errCode.Code, api.ErrorCode_SUCCESS)
}
func TestWrongRegisterRequest(t *testing.T) {
......@@ -29,29 +27,56 @@ func TestWrongRegisterRequest(t *testing.T) {
request := &api.RegisterRequest{}
errCode, err := client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Nil(t, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
// Wrong email, good request
request.Email = "foo"
request.Request = string(csr)
errCode, err = client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Nil(t, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
// Good email, wrong request
request.Email = "foo@foo.foo"
request.Request = "foo"
errCode, err = client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Nil(t, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
}
func TestAuthUserNotFound(t *testing.T) {
mail := "wrong@wrong.wrong"
token := "wrong"
// An entry already exists with the same email and a very close registration date
// -> Unable to register, even with a different case
func TestRegisterTwice(t *testing.T) {
user := entities.NewUser()
user.Email = "twice@twice.twice"
_, err = repository.Collection.Insert(*user)
assert.Nil(t, err)
client := clientTest(t, ValidServ)
request := &api.AuthRequest{Email: mail, Token: token}
msg, err := client.Auth(context.Background(), request)
assert.Equal(t, (*api.RegisteredUser)(nil), msg)
if err == nil {
t.Fatal("The request user should not have been found in the database")
}
request := &api.RegisterRequest{Email: "twice@twice.twIce", Request: string(csr)}
errCode, err := client.Register(context.Background(), request)
assert.Equal(t, err, nil)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
}
// An entry already exists with the same email, BUT expiration is behind us
// -> Able to register
func TestRegisterRenew(t *testing.T) {
user := entities.NewUser()
user.Email = "renew@renew.renew"
user.Registration = time.Now().AddDate(0, 0, -2)
user.Expiration = time.Now().Add(-1 * time.Hour)
_, err = repository.Collection.Insert(*user)
assert.Nil(t, err)
client := clientTest(t, ValidServ)
request := &api.RegisterRequest{Email: "renew@renew.renew", Request: string(csr)}
errCode, err := client.Register(context.Background(), request)
assert.Equal(t, err, nil)
assert.Equal(t, errCode.Code, api.ErrorCode_SUCCESS)
}
......@@ -5,10 +5,10 @@ import (
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
"time"
"dfss/auth"
"dfss/dfssp/api"
......@@ -17,7 +17,6 @@ import (
"dfss/mgdb"
"dfss/net"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"gopkg.in/mgo.v2/bson"
)
......@@ -29,6 +28,20 @@ var (
rootKey, pkey *rsa.PrivateKey
)
const (
// ValidServ is a host/port adress to a platform server with good setup
ValidServ = "local