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/spiffeproject/
├── 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.0mkdir -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/spiffeproject/
├── 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