Skip to content

Commit

Permalink
Verification by CRL
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielius1922 committed Nov 4, 2024
1 parent 5ad88cc commit 983b8cc
Show file tree
Hide file tree
Showing 50 changed files with 987 additions and 225 deletions.
1 change: 0 additions & 1 deletion certificate-authority/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ apis:
keyFile: "/secrets/private/cert.key"
certFile: "/secrets/private/cert.crt"
clientCertificateRequired: true

authorization:
ownerClaim: "sub"
audience: ""
Expand Down
4 changes: 2 additions & 2 deletions certificate-authority/service/grpc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func (c *CRLConfig) Validate() error {
if !c.Enabled {
return nil
}
if c.ExpiresIn <= time.Minute {
return fmt.Errorf("expiresIn('%v') - less than %v", c.ExpiresIn, time.Minute)
if c.ExpiresIn < time.Second*10 { // TODO: make configurable for tests
return fmt.Errorf("expiresIn('%v') - less than %v", c.ExpiresIn, time.Second*10)
}
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions certificate-authority/service/grpc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ func TestCRLConfigValidate(t *testing.T) {
},
},
{
name: "Enabled CRLConfig with ExpiresIn less than 1 minute",
name: "Enabled CRLConfig with ExpiresIn less than 10 seconds",
input: grpc.CRLConfig{
Enabled: true,
ExpiresIn: 30 * time.Second,
ExpiresIn: 9 * time.Second,
},
wantErr: true,
},
Expand Down
5 changes: 3 additions & 2 deletions certificate-authority/service/grpc/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import (
"github.com/plgd-dev/hub/v2/pkg/log"
"github.com/plgd-dev/hub/v2/pkg/net/grpc/server"
"github.com/plgd-dev/hub/v2/pkg/security/jwt/validator"
pkgX509 "github.com/plgd-dev/hub/v2/pkg/security/x509"
"go.opentelemetry.io/otel/trace"
)

type Service struct {
*server.Server
}

func New(config Config, clientApplicationServer *CertificateAuthorityServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) {
func New(config Config, clientApplicationServer *CertificateAuthorityServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider, customDistributionPointCRLVerification pkgX509.CustomDistributionPointVerification) (*Service, error) {
opts, err := server.MakeDefaultOptions(server.NewAuth(validator), logger, tracerProvider)
if err != nil {
return nil, fmt.Errorf("cannot create grpc server options: %w", err)
}
server, err := server.New(config.BaseConfig, fileWatcher, logger, tracerProvider, opts...)
server, err := server.New(config.BaseConfig, fileWatcher, logger, tracerProvider, customDistributionPointCRLVerification, opts...)
if err != nil {
return nil, err
}
Expand Down
5 changes: 2 additions & 3 deletions certificate-authority/service/grpc/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ func (s *Signer) sign(ctx context.Context, isIdentityCertificate bool, csr []byt
}),
}
if s.IsCRLEnabled() {
opts = append(opts, certificateSigner.WithCRLDistributionPoints(
[]string{path.Join(s.crl.serverAddress, uri.SigningRevocationListBase, s.issuerID)},
))
dp := s.crl.serverAddress + path.Join(uri.SigningRevocationListBase, s.issuerID)
opts = append(opts, certificateSigner.WithCRLDistributionPoints([]string{dp}))
}
signer, err := s.newCertificateSigner(isIdentityCertificate, opts...)
if err != nil {
Expand Down
77 changes: 72 additions & 5 deletions certificate-authority/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package service

import (
"context"
"crypto/x509"
"errors"
"fmt"
"math/big"
"strings"
"time"

"github.com/go-co-op/gocron/v2"
grpcService "github.com/plgd-dev/hub/v2/certificate-authority/service/grpc"
httpService "github.com/plgd-dev/hub/v2/certificate-authority/service/http"
"github.com/plgd-dev/hub/v2/certificate-authority/service/uri"
"github.com/plgd-dev/hub/v2/certificate-authority/store"
storeConfig "github.com/plgd-dev/hub/v2/certificate-authority/store/config"
"github.com/plgd-dev/hub/v2/certificate-authority/store/cqldb"
Expand All @@ -17,9 +21,11 @@ import (
"github.com/plgd-dev/hub/v2/pkg/fn"
"github.com/plgd-dev/hub/v2/pkg/fsnotify"
"github.com/plgd-dev/hub/v2/pkg/log"
pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri"
"github.com/plgd-dev/hub/v2/pkg/net/listener"
otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client"
"github.com/plgd-dev/hub/v2/pkg/security/jwt/validator"
pkgX509 "github.com/plgd-dev/hub/v2/pkg/security/x509"
"github.com/plgd-dev/hub/v2/pkg/service"
"go.opentelemetry.io/otel/trace"
)
Expand Down Expand Up @@ -84,6 +90,58 @@ func newStore(ctx context.Context, config StorageConfig, fileWatcher *fsnotify.W
return db, fl.ToFunction(), nil
}

func errVerifyDistributionPoint(err error) error {
return fmt.Errorf("failed to verify distribution point: %w", err)
}

func getIssuerFromEndpoint(addr string) (string, error) {
prefix := uri.SigningRevocationListBase + "/"
var issuerID string
if strings.HasPrefix(addr, prefix) {
issuerID = strings.TrimPrefix(addr, prefix)
// ignore everything after next "/"
if len(issuerID) > 36 && issuerID[36] == '/' {
issuerID = issuerID[:36]
}
}
// uuid string is 36 chars long
if len(issuerID) != 36 {
return "", errVerifyDistributionPoint(fmt.Errorf("invalid issuerID(%s)", issuerID))
}
return issuerID, nil
}

func getVerifyCertificateByCRLFromStorage(s store.Store, logger log.Logger) func(ctx context.Context, certificate *x509.Certificate, endpoint string) error {
return func(ctx context.Context, certificate *x509.Certificate, endpoint string) error {
// get issuer from endpoint /certificate-authority/api/v1/signing/crl/{$issuerID}
issuerID, err := getIssuerFromEndpoint(endpoint)
if err != nil {
return err
}

rl, err := s.GetRevocationList(ctx, issuerID, false)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return nil
}
return errVerifyDistributionPoint(err)
}

for _, revoked := range rl.Certificates {
serial := &big.Int{}
_, ok := serial.SetString(revoked.Serial, 10)
if !ok {
logger.Debugf("invalid serial number: %s", revoked.Serial)
continue
}
if certificate.SerialNumber.Cmp(serial) == 0 {
return pkgX509.ErrRevoked
}
}
return nil
}
}

func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger) (*service.Service, error) {
otelClient, err := otelClient.New(ctx, config.Clients.OpenTelemetryCollector, "certificate-authority", fileWatcher, logger)
if err != nil {
Expand All @@ -100,13 +158,22 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg
}
closerFn.AddFunc(closeStore)

ca, err := grpcService.NewCertificateAuthorityServer(config.APIs.GRPC.Authorization.OwnerClaim, config.HubID, config.APIs.HTTP.ExternalAddress, config.Signer, dbStorage, fileWatcher, logger)
externalAddress := pkgHttpUri.CanonicalURI(config.APIs.HTTP.ExternalAddress)
ca, err := grpcService.NewCertificateAuthorityServer(config.APIs.GRPC.Authorization.OwnerClaim, config.HubID, externalAddress, config.Signer, dbStorage, fileWatcher, logger)
if err != nil {
closerFn.Execute()
return nil, fmt.Errorf("cannot create grpc certificate authority server: %w", err)
}
closerFn.AddFunc(ca.Close)
httpValidator, err := validator.New(ctx, config.APIs.GRPC.Authorization.Config, fileWatcher, logger, tracerProvider)

var customDistributionPointCRLVerification pkgX509.CustomDistributionPointVerification
crlEnabled := externalAddress != "" && dbStorage.SupportsRevocationList()
if crlEnabled {
customDistributionPointCRLVerification = pkgX509.CustomDistributionPointVerification{
externalAddress: getVerifyCertificateByCRLFromStorage(dbStorage, logger),
}
}
httpValidator, err := validator.New(ctx, config.APIs.GRPC.Authorization.Config, fileWatcher, logger, tracerProvider, validator.WithCustomDistributionPointVerification(customDistributionPointCRLVerification))
if err != nil {
closerFn.Execute()
return nil, fmt.Errorf("cannot create http validator: %w", err)
Expand All @@ -119,19 +186,19 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg
},
Authorization: config.APIs.GRPC.Authorization.Config,
Server: config.APIs.HTTP.Server,
CRLEnabled: config.Signer.CRL.Enabled,
CRLEnabled: crlEnabled,
}, dbStorage, ca, httpValidator, fileWatcher, logger, tracerProvider)
if err != nil {
closerFn.Execute()
return nil, fmt.Errorf("cannot create http service: %w", err)
}
grpcValidator, err := validator.New(ctx, config.APIs.GRPC.Authorization.Config, fileWatcher, logger, tracerProvider)
grpcValidator, err := validator.New(ctx, config.APIs.GRPC.Authorization.Config, fileWatcher, logger, tracerProvider, validator.WithCustomDistributionPointVerification(customDistributionPointCRLVerification))
if err != nil {
closerFn.Execute()
_ = httpService.Close()
return nil, fmt.Errorf("cannot create grpc validator: %w", err)
}
grpcService, err := grpcService.New(config.APIs.GRPC, ca, grpcValidator, fileWatcher, logger, tracerProvider)
grpcService, err := grpcService.New(config.APIs.GRPC, ca, grpcValidator, fileWatcher, logger, tracerProvider, customDistributionPointCRLVerification)
if err != nil {
closerFn.Execute()
_ = httpService.Close()
Expand Down
136 changes: 136 additions & 0 deletions certificate-authority/service/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package service_test

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"testing"
"time"

"github.com/plgd-dev/device/v2/pkg/security/generateCertificate"
pbCA "github.com/plgd-dev/hub/v2/certificate-authority/pb"
caTest "github.com/plgd-dev/hub/v2/certificate-authority/test"
"github.com/plgd-dev/hub/v2/pkg/config/database"
pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc"
"github.com/plgd-dev/hub/v2/test"
"github.com/plgd-dev/hub/v2/test/config"
oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test"
testService "github.com/plgd-dev/hub/v2/test/service"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

func getSigningRecords(ctx context.Context, t *testing.T, addr string, certificates []tls.Certificate) error {
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{
RootCAs: test.GetRootCertificatePool(t),
Certificates: certificates,
})),
)
require.NoError(t, err)
caClient := pbCA.NewCertificateAuthorityClient(conn)
_, err = caClient.GetSigningRecords(ctx, &pbCA.GetSigningRecordsRequest{})
return err
}

