-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsigned_mail.cr
More file actions
130 lines (112 loc) · 4.16 KB
/
Copy pathsigned_mail.cr
File metadata and controls
130 lines (112 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
require "openssl"
require "./body"
require "./dkim_header"
require "./header"
require "./canonicalized_headers"
module Dkim
class SignedMail
@private_key : OpenSSL::PKey::RSA?
def private_key; @private_key; end
def private_key=(key : OpenSSL::PKey::RSA | String)
key = OpenSSL::PKey::RSA.new(key) if key.is_a?(String)
@private_key = key
end
@original_message : String
@headers : Array(Header)
property headers
# A new instance of SignedMail
#
# @param [String,#to_s] message mail message to be signed
# @param [Hash] options hash of options for signing. Defaults are taken from {Dkim}. See {Options} for details.
def initialize(message,
@time : Time = Time.utc,
@domain : String = "example.com",
private_key : String | OpenSSL::PKey::RSA | Nil = nil,
@selector : String = "dkim",
@identity : String? = nil,
@expire : Time? = nil,
@signing_algorithm : String = "rsa-sha256",
@header_canonicalization : String = "relaxed",
@body_canonicalization : String = "relaxed",
@signable_headers : Array(String) = Dkim::DefaultHeaders,
@body_length : Int32? = nil)
message = message.to_s.gsub(/\r?\n/, "\r\n")
headers, body = message.split(/\r?\n\r?\n/, 2)
@original_message = message
@headers = Header.parse headers
@body = Body.new body
self.private_key = private_key unless private_key.nil?
end
def canonicalized_headers
CanonicalizedHeaders.new(@headers, signed_headers)
end
# @return [Array<String>] lowercased names of headers in the order they are signed
def signed_headers
@headers.map do |h|
h.relaxed_key
end.select do |key|
@signable_headers.map do |hdr|
hdr.downcase
end.includes?(key)
end
end
# @return [String] Signed headers of message in their canonical forms
def canonical_header
canonicalized_headers.canonical(@header_canonicalization)
end
# @return [String] Body of message in its canonical form
def canonical_body
@body.canonical(@body_canonicalization)
end
# @return [DkimHeader] Constructed signature for the mail message
def dkim_header : DkimHeader
dkim_header = DkimHeader.new
raise "A private key is required" unless private_key
raise "A domain is required" unless @domain
raise "A selector is required" unless @selector
# Add basic DKIM info
dkim_header["v"] = "1"
dkim_header["a"] = @signing_algorithm
dkim_header["c"] = "#{@header_canonicalization}/#{@body_canonicalization}"
dkim_header["d"] = @domain
dkim_header["i"] = @identity.as(String) unless @identity.nil?
dkim_header["q"] = "dns/txt"
dkim_header["s"] = @selector
dkim_header["t"] = @time.to_unix.to_s
dkim_header["x"] = @expire.as(Time).to_unix.to_s unless @expire.nil?
# Add body hash (truncate if body_length set)
body = canonical_body
if bl = @body_length
body = body.byte_slice(0, bl)
dkim_header["l"] = bl.to_s
end
dkim_header["bh"]= String.new(digest_alg.update(body).final)
dkim_header["h"] = signed_headers.join(":")
dkim_header["b"] = ""
# Calculate signature based on intermediate signature header
headers = canonical_header
# puts "dkim_header.class: #{dkim_header.class}\n"
# puts "header_canonicalization: #{header_canonicalization}\n"
headers += dkim_header.canonical(@header_canonicalization)
dkim_header["b"] = String.new(private_key.as(OpenSSL::PKey::RSA).sign(digest_alg, headers))
dkim_header
end
# @return [String] Message combined with calculated dkim header signature
def signed_message
"#{dkim_header.canonical}\r\n#{@original_message}"
end
# private
def digest_alg
case @signing_algorithm
when "rsa-sha1"
OpenSSL::Digest.new("SHA1")
# OpenSSL::Digest::SHA1.new
when "rsa-sha256"
OpenSSL::Digest.new("SHA256")
# OpenSSL::Digest::SHA256.new
else
raise "Unknown digest algorithm: '#{@signing_algorithm}'"
end
end
end
end