0.0
No release in over 3 years
Low commit activity in last 3 years
Create and validate HTTP Message Signatures according to RFC 9421: https://www.rfc-editor.org/rfc/rfc9421.html
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0
>= 2.7
>= 5.24
>= 0

Runtime

>= 0
 Project Readme

HTTP Signature

Create and validate HTTP Message Signatures per RFC 9421 using the Signature-Input and Signature headers.

TL;DR: You specify what should be signed in Signature-Input with components and lowercase headers. And then the signature is in the Signature header

Example:

Signature-Input: sig1=("@method" "@target-uri" "date");created=1767816111;keyid="Test";alg="hmac-sha256"
Signature: sig1=:7a1ajkE2rOu+gnW3WLZ4ZEcgCm3TfExmypM/giIgdM0=:

Installation

bundle add http_signature

Usage

Create signature

HTTPSignature.create returns both Signature-Input and Signature headers that you can include in your request.

headers = { "date" => "Tue, 20 Apr 2021 02:07:55 GMT" }

sig_headers = HTTPSignature.create(
  url: "https://example.com/foo?pet=dog",
  method: :get,
  key_id: "Test",
  key: "secret",
  headers: headers,
  components: %w[@method @target-uri date]
)

request["Signature-Input"] = sig_headers["Signature-Input"]
request["Signature"] = sig_headers["Signature"]

All options

HTTPSignature.create(
  url: "https://example.com/foo?pet=dog",
  method: :get,
  key_id: "Test",
  key: "secret",
  # Optional arguments
  headers: headers, # Default: {}
  body: "Hello world", # Default: ""
  components: %w[@method @target-uri date], # Default: %w[@method @target-uri content-digest content-type]
  created: Time.now.to_i, # Default: Time.now.to_i
  expires: Time.now.to_i + 600, # Default: nil
  nonce: "1", # Default: nil
  label: "sig1", # Default: "sig1",
  query_string_params: {pet2: "cat"} # Default: {}, you can pass query string params both here and in the `url` param
  algorithm: "hmac-sha512" # Default: "hmac-sha256"
)

Supported components

Derived components (prefixed with @) per RFC 9421 Section 2.2:

Component Description
@method HTTP method (e.g., GET, POST)
@target-uri Full request URI (https://example.com/foo?bar=1)
@authority Host (and port if non-default)
@scheme URI scheme (http or https)
@path Request path (/foo)
@query Query string (including ?; ?bar=1)
@status Response status code (responses only)

Any lowercase header name (e.g., content-type, date) can also be used as a component.

Default components are: @method @target-uri content-digest content-type

Validate signature

Call valid? with the incoming request headers (including Signature-Input and Signature)

HTTPSignature.valid?(
  url: "https://example.com/foo",
  method: :get,
  headers: headers,
  key: "secret"
)

# Returns true when all is good.
# Raises `SignatureError` for invalid signatures

Limiting signature age

Use max_age to reject signatures older than a specified number of seconds, regardless of the signature's expires parameter. This helps protect against replay attacks.

HTTPSignature.valid?(
  url: "https://example.com/foo",
  method: :get,
  headers: headers,
  key: "secret",
  max_age: 300 # Reject signatures older than 5 minutes
)

# Raises `ExpiredError` if the signature was created more than 300 seconds ago

Outgoing request examples

NET::HTTP

require "net/http"
require "http_signature"

uri = URI("http://example.com/hello")

Net::HTTP.start(uri.host, uri.port) do |http|
  request = Net::HTTP::Get.new(uri)

  sig_headers = HTTPSignature.create(
    url: request.uri,
    method: request.method,
    headers: request.each_header.map { |k, v| [k, v] }.to_h,
    key: "MYSECRETKEY",
    key_id: "KEY_1",
    algorithm: "hmac-sha256",
    body: request.body || ""
  )

  request["Signature-Input"] = sig_headers["Signature-Input"]
  request["Signature"] = sig_headers["Signature"]

  response = http.request(request) # Net::HTTPResponse
end

Faraday

As a faraday middleware

require "http_signature/faraday"

HTTPSignature::Faraday.key = "secret"
HTTPSignature::Faraday.key_id = "key-1"

Faraday.new("http://example.com") do |faraday|
  faraday.use(HTTPSignature::Faraday)
  faraday.adapter(Faraday.default_adapter)
end

# Now this request will contain the `Signature-Input` and `Signature` headers
response = conn.get("/")

# Request looking like:
# Signature-Input: sig1=("@method" "@authority" "@target-uri" "date");created=...
# Signature: sig1=:BASE64_SIGNATURE:

Incoming request examples

Rack middleware

Rack middlewares sits in between your app and the HTTP request and validate the signature before hitting your app. Read more about rack middlewares here.

Here is how it could be used with sinatra:

require "http_signature/rack"

HTTPSignature.configure do |config|
  config.keys = [
    {id: "key-1", value: "MySecureKey"}
  ]
end
HTTPSignature::Rack.exclude_paths = ["/", "/hello/*"]

use HTTPSignature::Rack
run MyApp

Rails

Opt-in per controller/action using a before_action. It responds with 401 Unauthorized if the signature is invalid

# app/controllers/api/base_controller.rb

require "http_signature/rails"

class Api::BaseController < ApplicationController
  include HTTPSignature::Rails::Controller

  before_action :verify_http_signature!
end

Set the keys in an initializer

# config/initializers/http_signature.rb

HTTPSignature.configure do |config|
  config.keys = [
    {id: "key-1", value: "MySecureKey"}
  ]
end

Development

Install dependencies and then you can start running the tests!

bundle install

Test

The tests are written with minitest using specs. Run them all with rake:

rake test

Or a single with pattern matching:

rake test TEST=test/http_signature_test.rb TESTOPTS="--name=/appends\ the\ query_string_params/"

License

This project is licensed under the terms of the MIT license.

Why/when should I use this?

When you need to make sure that the request or response has not been tampered with (integrity). And you can be sure that the request was sent by someone that had the key (authenticity). Don't confuse this with encryption, the signed message is not encrypted. It's just signed. You could add a layer of encryption on top of this. Or just use HTTPS and you're kinda safe for not that much hassle, which is totally fine in most cases.

Read more about HMAC here, even though you can sign your messages with RSA as well, but it's the same principle.

Beware that this has not been audited and should be used at your own risk!