Class: DatagroutConduit::Registration
- Inherits:
-
Object
- Object
- DatagroutConduit::Registration
- Defined in:
- lib/datagrout_conduit/registration.rb
Overview
Substrate identity registration with the DataGrout CA.
Handles the issuance flow — turning a freshly-generated keypair into a DG-CA-signed Identity that DataGrout will accept for mTLS.
Flow
-
Generate an ECDSA P-256 keypair with Registration.generate_keypair. The private key never leaves the client.
-
Send the *public key* to the DataGrout CA via Registration.register_identity (authenticated with a bearer token — user access token or API key).
-
Persist the returned identity to ~/.conduit/ via Registration.save_identity for auto-discovery by future sessions.
-
On renewal (cert near expiry), call Registration.rotate_identity which presents the existing client certificate over mTLS — no API key needed.
Constant Summary collapse
- DG_CA_URL =
"https://ca.datagrout.ai/ca.pem"- DG_SUBSTRATE_ENDPOINT =
"https://app.datagrout.ai/api/v1/substrate/identity"
Class Method Summary collapse
-
.default_identity_dir ⇒ String?
Returns ~/.conduit/ as the canonical identity directory.
-
.fetch_ca_cert(ca_url: DG_CA_URL) ⇒ String
Fetch the DataGrout CA certificate from
ca.datagrout.ai. -
.generate_keypair ⇒ Array(String, String)
Generate an ECDSA P-256 keypair.
-
.refresh_ca_cert(dir, ca_url: DG_CA_URL) ⇒ String
Refresh CA cert in the given directory.
-
.register_identity(public_key_pem, auth_token:, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) ⇒ RegistrationResponse
Register identity with the DataGrout CA.
-
.rotate_identity(identity, new_public_key_pem, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) ⇒ RegistrationResponse
Rotate identity using existing mTLS cert.
-
.save_identity(cert_pem, key_pem, dir, ca_pem: nil) ⇒ Hash
Save identity files to a directory with secure permissions (0600).
Class Method Details
.default_identity_dir ⇒ String?
Returns ~/.conduit/ as the canonical identity directory.
209 210 211 212 |
# File 'lib/datagrout_conduit/registration.rb', line 209 def self.default_identity_dir home = ENV["HOME"] || ENV["USERPROFILE"] home ? File.join(home, ".conduit") : nil end |
.fetch_ca_cert(ca_url: DG_CA_URL) ⇒ String
Fetch the DataGrout CA certificate from ca.datagrout.ai.
Uses the system trust store for TLS (not the DG CA itself), so there is no circularity.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/datagrout_conduit/registration.rb', line 177 def self.fetch_ca_cert(ca_url: DG_CA_URL) response = Faraday.get(ca_url) unless response.success? raise ConnectionError, "Failed to fetch CA cert (HTTP #{response.status})" end pem = response.body unless pem.include?("-----BEGIN CERTIFICATE-----") raise ConnectionError, "Response from #{ca_url} does not look like a PEM certificate" end pem end |
.generate_keypair ⇒ Array(String, String)
Generate an ECDSA P-256 keypair.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/datagrout_conduit/registration.rb', line 32 def self.generate_keypair key = OpenSSL::PKey::EC.generate("prime256v1") private_pem = key.to_pem public_pem = if key.respond_to?(:public_to_pem) key.public_to_pem else pub = OpenSSL::PKey::EC.new(key.group) pub.public_key = key.public_key pub.to_pem end [private_pem, public_pem] end |
.refresh_ca_cert(dir, ca_url: DG_CA_URL) ⇒ String
Refresh CA cert in the given directory.
197 198 199 200 201 202 203 204 |
# File 'lib/datagrout_conduit/registration.rb', line 197 def self.refresh_ca_cert(dir, ca_url: DG_CA_URL) ca_pem = fetch_ca_cert(ca_url: ca_url) FileUtils.mkdir_p(dir) ca_path = File.join(dir, "ca.pem") File.write(ca_path, ca_pem) File.chmod(0o600, ca_path) ca_path end |
.register_identity(public_key_pem, auth_token:, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) ⇒ RegistrationResponse
Register identity with the DataGrout CA.
Sends only the public key. The private key never leaves the client. Authenticated with a bearer token (user access token or API key).
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/datagrout_conduit/registration.rb', line 57 def self.register_identity(public_key_pem, auth_token:, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) conn = Faraday.new(url: substrate_endpoint) do |f| f.request :json f.response :json, content_type: /\bjson$/ f.adapter Faraday.default_adapter end response = conn.post do |req| req.url "register" req.headers["Authorization"] = "Bearer #{auth_token}" req.headers["Content-Type"] = "application/json" req.body = JSON.generate( name: name, public_key_pem: public_key_pem ) end unless response.success? raise AuthError, "Registration failed (HTTP #{response.status}): #{response.body}" end body = response.body body = JSON.parse(body) if body.is_a?(String) RegistrationResponse.new( id: body["id"], cert_pem: body["cert_pem"], ca_cert_pem: body["ca_cert_pem"], fingerprint: body["fingerprint"], name: body["name"], registered_at: body["registered_at"], valid_until: body["valid_until"] ) end |
.rotate_identity(identity, new_public_key_pem, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) ⇒ RegistrationResponse
Rotate identity using existing mTLS cert.
Generates a new public key, sends it to the /rotate endpoint authenticated by the current cert over mTLS (no API key needed), and returns a fresh DG-CA-signed certificate.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/datagrout_conduit/registration.rb', line 104 def self.rotate_identity(identity, new_public_key_pem, name: "conduit-client", substrate_endpoint: DG_SUBSTRATE_ENDPOINT) conn = Faraday.new(url: substrate_endpoint) do |f| f.request :json f.response :json, content_type: /\bjson$/ f.adapter Faraday.default_adapter identity.configure_ssl(f.ssl) end response = conn.post do |req| req.url "rotate" req.headers["Content-Type"] = "application/json" req.body = JSON.generate( name: name, public_key_pem: new_public_key_pem ) end unless response.success? raise ConnectionError, "Rotation failed (HTTP #{response.status}): #{response.body}" end body = response.body body = JSON.parse(body) if body.is_a?(String) RegistrationResponse.new( id: body["id"], cert_pem: body["cert_pem"], ca_cert_pem: body["ca_cert_pem"], fingerprint: body["fingerprint"], name: body["name"], registered_at: body["registered_at"], valid_until: body["valid_until"] ) end |
.save_identity(cert_pem, key_pem, dir, ca_pem: nil) ⇒ Hash
Save identity files to a directory with secure permissions (0600).
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/datagrout_conduit/registration.rb', line 147 def self.save_identity(cert_pem, key_pem, dir, ca_pem: nil) FileUtils.mkdir_p(dir) cert_path = File.join(dir, "identity.pem") key_path = File.join(dir, "identity_key.pem") File.write(cert_path, cert_pem) File.write(key_path, key_pem) File.chmod(0o600, cert_path) File.chmod(0o600, key_path) paths = { cert: cert_path, key: key_path } if ca_pem ca_path = File.join(dir, "ca.pem") File.write(ca_path, ca_pem) File.chmod(0o600, ca_path) paths[:ca] = ca_path end paths end |