307 lines
9.3 KiB
Go
307 lines
9.3 KiB
Go
// Copyright (c) Mainflux
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package certs
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/mainflux/mainflux"
|
|
"github.com/mainflux/mainflux/certs/pki"
|
|
"github.com/mainflux/mainflux/pkg/errors"
|
|
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
|
)
|
|
|
|
var (
|
|
// ErrNotFound indicates a non-existent entity request.
|
|
ErrNotFound = errors.New("non-existent entity")
|
|
|
|
// ErrMalformedEntity indicates malformed entity specification.
|
|
ErrMalformedEntity = errors.New("malformed entity specification")
|
|
|
|
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
|
// when accessing a protected resource.
|
|
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
|
|
|
errFailedKeyCreation = errors.New("failed to create client private key")
|
|
errFailedDateSetting = errors.New("failed to set date for certificate")
|
|
errKeyBitsValueWrong = errors.New("missing RSA bits for certificate creation")
|
|
errMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
|
errFailedSerialGeneration = errors.New("failed to generate certificate serial")
|
|
errFailedPemKeyWrite = errors.New("failed to write PEM key")
|
|
errFailedPemDataWrite = errors.New("failed to write pem data for certificate")
|
|
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
|
|
errPrivateKeyEmpty = errors.New("private key is empty")
|
|
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
|
errFailedCertCreation = errors.New("failed to create client certificate")
|
|
errFailedCertRevocation = errors.New("failed to revoke certificate")
|
|
)
|
|
|
|
var _ Service = (*certsService)(nil)
|
|
|
|
// Service specifies an API that must be fulfilled by the domain service
|
|
// implementation, and all of its decorators (e.g. logging & metrics).
|
|
type Service interface {
|
|
// IssueCert issues certificate for given thing id if access is granted with token
|
|
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
|
|
|
|
// ListCerts lists all certificates issued for given owner
|
|
ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error)
|
|
|
|
// RevokeCert revokes certificate for given thing
|
|
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
|
|
}
|
|
|
|
// Config defines the service parameters
|
|
type Config struct {
|
|
LogLevel string
|
|
ClientTLS bool
|
|
CaCerts string
|
|
HTTPPort string
|
|
ServerCert string
|
|
ServerKey string
|
|
BaseURL string
|
|
ThingsPrefix string
|
|
JaegerURL string
|
|
AuthURL string
|
|
AuthTimeout time.Duration
|
|
SignTLSCert tls.Certificate
|
|
SignX509Cert *x509.Certificate
|
|
SignRSABits int
|
|
SignHoursValid string
|
|
PKIHost string
|
|
PKIPath string
|
|
PKIRole string
|
|
PKIToken string
|
|
}
|
|
|
|
type certsService struct {
|
|
auth mainflux.AuthServiceClient
|
|
certsRepo Repository
|
|
sdk mfsdk.SDK
|
|
conf Config
|
|
pki pki.Agent
|
|
}
|
|
|
|
// New returns new Certs service.
|
|
func New(auth mainflux.AuthServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
|
|
return &certsService{
|
|
certsRepo: certs,
|
|
sdk: sdk,
|
|
auth: auth,
|
|
conf: config,
|
|
pki: pki,
|
|
}
|
|
}
|
|
|
|
// Revoke defines the conditions to revoke a certificate
|
|
type Revoke struct {
|
|
RevocationTime time.Time `mapstructure:"revocation_time"`
|
|
}
|
|
|
|
// Cert defines the certificate paremeters
|
|
type Cert struct {
|
|
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
|
|
ThingID string `json:"thing_id" mapstructure:"thing_id"`
|
|
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
|
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
|
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
|
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
|
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
|
Serial string `json:"serial" mapstructure:"serial_number"`
|
|
Expire time.Time `json:"expire" mapstructure:"-"`
|
|
}
|
|
|
|
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
|
|
var c Cert
|
|
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
|
if err != nil {
|
|
return c, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
|
|
thing, err := cs.sdk.Thing(thingID, token)
|
|
if err != nil {
|
|
return c, errors.Wrap(errFailedCertCreation, err)
|
|
}
|
|
|
|
// If PKIHost is not set we don't use 3rd party PKI service.
|
|
if cs.conf.PKIHost == "" {
|
|
c.ClientCert, c.ClientKey, err = cs.certs(thing.Key, daysValid, keyBits)
|
|
if err != nil {
|
|
return c, errors.Wrap(errFailedCertCreation, err)
|
|
}
|
|
return c, err
|
|
}
|
|
|
|
cert, err := cs.pki.IssueCert(thingID, daysValid, keyType, keyBits)
|
|
if err != nil {
|
|
return c, errors.Wrap(errFailedCertCreation, err)
|
|
}
|
|
|
|
c.ThingID = thingID
|
|
c.OwnerID = owner.GetEmail()
|
|
c.ClientCert = cert.ClientCert
|
|
c.IssuingCA = cert.IssuingCA
|
|
c.CAChain = cert.CAChain
|
|
c.ClientKey = cert.ClientKey
|
|
c.PrivateKeyType = cert.PrivateKeyType
|
|
c.Serial = cert.Serial
|
|
c.Expire = cert.Expire
|
|
|
|
_, err = cs.certsRepo.Save(context.Background(), c)
|
|
return c, err
|
|
}
|
|
|
|
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
|
var revoke Revoke
|
|
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
|
if err != nil {
|
|
return revoke, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
thing, err := cs.sdk.Thing(thingID, token)
|
|
if err != nil {
|
|
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
|
}
|
|
|
|
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
|
|
if err != nil {
|
|
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
|
}
|
|
|
|
r, err := cs.pki.Revoke(cert.Serial)
|
|
if err != nil {
|
|
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
|
}
|
|
revoke.RevocationTime = r.RevocationTime
|
|
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
|
|
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
|
}
|
|
return revoke, nil
|
|
}
|
|
|
|
func (cs *certsService) ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error) {
|
|
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
|
if err != nil {
|
|
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
|
|
return cs.certsRepo.RetrieveAll(ctx, u.GetEmail(), offset, limit)
|
|
}
|
|
|
|
func (cs *certsService) certs(thingKey, daysValid string, keyBits int) (string, string, error) {
|
|
if cs.conf.SignX509Cert == nil {
|
|
return "", "", errors.Wrap(errFailedCertCreation, errMissingCACertificate)
|
|
}
|
|
if keyBits == 0 {
|
|
return "", "", errors.Wrap(errFailedCertCreation, errKeyBitsValueWrong)
|
|
}
|
|
var priv interface{}
|
|
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedKeyCreation, err)
|
|
}
|
|
|
|
if daysValid == "" {
|
|
daysValid = cs.conf.SignHoursValid
|
|
}
|
|
|
|
notBefore := time.Now()
|
|
validFor, err := time.ParseDuration(daysValid)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedDateSetting, err)
|
|
}
|
|
notAfter := notBefore.Add(validFor)
|
|
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedSerialGeneration, err)
|
|
}
|
|
|
|
tmpl := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{
|
|
Organization: []string{"Mainflux"},
|
|
CommonName: thingKey,
|
|
OrganizationalUnit: []string{"mainflux"},
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
|
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
|
}
|
|
|
|
pubKey, err := publicKey(priv)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedCertCreation, err)
|
|
}
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, cs.conf.SignX509Cert, pubKey, cs.conf.SignTLSCert.PrivateKey)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedCertCreation, err)
|
|
}
|
|
|
|
var bw, keyOut bytes.Buffer
|
|
buffWriter := bufio.NewWriter(&bw)
|
|
buffKeyOut := bufio.NewWriter(&keyOut)
|
|
|
|
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
|
return "", "", errors.Wrap(errFailedPemDataWrite, err)
|
|
}
|
|
buffWriter.Flush()
|
|
cert := bw.String()
|
|
|
|
block, err := pemBlockForKey(priv)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
|
}
|
|
if err := pem.Encode(buffKeyOut, block); err != nil {
|
|
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
|
}
|
|
buffKeyOut.Flush()
|
|
key := keyOut.String()
|
|
|
|
return cert, key, nil
|
|
}
|
|
|
|
func publicKey(priv interface{}) (interface{}, error) {
|
|
if priv == nil {
|
|
return nil, errPrivateKeyEmpty
|
|
}
|
|
switch k := priv.(type) {
|
|
case *rsa.PrivateKey:
|
|
return &k.PublicKey, nil
|
|
case *ecdsa.PrivateKey:
|
|
return &k.PublicKey, nil
|
|
default:
|
|
return nil, errPrivateKeyUnsupportedType
|
|
}
|
|
}
|
|
|
|
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
|
|
switch k := priv.(type) {
|
|
case *rsa.PrivateKey:
|
|
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
|
|
case *ecdsa.PrivateKey:
|
|
b, err := x509.MarshalECPrivateKey(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|