Skip to content

Commit bcdf686

Browse files
committed
add spec tests for hmac
1 parent 9ba0646 commit bcdf686

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# frozen_string_literal: true
2+
3+
describe Hooks::Plugins::RequestValidator::HMAC do
4+
let(:secret) { "supersecret" }
5+
let(:payload) { '{"foo":"bar"}' }
6+
let(:default_header) { "X-Hub-Signature-256" }
7+
let(:default_algorithm) { "sha256" }
8+
let(:default_config) do
9+
{
10+
request_validator: {
11+
header: default_header,
12+
algorithm: default_algorithm,
13+
format: "algorithm=signature"
14+
}
15+
}
16+
end
17+
18+
def valid_with(args = {})
19+
args = { config: default_config }.merge(args)
20+
described_class.valid?(payload:, secret:, **args)
21+
end
22+
23+
describe ".valid?" do
24+
context "with algorithm-prefixed format" do
25+
let(:signature) { "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
26+
let(:headers) { { default_header => signature } }
27+
28+
it "returns true for a valid signature" do
29+
expect(valid_with(headers:)).to be true
30+
end
31+
32+
it "returns false for an invalid signature" do
33+
bad_headers = { default_header => "sha256=bad" }
34+
expect(valid_with(headers: bad_headers)).to be false
35+
end
36+
37+
it "returns false if signature header is missing" do
38+
expect(valid_with(headers: {})).to be false
39+
end
40+
41+
it "returns false if secret is nil or empty" do
42+
expect(valid_with(headers:, secret: nil)).to be false
43+
expect(valid_with(headers:, secret: "")).to be false
44+
end
45+
46+
it "normalizes header names to lowercase" do
47+
upcase_headers = { default_header.upcase => signature }
48+
expect(valid_with(headers: upcase_headers)).to be true
49+
end
50+
end
51+
52+
context "with hash-only format" do
53+
let(:header) { "X-Signature-Hash" }
54+
let(:config) do
55+
{
56+
request_validator: {
57+
header: header,
58+
algorithm: "sha256",
59+
format: "signature_only"
60+
}
61+
}
62+
end
63+
let(:signature) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
64+
let(:headers) { { header => signature } }
65+
66+
it "returns true for a valid hash-only signature" do
67+
expect(valid_with(headers:, config:)).to be true
68+
end
69+
70+
it "returns false for an invalid hash-only signature" do
71+
bad_headers = { header => "bad" }
72+
expect(valid_with(headers: bad_headers, config:)).to be false
73+
end
74+
end
75+
76+
context "with version-prefixed format and timestamp" do
77+
let(:header) { "X-Signature-Versioned" }
78+
let(:timestamp_header) { "X-Request-Timestamp" }
79+
let(:timestamp) { Time.now.to_i.to_s }
80+
let(:payload_template) { "v0:{timestamp}:{body}" }
81+
let(:signing_payload) { "v0:#{timestamp}:#{payload}" }
82+
let(:signature) { "v0=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, signing_payload) }
83+
let(:headers) { { header => signature, timestamp_header => timestamp } }
84+
let(:config) do
85+
{
86+
request_validator: {
87+
header: header,
88+
timestamp_header: timestamp_header,
89+
algorithm: "sha256",
90+
format: "version=signature",
91+
version_prefix: "v0",
92+
payload_template: payload_template,
93+
timestamp_tolerance: 300
94+
}
95+
}
96+
end
97+
98+
it "returns true for a valid versioned signature with valid timestamp" do
99+
expect(valid_with(headers:, config:)).to be true
100+
end
101+
102+
it "returns false for an expired timestamp" do
103+
old_timestamp = (Time.now.to_i - 1000).to_s
104+
old_signing_payload = "v0:#{old_timestamp}:#{payload}"
105+
old_signature = "v0=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, old_signing_payload)
106+
bad_headers = { header => old_signature, timestamp_header => old_timestamp }
107+
expect(valid_with(headers: bad_headers, config:)).to be false
108+
end
109+
110+
it "returns false if timestamp header is missing" do
111+
bad_headers = { header => signature }
112+
expect(valid_with(headers: bad_headers, config:)).to be false
113+
end
114+
115+
it "returns false if timestamp is not an integer string" do
116+
bad_headers = { header => signature, timestamp_header => "notanumber" }
117+
expect(valid_with(headers: bad_headers, config:)).to be false
118+
end
119+
end
120+
121+
context "with unsupported algorithm" do
122+
let(:header) { "X-Unsupported-Alg" }
123+
let(:config) do
124+
{
125+
request_validator: {
126+
header: header,
127+
algorithm: "md5",
128+
format: "algorithm=signature"
129+
}
130+
}
131+
end
132+
let(:signature) { "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
133+
let(:headers) { { header => signature } }
134+
135+
it "returns false for unsupported algorithm" do
136+
expect(valid_with(headers:, config:)).to be false
137+
end
138+
end
139+
140+
context "with missing config values" do
141+
let(:headers) { { "X-Signature" => "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) } }
142+
let(:config) { {} }
143+
144+
it "uses defaults and validates correctly" do
145+
expect(valid_with(headers:, config:)).to be true
146+
end
147+
end
148+
149+
context "with tampered payload" do
150+
let(:signature) { "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
151+
let(:headers) { { default_header => signature } }
152+
let(:tampered_payload) { '{"foo":"evil"}' }
153+
154+
it "returns false if payload does not match signature" do
155+
expect(valid_with(payload: tampered_payload, headers:)).to be false
156+
end
157+
end
158+
159+
context "with nil headers" do
160+
let(:headers) { nil }
161+
it "returns false" do
162+
expect(valid_with(headers:)).to be false
163+
end
164+
end
165+
166+
context "with invalid config structure" do
167+
let(:headers) { { default_header => "sha256=bad" } }
168+
let(:config) { { not_validator: true } }
169+
it "returns false" do
170+
expect(valid_with(headers:, config:)).to be false
171+
end
172+
end
173+
end
174+
175+
describe ".build_signing_payload" do
176+
let(:headers) { { "x-timestamp" => "12345" } }
177+
it "substitutes variables in template" do
178+
template = "v0:{timestamp}:{body}"
179+
config = { version_prefix: "v0", timestamp_header: "x-timestamp", payload_template: template }
180+
result = described_class.send(:build_signing_payload, payload:, headers:, config:)
181+
expect(result).to eq("v0:12345:{\"foo\":\"bar\"}")
182+
end
183+
it "returns payload if no template" do
184+
config = {}
185+
result = described_class.send(:build_signing_payload, payload:, headers:, config:)
186+
expect(result).to eq(payload)
187+
end
188+
end
189+
190+
describe ".format_signature" do
191+
it "formats algorithm-prefixed" do
192+
config = { algorithm: "sha256", format: "algorithm=signature" }
193+
expect(described_class.send(:format_signature, "abc123", config)).to eq("sha256=abc123")
194+
end
195+
it "formats hash-only" do
196+
config = { format: "signature_only" }
197+
expect(described_class.send(:format_signature, "abc123", config)).to eq("abc123")
198+
end
199+
it "formats version-prefixed" do
200+
config = { version_prefix: "v0", format: "version=signature" }
201+
expect(described_class.send(:format_signature, "abc123", config)).to eq("v0=abc123")
202+
end
203+
it "defaults to algorithm-prefixed" do
204+
config = { algorithm: "sha256", format: "unknown" }
205+
expect(described_class.send(:format_signature, "abc123", config)).to eq("sha256=abc123")
206+
end
207+
end
208+
209+
describe ".normalize_headers" do
210+
it "returns empty hash for nil headers" do
211+
expect(described_class.send(:normalize_headers, nil)).to eq({})
212+
end
213+
it "downcases header keys" do
214+
headers = { "X-FOO" => "bar" }
215+
expect(described_class.send(:normalize_headers, headers)).to eq({ "x-foo" => "bar" })
216+
end
217+
end
218+
219+
describe ".build_config" do
220+
it "applies defaults when config is missing" do
221+
expect(described_class.send(:build_config, {})).to include(:algorithm, :format, :timestamp_tolerance, :version_prefix)
222+
end
223+
it "overrides defaults with provided config" do
224+
config = { request_validator: { algorithm: "sha512", format: "signature_only", header: "X-My-Sig" } }
225+
result = described_class.send(:build_config, config)
226+
expect(result[:algorithm]).to eq("sha512")
227+
expect(result[:format]).to eq("signature_only")
228+
expect(result[:header]).to eq("X-My-Sig")
229+
end
230+
end
231+
end

0 commit comments

Comments
 (0)