Module: Angarium::Signature

Defined in:
lib/angarium/signature.rb

Class Method Summary collapse

Class Method Details

.parse(header) ⇒ Object

webhook-signature is space-delimited v1,<base64> tokens; keep the base64 payload of each v1 token.



44
45
46
47
48
49
# File 'lib/angarium/signature.rb', line 44

def parse(header)
  header.to_s.split(" ").filter_map { |token|
    version, sig = token.split(",", 2)
    sig if version == "v1" && sig && !sig.empty?
  }
end

.secure_compare(a, b) ⇒ Object



51
52
53
54
55
# File 'lib/angarium/signature.rb', line 51

def secure_compare(a, b)
  ActiveSupport::SecurityUtils.secure_compare(a, b)
rescue ArgumentError
  false
end

.sign(payload:, id:, timestamp:, secret:) ⇒ Object

secret may be a String or Array of secrets (dual-secret rotation grace); produces one v1,<base64> token per secret, space-delimited.



10
11
12
# File 'lib/angarium/signature.rb', line 10

def sign(payload:, id:, timestamp:, secret:)
  Array(secret).map { |s| "v1,#{signature_for(s, id, timestamp, payload)}" }.join(" ")
end

.signature_for(secret, id, timestamp, payload) ⇒ Object



36
37
38
39
40
# File 'lib/angarium/signature.rb', line 36

def signature_for(secret, id, timestamp, payload)
  key = Base64.decode64(secret.to_s.delete_prefix("whsec_"))
  digest = OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{timestamp}.#{payload}")
  Base64.strict_encode64(digest)
end

.verify(secret:, request: nil, payload: nil, id: nil, timestamp: nil, signature: nil, tolerance: 300, now: Time.now.to_i) ⇒ Object

Verify a Standard Webhooks signature. Pass the fields explicitly, or pass a Rails request: and Angarium pulls the raw body and webhook-* headers for you, so a receiver is a one-liner:

Angarium::Signature.verify(request:, secret: endpoint.signing_secret)


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/angarium/signature.rb', line 20

def verify(secret:, request: nil, payload: nil, id: nil, timestamp: nil, signature: nil,
  tolerance: 300, now: Time.now.to_i)
  if request
    payload ||= request.raw_post
    id ||= request.headers["webhook-id"]
    timestamp ||= request.headers["webhook-timestamp"]
    signature ||= request.headers["webhook-signature"]
  end

  return false unless timestamp.to_s.match?(/\A\d+\z/)
  return false if (now - timestamp.to_i).abs > tolerance

  expected = signature_for(secret, id, timestamp.to_i, payload)
  parse(signature).any? { |sig| secure_compare(expected, sig) }
end