diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..89b82d8c3eefe58e891480f0a0246bd7d4635b1f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,35 @@
+stages:
+  - test
+
+Unit tests:
+  stage: test
+  tags:
+    - golang
+    - mongo  # Require an available mongo service
+    - strong # Disable this build on small runners
+  services:
+    - "lesterpig/mongo:latest" # Use this light version of mongo
+  #artifacts: # Waiting GitLab 8.2.1...
+  #  paths:
+  #    - "coverage.html"
+  script:
+    - "go test -coverprofile c.out -v ./..."
+    - "go tool cover -html=c.out -o coverage.html"
+
+ARM tests:
+  stage: test
+  tags:
+    - arm
+  script:
+    - "go test -cover -short -v ./..."
+
+Code lint:
+  stage: test
+  allow_failure: True
+  tags:
+    - golang
+    - lint
+  script:
+    - "go get github.com/alecthomas/gometalinter"
+    - "gometalinter --install"
+    - "gometalinter -t --deadline=60s ./..."
diff --git a/auth/cert.go b/auth/cert.go
new file mode 100644
index 0000000000000000000000000000000000000000..615808ab77f44a1e5346a52a4497120a076b7b43
--- /dev/null
+++ b/auth/cert.go
@@ -0,0 +1,109 @@
+package auth
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"math/big"
+	"time"
+)
+
+// GetCertificateRequest creates a request to be sent to any authoritative signer, as a PEM-encoded array of bytes.
+//
+// It can be safely sent via the network.
+func GetCertificateRequest(country, organization, unit, mail string, key *rsa.PrivateKey) ([]byte, error) {
+	template := &x509.CertificateRequest{
+		Subject: pkix.Name{
+			Country:            []string{country},
+			Organization:       []string{organization},
+			OrganizationalUnit: []string{unit},
+			CommonName:         mail,
+		},
+	}
+
+	der, err := x509.CreateCertificateRequest(rand.Reader, template, key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE REQUEST",
+		Bytes: der,
+	}), nil
+}
+
+// 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)
+	return x509.ParseCertificateRequest(block.Bytes)
+}
+
+// GetCertificate builds a certificate from a certificate request and an authoritative certificate (CA), as a PEM-encoded array of bytes.
+// This function assumes that the identity of the signee is valid.
+//
+// The serial has to be unique.
+//
+// The generated certificate can safely be distributed to unknown actors.
+func GetCertificate(days int, serial int64, req *x509.CertificateRequest, parent *x509.Certificate, key *rsa.PrivateKey) ([]byte, error) {
+
+	template := &x509.Certificate{
+		SerialNumber: big.NewInt(serial),
+		Subject:      req.Subject,
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().AddDate(0, 0, days),
+		IsCA:         false,
+	}
+
+	der, err := x509.CreateCertificate(rand.Reader, template, parent, req.PublicKey, key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: der,
+	}), nil
+}
+
+// GetSelfSignedCertificate builds a CA certificate from a private key, as a PEM-encoded array of bytes.
+//
+// The serial has to be unique.
+//
+// The generated certificate should be distributed to any other actor in the network under this CA.
+func GetSelfSignedCertificate(days int, serial int64, country, organization, unit, cn string, key *rsa.PrivateKey) ([]byte, error) {
+
+	template := &x509.Certificate{
+		SerialNumber: big.NewInt(serial),
+		Subject: pkix.Name{
+			Country:            []string{country},
+			Organization:       []string{organization},
+			OrganizationalUnit: []string{unit},
+			CommonName:         cn,
+		},
+		NotBefore: time.Now(),
+		NotAfter:  time.Now().AddDate(0, 0, days),
+		IsCA:      true,
+	}
+
+	der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE",
+		Bytes: der,
+	}), nil
+
+}
+
+// PEMToCertificate tries to decode a PEM-encoded array of bytes to a certificate
+func PEMToCertificate(data []byte) (*x509.Certificate, error) {
+	block, _ := pem.Decode(data)
+	return x509.ParseCertificate(block.Bytes)
+}
diff --git a/auth/cert_test.go b/auth/cert_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ffd0e37e961055ec7d5296377677e1b6d9a34b4
--- /dev/null
+++ b/auth/cert_test.go
@@ -0,0 +1,179 @@
+package auth
+
+import (
+	"crypto/rsa"
+	"crypto/x509"
+	"fmt"
+	"os"
+	"testing"
+)
+
+var pkey *rsa.PrivateKey
+var csrFixture = `-----BEGIN CERTIFICATE REQUEST-----
+MIIBgjCB7AIBADBDMQswCQYDVQQGEwJGUjEMMAoGA1UECgwDT1JHMRAwDgYDVQQL
+DAdPUkdVTklUMRQwEgYDVQQDDAt0ZXN0QGdvLnRsZDCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEAtbEFS3VyXHAcNzZ49XKgXzv9SaBszbHWAXmuQlgH4dyjL7OX
+w6NOpjSrIW2MVN99/boW1CilMpJzyMRkfkYg2u/HQw1KRUqP62Tl9FIbFjO+rITC
+JI4fHsMpOh6+oWw62wf9mbKfL+kmFTTTfAWZpcE/R8IM+vJK4R+6DE7qvXMCAwEA
+AaAAMA0GCSqGSIb3DQEBCwUAA4GBAJKNtd8IsxkJyWnOoJjckX+djFxoCNgo7JS1
+6evVTU3esDRQ0P4T6oqn4D+yGQlRNtO6/Ko1D9Vv8v14hG7ZJ23Xr6PNBCQEB1a4
+vzcnqUbk1ftU8qbOoTEEElEEeGu/gaDYjHPt/P9apngZpV3KXVAepAyRRLdXPfKa
+Shc+gMEf
+-----END CERTIFICATE REQUEST-----
+`
+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 keyFixture = `-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQCo1tBaXZRmI6YlxkMDnHYfZTmbaoLQPc00+/SM06bqyEZMqFws
+fVbwOsO2/k2NFY3VdOl/LRWSuyjI+mxTo8UXNkIaaQKhcoEP928M2uJ3YyBtRHPH
+7Rr4SZR8WHT61U/YPPLTOBv8VlaQzodPRVDwoBtKaVEFy2K6MNXVEFQ6bwIDAQAB
+AoGAWaKZwK/XvhYE+h70qvEgwPAzkjAMvNNio1Nz9GPVROYIdGAZd0Efq6/3Aaqm
+r1UXFJDZ+buMrXaRY4mXgxv54MjkX4d4KRVfAfIRbZQlP1jjnT6eRFhLFGOZe6pT
+FUxMWO8wJDPwuNJgHFZOm+Ja3v2Hgvt6wgqu+l1onx4RNtECQQDZZbPPtgqTIsrv
+BO0PZvL2BLFpg55NUYvpm57JU0/wU+rqG27gVUx7IYGWQ7J61BRIwmMR9HwrDRtE
+EwPDelXnAkEAxtHJqHdElDcJGH0Um1WVjd2U229Imo9FBTbvFjj8uaJwk1MYhwks
+fUcvD6+s0uoKZAdHogQBM7nN5OrtrnjWOQJBAM+Fpf/BZpbNv6oqqaDqRUNTd4eh
+fJuSHF0DkK/eN5DSioyvY0gCJN/lPC6UsOtPR42tAaVCHMV73Ws+O3l+bkECQGvT
+pRGHtZrIildMpttjvBtXe/7SSMcCQoWEeIBN4cpvraxI2bmKoSVEcOKJ/SnaIk6D
+oDbfAyPhdifbvZHtGQkCQQCDH0Jo3JY7TlOripsIWm8hyikOzw9Lfonhbvnaofjt
+amR9w6/SM5D0y20NqMVCmJxHWYW9sRIfZOmRjprYbczH
+-----END RSA PRIVATE KEY-----
+`
+
+func TestMain(m *testing.M) {
+	pkey, _ = GeneratePrivateKey(1024)
+	os.Exit(m.Run())
+}
+
+func TestGetCertificateRequest(t *testing.T) {
+
+	res, err := GetCertificateRequest("FR", "ORG", "ORGUNIT", "test@go.tld", pkey)
+
+	if res == nil || err != nil {
+		t.Fatal(err)
+	}
+
+	if res[0] != '-' {
+		t.Fatalf("Bad format\n%s", res)
+	}
+}
+
+func TestPEMToCertificateRequest(t *testing.T) {
+
+	res, err := PEMToCertificateRequest([]byte(csrFixture))
+
+	if res == nil || err != nil {
+		t.Fatal(err)
+	}
+
+	if res.Subject.Country[0] != "FR" {
+		t.Fatal("Wrong country: ", res.Subject.Country)
+	}
+
+	if res.Subject.CommonName != "test@go.tld" {
+		t.Fatal("Wrong CN: ", res.Subject.CommonName)
+	}
+
+}
+
+func TestGetSelfSignedCertificate(t *testing.T) {
+	res, err := GetSelfSignedCertificate(10, 20, "FR", "TEST", "TEST UNIT", "My Cn", pkey)
+
+	if res == nil || err != nil {
+		t.Fatal(err)
+	}
+
+	if res[0] != '-' {
+		t.Fatalf("Bad format\n%s", res)
+	}
+
+}
+
+func TestPEMToCertificate(t *testing.T) {
+
+	res, err := PEMToCertificate([]byte(crtFixture))
+
+	if res == nil || err != nil {
+		t.Fatal(err)
+	}
+
+	if res.Subject.CommonName != "CA" {
+		t.Fatal("Wrong CN: ", res.Subject.CommonName)
+	}
+
+	if res.Issuer.CommonName != "CA" {
+		t.Fatal("Wrong issuer: ", res.Issuer.CommonName)
+	}
+
+}
+
+func TestGetCertificate(t *testing.T) {
+
+	req, _ := PEMToCertificateRequest([]byte(csrFixture))
+	crt, _ := PEMToCertificate([]byte(crtFixture))
+	key, _ := PEMToPrivateKey([]byte(keyFixture))
+
+	res, err := GetCertificate(10, 21, req, crt, key)
+
+	if res == nil || err != nil {
+		t.Fatal(err)
+	}
+
+	if res[0] != '-' {
+		t.Fatalf("Bad format\n%s", res)
+	}
+
+}
+
+func ExampleGetCertificate() {
+
+	// Load elements from PEM files
+	certificateRequest, _ := PEMToCertificateRequest([]byte(csrFixture))
+	signerCertificate, _ := PEMToCertificate([]byte(crtFixture))
+	signerKey, _ := PEMToPrivateKey([]byte(keyFixture))
+
+	// Generate the certificate for 365 days with a serial of 0x10 (16)
+	cert, err := GetCertificate(365, int64(0x10), certificateRequest, signerCertificate, signerKey)
+
+	if cert == nil || err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Certificate generated")
+	}
+
+	// Check certificate validity
+
+	roots := x509.NewCertPool()
+	roots.AddCert(signerCertificate)
+
+	signeeCertificate, _ := PEMToCertificate(cert)
+
+	_, err = signeeCertificate.Verify(x509.VerifyOptions{
+		Roots: roots,
+	})
+
+	if err != nil {
+		fmt.Println(err)
+	} else {
+		fmt.Println("Certificate authenticated")
+	}
+
+	// Output:
+	// Certificate generated
+	// Certificate authenticated
+
+}
diff --git a/auth/privkey.go b/auth/privkey.go
new file mode 100644
index 0000000000000000000000000000000000000000..b866b10750154ae9a844fe4d310849cf231eecff
--- /dev/null
+++ b/auth/privkey.go
@@ -0,0 +1,76 @@
+// Package auth provides simple ways to handle user authentication
+package auth
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+)
+
+// Cipher used to encrypt private key content in PEM format
+var Cipher = x509.PEMCipherAES256
+
+// GeneratePrivateKey builds a private key of given size from default random
+func GeneratePrivateKey(bits int) (*rsa.PrivateKey, error) {
+	return rsa.GenerateKey(rand.Reader, bits)
+}
+
+// PrivateKeyToEncryptedPEM builds a PEM-encoded array of bytes from a private key and a password.
+// If pwd is empty, then the resulting PEM will not be encrypted.
+func PrivateKeyToEncryptedPEM(key *rsa.PrivateKey, pwd string) ([]byte, error) {
+	var err error
+
+	block := &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(key),
+	}
+
+	if pwd != "" {
+		block, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, []byte(pwd), Cipher)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return pem.EncodeToMemory(block), nil
+}
+
+// PrivateKeyToPEM produces a unencrypted PEM-encoded array of bytes from a private key.
+func PrivateKeyToPEM(key *rsa.PrivateKey) []byte {
+	p, _ := PrivateKeyToEncryptedPEM(key, "")
+	return p
+}
+
+// EncryptedPEMToPrivateKey tries to decrypt and decode a PEM-encoded array of bytes to a private key.
+// If pwd is empty, then the function will not try to decrypt the PEM block.
+//
+// In case of wrong password, the returned error will be equals to x509.IncorrectPasswordError
+func EncryptedPEMToPrivateKey(data []byte, pwd string) (*rsa.PrivateKey, error) {
+	var err error
+
+	block, _ := pem.Decode(data)
+	decodedData := block.Bytes
+
+	if pwd != "" {
+		decodedData, err = x509.DecryptPEMBlock(block, []byte(pwd))
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return x509.ParsePKCS1PrivateKey(decodedData)
+
+}
+
+// PEMToPrivateKey tries to decode a plain PEM-encoded array of bytes to a private key.
+func PEMToPrivateKey(data []byte) (*rsa.PrivateKey, error) {
+	return EncryptedPEMToPrivateKey(data, "")
+}
+
+// IsPEMEncrypted tests whether a PEM-encoded array of bytes is encrypted or not.
+func IsPEMEncrypted(data []byte) bool {
+	var block, _ = pem.Decode(data)
+	return x509.IsEncryptedPEMBlock(block)
+}
diff --git a/auth/privkey_test.go b/auth/privkey_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c5c3b6768f2cb0225e03079a083b4f21ddf00f34
--- /dev/null
+++ b/auth/privkey_test.go
@@ -0,0 +1,110 @@
+package auth
+
+import (
+	"crypto/x509"
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestGeneratePrivateKey(t *testing.T) {
+	_, err := GeneratePrivateKey(512)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !testing.Short() {
+		_, err = GeneratePrivateKey(2048)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+}
+
+func TestPrivateKeyToPEM(t *testing.T) {
+	key, _ := GeneratePrivateKey(512)
+	res := PrivateKeyToPEM(key)
+
+	if res[0] != '-' {
+		t.Fatalf("Bad format\n%s", res)
+	}
+
+	if IsPEMEncrypted(res) {
+		t.Fatal("Result is encrypted")
+	}
+}
+
+func TestPrivateKeyToEncryptedPEM(t *testing.T) {
+	key, _ := GeneratePrivateKey(512)
+	res, err := PrivateKeyToEncryptedPEM(key, "password")
+
+	if res[0] != '-' {
+		t.Fatalf("Bad format\n%s", res)
+	}
+
+	if !IsPEMEncrypted(res) || err != nil {
+		t.Fatal("Result is not encrypted: ", err)
+	}
+}
+
+func TestPEMToPrivateKey(t *testing.T) {
+	key, _ := GeneratePrivateKey(512)
+	key2, err := PEMToPrivateKey(PrivateKeyToPEM(key))
+	if !reflect.DeepEqual(key, key2) || err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestEncryptedPEMToPrivateKey(t *testing.T) {
+	key, _ := GeneratePrivateKey(512)
+	res, _ := PrivateKeyToEncryptedPEM(key, "password")
+
+	goodKey, err := EncryptedPEMToPrivateKey(res, "password")
+
+	if !reflect.DeepEqual(key, goodKey) || err != nil {
+		t.Fatal(err)
+	}
+
+	badKey, err := EncryptedPEMToPrivateKey(res, "badpass")
+
+	if badKey != nil || err != x509.IncorrectPasswordError {
+		t.Fatal(err)
+	}
+}
+
+func ExampleEncryptedPEMToPrivateKey() {
+
+	// Generate a new private key for example
+	key, err := GeneratePrivateKey(512)
+
+	if err != nil {
+		panic(err)
+	}
+
+	// Get the encrypted PEM data
+
+	p, err := PrivateKeyToEncryptedPEM(key, "myPassword")
+
+	if err != nil {
+		panic(err)
+	}
+
+	// Reverse the process
+
+	newKey, err := EncryptedPEMToPrivateKey(p, "badPassword")
+
+	if err == x509.IncorrectPasswordError {
+		fmt.Println("Bad password")
+	}
+
+	newKey, err = EncryptedPEMToPrivateKey(p, "myPassword")
+
+	if newKey != nil && err == nil {
+		fmt.Println("OK")
+	}
+
+	// Output:
+	// Bad password
+	// OK
+}