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 ( ...@@ -5,13 +5,11 @@ import (
"regexp" "regexp"
"time" "time"
"github.com/spf13/viper"
"dfss/dfssc/common" "dfss/dfssc/common"
"dfss/dfssc/security" "dfss/dfssc/security"
pb "dfss/dfssp/api" pb "dfss/dfssp/api"
"dfss/net" "dfss/net"
"github.com/spf13/viper"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
...@@ -27,6 +25,8 @@ type RegisterManager struct { ...@@ -27,6 +25,8 @@ type RegisterManager struct {
bits int bits int
} }
var mailRegex = regexp.MustCompile(`.+@.+\..+`)
// NewRegisterManager return a new Register Manager to register a user // 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) { func NewRegisterManager(passphrase, country, organization, unit, mail string, bits int, v *viper.Viper) (*RegisterManager, error) {
m := &RegisterManager{v, passphrase, country, organization, unit, mail, bits} m := &RegisterManager{v, passphrase, country, organization, unit, mail, bits}
...@@ -44,8 +44,7 @@ func NewRegisterManager(passphrase, country, organization, unit, mail string, bi ...@@ -44,8 +44,7 @@ func NewRegisterManager(passphrase, country, organization, unit, mail string, bi
// Check the validity of the provided email, passphrase and bits // Check the validity of the provided email, passphrase and bits
func (m *RegisterManager) checkValidParams() error { func (m *RegisterManager) checkValidParams() error {
re, _ := regexp.Compile(`.+@.+\..+`) if b := mailRegex.MatchString(m.mail); !b {
if b := re.MatchString(m.mail); !b {
return errors.New("Provided mail is not valid") return errors.New("Provided mail is not valid")
} }
......
...@@ -2,6 +2,7 @@ package contract_test // Using another package to avoid import cycles ...@@ -2,6 +2,7 @@ package contract_test // Using another package to avoid import cycles
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
...@@ -49,7 +50,7 @@ func TestMain(m *testing.M) { ...@@ -49,7 +50,7 @@ func TestMain(m *testing.M) {
viper.Set("verbose", true) viper.Set("verbose", true)
srv := server.GetServer() srv := server.GetServer()
go func() { _ = net.Listen("localhost:9090", srv) }() go func() { log.Fatal(net.Listen("localhost:9090", srv)) }()
// Run // Run
code := m.Run() code := m.Run()
...@@ -142,7 +143,7 @@ func TestGetWaitingForUser(t *testing.T) { ...@@ -142,7 +143,7 @@ func TestGetWaitingForUser(t *testing.T) {
c1.Ready = false c1.Ready = false
c2 := entities.NewContract() c2 := entities.NewContract()
c2.AddSigner(nil, "mail1", []byte{}) c2.AddSigner(nil, "mAil1", []byte{})
c2.AddSigner(&knownID, "mail2", []byte{0x12}) c2.AddSigner(&knownID, "mail2", []byte{0x12})
c2.Ready = false c2.Ready = false
......
...@@ -4,6 +4,7 @@ package contract ...@@ -4,6 +4,7 @@ package contract
import ( import (
"crypto/sha512" "crypto/sha512"
"log" "log"
"strings"
"time" "time"
"dfss/dfssp/api" "dfss/dfssp/api"
...@@ -62,7 +63,6 @@ func (c *Builder) Execute() *api.ErrorCode { ...@@ -62,7 +63,6 @@ func (c *Builder) Execute() *api.ErrorCode {
// checkInput checks that a PostContractRequest is well-formed // checkInput checks that a PostContractRequest is well-formed
func (c *Builder) checkInput() *api.ErrorCode { func (c *Builder) checkInput() *api.ErrorCode {
if len(c.in.Signer) == 0 { if len(c.in.Signer) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Expecting at least one signer"} return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Expecting at least one signer"}
} }
...@@ -76,18 +76,23 @@ func (c *Builder) checkInput() *api.ErrorCode { ...@@ -76,18 +76,23 @@ func (c *Builder) checkInput() *api.ErrorCode {
} }
return nil return nil
} }
// fetchSigners fetches authenticated users for this contract from the DB // fetchSigners fetches authenticated users for this contract from the DB
func (c *Builder) fetchSigners() error { func (c *Builder) fetchSigners() error {
var users []entities.User 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 // Fetch users where email is part of the signers slice in request
// and authentication is valid // and authentication is valid
err := c.m.Get("users").FindAll(bson.M{ err := c.m.Get("users").FindAll(bson.M{
"expiration": bson.M{"$gt": time.Now()}, "expiration": bson.M{"$gt": time.Now()},
"email": bson.M{"$in": c.in.Signer}, "email": bson.M{"$in": conditions},
}, &users) }, &users)
if err != nil { if err != nil {
return err return err
...@@ -96,8 +101,9 @@ func (c *Builder) fetchSigners() error { ...@@ -96,8 +101,9 @@ func (c *Builder) fetchSigners() error {
// Locate missing users // Locate missing users
for _, s := range c.in.Signer { for _, s := range c.in.Signer {
found := false found := false
lowerEmail := strings.ToLower(s)
for _, u := range users { for _, u := range users {
if s == u.Email { if lowerEmail == strings.ToLower(u.Email) {
found = true found = true
break break
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
...@@ -88,7 +89,7 @@ func TestAddContract(t *testing.T) { ...@@ -88,7 +89,7 @@ func TestAddContract(t *testing.T) {
errorCode, err := client.PostContract(context.Background(), &api.PostContractRequest{ errorCode, err := client.PostContract(context.Background(), &api.PostContractRequest{
Hash: defaultHash[:], Hash: defaultHash[:],
Filename: "ContractFilename", Filename: "ContractFilename",
Signer: []string{user1.Email, user2.Email}, Signer: []string{strings.ToUpper(user1.Email), user2.Email},
Comment: "ContractComment", Comment: "ContractComment",
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
......
...@@ -89,7 +89,7 @@ func (r *ContractRepository) GetWaitingForUser(email string) ([]Contract, error) ...@@ -89,7 +89,7 @@ func (r *ContractRepository) GetWaitingForUser(email string) ([]Contract, error)
"ready": false, "ready": false,
"signers": bson.M{ "signers": bson.M{
"$elemMatch": bson.M{ "$elemMatch": bson.M{
"email": email, "email": bson.M{"$regex": bson.RegEx{Pattern: "^" + email + "$", Options: "i"}},
"hash": []byte{}, "hash": []byte{},
}}, }},
}, &res) }, &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 ...@@ -3,17 +3,14 @@ package user
import ( import (
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors" "errors"
"fmt" "fmt"
"log" "log"
"regexp"
"time" "time"
"dfss/auth" "dfss/auth"
"dfss/dfssp/api" "dfss/dfssp/api"
"dfss/dfssp/authority"
"dfss/dfssp/contract"
"dfss/dfssp/entities" "dfss/dfssp/entities"
"dfss/dfssp/templates" "dfss/dfssp/templates"
"dfss/mgdb" "dfss/mgdb"
...@@ -21,12 +18,21 @@ import ( ...@@ -21,12 +18,21 @@ import (
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
var (
mailRegex = regexp.MustCompile(`.+@.+\..+`)
maxRegistrationDelay = 24 * time.Hour
)
// Check if the registration request has usable fields // Check if the registration request has usable fields
func checkRegisterRequest(in *api.RegisterRequest) *api.ErrorCode { func checkRegisterRequest(in *api.RegisterRequest) *api.ErrorCode {
if len(in.Email) == 0 { if len(in.Email) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid email length"} 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 { if len(in.Request) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid request length"} 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 ...@@ -97,10 +103,14 @@ func Register(manager *mgdb.MongoManager, in *api.RegisterRequest) (*api.ErrorCo
} }
token := fmt.Sprintf("%x", b) 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 var res []entities.User
err = manager.Get("users").FindAll(bson.M{ 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) }, &res)
if len(res) != 0 { if len(res) != 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "An entry already exists with the same mail"}, nil 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 { ...@@ -140,137 +150,3 @@ func checkAuthRequest(in *api.AuthRequest) error {
return nil 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 {