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
# File 'lib/datagrout_conduit/identity.rb', line 69

def self.try_discover(override_dir: nil)
  # When an explicit directory is given, scope search to that dir only.
  if override_dir
    return try_load_from_dir(override_dir)
  end

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

  # 2. 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

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

  # 4. .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.



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

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)


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

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.



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

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.



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

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

#openssl_keyObject

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



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

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

#with_expiry(expires_at) ⇒ Object



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

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