diff --git a/apps/secrethub_core/lib/secrethub_core/pki/ca.ex b/apps/secrethub_core/lib/secrethub_core/pki/ca.ex index 434ba89..c36c0d7 100644 --- a/apps/secrethub_core/lib/secrethub_core/pki/ca.ex +++ b/apps/secrethub_core/lib/secrethub_core/pki/ca.ex @@ -1060,17 +1060,19 @@ defmodule SecretHub.Core.PKI.CA do @spec issue_agent_certificate(String.t(), keyword()) :: {:ok, Certificate.t()} | {:error, String.t()} def issue_agent_certificate(agent_id, opts \\ []) do - key_type = Keyword.get(opts, :key_type, :rsa) - key_size = Keyword.get(opts, :key_size, 2048) validity_days = Keyword.get(opts, :validity_days, @client_cert_validity_days) organization = "SecretHub" Logger.info("Issuing agent certificate for: #{agent_id}") - with {:ok, private_key} <- generate_private_key(key_type, key_size), - {:ok, public_key} <- extract_public_key(private_key, key_type) do - case find_active_ca() do - {:ok, ca_cert} -> + case find_active_ca() do + {:ok, ca_cert} -> + # Full CA-signed certificate with key generation + key_type = Keyword.get(opts, :key_type, :rsa) + key_size = Keyword.get(opts, :key_size, 2048) + + with {:ok, private_key} <- generate_private_key(key_type, key_size), + {:ok, public_key} <- extract_public_key(private_key, key_type) do issue_ca_signed_agent_cert( agent_id, organization, @@ -1079,22 +1081,37 @@ defmodule SecretHub.Core.PKI.CA do validity_days, opts ) + else + {:error, reason} -> + Logger.error("Failed to issue agent certificate for #{agent_id}: #{inspect(reason)}") + + {:error, "Failed to generate key: #{inspect(reason)}"} + end - {:error, _} -> + {:error, _} -> + # No CA available - generate a fast self-signed cert with ECDSA + with {:ok, private_key} <- generate_private_key(:ecdsa, nil), + {:ok, public_key} <- extract_public_key(private_key, :ecdsa) do issue_self_signed_agent_cert( agent_id, organization, private_key, public_key, - validity_days, - opts + validity_days ) - end - else - {:error, reason} -> - Logger.error("Failed to issue agent certificate for #{agent_id}: #{inspect(reason)}") - {:error, "Failed to generate key: #{inspect(reason)}"} + else + {:error, reason} -> + Logger.error( + "Failed to issue self-signed agent cert for #{agent_id}: #{inspect(reason)}" + ) + + {:error, "Failed to generate key: #{inspect(reason)}"} + end end + rescue + e -> + Logger.error("Failed to issue agent certificate for #{agent_id}: #{inspect(e)}") + {:error, "Certificate issuance failed: #{inspect(e)}"} end defp find_active_ca do @@ -1154,8 +1171,7 @@ defmodule SecretHub.Core.PKI.CA do organization, private_key, public_key, - validity_days, - opts + validity_days ) do with {:ok, cert_der} <- create_self_signed_certificate( @@ -1164,7 +1180,7 @@ defmodule SecretHub.Core.PKI.CA do agent_id, organization, validity_days, - opts + [] ), {:ok, cert_pem} <- der_to_pem(cert_der, :certificate), {:ok, cert_record} <- diff --git a/apps/secrethub_web/lib/secret_hub/web/channels/agent_channel.ex b/apps/secrethub_web/lib/secret_hub/web/channels/agent_channel.ex index 127a209..3131ef5 100644 --- a/apps/secrethub_web/lib/secret_hub/web/channels/agent_channel.ex +++ b/apps/secrethub_web/lib/secret_hub/web/channels/agent_channel.ex @@ -59,19 +59,20 @@ defmodule SecretHub.Web.AgentChannel do def join("agent:" <> agent_id, _payload, socket) do Logger.info("Agent #{agent_id} attempting to join channel directly") - # Set up heartbeat monitoring - schedule_heartbeat_check() + # Specific agent channels require prior authentication + if socket.assigns[:authenticated] do + schedule_heartbeat_check() + ensure_agent_registered(agent_id) - # Auto-register or update agent on direct topic join - ensure_agent_registered(agent_id) - - socket = - socket - |> assign(:authenticated, true) - |> assign(:agent_id, agent_id) - |> assign(:last_heartbeat, DateTime.utc_now() |> DateTime.truncate(:second)) + socket = + socket + |> assign(:agent_id, agent_id) + |> assign(:last_heartbeat, DateTime.utc_now() |> DateTime.truncate(:second)) - {:ok, %{status: "connected", authenticated: true, agent_id: agent_id}, socket} + {:ok, %{status: "connected", authenticated: true, agent_id: agent_id}, socket} + else + {:error, %{reason: "unauthorized"}} + end end @doc """ diff --git a/apps/secrethub_web/test/secrethub_web_web/plugs/verify_client_certificate_test.exs b/apps/secrethub_web/test/secrethub_web_web/plugs/verify_client_certificate_test.exs index 5de7df1..dc4ce8b 100644 --- a/apps/secrethub_web/test/secrethub_web_web/plugs/verify_client_certificate_test.exs +++ b/apps/secrethub_web/test/secrethub_web_web/plugs/verify_client_certificate_test.exs @@ -149,9 +149,7 @@ defmodule SecretHub.Web.Plugs.VerifyClientCertificateTest do stderr_to_stdout: true ) - # Sign with 1 day validity, but set startdate to far in the past - # Use faketime approach: sign normally then manipulate the validity via raw cert - # Simpler approach: generate a self-signed cert with past dates using -days 1 and startdate + # Sign a certificate with past validity dates (already expired) {_, 0} = System.cmd( "openssl", @@ -169,14 +167,50 @@ defmodule SecretHub.Web.Plugs.VerifyClientCertificateTest do client_cert_path, "-days", "1", - "-not_before", - "20240101000000Z", - "-not_after", - "20240102000000Z" + "-set_serial", + "01" ], stderr_to_stdout: true ) + # Re-sign with explicit past dates using Erlang :public_key to set validity + # Read the cert we just created, modify validity, and re-sign + ca_key_pem = File.read!(ca.key_path) + [{key_type, key_der, _}] = :public_key.pem_decode(ca_key_pem) + ca_key = :public_key.pem_entry_decode({key_type, key_der, :not_encrypted}) + + client_pem_tmp = File.read!(client_cert_path) + [{:Certificate, client_der_tmp, _}] = :public_key.pem_decode(client_pem_tmp) + otp_cert = :public_key.pkix_decode_cert(client_der_tmp, :otp) + + # Extract TBS certificate and modify validity + { + :OTPTBSCertificate, + version, + serial, + sig_alg, + issuer, + _validity, + subject, + spki, + issuer_uid, + subject_uid, + extensions + } = elem(otp_cert, 1) + + # Set validity to past dates + new_validity = + {:Validity, {:utcTime, ~c"240101000000Z"}, {:utcTime, ~c"240102000000Z"}} + + new_tbs = + {:OTPTBSCertificate, version, serial, sig_alg, issuer, new_validity, subject, spki, + issuer_uid, subject_uid, extensions} + + # Re-sign the certificate + new_cert_der = :public_key.pkix_sign(new_tbs, ca_key) + new_cert_pem = :public_key.pem_encode([{:Certificate, new_cert_der, :not_encrypted}]) + File.write!(client_cert_path, new_cert_pem) + client_pem = File.read!(client_cert_path) [{:Certificate, client_der, _}] = :public_key.pem_decode(client_pem) otp_cert = :public_key.pkix_decode_cert(client_der, :otp) @@ -217,7 +251,7 @@ defmodule SecretHub.Web.Plugs.VerifyClientCertificateTest do stderr_to_stdout: true ) - # Certificate valid from far future + # Certificate valid from far future - first create a normal cert, then re-sign with future dates {_, 0} = System.cmd( "openssl", @@ -235,14 +269,47 @@ defmodule SecretHub.Web.Plugs.VerifyClientCertificateTest do client_cert_path, "-days", "365", - "-not_before", - "20500101000000Z", - "-not_after", - "20510101000000Z" + "-set_serial", + "02" ], stderr_to_stdout: true ) + # Re-sign with future validity dates using Erlang :public_key + ca_key_pem = File.read!(ca.key_path) + [{key_type, key_der, _}] = :public_key.pem_decode(ca_key_pem) + ca_key = :public_key.pem_entry_decode({key_type, key_der, :not_encrypted}) + + client_pem_tmp = File.read!(client_cert_path) + [{:Certificate, client_der_tmp, _}] = :public_key.pem_decode(client_pem_tmp) + otp_cert = :public_key.pkix_decode_cert(client_der_tmp, :otp) + + { + :OTPTBSCertificate, + version, + serial, + sig_alg, + issuer, + _validity, + subject, + spki, + issuer_uid, + subject_uid, + extensions + } = elem(otp_cert, 1) + + # Set validity to future dates (use utcTime with 2-digit year for ASN1 compatibility) + new_validity = + {:Validity, {:utcTime, ~c"490101000000Z"}, {:utcTime, ~c"500101000000Z"}} + + new_tbs = + {:OTPTBSCertificate, version, serial, sig_alg, issuer, new_validity, subject, spki, + issuer_uid, subject_uid, extensions} + + new_cert_der = :public_key.pkix_sign(new_tbs, ca_key) + new_cert_pem = :public_key.pem_encode([{:Certificate, new_cert_der, :not_encrypted}]) + File.write!(client_cert_path, new_cert_pem) + client_pem = File.read!(client_cert_path) [{:Certificate, client_der, _}] = :public_key.pem_decode(client_pem) otp_cert = :public_key.pkix_decode_cert(client_der, :otp)