مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
feat: add sha256 signatures to outgoing http requests (#2874)
هذا الالتزام موجود في:
2
Gemfile
2
Gemfile
@@ -8,12 +8,12 @@ gem "chronic"
|
|||||||
gem "domain_name"
|
gem "domain_name"
|
||||||
gem "dotenv"
|
gem "dotenv"
|
||||||
gem "dynamic_form"
|
gem "dynamic_form"
|
||||||
gem "encrypto_signo"
|
|
||||||
gem "execjs", "~> 2.7", "< 2.8"
|
gem "execjs", "~> 2.7", "< 2.8"
|
||||||
gem "gelf"
|
gem "gelf"
|
||||||
gem "haml"
|
gem "haml"
|
||||||
gem "hashie"
|
gem "hashie"
|
||||||
gem "highline", require: false
|
gem "highline", require: false
|
||||||
|
gem "jwt"
|
||||||
gem "kaminari"
|
gem "kaminari"
|
||||||
gem "klogger-logger"
|
gem "klogger-logger"
|
||||||
gem "konfig-config", "~> 3.0"
|
gem "konfig-config", "~> 3.0"
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ GEM
|
|||||||
activemodel (> 5.2.0)
|
activemodel (> 5.2.0)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
encrypto_signo (1.0.0)
|
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
factory_bot (6.4.6)
|
factory_bot (6.4.6)
|
||||||
@@ -152,6 +151,8 @@ GEM
|
|||||||
bindata
|
bindata
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
|
jwt (2.8.1)
|
||||||
|
base64
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
@@ -410,7 +411,6 @@ DEPENDENCIES
|
|||||||
domain_name
|
domain_name
|
||||||
dotenv
|
dotenv
|
||||||
dynamic_form
|
dynamic_form
|
||||||
encrypto_signo
|
|
||||||
execjs (~> 2.7, < 2.8)
|
execjs (~> 2.7, < 2.8)
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
gelf
|
gelf
|
||||||
@@ -418,6 +418,7 @@ DEPENDENCIES
|
|||||||
hashie
|
hashie
|
||||||
highline
|
highline
|
||||||
jquery-rails
|
jquery-rails
|
||||||
|
jwt
|
||||||
kaminari
|
kaminari
|
||||||
klogger-logger
|
klogger-logger
|
||||||
konfig-config (~> 3.0)
|
konfig-config (~> 3.0)
|
||||||
|
|||||||
15
app/controllers/well_known_controller.rb
Normal file
15
app/controllers/well_known_controller.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WellKnownController < ApplicationController
|
||||||
|
|
||||||
|
layout false
|
||||||
|
|
||||||
|
skip_before_action :set_browser_id
|
||||||
|
skip_before_action :login_required
|
||||||
|
skip_before_action :set_timezone
|
||||||
|
|
||||||
|
def jwks
|
||||||
|
render json: JWT::JWK::Set.new(Postal.signer.jwk).export.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -9,7 +9,7 @@ class DKIMHeader
|
|||||||
@dkim_identifier = domain.dkim_identifier
|
@dkim_identifier = domain.dkim_identifier
|
||||||
else
|
else
|
||||||
@domain_name = Postal::Config.dns.return_path_domain
|
@domain_name = Postal::Config.dns.return_path_domain
|
||||||
@dkim_key = Postal.signing_key
|
@dkim_key = Postal.signer.private_key
|
||||||
@dkim_identifier = Postal::Config.dns.dkim_identifier
|
@dkim_identifier = Postal::Config.dns.dkim_identifier
|
||||||
end
|
end
|
||||||
@domain = domain
|
@domain = domain
|
||||||
|
|||||||
66
app/lib/signer.rb
Normal file
66
app/lib/signer.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "base64"
|
||||||
|
|
||||||
|
class Signer
|
||||||
|
|
||||||
|
# Create a new Signer
|
||||||
|
#
|
||||||
|
# @param [OpenSSL::PKey::RSA] private_key The private key to use for signing
|
||||||
|
# @return [Signer]
|
||||||
|
def initialize(private_key)
|
||||||
|
@private_key = private_key
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the private key
|
||||||
|
#
|
||||||
|
# @return [OpenSSL::PKey::RSA]
|
||||||
|
attr_reader :private_key
|
||||||
|
|
||||||
|
# Return the public key for the private key
|
||||||
|
#
|
||||||
|
# @return [OpenSSL::PKey::RSA]
|
||||||
|
def public_key
|
||||||
|
@private_key.public_key
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sign the given data
|
||||||
|
#
|
||||||
|
# @param [String] data The data to sign
|
||||||
|
# @return [String] The signature
|
||||||
|
def sign(data)
|
||||||
|
private_key.sign(OpenSSL::Digest.new("SHA256"), data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sign the given data and return a Base64-encoded signature
|
||||||
|
#
|
||||||
|
# @param [String] data The data to sign
|
||||||
|
# @return [String] The Base64-encoded signature
|
||||||
|
def sign64(data)
|
||||||
|
Base64.strict_encode64(sign(data))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return a JWK for the private key
|
||||||
|
#
|
||||||
|
# @return [JWT::JWK] The JWK
|
||||||
|
def jwk
|
||||||
|
@jwk ||= JWT::JWK.new(private_key, { use: "sig", alg: "RS256" })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sign the given data using SHA1 (for legacy use)
|
||||||
|
#
|
||||||
|
# @param [String] data The data to sign
|
||||||
|
# @return [String] The signature
|
||||||
|
def sha1_sign(data)
|
||||||
|
private_key.sign(OpenSSL::Digest.new("SHA1"), data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sign the given data using SHA1 (for legacy use) and return a Base64-encoded string
|
||||||
|
#
|
||||||
|
# @param [String] data The data to sign
|
||||||
|
# @return [String] The signature
|
||||||
|
def sha1_sign64(data)
|
||||||
|
Base64.strict_encode64(sha1_sign(data))
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -89,6 +89,8 @@ Rails.application.routes.draw do
|
|||||||
get "auth/oidc/callback", to: "sessions#create_from_oidc"
|
get "auth/oidc/callback", to: "sessions#create_from_oidc"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get ".well-known/jwks.json" => "well_known#jwks"
|
||||||
|
|
||||||
get "ip" => "sessions#ip"
|
get "ip" => "sessions#ip"
|
||||||
|
|
||||||
root "organizations#index"
|
root "organizations#index"
|
||||||
|
|||||||
@@ -98,12 +98,15 @@ module Postal
|
|||||||
"#{locker_name} #{suffix}"
|
"#{locker_name} #{suffix}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def signing_key
|
def signer
|
||||||
@signing_key ||= OpenSSL::PKey::RSA.new(File.read(Config.postal.signing_key_path))
|
@signer ||= begin
|
||||||
|
key = OpenSSL::PKey::RSA.new(File.read(Config.postal.signing_key_path))
|
||||||
|
Signer.new(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rp_dkim_dns_record
|
def rp_dkim_dns_record
|
||||||
public_key = signing_key.public_key.to_s.gsub(/-+[A-Z ]+-+\n/, "").gsub(/\n/, "")
|
public_key = signer.private_key.public_key.to_s.gsub(/-+[A-Z ]+-+\n/, "").gsub(/\n/, "")
|
||||||
"v=DKIM1; t=s; h=sha256; p=#{public_key};"
|
"v=DKIM1; t=s; h=sha256; p=#{public_key};"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ module Postal
|
|||||||
end
|
end
|
||||||
|
|
||||||
if options[:sign]
|
if options[:sign]
|
||||||
signature = EncryptoSigno.sign(Postal.signing_key, request.body.to_s).gsub("\n", "")
|
request.add_field "X-Postal-Signature-KID", Postal.signer.jwk.kid
|
||||||
request.add_field "X-Postal-Signature", signature
|
request.add_field "X-Postal-Signature", Postal.signer.sha1_sign64(request.body.to_s)
|
||||||
|
request.add_field "X-Postal-Signature-256", Postal.signer.sign64(request.body.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
request["User-Agent"] = options[:user_agent] || "Postal/#{Postal.version}"
|
request["User-Agent"] = options[:user_agent] || "Postal/#{Postal.version}"
|
||||||
|
|||||||
12
spec/lib/postal_spec.rb
Normal file
12
spec/lib/postal_spec.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Postal do
|
||||||
|
describe "#signer" do
|
||||||
|
it "returns a signer with the installation's signing key" do
|
||||||
|
expect(Postal.signer).to be_a(Signer)
|
||||||
|
expect(Postal.signer.private_key.to_pem).to eq OpenSSL::PKey::RSA.new(File.read(Postal::Config.postal.signing_key_path)).to_pem
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
76
spec/lib/signer_spec.rb
Normal file
76
spec/lib/signer_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Signer do
|
||||||
|
STATIC_PRIVATE_KEY = OpenSSL::PKey::RSA.new(2048) # rubocop:disable Lint/ConstantDefinitionInBlock
|
||||||
|
|
||||||
|
subject(:signer) { described_class.new(STATIC_PRIVATE_KEY) }
|
||||||
|
|
||||||
|
describe "#private_key" do
|
||||||
|
it "returns the private key" do
|
||||||
|
expect(signer.private_key).to eq(STATIC_PRIVATE_KEY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#public_key" do
|
||||||
|
it "returns the public key" do
|
||||||
|
expect(signer.public_key.to_s).to eq(STATIC_PRIVATE_KEY.public_key.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#sign" do
|
||||||
|
it "returns a valid signature" do
|
||||||
|
data = "hello world!"
|
||||||
|
signature = signer.sign(data)
|
||||||
|
expect(signature).to be_a(String)
|
||||||
|
verification = STATIC_PRIVATE_KEY.public_key.verify(OpenSSL::Digest.new("SHA256"),
|
||||||
|
signature,
|
||||||
|
data)
|
||||||
|
expect(verification).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#sign64" do
|
||||||
|
it "returns a valid Base64-encoded signature" do
|
||||||
|
data = "hello world!"
|
||||||
|
signature = signer.sign64(data)
|
||||||
|
expect(signature).to be_a(String)
|
||||||
|
verification = STATIC_PRIVATE_KEY.public_key.verify(OpenSSL::Digest.new("SHA256"),
|
||||||
|
Base64.strict_decode64(signature),
|
||||||
|
data)
|
||||||
|
expect(verification).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#jwk" do
|
||||||
|
it "returns a valid JWK" do
|
||||||
|
jwk = signer.jwk
|
||||||
|
expect(jwk).to be_a(JWT::JWK::RSA)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#sha1_sign" do
|
||||||
|
it "returns a valid signature" do
|
||||||
|
data = "hello world!"
|
||||||
|
signature = signer.sha1_sign(data)
|
||||||
|
expect(signature).to be_a(String)
|
||||||
|
verification = STATIC_PRIVATE_KEY.public_key.verify(OpenSSL::Digest.new("SHA1"),
|
||||||
|
signature,
|
||||||
|
data)
|
||||||
|
expect(verification).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#sha1_sign64" do
|
||||||
|
it "returns a valid Base64-encoded signature" do
|
||||||
|
data = "hello world!"
|
||||||
|
signature = signer.sha1_sign64(data)
|
||||||
|
expect(signature).to be_a(String)
|
||||||
|
verification = STATIC_PRIVATE_KEY.public_key.verify(OpenSSL::Digest.new("SHA1"),
|
||||||
|
Base64.strict_decode64(signature),
|
||||||
|
data)
|
||||||
|
expect(verification).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,7 +28,9 @@ RSpec.describe WebhookDeliveryService do
|
|||||||
}.to_json,
|
}.to_json,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type" => "application/json",
|
"Content-Type" => "application/json",
|
||||||
"X-Postal-Signature" => /\A[a-z0-9\/+]+=*\z/i
|
"X-Postal-Signature" => /\A[a-z0-9\/+]+=*\z/i,
|
||||||
|
"X-Postal-Signature-256" => /\A[a-z0-9\/+]+=*\z/i,
|
||||||
|
"X-Postal-Signature-KID" => /\A[a-f0-9\/+]{64}\z/i
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم