Commit 965b5e00 authored by Tristan Claverie's avatar Tristan Claverie Committed by Loïck Bonniot

[DFSSC] Add register command to the client

parent c06ec0b1
Pipeline #252 passed with stage
......@@ -24,11 +24,14 @@ Unit tests:
- "go test -coverprofile mgdb.part -v dfss/mgdb"
- "go test -coverprofile mails.part -v dfss/mails"
- "go test -coverprofile net.part -v dfss/net"
- "go test -coverprofile authority.part -v dfss/dfssp/authority"
- "go test -coverprofile dfssp_authority.part -v dfss/dfssp/authority"
- "go test -coverprofile dfssp_user.part -v dfss/dfssp/user"
- "go test -coverprofile dfssp_contract.part -v dfss/dfssp/contract"
- "go test -coverprofile dfssp_templates.part -v dfss/dfssp/templates"
- "go test -coverprofile dfssd.part -v dfss/dfssd"
- "go test -coverprofile dfssc_common.part -v dfss/dfssc/common"
- "go test -coverprofile dfssc_security.part -v dfss/dfssc/security"
- "go test -coverprofile dfssc_user.part -v dfss/dfssc/user"
- "echo 'mode: set' *part > c.out"
- "grep -h -v 'mode: set' *part >> c.out"
- "go tool cover -html=c.out -o coverage.html"
......@@ -49,6 +52,9 @@ ARM tests:
- "go test -cover -short -v dfss/dfssp/contract"
- "go test -cover -short -v dfss/dfssp/templates"
- "go test -cover -short -v dfss/dfssd"
- "go test -cover -short -v dfss/dfssc/common"
- "go test -cover -short -v dfss/dfssc/security"
- "go test -cover -short -v dfss/dfssc/user"
Code lint:
stage: test
......@@ -62,4 +68,4 @@ Code lint:
- "./build/deps.sh"
- "cd $GOPATH/src/dfss && go install ./..."
- "gometalinter --install"
- "gometalinter -t --deadline=300s -j1 --skip=api --skip=fixtures ./..."
- "gometalinter -t --deadline=600s -j1 --skip=api --skip=fixtures ./..."
......@@ -6,3 +6,4 @@ go get -u github.com/golang/protobuf/protoc-gen-go
go get -u google.golang.org/grpc
go get -u github.com/pborman/uuid
go get -u github.com/bmizerany/assert
go get -u golang.org/x/crypto/ssh/terminal
// Package common Holds the common function to be used by other package
// Essentially handles I/O functions
package common
import (
"bytes"
"io/ioutil"
"os"
)
// SaveToDisk saves the given array of bytes to disk with the given filename
func SaveToDisk(bytes []byte, filename string) error {
return ioutil.WriteFile(filename, bytes, 0644)
}
// SaveStringToDisk saves the given string to disk with the given filename
func SaveStringToDisk(str, filename string) error {
buffer := bytes.NewBufferString(str)
return ioutil.WriteFile(filename, buffer.Bytes(), 0644)
}
// DeleteQuietly try to delete a file, do not fail if an error is raised
func DeleteQuietly(filename string) {
_ = os.Remove(filename)
}
// ReadFile try to Read a file on the disk
func ReadFile(filename string) ([]byte, error) {
data, err := ioutil.ReadFile(filename)
return data, err
}
// FileExists check if a file exists or not
func FileExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return true
}
package common
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/bmizerany/assert"
)
var path = os.TempDir()
var ffoo = filepath.Join(path, "foo.txt")
var fbar = filepath.Join(path, "bar.txt")
var fbaz = filepath.Join(path, "baz.txt")
var fqux = filepath.Join(path, "qux.txt")
func TestMain(m *testing.M) {
// Setup
// Run tests
code := m.Run()
// Teardown
DeleteQuietly(ffoo)
DeleteQuietly(fbar)
DeleteQuietly(fbaz)
DeleteQuietly(fqux)
os.Exit(code)
}
// Save an array of bytes on the disk
func TestSaveToDisk(t *testing.T) {
data := make([]byte, 3)
data[0] = 'f'
data[1] = 'o'
data[2] = 'o'
err := SaveToDisk(data, ffoo)
assert.T(t, err == nil, "An error has been raised")
assert.T(t, FileExists(ffoo), "foo.txt should be present")
}
// Save a string on the disk
func TestSaveStringToDisk(t *testing.T) {
s := "bar"
err := SaveStringToDisk(s, fbar)
assert.T(t, err == nil, "An error has been raised")
assert.T(t, FileExists(fbar), "bar.txt should be present")
}
// DeleteQuietly should never raise an error, even with non-existant file
func TestDeleteQuietly(t *testing.T) {
s := "baz"
_ = SaveStringToDisk(s, fbaz)
assert.T(t, FileExists(fbaz), "baz.txt should be present")
DeleteQuietly(fbaz)
assert.T(t, !FileExists(fbaz), "baz.txt should not be present")
// Assert it does not panic when deleting an inexistant file
DeleteQuietly("dummy")
}
// Test the reading of a file
func TestReadFile(t *testing.T) {
s := "qux"
_ = SaveStringToDisk(s, fqux)
assert.T(t, FileExists(fqux), "qux.txt should be present")
data, err := ReadFile(fqux)
if err != nil {
fmt.Println(err.Error())
assert.T(t, err == nil, "An error has been raised while reading the file")
}
assert.Equal(t, s, fmt.Sprintf("%s", data), "Expected qux, received ", fmt.Sprintf("%s", data))
}
......@@ -4,16 +4,28 @@ import (
"dfss"
"flag"
"fmt"
osuser "os/user"
"runtime"
"time"
"dfss/dfssc/user"
)
var (
verbose bool
verbose bool
fca string // Path to the CA
fcert string // Path to the certificate
fkey string // Path to the private key
addrPort string // Address and port of the platform
)
func init() {
flag.BoolVar(&verbose, "v", false, "Print verbose messages")
flag.StringVar(&fca, "ca", "ca.pem", "Path to the root certificate")
flag.StringVar(&fcert, "cert", "cert.pem", "Path to the user certificate")
flag.StringVar(&fkey, "key", "priv_key.pem", "Path to the private key")
flag.StringVar(&addrPort, "host", "127.0.0.1:9000", "Host of the DFSS platform (e.g. 127.0.0.1:9000)")
flag.Usage = func() {
fmt.Println("DFSS client command line v" + dfss.Version)
......@@ -23,8 +35,9 @@ func init() {
fmt.Println(" dfssc [flags] command")
fmt.Println("\nThe commands are:")
fmt.Println(" help print this help")
fmt.Println(" version print dfss client version")
fmt.Println(" help print this help")
fmt.Println(" version print dfss client version")
fmt.Println(" register register a new client")
fmt.Println("\nFlags:")
flag.PrintDefaults()
......@@ -35,11 +48,43 @@ func init() {
func main() {
flag.Parse()
command := flag.Arg(0)
switch command {
case "version":
fmt.Println("v"+dfss.Version, runtime.GOOS, runtime.GOARCH)
case "register":
fmt.Println("Registering a new user")
// Initialize variables
var country, mail, organization, unit, passphrase string
var bits int
u, err := osuser.Current()
if err != nil {
fmt.Println("An error occurred : ", err.Error())
break
}
// Get all the necessary parameters
readStringParam("Mail", "", &mail)
readStringParam("Country", time.Now().Location().String(), &country)
readStringParam("Organization", u.Name, &organization)
readStringParam("Organizational unit", u.Name, &unit)
readIntParam("Length of the key (2048 or 4096)", 2048, &bits)
err = readPassword(&passphrase)
if err != nil {
fmt.Println("An error occurred : ", err.Error())
break
}
recapUser(fca, fcert, fkey, addrPort, country, organization, unit, mail, bits)
err = user.Register(fca, fcert, fkey, addrPort, passphrase, country, organization, unit, mail, bits)
if err != nil {
fmt.Println("An error occurred : ", err.Error())
}
default:
flag.Usage()
}
......
package main
import (
"errors"
"fmt"
"golang.org/x/crypto/ssh/terminal"
)
// Get a string parameter from standard input
func readStringParam(message, def string, ptr *string) {
fmt.Printf("%s [%s]: ", message, def)
fmt.Scanf("%s", ptr)
if *ptr == "" {
*ptr = def
}
}
// Get an integer parameter from standard input
func readIntParam(message string, def int, ptr *int) {
fmt.Printf("%s [%d]: ", message, def)
fmt.Scanf("%d", ptr)
if *ptr == 0 {
*ptr = def
}
}
// Get the password from standard input
func readPassword(ptr *string) error {
oldState, err := terminal.MakeRaw(0)
if err != nil {
return err
}
fmt.Println("Enter your passphrase :")
passphrase, err := terminal.ReadPassword(0)
if err != nil {
return err
}
fmt.Println("Confirm your passphrase :")
confirm, err := terminal.ReadPassword(0)
if err != nil {
return err
}
if fmt.Sprintf("%s", passphrase) != fmt.Sprintf("%s", confirm) {
return errors.New("Password do not match")
}
*ptr = fmt.Sprintf("%s", passphrase)
_ = terminal.Restore(0, oldState)
return nil
}
func recapUser(fca, fcert, fkey, addrPort, mail, country, organization, unit string, bits int) {
// Recap informations
fmt.Println(fmt.Sprintf("Summary of the new user : Mail : %s; Country : %s; Organization : %s; Organizational unit : %s; bits : %d", mail, country, organization, unit, bits))
fmt.Println(fmt.Sprintf("Address of the platform is %s", addrPort))
fmt.Println(fmt.Sprintf("File storing the CA : %s", fca))
fmt.Println(fmt.Sprintf("File to store the certificate : %s", fcert))
fmt.Println(fmt.Sprintf("File to store the private key : %s", fkey))
}
package security
import (
"crypto/rsa"
"crypto/x509"
"dfss/auth"
"dfss/dfssc/common"
)
// GetCertificate return the Certificate stored on the disk
func GetCertificate(filename string) (*x509.Certificate, error) {
data, err := common.ReadFile(filename)
if err != nil {
return nil, err
}
cert, err := auth.PEMToCertificate(data)
if err != nil {
return nil, err
}
return cert, nil
}
// GetPrivateKey return the private key stored on the disk
func GetPrivateKey(filename, passphrase string) (*rsa.PrivateKey, error) {
data, err := common.ReadFile(filename)
if err != nil {
return nil, err
}
key, err := auth.EncryptedPEMToPrivateKey(data, passphrase)
if err != nil {
return nil, err
}
return key, nil
}
// Package security is responsible for generating keys and certificate requests
package security
import (
"crypto/rsa"
"dfss/auth"
"dfss/dfssc/common"
"fmt"
)
// GenerateKeys generate a pair of keys and save it to the disk
func GenerateKeys(bits int, passphrase, filename string) (*rsa.PrivateKey, error) {
key, err := auth.GeneratePrivateKey(bits)
if err != nil {
return nil, err
}
pem, err := auth.PrivateKeyToEncryptedPEM(key, passphrase)
if err != nil {
return nil, err
}
err = common.SaveToDisk(pem, filename)
if err != nil {
return nil, err
}
return key, nil
}
// GenerateCertificateRequest generate a certificate request from data, and
// return a PEM-encoded certificate as a string
func GenerateCertificateRequest(country, organization, unit, mail string, key *rsa.PrivateKey) (string, error) {
data, err := auth.GetCertificateRequest(country, organization, unit, mail, key)
if err != nil {
return "", err
}
return fmt.Sprintf("%s", data), nil
}
// SaveCertificate saves a PEM-encoded certificate on disk
func SaveCertificate(cert, filename string) error {
return common.SaveStringToDisk(cert, filename)
}
package security
import (
"dfss/dfssc/common"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/bmizerany/assert"
)
var crtFixture = `-----BEGIN CERTIFICATE-----
MIICRDCCAa2gAwIBAgIJAIf+q5v9t+rTMA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV
BAYTAkZSMQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxCzAJBgNVBAMM
AkNBMCAXDTE1MTEyMDE1NDMwOVoYDzQ3NTMxMDE2MTU0MzA5WjA6MQswCQYDVQQG
EwJGUjEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQswCQYDVQQDDAJD
QTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqNbQWl2UZiOmJcZDA5x2H2U5
m2qC0D3NNPv0jNOm6shGTKhcLH1W8DrDtv5NjRWN1XTpfy0VkrsoyPpsU6PFFzZC
GmkCoXKBD/dvDNrid2MgbURzx+0a+EmUfFh0+tVP2Dzy0zgb/FZWkM6HT0VQ8KAb
SmlRBctiujDV1RBUOm8CAwEAAaNQME4wHQYDVR0OBBYEFNSIxFzdlyGUGBnqsKpd
bS4te57xMB8GA1UdIwQYMBaAFNSIxFzdlyGUGBnqsKpdbS4te57xMAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQELBQADgYEAECSnGMqnlgyBdqTC02/Uo1jiPqJjLZV1
TRJFHxs4JPAsff+rdAQ1TQVfaNnvAkAoXVzM34xGPkJserMUBc7aQ61WrByGImai
RqEe6wUqHGuH2blNt+2LSSuFWuR02+LxsJARDVSLViAS3lNgXlgGnzOaRs31iwwU
czHnSiYoCog=
-----END CERTIFICATE-----
`
var path = os.TempDir()
// Test the generation of keys
func TestGenerateKeys(t *testing.T) {
fkey := filepath.Join(path, "genKey.pem")
rsa, err := GenerateKeys(2048, "pwd", fkey)
assert.T(t, err == nil, "An error has been raised during generation")
assert.T(t, rsa != nil, "RSA key should not be nil")
assert.T(t, common.FileExists(fkey), "File is missing")
common.DeleteQuietly(fkey)
rsa, err = GenerateKeys(4096, "pwd", fkey)
assert.T(t, err == nil, "An error has been raised during generation")
assert.T(t, rsa != nil, "RSA key should not be nil")
assert.T(t, common.FileExists(fkey), "File is missing")
common.DeleteQuietly(fkey)
}
// Test the generation of a certificate request
func TestCertificateRequest(t *testing.T) {
fkey := filepath.Join(path, "genCsr.pem")
rsa, err := GenerateKeys(2048, "pwd", fkey)
defer common.DeleteQuietly(fkey)
assert.T(t, err == nil, "An error has been raised during generation")
assert.T(t, rsa != nil, "RSA key should not be nil")
assert.T(t, common.FileExists(fkey), "File is missing")
csr, err := GenerateCertificateRequest("France", "DFSS", "DFSS_C", "dfssc@dfss.org", rsa)
assert.T(t, err == nil, "An error has been raised during generation of certificate request")
assert.T(t, csr != "", "Certificate request should not be nil")
}
// Test saving rsa key on the disk
func TestDumpingKey(t *testing.T) {
fkey := filepath.Join(path, "dumpKey.pem")
rsa, err := GenerateKeys(2048, "pwd", fkey)
defer common.DeleteQuietly(fkey)
assert.T(t, err == nil, "An error has been raised during generation")
assert.T(t, rsa != nil, "RSA key should not be nil")
assert.T(t, common.FileExists(fkey), "File is missing")
k, err := GetPrivateKey(fkey, "")
assert.T(t, err != nil, "An error should have been raised")
k, err = GetPrivateKey(fkey, "dummypwd")
assert.T(t, err != nil, "An error should have been raised")
k, err = GetPrivateKey(fkey, "pwd")
assert.T(t, err == nil, "No error should have been raised")
assert.Equal(t, *rsa, *k, "Keys should be equal")
}
// Test the saving of a certificate in a file
func TestDumpCrt(t *testing.T) {
fcert := filepath.Join(path, "dumpCert.pem")
err := SaveCertificate(crtFixture, fcert)
defer common.DeleteQuietly(fcert)
assert.T(t, err == nil, "An error has been raised during saving")
assert.T(t, common.FileExists(fcert), "File is missing")
data, err := common.ReadFile(fcert)
assert.T(t, err == nil, "An error has been raised while reading file")
assert.Equal(t, crtFixture, fmt.Sprintf("%s", data), "Certificates are not equal")
crt, err := GetCertificate(fcert)
assert.T(t, err == nil, "An error has been raised while parsing certificate")
assert.T(t, crt != nil, "Certificate is nil")
}
// Package user handles all the user-related logic
package user
// Register a user using the provided parameters
func Register(fileCA, fileCert, fileKey, addrPort, passphrase, country, organization, unit, mail string, bits int) error {
manager, err := NewRegisterManager(fileCA, fileCert, fileKey, addrPort, passphrase, country, organization, unit, mail, bits)
if err != nil {
return err
}
err = manager.GetCertificate()
if err != nil {
return err
}
return nil
}
package user
import (
"dfss/dfssc/common"
"dfss/dfssc/security"
pb "dfss/dfssp/api"
"dfss/net"
"errors"
"regexp"
"time"
"golang.org/x/net/context"
)
// RegisterManager handles the registration of a user
type RegisterManager struct {
fileCA string
fileCert string
fileKey string
addrPort string
passphrase string
country string
organization string
unit string
mail string
bits int
}
// NewRegisterManager return a new Register Manager to register a user
func NewRegisterManager(fileCA, fileCert, fileKey, addrPort, passphrase, country, organization, unit, mail string, bits int) (*RegisterManager, error) {
m := &RegisterManager{
fileCA: fileCA,
fileCert: fileCert,
fileKey: fileKey,
addrPort: addrPort,
passphrase: passphrase,
country: country,
organization: organization,
unit: unit,
mail: mail,
bits: bits,
}
if err := m.checkValidParams(); err != nil {
return nil, err
}
if err := m.checkFilePresence(); err != nil {
return nil, err
}
return m, nil
}
// 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 {
return errors.New("Provided mail is not valid")
}
if m.bits != 2048 && m.bits != 4096 {
return errors.New("Length of the key should be 2048 or 4096 bits")
}
return nil
}
// Check there is no private key nor client certificate
// Check the CA is present and valid
// Check there is not a duplicate file
func (m *RegisterManager) checkFilePresence() error {
if b := common.FileExists(m.fileKey); b {
return errors.New("A private key is already present at path " + m.fileKey)
}
if b := common.FileExists(m.fileCert); b {
return errors.New("A certificate is already present at path " + m.fileCert)
}
if m.fileKey == m.fileCert {
return errors.New("Cannot store certificate and key in the same file")
}
if b := common.FileExists(m.fileCA); !b {
return errors.New("You need the certificate of the platform at path " + m.fileCA)
}
data, err := security.GetCertificate(m.fileCA)
if err != nil {
return err
}
if time.Now().After(data.NotAfter) {
return errors.New("Root certificate has expired")
}
return nil
}
// GetCertificate handles the creation of a certificate, delete private key upon failure
func (m *RegisterManager) GetCertificate() error {
request, err := m.buildCertificateRequest()
if err != nil {
common.DeleteQuietly(m.fileKey)
return err
}
code, err := m.sendRequest(request)
if err != nil {
common.DeleteQuietly(m.fileKey)
return err
}