Class: DatagroutConduit::Identity

Inherits:
Object
  • Object
show all
Defined in:
lib/datagrout_conduit/identity.rb

Overview

mTLS client identity for Conduit connections.

Holds the client certificate and private key presented during every TLS handshake. The server verifies the caller’s identity without any application-layer token.

Auto-discovery order (try_discover)

  1. override_dir (if provided)

  2. CONDUIT_MTLS_CERT / CONDUIT_MTLS_KEY env vars (PEM strings)

  3. CONDUIT_IDENTITY_DIR env var → directory with identity.pem + identity_key.pem

  4. ~/.conduit/identity.pem + identity_key.pem

  5. .conduit/ relative to cwd

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cert_pem:, key_pem:, ca_pem: nil, expires_at: nil) ⇒ Identity

Returns a new instance of Identity.



22
23
24
25
26
27
28
29
# File 'lib/datagrout_conduit/identity.rb', line 22

def initialize(cert_pem:, key_pem:, ca_pem: nil, expires_at: nil)
  validate_cert!(cert_pem)
  validate_key!(key_pem)
  @cert_pem = cert_pem
  @key_pem = key_pem
  @ca_pem = ca_pem
  @expires_at = expires_at
end

Instance Attribute Details

#ca_pemObject (readonly)

Returns the value of attribute ca_pem.



20
21
22
# File 'lib/datagrout_conduit/identity.rb', line 20

def ca_pem
  @ca_pem
end

#cert_pemObject (readonly)

Returns the value of attribute cert_pem.



20
21
22
# File 'lib/datagrout_conduit/identity.rb', line 20

def cert_pem
  @cert_pem
end

#expires_atObject (readonly)

Returns the value of attribute expires_at.



20
21
22
# File 'lib/datagrout_conduit/identity.rb', line 20

def expires_at
  @expires_at
end

#key_pemObject (readonly)

Returns the value of attribute key_pem.



20
21
22
# File 'lib/datagrout_conduit/identity.rb', line 20

def key_pem
  @key_pem
end

Class Method Details

.from_envObject

Build from environment variables.

Variables:

  • CONDUIT_MTLS_CERT — PEM string for the client certificate

  • CONDUIT_MTLS_KEY — PEM string for the private key

  • CONDUIT_MTLS_CA — PEM string for the CA (optional)

Returns nil if CONDUIT_MTLS_CERT is not set.

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/datagrout_conduit/identity.rb', line 54

def self.from_env
  cert = ENV["CONDUIT_MTLS_CERT"]
  return nil if cert.nil? || cert.empty?

  key = ENV["CONDUIT_MTLS_KEY"]
  raise ConfigError, "CONDUIT_MTLS_CERT is set but CONDUIT_MTLS_KEY is missing" if key.nil? || key.empty?

  ca = ENV["CONDUIT_MTLS_CA"]
  ca = nil if ca && ca.empty?

  new(cert_pem: cert, key_pem: key, ca_pem: ca)
end

.from_paths(cert_path, key_path, ca_path: nil) ⇒ Object

Build by reading PEM files from disk.



37
38
39
40
41
42
43
44
# File 'lib/datagrout_conduit/identity.rb', line 37

def self.from_paths(cert_path, key_path, ca_path: nil)
  cert_pem = File.read(cert_path)
  key_pem = File.read(key_path)
  ca_pem = ca_path ? File.read(ca_path) : nil
  new(cert_pem: cert_pem, key_pem: key_pem, ca_pem: ca_pem)
rescue Errno::ENOENT => e
  raise ConfigError, "Cannot read identity file: #{e.message}"
end

.from_pem(cert_pem, key_pem, ca_pem: nil) ⇒ Object

Build from PEM strings already in memory.



32
33
34
# File 'lib/datagrout_conduit/identity.rb', line 32

def self.from_pem(cert_pem, key_pem, ca_pem: nil)
  new(cert_pem: cert_pem, key_pem: key_pem, ca_pem: ca_pem)
end

.try_discover(override_dir: nil) ⇒ Object

Walk the auto-discovery chain and return the first identity found, or nil if nothing is available.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/datagrout_conduit/identity.rb', line 69

def self.try_discover(override_dir: nil)
  # 1. Override directory
  if override_dir
    id = try_load_from_dir(override_dir)
    return id if id
  end

  # 2. Environment variables (individual cert/key PEMs)
  id = from_env
  return id if id

  # 3. CONDUIT_IDENTITY_DIR env var
  identity_dir = ENV["CONDUIT_IDENTITY_DIR"]
  if identity_dir && !identity_dir.empty?
    id = try_load_from_dir(identity_dir)
    return id if id
  end

  # 4. ~/.conduit/
  home = ENV["HOME"] || ENV["USERPROFILE"]
  if home
    id = try_load_from_dir(File.join(home, ".conduit"))
    return id if id
  end

  # 5. .conduit/ relative to cwd
  id = try_load_from_dir(File.join(Dir.pwd, ".conduit"))
  return id if id

  nil
rescue ConfigError
  nil
end

Instance Method Details

#configure_ssl(ssl) ⇒ Object

Configure Faraday SSL options with this identity’s mTLS credentials.



132
133
134
135
136
137
138
139
140
# File 'lib/datagrout_conduit/identity.rb', line 132

def configure_ssl(ssl)
  ssl.client_cert = openssl_cert
  ssl.client_key = openssl_key
  if @ca_pem
    store = OpenSSL::X509::Store.new
    store.add_cert(openssl_ca)
    ssl.cert_store = store
  end
end

#needs_rotation?(threshold_days: 30) ⇒ Boolean

Returns true if the certificate expires within threshold_days. Returns false when no expiry is known.

Returns:

  • (Boolean)


109
110
111
112
113
114
# File 'lib/datagrout_conduit/identity.rb', line 109

def needs_rotation?(threshold_days: 30)
  return false if @expires_at.nil?

  deadline = Time.now + (threshold_days * 86_400)
  deadline > @expires_at
end

#openssl_caObject

Return an OpenSSL::X509::Certificate for the CA, if present.



127
128
129
# File 'lib/datagrout_conduit/identity.rb', line 127

def openssl_ca
  @ca_pem ? OpenSSL::X509::Certificate.new(@ca_pem) : nil
end

#openssl_certObject

Return an OpenSSL::X509::Certificate for use with Faraday SSL config.



117
118
119
# File 'lib/datagrout_conduit/identity.rb', line 117

def openssl_cert
  OpenSSL::X509::Certificate.new(@cert_pem)
end

#openssl_keyObject

Return an OpenSSL::PKey for use with Faraday SSL config.



122
123
124
# File 'lib/datagrout_conduit/identity.rb', line 122

def openssl_key
  OpenSSL::PKey.read(@key_pem)
end

#with_expiry(expires_at) ⇒ Object



103
104
105
# File 'lib/datagrout_conduit/identity.rb', line 103

def with_expiry(expires_at)
  dup.tap { |i| i.instance_variable_set(:@expires_at, expires_at) }
end