func getNewCertificate(ctx context.Context, t *testing.T, addr string, pk *ecdsa.PrivateKey, certificates []tls.Certificate) ([]byte, error) {
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{
RootCAs: test.GetRootCertificatePool(t),
Certificates: certificates,
})),
)
require.NoError(t, err)
caClient := pbCA.NewCertificateAuthorityClient(conn)

var cfg generateCertificate.Configuration
cfg.Subject.CommonName = "test"
csr, err := generateCertificate.GenerateCSR(cfg, pk)
require.NoError(t, err)

resp, err := caClient.SignCertificate(ctx, &pbCA.SignCertificateRequest{CertificateSigningRequest: csr})
if err != nil {
return nil, err
}
return resp.GetCertificate(), nil
}

func TestGetSigningRecords(t *testing.T) {
if config.ACTIVE_DATABASE() == database.CqlDB {
t.Skip("revocation list not supported for CqlDB")
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
shutdown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth)
defer shutdown()

// start insecure ca
caCfg := caTest.MakeConfig(t)
// CRL list should be valid for 10 sec after it is issued
caCfg.Signer.CRL.Enabled = true
caCfg.Signer.CRL.ExpiresIn = time.Hour
err := caCfg.Validate()
require.NoError(t, err)
caShutdown := caTest.New(t, caCfg)
defer caShutdown()

pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// get certificate - insecure
ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t))
certData1, err := getNewCertificate(ctx, t, config.CERTIFICATE_AUTHORITY_HOST, pk, nil)
require.NoError(t, err)
caShutdown()
b, err := x509.MarshalECPrivateKey(pk)
require.NoError(t, err)
key := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
crt1, err := tls.X509KeyPair(certData1, key)
require.NoError(t, err)

