iop/packages/go/auth/auth.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

176 lines
6.1 KiB
Go

// Package auth provides mTLS certificate helpers for IOP transport.
package auth
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
)
// WorkloadIdentity is the authenticated role and logical enrollment name from
// one canonical IOP SPIFFE URI SAN.
type WorkloadIdentity struct {
Role string
Name string
}
// LoadServerTLS builds a *tls.Config for the IOP TCP server with mTLS.
// cert, key: server certificate and private key paths.
// ca: path to the CA certificate that signs client certificates.
func LoadServerTLS(cert, key, ca string) (*tls.Config, error) {
return LoadServerTLSWithIdentity(cert, key, ca, "", "")
}
// LoadHTTPServerTLS builds a server-authenticated TLS 1.3 configuration for a
// principal-authenticated HTTPS boundary. It deliberately does not request a
// client certificate; principal authentication is performed by the handler.
func LoadHTTPServerTLS(cert, key string) (*tls.Config, error) {
serverCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("load server cert: %w", err)
}
return &tls.Config{Certificates: []tls.Certificate{serverCert}, MinVersion: tls.VersionTLS13}, nil
}
// LoadServerTLSWithIdentity builds an mTLS TLS 1.3 server configuration and,
// when peerRole is non-empty, requires the client certificate to contain the
// one exact URI SAN spiffe://iop/<role>/<name>. An empty peerName accepts any
// canonical non-empty name for the required role.
func LoadServerTLSWithIdentity(cert, key, ca, peerRole, peerName string) (*tls.Config, error) {
serverCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("load server cert: %w", err)
}
caCert, err := os.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("read CA cert: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA cert")
}
cfg := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
}
installPeerIdentityVerifier(cfg, peerRole, peerName)
return cfg, nil
}
// LoadClientTLS builds a *tls.Config for an IOP TCP client with mTLS.
func LoadClientTLS(cert, key, ca string) (*tls.Config, error) {
return LoadClientTLSWithIdentity(cert, key, ca, "", "", "")
}
// LoadClientTLSWithIdentity builds an mTLS TLS 1.3 client configuration with
// normal DNS-name validation plus optional exact IOP workload identity checks.
func LoadClientTLSWithIdentity(cert, key, ca, serverName, peerRole, peerName string) (*tls.Config, error) {
clientCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("load client cert: %w", err)
}
caCert, err := os.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("read CA cert: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA cert")
}
cfg := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: pool,
MinVersion: tls.VersionTLS13,
ServerName: strings.TrimSpace(serverName),
}
installPeerIdentityVerifier(cfg, peerRole, peerName)
return cfg, nil
}
func installPeerIdentityVerifier(cfg *tls.Config, role, name string) {
role = strings.TrimSpace(role)
name = strings.TrimSpace(name)
if cfg == nil || role == "" {
return
}
cfg.VerifyConnection = func(state tls.ConnectionState) error {
if len(state.PeerCertificates) == 0 {
return fmt.Errorf("peer certificate is required")
}
identity, err := ParseWorkloadIdentity(state.PeerCertificates[0])
if err != nil {
return err
}
if identity.Role != role || (name != "" && identity.Name != name) {
return fmt.Errorf("peer workload identity mismatch")
}
return nil
}
}
// ParseWorkloadIdentity returns the one canonical
// spiffe://iop/<role>/<name> identity in certificate. Non-IOP URI SANs are
// ignored, while missing, malformed, or multiple IOP identities are rejected.
func ParseWorkloadIdentity(certificate *x509.Certificate) (WorkloadIdentity, error) {
if certificate == nil {
return WorkloadIdentity{}, fmt.Errorf("peer certificate is required")
}
var identity *WorkloadIdentity
for _, uri := range certificate.URIs {
if uri == nil || uri.Scheme != "spiffe" || uri.Host != "iop" {
continue
}
if identity != nil {
return WorkloadIdentity{}, fmt.Errorf("multiple IOP workload identities")
}
if uri.User != nil || uri.Opaque != "" || uri.RawQuery != "" || uri.Fragment != "" || uri.ForceQuery {
return WorkloadIdentity{}, fmt.Errorf("malformed IOP workload identity")
}
segments := strings.Split(strings.TrimPrefix(uri.Path, "/"), "/")
if !strings.HasPrefix(uri.Path, "/") || len(segments) != 2 || !validWorkloadSegment(segments[0]) || !validWorkloadSegment(segments[1]) {
return WorkloadIdentity{}, fmt.Errorf("malformed IOP workload identity")
}
candidate := WorkloadIdentity{Role: segments[0], Name: segments[1]}
if uri.String() != "spiffe://iop/"+candidate.Role+"/"+candidate.Name {
return WorkloadIdentity{}, fmt.Errorf("non-canonical IOP workload identity")
}
identity = &candidate
}
if identity == nil {
return WorkloadIdentity{}, fmt.Errorf("IOP workload identity is required")
}
return *identity, nil
}
func validWorkloadSegment(value string) bool {
return value != "" && value == strings.TrimSpace(value) && value != "." && value != ".." &&
!strings.ContainsAny(value, "/ \t\r\n")
}
// PeerWorkloadIdentity reads the authenticated identity from a completed TLS
// connection. Enrollment handlers call it only after receiving the first
// application request, when the TLS handshake has completed.
func PeerWorkloadIdentity(conn net.Conn) (WorkloadIdentity, error) {
tlsConn, ok := conn.(*tls.Conn)
if !ok {
return WorkloadIdentity{}, fmt.Errorf("TLS peer connection is required")
}
state := tlsConn.ConnectionState()
if !state.HandshakeComplete {
return WorkloadIdentity{}, fmt.Errorf("TLS peer handshake is incomplete")
}
if len(state.PeerCertificates) == 0 {
return WorkloadIdentity{}, fmt.Errorf("peer certificate is required")
}
return ParseWorkloadIdentity(state.PeerCertificates[0])
}