Commit d6abf53a authored by Tristan Claverie's avatar Tristan Claverie

Merge branch '178_platform_create_user' into 'master'

178 platform create user

Handle Register and Auth requests
- validity check
- database update
- redudant requests
- gRPC answer messages

See merge request !24
parents fc16f702 e5a06e94
Pipeline #267 passed with stage
......@@ -6,6 +6,8 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"github.com/pborman/uuid"
"math/big"
"time"
)
......@@ -38,6 +40,11 @@ func GetCertificateRequest(country, organization, unit, mail string, key *rsa.Pr
// PEMToCertificateRequest tries to decode a PEM-encoded array of bytes to a certificate request
func PEMToCertificateRequest(data []byte) (*x509.CertificateRequest, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("Couldn't decode the PEM data as a x509 Certificate request")
}
return x509.ParseCertificateRequest(block.Bytes)
}
......@@ -108,3 +115,13 @@ func PEMToCertificate(data []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(data)
return x509.ParseCertificate(block.Bytes)
}
// GenerateUID generates a unique identifier as a uint64
func GenerateUID() uint64 {
// Generating and converting the uuid to fit our needs: an 8 bytes unsigned integer
uuid := uuid.NewRandom()
var slice []byte
slice = uuid[:8]
// TODO: improve this conversion method/need
return new(big.Int).SetBytes(slice).Uint64()
}
......@@ -88,6 +88,12 @@ func TestPEMToCertificateRequest(t *testing.T) {
t.Fatal("Wrong CN: ", res.Subject.CommonName)
}
res, err = PEMToCertificateRequest([]byte("invalid"))
if err == nil {
t.Fatal("The request should not have been decoded as is was invalid format")
}
}
func TestGetSelfSignedCertificate(t *testing.T) {
......
......@@ -74,7 +74,7 @@ func (x ErrorCode_Code) String() string {
func (ErrorCode_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1, 0} }
// RegisterRequest message contains the client's email adress and his
// certificate request
// request (ie the PEM-encoded certificate request)
type RegisterRequest struct {
Email string `protobuf:"bytes,1,opt,name=email" json:"email,omitempty"`
Request string `protobuf:"bytes,2,opt,name=request" json:"request,omitempty"`
......
......@@ -12,7 +12,7 @@ service Platform {
}
// RegisterRequest message contains the client's email adress and his
// certificate request
// request (ie the PEM-encoded certificate request)
message RegisterRequest {
string email = 1;
string request = 2;
......
......@@ -4,9 +4,7 @@ import (
"crypto/rsa"
"crypto/x509"
"dfss/auth"
"github.com/pborman/uuid"
"io/ioutil"
"math/big"
"os/user"
"path/filepath"
)
......@@ -35,14 +33,9 @@ func GetHomeDir() string {
return usr.HomeDir
}
// GenerateRootCA constructs a self-signed certificate, using a unique serial number randomly generated (see UUID)
// GenerateRootCA constructs a self-signed certificate, using a unique serial number randomly generated
func GenerateRootCA(days int, country, organization, unit, cn string, key *rsa.PrivateKey) ([]byte, error) {
// Generating and converting the uuid to fit our needs: an 8 bytes integer.
uuid := uuid.NewRandom()
var slice []byte
slice = uuid[:8]
// TODO: improve this conversion method/need
serial := new(big.Int).SetBytes(slice).Uint64()
serial := auth.GenerateUID()
cert, err := auth.GetSelfSignedCertificate(days, serial, country, organization, unit, cn, key)
......
......@@ -71,8 +71,8 @@ func ExampleInitialize() {
fmt.Println(err)
}
checkFile(keyPath, "Private key")
checkFile(certPath, "Certificate")
CheckFile(keyPath, "Private key")
CheckFile(certPath, "Certificate")
// Output:
// Private key file has been found
......@@ -81,7 +81,7 @@ func ExampleInitialize() {
// Certificate file has been deleted
}
func checkFile(path, name string) {
func CheckFile(path, name string) {
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Println(name + " file couldn't be found")
} else {
......
......@@ -40,7 +40,7 @@ func TestMain(m *testing.M) {
// Start platform server
keyPath := filepath.Join(os.Getenv("GOPATH"), "src", "dfss", "dfssp", "testdata")
srv := server.GetServer(keyPath, dbURI, true)
srv := server.GetServer(keyPath, dbURI, 365, true)
go func() { _ = net.Listen("localhost:9090", srv) }()
// Run
......
......@@ -15,7 +15,7 @@ import (
var (
verbose, demo bool
path, country, org, unit, cn, port, address, dbURI string
keySize, validity int
keySize, rootValidity, certValidity int
)
func init() {
......@@ -33,7 +33,8 @@ func init() {
flag.StringVar(&cn, "cn", "dfssp", "Common name for the root certificate")
flag.IntVar(&keySize, "keySize", 512, "Encoding size for the private key")
flag.IntVar(&validity, "validity", 21, "Root certificate's validity duration (days)")
flag.IntVar(&rootValidity, "rootValidity", 365, "Root certificate's validity duration (days)")
flag.IntVar(&certValidity, "certValidity", 365, "Validity duration for the certificates generated by this platform (days)")
flag.StringVar(&dbURI, "db", "mongodb://localhost/dfss", "Name of the environment variable containing the server url in standard MongoDB format")
......@@ -45,7 +46,7 @@ func init() {
fmt.Println(" dfssp [flags] command")
fmt.Println("\nThe commands are:")
fmt.Println(" init [cn, country, keySize, org, path, unit, validity]")
fmt.Println(" init [cn, country, keySize, org, path, unit, rootValidity]")
fmt.Println(" create and save the platform's private key and root certificate")
fmt.Println(" start [path, db, a, p]")
fmt.Println(" start the platform after loading its private key and root certificate")
......@@ -68,14 +69,14 @@ func main() {
case "version":
fmt.Println("v"+dfss.Version, runtime.GOOS, runtime.GOARCH)
case "init":
err := authority.Initialize(keySize, validity, country, org, unit, cn, path)
err := authority.Initialize(keySize, rootValidity, country, org, unit, cn, path)
if err != nil {
fmt.Println("An error occured during the initialization operation:", err)
os.Exit(1)
}
dapi.DLog("Private key generated !")
case "start":
srv := server.GetServer(path, dbURI, verbose)
srv := server.GetServer(path, dbURI, certValidity, verbose)
fmt.Println("Listening on " + address + ":" + port)
dapi.DLog("Platform server started on " + address + ":" + port)
err := net.Listen(address+":"+port, srv)
......
......@@ -7,6 +7,7 @@ import (
"dfss/dfssp/api"
"dfss/dfssp/authority"
"dfss/dfssp/contract"
"dfss/dfssp/user"
"dfss/mgdb"
"dfss/net"
"golang.org/x/net/context"
......@@ -14,26 +15,24 @@ import (
)
type platformServer struct {
Pid *authority.PlatformID
DB *mgdb.MongoManager
Verbose bool
Pid *authority.PlatformID
DB *mgdb.MongoManager
CertDuration int
Verbose bool
}
// Register handler
//
// Handle incoming RegisterRequest messages
func (s *platformServer) Register(ctx context.Context, in *api.RegisterRequest) (*api.ErrorCode, error) {
// TODO
_ = new(platformServer)
return nil, nil
return user.Register(s.DB, in)
}
// Auth handler
//
// Handle incoming AuthRequest messages
func (s *platformServer) Auth(ctx context.Context, in *api.AuthRequest) (*api.RegisteredUser, error) {
// TODO
return nil, nil
return user.Auth(s.Pid, s.DB, s.CertDuration, in)
}
// Unregister handler
......@@ -69,7 +68,7 @@ func (s *platformServer) ReadySign(ctx context.Context, in *api.ReadySignRequest
}
// GetServer returns the GRPC server associated with the platform
func GetServer(keyPath, db string, verbose bool) *grpc.Server {
func GetServer(keyPath, db string, certValidity int, verbose bool) *grpc.Server {
pid, err := authority.Start(keyPath)
if err != nil {
fmt.Println("An error occured during the private key and root certificate retrieval:", err)
......@@ -84,9 +83,10 @@ func GetServer(keyPath, db string, verbose bool) *grpc.Server {
server := net.NewServer(pid.RootCA, pid.Pkey, pid.RootCA)
api.RegisterPlatformServer(server, &platformServer{
Pid: pid,
DB: dbManager,
Verbose: verbose,
Pid: pid,
DB: dbManager,
CertDuration: certValidity,
Verbose: verbose,
})
return server
}
......@@ -24,6 +24,7 @@ func Init() {
_ = template.Must(tpl.Parse("{{define `signature`}}" + signature + "{{end}}"))
_ = template.Must(tpl.Parse("{{define `invitation`}}" + invitation + "{{end}}"))
_ = template.Must(tpl.Parse("{{define `contractDetails`}}" + contractDetails + "{{end}}"))
_ = template.Must(tpl.Parse("{{define `verificationMail`}}" + verificationMail + "{{end}}"))
ready = true
}
......
package templates
const verificationMail = `Dear sir or Madam,
You asked to register to the DFSS platform.
Please send us your authentication request with
the following text as token:
{{.Token}}
If you did not ask for registration, we deeply excuse
for the error.
{{template "signature"}}
`
// VerificationMail contains the token to be sent in the verification mail
type VerificationMail struct {
Token string
}
package user
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"errors"
"fmt"
"io"
"time"
"dfss/auth"
"dfss/dfssp/api"
"dfss/dfssp/authority"
"dfss/dfssp/entities"
"dfss/dfssp/templates"
"dfss/mgdb"
"gopkg.in/mgo.v2/bson"
)
// 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 len(in.Request) == 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "Invalid request length"}
}
_, err := auth.PEMToCertificateRequest([]byte(in.Request))
if err != nil {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: err.Error()}
}
return nil
}
// Send the verification email in response to the specified registration request
//
// This method should only be called AFTER checking the RegisterRequest for validity
func sendVerificationMail(in *api.RegisterRequest, token string) error {
conn := templates.MailConn()
if conn == nil {
return errors.New("Couldn't connect to the dfssp mail server")
}
defer func() { _ = conn.Close() }()
rcpts := []string{in.Email}
mail := templates.VerificationMail{Token: token}
content, err := templates.Get("verificationMail", mail)
if err != nil {
return err
}
err = conn.Send(
rcpts,
"[DFSS] Registration email validation",
content,
nil,
nil,
nil,
)
if err != nil {
return err
}
return nil
}
// Register checks if the registration request is valid, and if so,
// creates the user entry in the database
//
// If there is already an entry in the database with the same email,
// 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 Register(manager *mgdb.MongoManager, in *api.RegisterRequest) (*api.ErrorCode, error) {
// Check the request validity
errCode := checkRegisterRequest(in)
if errCode != nil {
return errCode, nil
}
// Generating the random token
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
return &api.ErrorCode{Code: api.ErrorCode_INTERR, Message: "Error during the generation of the token"}, nil
}
token := fmt.Sprintf("%x", b)
// If there is already an entry with the same mail, do nothing
var res []entities.User
err = manager.Get("users").FindAll(bson.M{
"email": bson.M{"$eq": in.Email},
}, &res)
if len(res) != 0 {
return &api.ErrorCode{Code: api.ErrorCode_INVARG, Message: "An entry already exists with the same mail"}, nil
}
// Creating the new user
user := entities.NewUser()
user.Email = in.Email
user.RegToken = token
user.Csr = in.Request
// Adding the new user in the database
ok, err := manager.Get("users").Insert(*user)
if !ok {
return &api.ErrorCode{Code: api.ErrorCode_INTERR, Message: "Error during the insertion of the new user"}, err
}
// Sending the email
err = sendVerificationMail(in, token)
if err != nil {
return &api.ErrorCode{Code: api.ErrorCode_INTERR, Message: "Error during the sending of the email"}, err
}
return &api.ErrorCode{Code: api.ErrorCode_SUCCESS, Message: "Registration successful ; email sent"}, nil
}
// Check if the authentication request has usable fields
func checkAuthRequest(in *api.AuthRequest, certDuration int) error {
if len(in.Email) == 0 {
return errors.New("Invalid email length")
}
if len(in.Token) == 0 {
return errors.New("Invalid token length")
}
if certDuration < 1 {
return errors.New("Invalid validity duration")
}
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, certDuration int, 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(certDuration, auth.GenerateUID(), x509csr, parent, key)
if err != nil {
return nil, nil, err
}
h := sha512.New()
_, err = io.WriteString(h, string(cert))
if err != nil {
return nil, nil, err
}
certHash := h.Sum(nil)
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, certDuration int, in *api.AuthRequest) (*api.RegisteredUser, error) {
// Check the request validity
err := checkAuthRequest(in, certDuration)
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 != "" || user.CertHash != "" {
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, certDuration, pid.RootCA, pid.Pkey)
if err != nil {
return nil, err
}
user.Certificate = string(cert)
user.CertHash = string(certHash)
// Updating the database
ok, err := manager.Get("users").UpdateByID(user)
if !ok {
return nil, err
}
// Returning the RegisteredUser message
return &api.RegisteredUser{ClientCert: user.Certificate}, nil
}
package user_test
import (
"dfss/dfssp/api"
"dfss/net"
"github.com/bmizerany/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 clientTest(t *testing.T, hostPort string) api.PlatformClient {
conn, err := net.Connect(hostPort, nil, nil, rootCA)
if err != nil {
t.Fatal("Unable to connect: ", err)
}
return api.NewPlatformClient(conn)
}
func TestWrongRegisterRequest(t *testing.T) {
client := clientTest(t, ValidServ)
request := &api.RegisterRequest{}
errCode, err := client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
request.Email = "foo"
errCode, err = client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
request.Request = "foo"
errCode, err = client.Register(context.Background(), request)
assert.Equal(t, nil, err)
assert.Equal(t, errCode.Code, api.ErrorCode_INVARG)
}
func TestWrongAuthRequest(t *testing.T) {
// Get a client to the invalid server (cert duration is -1)
client := clientTest(t, InvalidServ)
// Invalid mail length
inv := &api.AuthRequest{}
msg, err := client.Auth(context.Background(), inv)
if msg != nil || err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
// Invalid token length
inv.Email = "foo"
msg, err = client.Auth(context.Background(), inv)
if msg != nil || err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
// Invalid certificate validity duration
inv.Token = "foo"
msg, err = client.Auth(context.Background(), inv)
if msg != nil || err == nil {
t.Fatal("The request should have been evaluated as invalid")
}
}
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")
}
}
package user
// TODO: include code here
package user
package user_test
import (
"crypto/rsa"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"dfss/auth"
"dfss/dfssp/api"
"dfss/dfssp/entities"
"dfss/dfssp/server"
"dfss/mgdb"
"dfss/net"
"github.com/bmizerany/assert"
"golang.org/x/net/context"
"gopkg.in/mgo.v2/bson"
"time"
)
var (