From 7c83fbe038371250922442db286b4137504cae72 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 25 Feb 2026 21:30:39 -0800 Subject: [PATCH 1/3] add agent and primitives --- lib/vestauth.rb | 9 +++++-- lib/vestauth/agent.rb | 15 ++++++++++++ lib/vestauth/binary.rb | 36 ++++++++++++++++++++++++++++ lib/vestauth/primitives.rb | 22 +++++++++++++++++ spec/vestauth_spec.rb | 49 +++++++++++++++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 lib/vestauth/primitives.rb diff --git a/lib/vestauth.rb b/lib/vestauth.rb index 6d35759..fee811b 100644 --- a/lib/vestauth.rb +++ b/lib/vestauth.rb @@ -3,12 +3,17 @@ require_relative "vestauth/version" require_relative "vestauth/agent" require_relative "vestauth/binary" +require_relative "vestauth/primitives" require_relative "vestauth/tool" require_relative "vestauth/provider" module Vestauth class Error < StandardError; end + def self.agent + Agent + end + def self.tool Tool end @@ -17,8 +22,8 @@ class << self alias provider tool end - def self.agent - Agent + def self.primitives + Primitives end def self.binary diff --git a/lib/vestauth/agent.rb b/lib/vestauth/agent.rb index 27c6727..0ab09ad 100644 --- a/lib/vestauth/agent.rb +++ b/lib/vestauth/agent.rb @@ -2,5 +2,20 @@ module Vestauth module Agent + module_function + + def headers(http_method:, uri:, private_key:, id:) + vestauth_binary.agent_headers( + http_method: http_method, + uri: uri, + private_key: private_key, + id: id + ) + end + + def vestauth_binary + Vestauth::Binary.new + end + private_class_method :vestauth_binary end end diff --git a/lib/vestauth/binary.rb b/lib/vestauth/binary.rb index 5d98dc8..3076bac 100644 --- a/lib/vestauth/binary.rb +++ b/lib/vestauth/binary.rb @@ -28,6 +28,42 @@ def tool_verify(http_method:, uri:, signature:, signature_input:, signature_agen run_json_command(command) end + def agent_headers(http_method:, uri:, private_key:, id:) + command = [ + @executable, + "agent", + "headers", + http_method, + uri, + "--private-jwk", + private_key, + "--uid", + id + ] + + run_json_command(command) + end + + def primitives_verify(http_method:, uri:, signature_header:, signature_input_header:, public_key:) + public_jwk = public_key.as_json.to_json + + command = [ + @executable, + "primitives", + "verify", + http_method, + uri, + "--signature", + signature_header, + "--signature-input", + signature_input_header, + "--public-jwk", + public_jwk + ] + + run_json_command(command) + end + private def run_json_command(command_args) diff --git a/lib/vestauth/primitives.rb b/lib/vestauth/primitives.rb new file mode 100644 index 0000000..d5e8ae6 --- /dev/null +++ b/lib/vestauth/primitives.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Vestauth + module Primitives + module_function + + def verify(http_method:, uri:, signature_header:, signature_input_header:, public_key:) + vestauth_binary.primitives_verify( + http_method: http_method, + uri: uri, + signature_header: signature_header, + signature_input_header: signature_input_header, + public_key: public_key + ) + end + + def vestauth_binary + Vestauth::Binary.new + end + private_class_method :vestauth_binary + end +end diff --git a/spec/vestauth_spec.rb b/spec/vestauth_spec.rb index 0f09444..efa5c5b 100644 --- a/spec/vestauth_spec.rb +++ b/spec/vestauth_spec.rb @@ -5,11 +5,12 @@ expect(Vestauth::VERSION).not_to be nil end - it "exposes namespaced tool/provider and agent modules" do + it "exposes namespaced tool/provider, agent, and primitives modules" do expect(Vestauth::Tool).to eq(Vestauth::Provider) expect(Vestauth.tool).to eq(Vestauth::Tool) expect(Vestauth.provider).to eq(Vestauth::Tool) expect(Vestauth.agent).to eq(Vestauth::Agent) + expect(Vestauth.primitives).to eq(Vestauth::Primitives) expect(Vestauth.binary).to eq(Vestauth::Binary) end @@ -57,4 +58,50 @@ signature_agent: nil ) end + + it "delegates agent headers to binary agent_headers" do + binary = instance_double(Vestauth::Binary) + allow(Vestauth::Binary).to receive(:new).and_return(binary) + allow(binary).to receive(:agent_headers).and_return({ "Signature" => "sig1=:abc:" }) + + private_key = { "kty" => "EC" } + result = Vestauth.agent.headers( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + private_key: private_key, + id: "agent-123" + ) + + expect(binary).to have_received(:agent_headers).with( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + private_key: private_key, + id: "agent-123" + ) + expect(result).to eq({ "Signature" => "sig1=:abc:" }) + end + + it "delegates primitives verify to binary primitives_verify" do + binary = instance_double(Vestauth::Binary) + allow(Vestauth::Binary).to receive(:new).and_return(binary) + allow(binary).to receive(:primitives_verify).and_return({ "success" => true }) + + public_key = instance_double("PublicKey") + result = Vestauth.primitives.verify( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + signature_header: "sig1=:abc:", + signature_input_header: "sig1=(\"@method\");keyid=\"kid-1\"", + public_key: public_key + ) + + expect(binary).to have_received(:primitives_verify).with( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + signature_header: "sig1=:abc:", + signature_input_header: "sig1=(\"@method\");keyid=\"kid-1\"", + public_key: public_key + ) + expect(result).to eq({ "success" => true }) + end end From 7ce4a951bb3fca676a92cd200e5434169d09d8f5 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 25 Feb 2026 21:32:39 -0800 Subject: [PATCH 2/3] rubocop --- lib/vestauth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vestauth.rb b/lib/vestauth.rb index fee811b..38e5722 100644 --- a/lib/vestauth.rb +++ b/lib/vestauth.rb @@ -13,7 +13,7 @@ class Error < StandardError; end def self.agent Agent end - + def self.tool Tool end From 71612494d2625cf208d167355dd85c7ea6ca2e07 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 25 Feb 2026 21:36:42 -0800 Subject: [PATCH 3/3] serialization fix --- lib/vestauth/binary.rb | 20 ++++++-- spec/binary_spec.rb | 105 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/lib/vestauth/binary.rb b/lib/vestauth/binary.rb index 3076bac..4462e6a 100644 --- a/lib/vestauth/binary.rb +++ b/lib/vestauth/binary.rb @@ -2,7 +2,6 @@ require "json" require "open3" -require "shellwords" module Vestauth class Binary @@ -29,6 +28,8 @@ def tool_verify(http_method:, uri:, signature:, signature_input:, signature_agen end def agent_headers(http_method:, uri:, private_key:, id:) + private_jwk = serialize_json_arg(private_key, name: "private_key") + command = [ @executable, "agent", @@ -36,7 +37,7 @@ def agent_headers(http_method:, uri:, private_key:, id:) http_method, uri, "--private-jwk", - private_key, + private_jwk, "--uid", id ] @@ -45,7 +46,7 @@ def agent_headers(http_method:, uri:, private_key:, id:) end def primitives_verify(http_method:, uri:, signature_header:, signature_input_header:, public_key:) - public_jwk = public_key.as_json.to_json + public_jwk = serialize_json_arg(public_key, name: "public_key") command = [ @executable, @@ -67,12 +68,21 @@ def primitives_verify(http_method:, uri:, signature_header:, signature_input_hea private def run_json_command(command_args) - command = command_args.map { |arg| Shellwords.escape(arg.to_s) }.join(" ") - stdout, stderr, status = Open3.capture3(command) + argv = command_args.map { |arg| arg.nil? ? "" : arg.to_s } + stdout, stderr, status = Open3.capture3(*argv) raise Vestauth::Error, (stderr.to_s.strip.empty? ? stdout : stderr) unless status.success? JSON.parse(stdout) end + + def serialize_json_arg(value, name:) + return value if value.is_a?(String) + return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Array) + return JSON.generate(value.to_h) if value.respond_to?(:to_h) + return JSON.generate(value.as_json) if value.respond_to?(:as_json) + + raise ArgumentError, "#{name} must be a JSON string, Hash/Array, or object responding to #to_h" + end end end diff --git a/spec/binary_spec.rb b/spec/binary_spec.rb index d757495..6853461 100644 --- a/spec/binary_spec.rb +++ b/spec/binary_spec.rb @@ -7,7 +7,17 @@ binary = described_class.new expect(Open3).to receive(:capture3).with( - include("vestauth tool verify GET https://api.vestauth.com/whoami") + "vestauth", + "tool", + "verify", + "GET", + "https://api.vestauth.com/whoami", + "--signature", + "sig1=:abc:", + "--signature-input", + "sig1=(\"@method\");keyid=\"kid-1\"", + "--signature-agent", + "sig1=agent-123.agents.vestauth.com" ).and_return(['{"uid":"agent-123"}', "", status]) result = binary.tool_verify( @@ -38,4 +48,97 @@ end.to raise_error(Vestauth::Error, "bad signature") end end + + describe "#agent_headers" do + it "serializes a hash private key to json" do + status = instance_double(Process::Status, success?: true) + binary = described_class.new + + expect(Open3).to receive(:capture3).with( + "vestauth", + "agent", + "headers", + "GET", + "https://api.vestauth.com/whoami", + "--private-jwk", + '{"kty":"EC"}', + "--uid", + "agent-123" + ).and_return(['{"Signature":"sig1=:abc:"}', "", status]) + + result = binary.agent_headers( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + private_key: { "kty" => "EC" }, + id: "agent-123" + ) + + expect(result).to eq({ "Signature" => "sig1=:abc:" }) + end + end + + describe "#primitives_verify" do + it "serializes a to_h public key without requiring as_json" do + status = instance_double(Process::Status, success?: true) + binary = described_class.new + public_key = Struct.new(:jwk) do + def to_h + jwk + end + end.new({ "kty" => "EC" }) + + expect(Open3).to receive(:capture3).with( + "vestauth", + "primitives", + "verify", + "GET", + "https://api.vestauth.com/whoami", + "--signature", + "sig1=:abc:", + "--signature-input", + "sig1=(\"@method\");keyid=\"kid-1\"", + "--public-jwk", + '{"kty":"EC"}' + ).and_return(['{"success":true}', "", status]) + + result = binary.primitives_verify( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + signature_header: "sig1=:abc:", + signature_input_header: "sig1=(\"@method\");keyid=\"kid-1\"", + public_key: public_key + ) + + expect(result).to eq({ "success" => true }) + end + + it "accepts a pre-serialized public key json string" do + status = instance_double(Process::Status, success?: true) + binary = described_class.new + + expect(Open3).to receive(:capture3).with( + "vestauth", + "primitives", + "verify", + "GET", + "https://api.vestauth.com/whoami", + "--signature", + "sig1=:abc:", + "--signature-input", + "sig1=(\"@method\");keyid=\"kid-1\"", + "--public-jwk", + '{"kty":"EC"}' + ).and_return(['{"success":true}', "", status]) + + result = binary.primitives_verify( + http_method: "GET", + uri: "https://api.vestauth.com/whoami", + signature_header: "sig1=:abc:", + signature_input_header: "sig1=(\"@method\");keyid=\"kid-1\"", + public_key: '{"kty":"EC"}' + ) + + expect(result).to eq({ "success" => true }) + end + end end