Class: As2::Message
- Inherits:
-
Object
- Object
- As2::Message
- Defined in:
- lib/as2/message.rb
Instance Attribute Summary collapse
-
#verification_error ⇒ Object
readonly
Returns the value of attribute verification_error.
Class Method Summary collapse
-
.choose_attachment(mail_parts) ⇒ Mail::Part?
given multiple parts of a message, choose the one most likely to be the actual content we care about.
-
.choose_signature(mail_parts) ⇒ Mail::Part?
return the mail part containing a digital signature.
-
.mic(attachment, mic_algorithm) ⇒ String
calculate the MIC for a given mail part.
-
.verify(content:, signature_text:, signing_certificate:) ⇒ Hash
Check that the signature is valid.
Instance Method Summary collapse
-
#attachment ⇒ Mail::Part
Return the attached file, use .filename and .body on the return value This is the content the sender is sending to us.
- #decrypted_message ⇒ Object
-
#initialize(message, private_key, public_certificate, mic_algorithm: nil) ⇒ Message
constructor
A new instance of Message.
- #mic ⇒ Object
- #mic_algorithm ⇒ Object
-
#parts ⇒ Object
TODO: deprecate this, or make it private.
-
#signature ⇒ Mail::Part
Return the digital signature which is part of the incoming message.
- #valid_signature?(partner_signing_certificate) ⇒ Boolean
Constructor Details
#initialize(message, private_key, public_certificate, mic_algorithm: nil) ⇒ Message
Returns a new instance of Message.
107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/as2/message.rb', line 107 def initialize(, private_key, public_certificate, mic_algorithm: nil) # TODO: might need to use OpenSSL::PKCS7.read_smime rather than .new sometimes @pkcs7 = OpenSSL::PKCS7.new() @private_key = private_key @public_certificate = public_certificate @verification_error = nil @mic_algorithm = mic_algorithm || 'sha256' if !As2::DigestSelector.valid?(@mic_algorithm) raise ArgumentError, "'#{@mic_algorithm}' is not a valid MIC algorithm." end end |
Instance Attribute Details
#verification_error ⇒ Object (readonly)
Returns the value of attribute verification_error.
3 4 5 |
# File 'lib/as2/message.rb', line 3 def verification_error @verification_error end |
Class Method Details
.choose_attachment(mail_parts) ⇒ Mail::Part?
given multiple parts of a message, choose the one most likely to be the actual content we care about
10 11 12 13 14 15 16 17 18 |
# File 'lib/as2/message.rb', line 10 def self.(mail_parts) return nil if mail_parts.nil? # if there are multiple content parts, try to prefer the EDI content. candidates = mail_parts .select { |part| part.content_type.to_s['pkcs7-signature'].nil? } # skip signature .sort_by { |part| part.content_type.to_s.match(/^application\/edi/i) ? 0 : 1 } candidates[0] end |
.choose_signature(mail_parts) ⇒ Mail::Part?
return the mail part containing a digital signature
24 25 26 27 28 |
# File 'lib/as2/message.rb', line 24 def self.choose_signature(mail_parts) return nil if mail_parts.nil? mail_parts.find { |part| part.content_type.to_s['pkcs7-signature'] } end |
.mic(attachment, mic_algorithm) ⇒ String
calculate the MIC for a given mail part
35 36 37 38 |
# File 'lib/as2/message.rb', line 35 def self.mic(, mic_algorithm) digest = As2::DigestSelector.for_code(mic_algorithm) digest.base64digest(.raw_source.lstrip) end |
.verify(content:, signature_text:, signing_certificate:) ⇒ Hash
Check that the signature is valid.
This confirms 2 things:
1. The `signature_text` is valid for `content`, ie: the `content` has
not been altered.
2. The `signature_text` was generated by the party who owns `certificate`,
ie: The same private key generated `signature_text` and `certificate`.
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 |
# File 'lib/as2/message.rb', line 56 def self.verify(content:, signature_text:, signing_certificate:) begin signature = OpenSSL::PKCS7.new(signature_text) # using an empty CA store. see notes on NOVERIFY flag below. store = OpenSSL::X509::Store.new # notes on verification process and flags used # # ## NOINTERN # # > If PKCS7_NOINTERN is set the certificates in the message itself are # > not searched when locating the signer's certificate. This means that # > all the signers certificates must be in the certs parameter. # > # > One application of PKCS7_NOINTERN is to only accept messages signed # > by a small number of certificates. The acceptable certificates would # > be passed in the certs parameter. In this case if the signer is not # > one of the certificates supplied in certs then the verify will fail # > because the signer cannot be found. # # https://www.openssl.org/docs/manmaster/man3/PKCS7_verify.html # # we want this so we can be sure that the `signing_certificate` we supply # was actually used to sign the message. otherwise we could get a positive # verification even if `signing_certificate` didn't sign the message # we're checking. # # ## NOVERIFY # # > If PKCS7_NOVERIFY is set the signer's certificates are not chain verified. # # ie: we won't attempt to connect signer (in the first param) to a root # CA (in `store`, which is empty). alternately, we could instead remove # this flag, and add `signing_certificate` to `store`. but what's the point? # we'd only be verifying that `signing_certificate` is connected to `signing_certificate`. valid = signature.verify([signing_certificate], store, content, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN) # when `signature.verify` fails, signature.error_string will be populated. error = signature.error_string rescue => e valid = false error = "#{e.class}: #{e.}" end { valid: valid, error: error } end |
Instance Method Details
#attachment ⇒ Mail::Part
maybe rename this to ‘payload`. ’attachment’ sounds very email.
Return the attached file, use .filename and .body on the return value This is the content the sender is sending to us.
219 220 221 |
# File 'lib/as2/message.rb', line 219 def @attachment ||= self.class.(parts) end |
#decrypted_message ⇒ Object
120 121 122 |
# File 'lib/as2/message.rb', line 120 def @decrypted_message ||= @pkcs7.decrypt @private_key, @public_certificate end |
#mic ⇒ Object
206 207 208 |
# File 'lib/as2/message.rb', line 206 def mic self.class.mic(, mic_algorithm) end |
#mic_algorithm ⇒ Object
210 211 212 |
# File 'lib/as2/message.rb', line 210 def mic_algorithm @mic_algorithm end |
#parts ⇒ Object
TODO: deprecate this, or make it private
232 233 234 |
# File 'lib/as2/message.rb', line 232 def parts mail&.parts end |
#signature ⇒ Mail::Part
Return the digital signature which is part of the incoming message. Will return ‘nil` for unsigned messages
227 228 229 |
# File 'lib/as2/message.rb', line 227 def signature @signature ||= self.class.choose_signature(parts) end |
#valid_signature?(partner_signing_certificate) ⇒ Boolean
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/as2/message.rb', line 124 def valid_signature?(partner_signing_certificate) content_type = mail.header_fields.find { |h| h.name == 'Content-Type' }.content_type # TODO: substantial overlap between this code & the fallback/rescue code in # As2::Client#verify_mdn_signature if content_type == "multipart/signed" # for a "multipart/signed" message, we will do 'detatched' signature # verification, where we supply the data to be verified as the 3rd parameter # to OpenSSL::PKCS7#verify. this is in keeping with how this content type # is described in the S/MIME RFC. # # > The multipart/signed MIME type has two parts. The first part contains # > the MIME entity that is signed; the second part contains the "detached signature" # > CMS SignedData object in which the encapContentInfo eContent field is absent. # # https://datatracker.ietf.org/doc/html/rfc3851#section-3.4.3 # # see also https://datatracker.ietf.org/doc/html/rfc1847#section-2.1 content = .raw_source # remove any leading \r\n characters (between headers & body i think). content = content.gsub(/\A\s+/, '') # TODO: why is signature.body.to_s different from signature.body.raw_source? signature_text = signature.body.to_s result = self.class.verify( content: content, signature_text: signature_text, signing_certificate: partner_signing_certificate ) output = result[:valid] @verification_error = result[:error] # HACK until https://github.com/mikel/mail/pull/1511 is available # # due to a bug in the mail gem (fixed in PR above), when using # 'Content-Transfer-Encoding: binary', the body given by `attachment.raw_source` # will have all "\n" replaced by "\r\n". This causes a signature verification # failure. # # here, we try reversing this behavior (changing "\r\n" in the body back # to "\n") and re-attempt verification. # # this entire block can should removed once the bugfix in mail gem is # released & integrated into as2. # # we don't really know that verification failed due to line-ending mismatch. # it's only a guess. if !output && .content_transfer_encoding == 'binary' # TODO: log when this happens. # include attachment.content_transfer_encoding, the results of the initial verification # and the results of the re-attempted verification body_delimiter = "\r\n\r\n" # split on first occurrence of `body_delimiter` # any trailing occurrences of `body_delimiter` are preserved as part of `body` headers, _, body = content.partition(body_delimiter) body.gsub!("\r\n", "\n") # cross fingers... content = headers + body_delimiter + body retry_output = self.class.verify( content: content, signature_text: signature_text, signing_certificate: partner_signing_certificate ) if retry_output[:valid] @attachment = Mail::Part.new(content) @verification_error = retry_output[:error] output = retry_output[:valid] end end output else # TODO: how to log this? false end end |