// start secure ca
caCfg.APIs.GRPC.TLS.ClientCertificateRequired = true
caCfg.APIs.GRPC.TLS.CRL.Enabled = true
httpClientConfig := config.MakeHttpClientConfig()
caCfg.APIs.GRPC.TLS.CRL.HTTP = &httpClientConfig
caCfg.Signer.CRL.Enabled = false // generate cert without distribution point
err = caCfg.Validate()
require.NoError(t, err)
caShutdown = caTest.New(t, caCfg)
defer caShutdown()

// second ca on different port
const ca_addr = "localhost:30011"
caCfg2 := caTest.MakeConfig(t)
caCfg2.APIs.GRPC = config.MakeGrpcServerConfig(ca_addr)
caCfg2.APIs.GRPC.TLS.ClientCertificateRequired = true
caCfg2.APIs.GRPC.TLS.CRL.Enabled = true
caCfg2.APIs.GRPC.TLS.CRL.HTTP = &httpClientConfig
caCfg2.APIs.HTTP.ExternalAddress = "https://localhost:30012"
caCfg2.APIs.HTTP.Addr = "localhost:30012"
err = caCfg2.Validate()
require.NoError(t, err)
caShutdown2 := caTest.New(t, caCfg2)
defer caShutdown2()

err = getSigningRecords(ctx, t, ca_addr, []tls.Certificate{crt1})
require.NoError(t, err)

