مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-11-30 21:32:30 +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 "dotenv"
|
||||
gem "dynamic_form"
|
||||
gem "encrypto_signo"
|
||||
gem "execjs", "~> 2.7", "< 2.8"
|
||||
gem "gelf"
|
||||
gem "haml"
|
||||
gem "hashie"
|
||||
gem "highline", require: false
|
||||
gem "jwt"
|
||||
gem "kaminari"
|
||||
gem "klogger-logger"
|
||||
gem "konfig-config", "~> 3.0"
|
||||
|
||||
@@ -112,7 +112,6 @@ GEM
|
||||
activemodel (> 5.2.0)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
encrypto_signo (1.0.0)
|
||||
erubi (1.12.0)
|
||||
execjs (2.7.0)
|
||||
factory_bot (6.4.6)
|
||||
@@ -152,6 +151,8 @@ GEM
|
||||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
jwt (2.8.1)
|
||||
base64
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
@@ -410,7 +411,6 @@ DEPENDENCIES
|
||||
domain_name
|
||||
dotenv
|
||||
dynamic_form
|
||||
encrypto_signo
|
||||
execjs (~> 2.7, < 2.8)
|
||||
factory_bot_rails
|
||||
gelf
|
||||
@@ -418,6 +418,7 @@ DEPENDENCIES
|
||||
hashie
|
||||
highline
|
||||
jquery-rails
|
||||
jwt
|
||||
kaminari
|
||||
klogger-logger
|
||||
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
|
||||
else
|
||||
@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
|
||||
end
|
||||
@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"
|
||||
end
|
||||
|
||||
get ".well-known/jwks.json" => "well_known#jwks"
|
||||
|
||||
get "ip" => "sessions#ip"
|
||||
|
||||
root "organizations#index"
|
||||
|
||||
@@ -98,12 +98,15 @@ module Postal
|
||||
"#{locker_name} #{suffix}"
|
||||
end
|
||||
|
||||
def signing_key
|
||||
@signing_key ||= OpenSSL::PKey::RSA.new(File.read(Config.postal.signing_key_path))
|
||||
def signer
|
||||
@signer ||= begin
|
||||
key = OpenSSL::PKey::RSA.new(File.read(Config.postal.signing_key_path))
|
||||
Signer.new(key)
|
||||
end
|
||||
end
|
||||
|
||||
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};"
|
||||
end
|
||||
|
||||
|
||||
@@ -40,8 +40,9 @@ module Postal
|
||||
end
|
||||
|
||||
if options[:sign]
|
||||
signature = EncryptoSigno.sign(Postal.signing_key, request.body.to_s).gsub("\n", "")
|
||||
request.add_field "X-Postal-Signature", signature
|
||||
request.add_field "X-Postal-Signature-KID", Postal.signer.jwk.kid
|
||||
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
|
||||
|
||||
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,
|
||||
headers: {
|
||||
"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
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم