Skip to content

Latest commit

 

History

History
1173 lines (998 loc) · 31.2 KB

File metadata and controls

1173 lines (998 loc) · 31.2 KB
mkdir -p project/internal/domain
mkdir -p project/internal/ports/input
mkdir -p project/internal/ports/output
mkdir -p project/internal/ports/network
mkdir -p project/internal/ports/spiffe
mkdir -p project/internal/adapters/input
mkdir -p project/internal/adapters/output
mkdir -p project/internal/adapters/network
mkdir -p project/internal/adapters/spiffe
project/
├── go.mod
├── main_server.go
├── main_client.go
└── internal/
    ├── domain/
    │   ├── server_service.go
    │   └── client_service.go
    ├── ports/
    │   ├── input/
    │   │   └── config_provider.go
    │   ├── output/
    │   │   └── logger.go
    │   ├── network/
    │   │   ├── tls_listener.go
    │   │   └── tls_dialer.go
    │   └── spiffe/
    │       └── source_provider.go
    └── adapters/
        ├── input/
        │   └── static_config.go
        ├── output/
        │   └── log_adapter.go
        ├── network/
        │   └── spiffe_tls_adapter.go
        └── spiffe/
            └── workload_source.go

internal/ports/input/config_provider.go

package input

import (
    "context"
    "net"
    "time"
)

// ConfigProvider provides configuration values.
type ConfigProvider interface {
    GetSocketPath() string
    GetServerAddress() string
    GetClientID() string
    GetServerID() string
    GetTimeout() time.Duration
}

internal/ports/output/logger.go

package output

import "fmt"

// Logger is the output port for logging messages.
type Logger interface {
    Printf(format string, v ...interface{})
    Fatal(err error)
}

internal/ports/network/tls_listener.go

package network

import "net"

// TLSListener is the port for mTLS server listening.
type TLSListener interface {
    net.Listener
    Close() error
}

internal/ports/network/tls_dialer.go

package network

import (
    "context"
    "net"
)

// TLSDialer is the port for mTLS client dialing.
type TLSDialer interface {
    DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}

internal/ports/spiffe/source_provider.go

package spiffe

import "context"

// SourceProvider provides SPIFFE source options for mTLS.
type SourceProvider interface {
    GetSourceOptions(ctx context.Context) []interface{}
}

internal/adapters/input/static_config.go

package input

import "time"

// StaticConfigProvider is the adapter for static configuration.
type StaticConfigProvider struct{}

// NewStaticConfigProvider creates a new StaticConfigProvider.
func NewStaticConfigProvider() ConfigProvider {
    return &StaticConfigProvider{}
}

// GetSocketPath implements ConfigProvider.
func (s *StaticConfigProvider) GetSocketPath() string {
    return "unix:///tmp/agent.sock"
}

// GetServerAddress implements ConfigProvider.
func (s *StaticConfigProvider) GetServerAddress() string {
    return "localhost:55555"
}

// GetClientID implements ConfigProvider.
func (s *StaticConfigProvider) GetClientID() string {
    return "spiffe://example.org/client"
}

// GetServerID implements ConfigProvider.
func (s *StaticConfigProvider) GetServerID() string {
    return "spiffe://example.org/server"
}

// GetTimeout implements ConfigProvider.
func (s *StaticConfigProvider) GetTimeout() time.Duration {
    return 3 * time.Second
}

internal/adapters/output/log_adapter.go

package output

import (
    "fmt"
    "log"

    ports "project/internal/ports/output"
)

// LogAdapter is the adapter for standard logging.
type LogAdapter struct{}

// NewLogAdapter creates a new LogAdapter.
func NewLogAdapter() ports.Logger {
    return &LogAdapter{}
}

// Printf implements Logger.
func (l *LogAdapter) Printf(format string, v ...interface{}) {
    log.Printf(format, v...)
}

// Fatal implements Logger.
func (l *LogAdapter) Fatal(err error) {
    log.Fatal(err)
}

internal/adapters/network/spiffe_tls_adapter.go

package network

import (
    "context"
    "fmt"
    "net"

    "github.com/spiffe/go-spiffe/v2/spiffeid"
    "github.com/spiffe/go-spiffe/v2/spiffetls"
    "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
    "github.com/spiffe/go-spiffe/v2/workloadapi"

    ports "project/internal/ports/network"
    spiffeports "project/internal/ports/spiffe"
)

// SpiffeTLSAdapter provides mTLS implementations.
type SpiffeTLSAdapter struct {
    source spiffeports.SourceProvider
}

// NewSpiffeTLSAdapter creates a new SpiffeTLSAdapter.
func NewSpiffeTLSAdapter(source spiffeports.SourceProvider) *SpiffeTLSAdapter {
    return &SpiffeTLSAdapter{source: source}
}

