@@ -18,11 +18,17 @@ package ffdx
1818
1919import (
2020 "context"
21+ "crypto/x509"
2122 "encoding/json"
23+ "encoding/pem"
24+ "errors"
2225 "fmt"
2326 "io"
2427 "strings"
2528 "sync"
29+ "time"
30+
31+ "github.com/hyperledger/firefly/internal/metrics"
2632
2733 "github.com/go-resty/resty/v2"
2834 "github.com/hyperledger/firefly-common/pkg/config"
@@ -54,6 +60,8 @@ type FFDX struct {
5460 retry * retry.Retry
5561 backgroundStart bool
5662 backgroundRetry * retry.Retry
63+
64+ metrics metrics.Manager // optional
5765}
5866
5967type dxNode struct {
@@ -168,7 +176,7 @@ func (h *FFDX) Name() string {
168176 return "ffdx"
169177}
170178
171- func (h * FFDX ) Init (ctx context.Context , cancelCtx context.CancelFunc , config config.Section ) (err error ) {
179+ func (h * FFDX ) Init (ctx context.Context , cancelCtx context.CancelFunc , config config.Section , metrics metrics. Manager ) (err error ) {
172180 h .ctx = log .WithLogField (ctx , "dx" , "https" )
173181 h .cancelCtx = cancelCtx
174182 h .ackChannel = make (chan * ack )
@@ -179,6 +187,7 @@ func (h *FFDX) Init(ctx context.Context, cancelCtx context.CancelFunc, config co
179187 }
180188 h .needsInit = config .GetBool (DataExchangeInitEnabled )
181189 h .nodes = make (map [string ]* dxNode )
190+ h .metrics = metrics
182191
183192 if config .GetString (ffresty .HTTPConfigURL ) == "" {
184193 return i18n .NewError (ctx , coremsgs .MsgMissingPluginConfig , "url" , "dataexchange.ffdx" )
@@ -295,6 +304,11 @@ func (h *FFDX) beforeConnect(ctx context.Context, w wsclient.WSClient) error {
295304 return fmt .Errorf ("DX returned non-ready status: %s" , status .Status )
296305 }
297306 }
307+
308+ for _ , cb := range h .callbacks .handlers {
309+ cb .DXConnect (h )
310+ }
311+
298312 h .initialized = true
299313 return nil
300314}
@@ -448,6 +462,95 @@ func (h *FFDX) TransferBlob(ctx context.Context, nsOpID string, peer, sender fft
448462 return nil
449463}
450464
465+ func (h * FFDX ) CheckNodeIdentityStatus (ctx context.Context , node * core.Identity ) error {
466+ if err := h .checkInitialized (ctx ); err != nil {
467+ return err
468+ }
469+
470+ if node == nil {
471+ return i18n .NewError (ctx , coremsgs .MsgNodeNotProvidedForCheck )
472+ }
473+
474+ var mismatchState = metrics .NodeIdentityDXCertMismatchStatusUnknown
475+ defer func () {
476+ if h .metrics != nil && h .metrics .IsMetricsEnabled () {
477+ h .metrics .NodeIdentityDXCertMismatch (node .Namespace , mismatchState )
478+ }
479+ log .L (ctx ).Debugf ("Identity status checked against DX node='%s' mismatch_state='%s'" , node .Name , mismatchState )
480+ }()
481+
482+ dxPeer , err := h .GetEndpointInfo (ctx , node .Name ) // should be the same as the local node
483+ if err != nil {
484+ return err
485+ }
486+
487+ dxPeerCert := dxPeer .GetString ("cert" )
488+ // if this occurs, it is either a misconfigured / broken DX or likely a DX that is compatible from an API perspective
489+ // but does not have the same peer info as the HTTPS mTLS DX
490+ if dxPeerCert == "" {
491+ log .L (ctx ).Debugf ("DX peer does not have a 'cert', DX plugin may be unsupported" )
492+ return nil
493+ }
494+
495+ expiry , err := extractSoonestExpiryFromCertBundle (strings .ReplaceAll (dxPeerCert , `\n` , "\n " ))
496+ if err == nil {
497+ if expiry .Before (time .Now ()) {
498+ log .L (ctx ).Warnf ("DX certificate for node '%s' has expired" , node .Name )
499+ }
500+
501+ if h .metrics != nil && h .metrics .IsMetricsEnabled () {
502+ h .metrics .NodeIdentityDXCertExpiry (node .Namespace , expiry )
503+ }
504+ } else {
505+ log .L (ctx ).Errorf ("Failed to find x509 cert within DX cert bundle node='%s' namespace='%s'" , node .Name , node .Namespace )
506+ }
507+
508+ if node .Profile == nil {
509+ return i18n .NewError (ctx , coremsgs .MsgNodeNotProvidedForCheck )
510+ }
511+
512+ nodeCert := node .Profile .GetString ("cert" )
513+ if nodeCert != "" {
514+ mismatchState = metrics .NodeIdentityDXCertMismatchStatusHealthy
515+ if dxPeerCert != nodeCert {
516+ log .L (ctx ).Warnf ("DX certificate for node '%s' is out-of-sync with on-chain identity" , node .Name )
517+ mismatchState = metrics .NodeIdentityDXCertMismatchStatusMismatched
518+ }
519+ }
520+
521+ return nil
522+ }
523+
524+ // We assume the cert with the soonest expiry is the leaf cert, but even if its the CA,
525+ // that's what will invalidate the leaf anyways, so really we only care about the soonest expiry.
526+ // So we loop through the bundle finding the soonest expiry, not necessarily the leaf.
527+ func extractSoonestExpiryFromCertBundle (certBundle string ) (time.Time , error ) {
528+ var expiringCert * x509.Certificate
529+ var block * pem.Block
530+ var rest = []byte (certBundle )
531+
532+ for {
533+ block , rest = pem .Decode (rest )
534+ if block == nil {
535+ break
536+ }
537+
538+ cert , err := x509 .ParseCertificate (block .Bytes )
539+ if err != nil {
540+ return time.Time {}, fmt .Errorf ("failed to parse non-certificate within bundle: %v" , err )
541+ }
542+ if expiringCert == nil || cert .NotAfter .Before (expiringCert .NotAfter ) {
543+ expiringCert = cert
544+ }
545+ }
546+
547+ if expiringCert == nil {
548+ return time.Time {}, errors .New ("no valid certificate found" )
549+ }
550+
551+ return expiringCert .NotAfter .UTC (), nil
552+ }
553+
451554func (h * FFDX ) ackLoop () {
452555 for {
453556 select {
0 commit comments