certData2, err := getNewCertificate(ctx, t, config.CERTIFICATE_AUTHORITY_HOST, pk, []tls.Certificate{crt1})
require.NoError(t, err)
crt2, err := tls.X509KeyPair(certData2, key)
require.NoError(t, err)

// use crt2 without distribution point
_, err = getNewCertificate(ctx, t, config.CERTIFICATE_AUTHORITY_HOST, pk, []tls.Certificate{crt2})
require.NoError(t, err)

// try use revoked crt1
err = getSigningRecords(ctx, t, config.CERTIFICATE_AUTHORITY_HOST, []tls.Certificate{crt1})
require.Error(t, err)
}
5 changes: 3 additions & 2 deletions certificate-authority/store/mongodb/revocationList.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/google/uuid"
"github.com/plgd-dev/hub/v2/certificate-authority/store"
"github.com/plgd-dev/hub/v2/pkg/mongodb"
pkgTls "github.com/plgd-dev/hub/v2/pkg/security/tls"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
Expand Down Expand Up @@ -98,7 +99,7 @@ func (s *Store) getRevocationListUpdate(ctx context.Context, query *store.Update
s.logger.Debugf("skipping duplicate serial number(%v)", c.Serial)
delete(cmap, c.Serial)
}
if len(cmap) == 0 && (!query.UpdateIfExpired || !rl.IsExpired()) {
if len(cmap) == 0 && (!query.UpdateIfExpired || !pkgTls.IsExpired(rl.ValidUntil)) {
return revocationListUpdate{
originalRevocationList: rl.RevocationList,
}, false, nil
Expand Down Expand Up @@ -229,7 +230,7 @@ func (s *Store) GetLatestIssuedOrIssueRevocationList(ctx context.Context, issuer
if err != nil {
return nil, err
}
if rl.IssuedAt > 0 && !rl.IsExpired() {
if rl.IssuedAt > 0 && !pkgTls.IsExpired(rl.ValidUntil) {
return rl, nil
}
issuedAt := time.Now()
Expand Down
8 changes: 0 additions & 8 deletions certificate-authority/store/revocationList.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"math/big"
"time"

"github.com/google/uuid"
)
Expand Down Expand Up @@ -59,13 +58,6 @@ func ParseBigInt(s string) (*big.Int, error) {
return &number, nil
}

// IsExpired checks whether the revocation list is expired
func (rl *RevocationList) IsExpired() bool {
// the crl is expiring soon, so we treat it as already expired
const delta = time.Minute
return rl.ValidUntil <= time.Now().Add(delta).UnixNano()
}

func (rl *RevocationList) Validate() error {
if _, err := uuid.Parse(rl.Id); err != nil {
return fmt.Errorf("invalid ID(%v): %w", rl.Id, err)
Expand Down
2 changes: 1 addition & 1 deletion cloud2cloud-gateway/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg
}
closeListener := func() {
if errC := listener.Close(); errC != nil {
logger.Errorf("cannot close http listener: %w", errC)
logger.Errorf("cannot close http listener: %v", errC)
}
}

Expand Down
Loading

0 comments on commit 983b8cc

Please sign in to comment.