Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions apps/secrethub_core/lib/secrethub_core/pki/ca.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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} <-
Expand Down
23 changes: 12 additions & 11 deletions apps/secrethub_web/lib/secret_hub/web/channels/agent_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand Down
Loading