// Listen creates a TLS listener.
func (s *SpiffeTLSAdapter) Listen(ctx context.Context, network, addr string, authorizeID string, sourceOpts []interface{}) (ports.TLSListener, error) {
    clientID := spiffeid.RequireFromString(authorizeID)
    listener, err := spiffetls.ListenWithMode(ctx, network, addr,
        spiffetls.MTLSServerWithSourceOptions(
            append([]interface{}{tlsconfig.AuthorizeID(clientID)}, sourceOpts...),
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("unable to create TLS listener: %w", err)
    }
    return listener, nil
}

// Dial creates a TLS connection.
func (s *SpiffeTLSAdapter) Dial(ctx context.Context, network, addr string, authorizeID string, sourceOpts []interface{}) (net.Conn, error) {
    serverID := spiffeid.RequireFromString(authorizeID)
    conn, err := spiffetls.DialWithMode(ctx, network, addr,
        spiffetls.MTLSClientWithSourceOptions(
            append([]interface{}{tlsconfig.AuthorizeID(serverID)}, sourceOpts...),
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("unable to create TLS connection: %w", err)
    }
    return conn, nil
}

internal/adapters/spiffe/workload_source.go

package spiffe

import (
    "context"

    "github.com/spiffe/go-spiffe/v2/workloadapi"

    ports "project/internal/ports/spiffe"
)

// WorkloadSourceProvider is the adapter for workload API source.
type WorkloadSourceProvider struct {
    socketPath string
}

// NewWorkloadSourceProvider creates a new WorkloadSourceProvider.
func NewWorkloadSourceProvider(socketPath string) ports.SourceProvider {
    return &WorkloadSourceProvider{socketPath: socketPath}
}

// GetSourceOptions implements SourceProvider.
func (w *WorkloadSourceProvider) GetSourceOptions(ctx context.Context) []interface{} {
    return []interface{}{
        workloadapi.WithClientOptions(workloadapi.WithAddr(w.socketPath)),
    }
}

internal/domain/server_service.go

package domain

import (
    "bufio"
    "fmt"
    "net"

    "project/internal/ports/input"
    "project/internal/ports/network"
    "project/internal/ports/output"
    "project/internal/ports/spiffe"
)

// ServerService is the core domain service for the mTLS server.
type ServerService struct {
    config   input.ConfigProvider
    logger   output.Logger
    listener network.TLSListener
}

// NewServerService creates a new ServerService.
func NewServerService(
    config input.ConfigProvider,
    logger output.Logger,
    tlsAdapter *network.SpiffeTLSAdapter,
    source spiffe.SourceProvider,
) (*ServerService, error) {
    sourceOpts := source.GetSourceOptions(context.Background())
    listener, err := tlsAdapter.Listen(
        context.Background(),
        "tcp",
        config.GetServerAddress(),
        config.GetClientID(),
        sourceOpts,
    )
    if err != nil {
        return nil, err
    }
    return &ServerService{
        config:   config,
        logger:   logger,
        listener: listener,
    }, nil
}

// Run starts the server loop.
func (s *ServerService) Run() error {
    defer s.listener.Close()
    for {
        conn, err := s.listener.Accept()
        if err != nil {
            return fmt.Errorf("failed to accept connection: %w", err)
        }
        go s.handleConnection(conn)
    }
}

func (s *ServerService) handleConnection(conn net.Conn) {
    defer conn.Close()
    req, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        s.logger.Printf("Error reading incoming data: %v", err)
        return
    }
    s.logger.Printf("Client says: %q", req)
    if _, err = conn.Write([]byte("Hello client\n")); err != nil {
        s.logger.Printf("Unable to send response: %v", err)
        return
    }
}

internal/domain/client_service.go

package domain

import (
    "bufio"
    "context"
    "fmt"
    "io"

    "project/internal/ports/input"
    "project/internal/ports/network"
    "project/internal/ports/output"
    "project/internal/ports/spiffe"
)

// ClientService is the core domain service for the mTLS client.
type ClientService struct {
    config input.ConfigProvider
    logger output.Logger
}

// NewClientService creates a new ClientService.
func NewClientService(
    config input.ConfigProvider,
    logger output.Logger,
    tlsAdapter *network.SpiffeTLSAdapter,
    source spiffe.SourceProvider,
) (*ClientService, error) {
    return &ClientService{
        config: config,
        logger: logger,
    }, nil
}

// Run executes the client operation.
func (c *ClientService) Run(tlsAdapter *network.SpiffeTLSAdapter, source spiffe.SourceProvider) error {
    ctx, cancel := context.WithTimeout(context.Background(), c.config.GetTimeout())
    defer cancel()

    sourceOpts := source.GetSourceOptions(ctx)
    conn, err := tlsAdapter.Dial(
        ctx,
        "tcp",
        c.config.GetServerAddress(),
        c.config.GetServerID(),
        sourceOpts,
    )
    if err != nil {
        return err
    }
    defer conn.Close()

    fmt.Fprintf(conn, "Hello server\n")
    status, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil && err != io.EOF {
        return fmt.Errorf("unable to read server response: %w", err)
    }
    c.logger.Printf("Server says: %q", status)
    return nil
}

main_server.go

package main

import (
    "context"

    "project/internal/adapters/input"
    "project/internal/adapters/network"
    "project/internal/adapters/output"
    "project/internal/adapters/spiffe"
    "project/internal/domain"
)

func main() {
    config := input.NewStaticConfigProvider()
    logger := output.NewLogAdapter()
    source := spiffe.NewWorkloadSourceProvider(config.GetSocketPath())
    tlsAdapter := network.NewSpiffeTLSAdapter(source)

    service, err := domain.NewServerService(config, logger, tlsAdapter, source)
    if err != nil {
        logger.Fatal(err)
    }

    if err := service.Run(); err != nil {
        logger.Fatal(err)
    }
}

main_client.go

package main

import (
    "context"

    "project/internal/adapters/input"
    "project/internal/adapters/network"
    "project/internal/adapters/output"
    "project/internal/adapters/spiffe"
    "project/internal/domain"
)

func main() {
    config := input.NewStaticConfigProvider()
    logger := output.NewLogAdapter()
    source := spiffe.NewWorkloadSourceProvider(config.GetSocketPath())
    tlsAdapter := network.NewSpiffeTLSAdapter(source)

    service, err := domain.NewClientService(config, logger, tlsAdapter, source)
    if err != nil {
        logger.Fatal(err)
    }

    if err := service.Run(tlsAdapter, source); err != nil {
        logger.Fatal(err)
    }
}

go.mod

module project

go 1.21

require github.com/spiffe/go-spiffe/v2 v2.0.0

Inmemory

mkdir -p project/internal/domain
mkdir -p project/internal/ports/input
mkdir -p project/internal/ports/output
mkdir -p project/internal/ports/network
mkdir -p project/internal/ports/spiffe
mkdir -p project/internal/adapters/input
mkdir -p project/internal/adapters/output
mkdir -p project/internal/adapters/network
mkdir -p project/internal/adapters/spiffe
project/
├── go.mod
├── main_server.go
├── main_client.go
└── internal/
    ├── domain/
    │   ├── server_service.go
    │   └── client_service.go
    ├── ports/
    │   ├── input/
    │   │   └── config_provider.go
    │   ├── output/
    │   │   └── logger.go
    │   ├── network/
    │   │   ├── tls_listener.go
    │   │   └── tls_dialer.go
    │   └── spiffe/
    │       └── source_provider.go
    └── adapters/
        ├── input/
        │   └── static_config.go
        ├── output/
        │   └── log_adapter.go
        ├── network/
        │   └── spiffe_tls_adapter.go
        └── spiffe/
            ├── mock_workload.go
            └── workload_source.go

internal/ports/input/config_provider.go

package input

import (
    "context"
    "net"
    "time"
)

// ConfigProvider provides configuration values.
type ConfigProvider interface {
    GetSocketPath() string
    GetServerAddress() string
    GetClientID() string
    GetServerID() string
    GetTimeout() time.Duration
}

internal/ports/output/logger.go

package output

import "fmt"

// Logger is the output port for logging messages.
type Logger interface {
    Printf(format string, v ...interface{})
    Fatal(err error)
}

internal/ports/network/tls_listener.go

package network

import (
    "context"
    "net"

    "github.com/spiffe/go-spiffe/v2/svid/x509svid"
)

// TLSListener is the port for mTLS server listening.
type TLSListener interface {
    net.Listener
    Close() error
}

// TLSListenerFactory creates TLS listeners.
type TLSListenerFactory interface {
    CreateServer(ctx context.Context, network, addr string, peerID string, source x509svid.Source) (TLSListener, error)
}

internal/ports/network/tls_dialer.go

package network

import (
    "context"
    "net"

    "github.com/spiffe/go-spiffe/v2/svid/x509svid"
)

// TLSDialer is the port for mTLS client dialing.
type TLSDialer interface {
    Dial(ctx context.Context, network, addr string, peerID string, source x509svid.Source) (net.Conn, error)
}

internal/ports/spiffe/source_provider.go

package spiffe

import (
    "context"

    "github.com/spiffe/go-spiffe/v2/svid/x509svid"
)

// SourceProvider provides SPIFFE X.509 source.
type SourceProvider interface {
    GetX509Source(ctx context.Context) (x509svid.Source, error)
}

internal/adapters/input/static_config.go

package input

import "time"

// StaticConfigProvider is the adapter for static configuration.
type StaticConfigProvider struct{}

// NewStaticConfigProvider creates a new StaticConfigProvider.
func NewStaticConfigProvider() ConfigProvider {
    return &StaticConfigProvider{}
}

// GetSocketPath implements ConfigProvider.
func (s *StaticConfigProvider) GetSocketPath() string {
    return "unix:///tmp/agent.sock"
}

// GetServerAddress implements ConfigProvider.
func (s *StaticConfigProvider) GetServerAddress() string {
    return "localhost:55555"
}

// GetClientID implements ConfigProvider.
func (s *StaticConfigProvider) GetClientID() string {
    return "spiffe://example.org/client"
}

// GetServerID implements ConfigProvider.
func (s *StaticConfigProvider) GetServerID() string {
    return "spiffe://example.org/server"
}

// GetTimeout implements ConfigProvider.
func (s *StaticConfigProvider) GetTimeout() time.Duration {
    return 3 * time.Second
}

internal/adapters/output/log_adapter.go

package output

import (
    "fmt"
    "log"

    ports "project/internal/ports/output"
)

// LogAdapter is the adapter for standard logging.
type LogAdapter struct{}

// NewLogAdapter creates a new LogAdapter.
func NewLogAdapter() ports.Logger {
    return &LogAdapter{}
}

// Printf implements Logger.
func (l *LogAdapter) Printf(format string, v ...interface{}) {
    log.Printf(format, v...)
}

// Fatal implements Logger.
func (l *LogAdapter) Fatal(err error) {
    log.Fatal(err)
}

internal/adapters/network/spiffe_tls_adapter.go

package network

import (
    "context"
    "fmt"
    "net"

    "github.com/spiffe/go-spiffe/v2/spiffeid"
    "github.com/spiffe/go-spiffe/v2/spiffetls"
    "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
    "github.com/spiffe/go-spiffe/v2/svid/x509svid"

    ports "project/internal/ports/network"
)

// SpiffeTLSAdapter provides mTLS implementations.
type SpiffeTLSAdapter struct{}

// NewSpiffeTLSAdapter creates a new SpiffeTLSAdapter.
func NewSpiffeTLSAdapter() *SpiffeTLSAdapter {
    return &SpiffeTLSAdapter{}
}

// CreateServer creates a TLS listener for server.
func (s *SpiffeTLSAdapter) CreateServer(ctx context.Context, network, addr string, peerID string, source x509svid.Source) (ports.TLSListener, error) {
    clientID := spiffeid.RequireFromString(peerID)
    listener, err := spiffetls.ListenWithMode(ctx, network, addr,
        spiffetls.MTLSServerWithSource(
            tlsconfig.AuthorizeID(clientID),
            source,
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("unable to create TLS listener: %w", err)
    }
    return listener, nil
}

// Dial creates a TLS connection for client.
func (s *SpiffeTLSAdapter) Dial(ctx context.Context, network, addr string, peerID string, source x509svid.Source) (net.Conn, error) {
    serverID := spiffeid.RequireFromString(peerID)
    conn, err := spiffetls.DialWithMode(ctx, network, addr,
        spiffetls.MTLSClientWithSource(
            tlsconfig.AuthorizeID(serverID),
            source,
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("unable to create TLS connection: %w", err)
    }
    return conn, nil
}

internal/adapters/spiffe/workload_source.go

package spiffe

import (
    "context"

    "github.com/spiffe/go-spiffe/v2/svid/x509svid"
    "github.com/spiffe/go-spiffe/v2/workloadapi"
)

// WorkloadSourceProvider is the adapter for workload API source (for production).
type WorkloadSourceProvider struct {
    socketPath string
}

// NewWorkloadSourceProvider creates a new WorkloadSourceProvider.
func NewWorkloadSourceProvider(socketPath string) *WorkloadSourceProvider {
    return &WorkloadSourceProvider{socketPath: socketPath}
}

// GetX509Source implements SourceProvider.
func (w *WorkloadSourceProvider) GetX509Source(ctx context.Context) (x509svid.Source, error) {
    return workloadapi.NewX509Source(ctx, workloadapi.WithAddr(w.socketPath))
}

internal/adapters/spiffe/mock_workload.go

package spiffe

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "encoding/pem"
    "errors"

    "github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
    "github.com/spiffe/go-spiffe/v2/spiffeid"
    "github.com/spiffe/go-spiffe/v2/svid/x509svid"
)

// MockSourceProvider is the in-memory mock for SPIFFE source.
type MockSourceProvider struct {
    localID string
}

// NewMockSourceProvider creates a new MockSourceProvider for the given local SPIFFE ID.
func NewMockSourceProvider(localID string) *MockSourceProvider {
    return &MockSourceProvider{localID: localID}
}

// GetX509Source implements SourceProvider.
func (m *MockSourceProvider) GetX509Source(ctx context.Context) (x509svid.Source, error) {
    var certPEM, keyPEM string
    switch m.localID {
    case "spiffe://example.org/client":
        certPEM = `-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIUS423Bl2c4wNTz3B1NwHKdbeGHv0wDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHTW9jayBDQTAeFw0yNTEwMDIwNDUzNDZaFw0yNjEwMDIw
NDUzNDZaMBExDzANBgNVBAMMBkNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAM3/4OyDsQKFNtFoo8cvtLLRmOoNicMr8OENBs1Ad8O3yn3aqtfU
taN1CaXXtc60dPbGhXv/Xlh1M7psSR8pdbTSPEAqufJp1P2KLzXbiVWmqtqfctgr
C/hW2HJ/Ashz5dJnQcsB3oMvVlLDgpHM9kKxFQW9eI7M9UrCWYgzFlSvyhAEUEf5
tQw7TONTI17XIAIdVViCqBVoZb6F982SypgABKuhRFl4f4rahGmV5mMMuR/T4lr8
ZGECb3kPhqMCEujqrriiBt3AU/79GrPiut7yOGr9v1EPMwzIzisiUey1Yj90rAUi
Eq/J/IukVteEPKL6BqoCuAZo4/VzMqCix3sCAwEAAaMqMCgwJgYDVR0RBB8wHYYb
c3BpZmZlOi8vZXhhbXBsZS5vcmcvY2xpZW50MA0GCSqGSIb3DQEBCwUAA4IBAQAH
q9zM2efWt6GJWA4ZVdZsMxFYcVaMJ4WjjHq0E6VT/IuK+NnhG2bhnuo26pzcAxFI
fUAHIXvWaem1wQPVGt7S0jwE1iwDm3ppJ0JDAIxsrL3hwzKR+4YuHpe6CZCCO04X
IEZuJu7qMn8psRXjTSRMhpcFsjuOhp4SA8sTcZS1jqgQexsyar4q9eMXuNadU1jf
QM/mFQaBGFjplxQLfmhWjl8/a+Hddg0yY5P399pHrwQKJV2qzEyfcqIC+yMSk3Wd
H2yGwOMjyazEJ3JzshO8yeSc9QDXEW5JTAzU7Ay5XtptOn7339TN5Rq85Q6iaIpP
kypC1VvHyxDyiz0s/kNM
-----END CERTIFICATE-----`
        keyPEM = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDN/+Dsg7EChTbR
aKPHL7Sy0ZjqDYnDK/DhDQbNQHfDt8p92qrX1LWjdQml17XOtHT2xoV7/15YdTO6
bEkfKXW00jxAKrnyadT9ii8124lVpqran3LYKwv4VthyfwLIc+XSZ0HLAd6DL1ZS
w4KRzPZCsRUFvXiOzPVKwlmIMxZUr8oQBFBH+bUMO0zjUyNe1yACHVVYgqgVaGW+
hffNksqYAASroURZeH+K2oRpleZjDLkf0+Ja/GRhAm95D4ajAhLo6q64ogbdwFP+
/Rqz4rre8jhq/b9RDzMMyM4rIlHstWI/dKwFIhKvyfyLpFbXhDyi+gaqArgGaOP1
czKgosd7AgMBAAECggEAFPjH3UxK6QbG3erZQZ4Jt2q/NpTKQaRnkYHR/3Ngvfwo
EUgBvJdbJpjzsn6Tc8fLYJQ1YrpM0JSd/4ympRWDdjUxDRdMdvFvTaCTzdfb02x0
WG2DM56uSUoOHxnvHpkt/I2mlRRLou9eym49DLrhF0kP6tWvQFp9Xm8KzmLlExnj
cO+aBs8RLsjcikT3X+j01MoDhBpY5fuWwVfevyOdE05EYc16UwD+nlAloIwbpRXC
DefsfgEWjuOIHAA54DAXZSHSj/Jt25vYZnzD8m3PhiEbmteCSW1qxrs/s3s+G3DV
ogMqQCOvcHMsu+8d8mi7ZjmRTnWGMc7XM1c5ulaNgQKBgQD8W/WJOZ1rVD/1eP4d
UFc3xOD9NIzmfLMbF59c8UsDQ0NfReaMQStXztNFV1d276wCGRGFKeN7IK9vrs2t
0o/wbF68uL0RnpbaNl5CxwPjwMrsFuoxwDDdHLB4KbZlp8oUyWklGjwFTigpq1Nn
O5+eXj7SyKccKhOu89bzMUlyrwKBgQDQ+LLib5+C5QaSOwDa5VrkB0GNAkboQpa4
qMuW6HfPC9xGPSPYnRXXVsfc0beFgXGhZiVrFZByxIqYq9sZQkAd/R0lgAKChWOk
qvRj7Wq8kYtMe9xpTzENVmL8bG+LoQvMblcuY03RrG2/4RbZcx5o5f+r5gHMsSqA
bvJOuQHa9QKBgHcndGe2rH1g6lIdLt7ocEYEz7a+eUja2DaSkMxxodmIiOcTOig/
HRf4sAitlL6zq6PR8lPN03rvk+ZHwxWvtgbvJEw3bg5DszaVNSySi2OeRDH1H98d
v/Sm9yvFhjm5X9EwyFScbnqSzKPgISE9JcDA/yjHVSAXPfP9biClxOhBAoGAJ97L
gph5fqfj0RKg3yLTbJ5Vy2mbzcSmrSTHc0t3UHGa3Wvc4v9Vo3hTObZppXdFDt5G
VWAP51XIe8iOTQtu4EnivbzmvbBlio1zg31Q2BmM0lLvgsgxO7I3wKGPxrw7XA3R
ZHBd8JtNdUak7WghbTI0zb9cqIlE6qxAXvS9feECgYEA1g3iac2xc0h6bDQVbnDx
emmJZaDEiayBjLK0rbbQ4rFN35Zvt5AvfWKZ2XPC5oxmISiPPeGyXSy6hLfPUXjf
jCXXdcMb2UkChEWZk4qV6Z0y0Aiz9Gco07s3c3NxSffor0HFHZd4Gvv2mCYdfvUN
f9Q9BYHJcypeSMHEx76Fcq4=
-----END PRIVATE KEY-----`
    case "spiffe://example.org/server":
        certPEM = `-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIUJ6VMAUIMvO4TFJAvQoOEVvSElccwDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHTW9jayBDQTAeFw0yNTEwMDIwNDUzNDZaFw0yNjEwMDIw
NDUzNDZaMBExDzANBgNVBAMMBkNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAM/CAL91aIUeVGoXKeCui2F1ySJSdLsf08IRqzeV1PE3zvIwk7Lp
9VYkblzul9gfWv8jbGPB1QFOZiTowrx56CPnBPXZ199k8/JeLbD0XQiOUwokYlZG
lCfnG/HBQHAutxrSk4ud5lf7BofoWQleijLS48UfUrJR5Yd/0FBbYAU7aLsn3SPY
egbR9w5Ej7kUTn89AvxBYUjTgCAYPeJ2+1pOb7yD+rk8XPswvn9C7y53Ws2Y6uN1
Q78ARA4h4nhAJKzWcGqBLT+eXhBTTatrihHEXVbaiRLrWv467VhqC4H1WllfZCq8
uaLX7vEJq2n2Zaon1YGXcHcgu+YUYrmfwaMCAwEAAaMqMCgwJgYDVR0RBB8wHYYb
c3BpZmZlOi8vZXhhbXBsZS5vcmcvc2VydmVyMA0GCSqGSIb3DQEBCwUAA4IBAQC7
p9I/S4As3FihpjtMtxyRqjZnRieG1rudqBFs/kdC7d3Nw0XLe13Fa9f0+LEt4TqF
Pqd+dLw+ZX0OqpvsHFW74GlCKIqkBL/Oabzm6r2bMFST53xuchBdwWz5ShOjGVzQ
QvCKdc3gxt9xYTQWNFOWdcOuHkFAaisWs0I7KwaLT6hCAaC5VEulNcJ5g80ZwX//
oj9RSaiawA4EF8gl+rRb/pekKyP/oDgH8PTIkvzaQTiZeWo+VLq4SsmCun8ABFY1
0gOz8BTHZHgLWABlBUX+ZcqVKq48bRvkVrBnTOt2xT3ARXZbWKxdaerWnV/A5Xrq
Y8KDPcC2P+pGwMNd/RGh
-----END CERTIFICATE-----`
        keyPEM = `-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDPwgC/dWiFHlRq
FyngrothdckiUnS7H9PCEas3ldTxN87yMJOy6fVWJG5c7pfYH1r/I2xjwdUBTmYk
6MK8eegj5wT12dffZPPyXi2w9F0IjlMKJGJWRpQn5xvxwUBwLrca0pOLneZX+waH
6FkJXooy0uPFH1KyUeWHf9BQW2AFO2i7J90j2HoG0fcORI+5FE5/PQL8QWFI04Ag
GD3idvtaTm+8g/q5PFz7ML5/Qu8ud1rNmOrjdUO/AEQOIeJ4QCSs1nBqgS0/nl4Q
U02ra4oRxF1W2okS61r+Ou1YaguB9VpZX2QqvLmi1+7xCatp9mWqJ9WBl3B3ILvm
FGK5n8GjAgMBAAECggEAGVuFedjbs/u6s+Q14U2Mh423ye+kfb5puzMq4bLw9awp
HxhcvqadojD6I2+fqupQ0flB3n8m/2zRPMoIErcSZWdqDJPVLFgHClsCWqnIvSWZ
7w/3zK36oVAs8fnzYn3hLW3sQks4dR6XcnkifOlEFuX0lPe4wTkLudkY1OYCN0EL
wCxLuCYNMC1QUfukKpuwxETnPFgGJ4A9su9BiVELdXlhM+Cpj/DdKNs/aKyIrKOj
OlG7Pw/QOVjRmnJdexVPA/oRQQaDV6iMALUryLk6Q4NKG12Pbtt6fMrgkqIf8jrF
Ar5cvZtdYWQ24niGpGLRQei97hVfmRsN9AwmdPlmIQKBgQDn4Oj/OxtB4oGqDB7A
qwsV8lVZ8YuyiZNtxcPC+FRcQHiArZe90BIkdmp5rqbJn1HP6qcKLVzhy97YC6du
UivjW9EA4dwf6iAGBVSrmuHUKYLzNy2S8DBIs7QikUPbV/ujXroBukz6Ov4+2j90
epiMm82gnXZjCJB81tFXW0GvcQKBgQDlXr2bKqSIj4LRuDALmTGpdjmCqT/LwgWg
QMKfceCrHilQiMQv1XgE7+ktOv46UwBLS8JdoSmn12G4M+S9xo3NnOZqmiiLdMwH
Seix0F6aAhCnEgVEVw9kFa0fBzLoPVjSrZm4Nf5sAtOhWGKMzfjCNsYAUGvCjyeX
YyxOMGHgUwKBgQCIo51JQbBHO82Cwg+hI2mt7hZjoRXcCHwK9L3t913m2srnsOO5
I5J9UBZgTA3Ww/520eTK935DRFq13vAz71ITcTiRF4cae2GRirShRuyKlEepQLun
WchqHxMoR4fACnGrjJd5iynvTrIhoYRBBDk+qlUar49ivlo+ZmwH2Su00QKBgQC0
auc9jDdLa4b9K6IvHVsg7uuhyTtzLLR8LzRnco3ES783p7yZ25HCYGvLtiGPAway
M285crFvYk1tNPRyCiI0X3wm0geeWkq6WPp+E2a2QXWT1OrrrIdgtGQuid0TLF4X
qZHGgkIT9807lmg+VS2cprTsPlXk/fGodMu+3PPAHQKBgQDiovmlxeRYLUPF7MJx
EST7fIcMFWMUxGcXPA5CEfjFbXlDSbdwnZZ8w/UoIr9ZNYfGfgNu6WzURq2KNdFQ
AlFKwS9p2l30fXe/7l5E7CZ6lehAFr32svPkrSrokubTIARYV529AqFg9Tc0lnYb
XZDiITy5b/8fJ6c3Z17/N8Ey3A==
-----END PRIVATE KEY-----`
    default:
        return nil, errors.New("unsupported local ID")
    }

    caCertPEM := `-----BEGIN CERTIFICATE-----
MIICxTCCAa2gAwIBAgIUfRAIyICv4R8MTpBdBqgRJFWzW+8wDQYJKoZIhvcNAQEL
BQAwEjEQMA4GA1UEAwwHTW9jayBDQTAeFw0yNTEwMDIwNDUzNDZaFw0yNjEwMDIw
NDUzNDZaMBIxEDAOBgNVBAMMB01vY2sgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDc1bKoPT15RBEbtU2mJs/4kauFT6gnQR2s8K+dFZpIs2tIl9iJ
MpH830S8M40QKtENTcgErKkJ0S4LPhA8q2rcVpMMkayj8j237DkRg7zU6xCqSnhv
Lt+2sqWf9zcEpuMj8B1r9pB/ChTtM9Xrweid6VLy93BHCahr7o4h5awWf+m7FCvQ
IDTezufssZP8QbDfj5fLzO8Hg+KFjzydafNibiYf3X86UcpYcR477iLFJkM90ozF
Nvsr481OrzdCdFfaL5qlc44OuHZUh2XbBNlHzB8Q+lcAnVLE7aeJ78bkGPDAzblw
YrsDbbePrPAui2sX85/Zab6o9XYjMcq94357AgMBAAGjEzARMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ/xwTipqaf8q4fPJbd0i6px7qpeViNZ
IAhQ9yYhE/sR6fekrINLqEWZZSlHkIBpqjsfeXvgT7IPmQDosp722Y8ovLrOwP4q
CwEOG1vk8tMJLItIeCNNNs+h8bjNc2zdUNfe776iWHCDKb9ksVJhF7JFotGYTIJN
TNMAv+Yo3q6p1SVxPvkuW9xLgrVyfVrGL8EZE0+h6q3FJCWbQ0UmvFfOttmBM9FW
mZlHQ9HHZRtNKH9o+X6hRCzqphtPAiwRZqcearcN7dyEuhFSupx1LdiGptVZH1r1
TlERgKfKths2rrdsl8daWB4xG4St4BipnbJKb/nVwdKgUknnRIX9ywc=
-----END CERTIFICATE-----`

    tlsCert, err := tls.LoadX509KeyPair(certPEM, keyPEM)
    if err != nil {
        return nil, err
    }

    svidInst, err := x509svid.NewX509SVID(spiffeid.RequireFromString(m.localID), tlsCert)
    if err != nil {
        return nil, err
    }

    caCertBlock, _ := pem.Decode([]byte(caCertPEM))
    if caCertBlock == nil {
        return nil, errors.New("failed to decode CA cert")
    }
    caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
    if err != nil {
        return nil, err
    }

    trustDomain := spiffeid.RequireTrustDomain("example.org")
    bundleInst, err := x509bundle.New(trustDomain, []*x509.Certificate{caCert})
    if err != nil {
        return nil, err
    }

    return mockX509Source{
        svid:    svidInst,
        bundle:  bundleInst,
    }, nil
}

type mockX509Source struct {
    svid   *x509svid.X509SVID
    bundle *x509bundle.Bundle
}

func (m mockX509Source) GetX509SVID() (*x509svid.X509SVID, error) {
    return m.svid, nil
}

func (m mockX509Source) GetX509Bundle() (*x509bundle.Bundle, error) {
    return m.bundle, nil
}

internal/domain/server_service.go

package domain

import (
    "bufio"
    "fmt"
    "net"

    "project/internal/ports/input"
    "project/internal/ports/network"
    "project/internal/ports/output"
    "project/internal/ports/spiffe"
)

// ServerService is the core domain service for the mTLS server.
type ServerService struct {
    config   input.ConfigProvider
    logger   output.Logger
    listener network.TLSListener
}

// NewServerService creates a new ServerService.
func NewServerService(
    config input.ConfigProvider,
    logger output.Logger,
    listenerFactory network.TLSListenerFactory,
    source spiffe.SourceProvider,
) (*ServerService, error) {
    ctx := context.Background()
    sourceX509, err := source.GetX509Source(ctx)
    if err != nil {
        return nil, err
    }
    listener, err := listenerFactory.CreateServer(
        ctx,
        "tcp",
        config.GetServerAddress(),
        config.GetClientID(),
        sourceX509,
    )
    if err != nil {
        return nil, err
    }
    return &ServerService{
        config:   config,
        logger:   logger,
        listener: listener,
    }, nil
}

// Run starts the server loop.
func (s *ServerService) Run() error {
    defer s.listener.Close()
    for {
        conn, err := s.listener.Accept()
        if err != nil {
            return fmt.Errorf("failed to accept connection: %w", err)
        }
        go s.handleConnection(conn)
    }
}

func (s *ServerService) handleConnection(conn net.Conn) {
    defer conn.Close()
    req, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        s.logger.Printf("Error reading incoming data: %v", err)
        return
    }
    s.logger.Printf("Client says: %q", req)
    if _, err = conn.Write([]byte("Hello client\n")); err != nil {
        s.logger.Printf("Unable to send response: %v", err)
        return
    }
}

internal/domain/client_service.go

package domain

import (
    "bufio"
    "context"
    "fmt"
    "io"

    "project/internal/ports/input"
    "project/internal/ports/network"
    "project/internal/ports/output"
    "project/internal/ports/spiffe"
)

// ClientService is the core domain service for the mTLS client.
type ClientService struct {
    config input.ConfigProvider
    logger output.Logger
}

// NewClientService creates a new ClientService.
func NewClientService(
    config input.ConfigProvider,
    logger output.Logger,
) *ClientService {
    return &ClientService{
        config: config,
        logger: logger,
    }
}

// Run executes the client operation.
func (c *ClientService) Run(
    dialer network.TLSDialer,
    source spiffe.SourceProvider,
) error {
    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, c.config.GetTimeout())
    defer cancel()

    sourceX509, err := source.GetX509Source(ctx)
    if err != nil {
        return err
    }

    conn, err := dialer.Dial(
        ctx,
        "tcp",
        c.config.GetServerAddress(),
        c.config.GetServerID(),
        sourceX509,
    )
    if err != nil {
        return err
    }
    defer conn.Close()

    fmt.Fprintf(conn, "Hello server\n")
    status, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil && err != io.EOF {
        return fmt.Errorf("unable to read server response: %w", err)
    }
    c.logger.Printf("Server says: %q", status)
    return nil
}

main_server.go

package main

import (
    "context"

    "project/internal/adapters/input"
    "project/internal/adapters/network"
    "project/internal/adapters/output"
    "project/internal/adapters/spiffe"
    "project/internal/domain"
)

func main() {
    config := input.NewStaticConfigProvider()
    logger := output.NewLogAdapter()
    source := spiffe.NewMockSourceProvider(config.GetServerID())
    tlsAdapter := network.NewSpiffeTLSAdapter()

    service, err := domain.NewServerService(config, logger, tlsAdapter, source)
    if err != nil {
        logger.Fatal(err)
    }

    if err := service.Run(); err != nil {
        logger.Fatal(err)
    }
}

main_client.go

package main

import (
    "context"

    "project/internal/adapters/input"
    "project/internal/adapters/network"
    "project/internal/adapters/output"
    "project/internal/adapters/spiffe"
    "project/internal/domain"
)

func main() {
    config := input.NewStaticConfigProvider()
    logger := output.NewLogAdapter()
    source := spiffe.NewMockSourceProvider(config.GetClientID())
    tlsAdapter := network.NewSpiffeTLSAdapter()

    service := domain.NewClientService(config, logger)

    if err := service.Run(tlsAdapter, source); err != nil {
        logger.Fatal(err)
    }
}

go.mod

module project

go 1.21

require github.com/spiffe/go-spiffe/v2 v2.1.0