Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow configuration of which base64-encoding scheme to use for outbound messages #36

Merged
merged 4 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.12.0 February 14, 2024

Allow configuration of which base64 encoding scheme to apply to outbound message bodies.
Improves compatibility with some versions of MuleSoft. [#36](https://github.com/alexdean/as2/pull/36)

## 0.11.0 September 14, 2023

* Allow configuration of which encryption cipher to use when sending outbound messages. [#35](https://github.com/alexdean/as2/pull/35)
Expand Down
48 changes: 48 additions & 0 deletions lib/as2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,54 @@ def self.generate_message_id(server_info)
"<#{server_info.name}-#{Time.now.strftime('%Y%m%d-%H%M%S')}-#{SecureRandom.uuid}@#{server_info.domain}>"
end

def self.valid_base64_schemes
[
'rfc2045',
'rfc4648'
]
end

# create a base64 string from content, based on the given encoding scheme
#
# @param [String] content
# @param [String] scheme one of As2.valid_base64_schemes
# @return [String]
def self.base64_encode(content, scheme: 'rfc4648')
case scheme.to_s
when 'rfc2045'
# "This method complies with RFC 2045."
# https://ruby-doc.org/stdlib-3.0.4/libdoc/base64/rdoc/Base64.html#method-i-encode64
# https://www.rfc-editor.org/rfc/rfc2045#section-6.8
then Base64.encode64(content)
when 'rfc4648'
# "This method complies with RFC 4648."
# https://ruby-doc.org/stdlib-3.0.4/libdoc/base64/rdoc/Base64.html#method-i-strict_encode64
# https://www.rfc-editor.org/rfc/rfc4648#section-4
then Base64.strict_encode64(content)
else
raise ArgumentError, "unsupported scheme '#{scheme}'. choose one of: #{valid_base64_schemes}"
end
end

# canonicalize all line endings in the given text.
#
# "\n" becomes "\r\n"
# "\r\n" remains "\r\n"
#
# Conversion to canonical form:
# The entire body ... is converted to a universal canonical
# form. ... For example, in the case of text/plain data, the text
# must be converted to a supported character set and lines must
# be delimited with CRLF delimiters in accordance with RFC 822.
#
# https://www.rfc-editor.org/rfc/rfc2049#page-9
#
# @param [String] content
# @return [String] content, but with all bare \n replaced by \r\n
def self.canonicalize_line_endings(content)
content.gsub(/(?<!\r)\n/, "\r\n")
end

# Select which algorithm to use for calculating a MIC, based on preferences
# stated by sender & our list of available algorithms.
#
Expand Down
11 changes: 8 additions & 3 deletions lib/as2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def format_body_v0(document_content, content_type:, file_name:)
document_payload << "Content-Transfer-Encoding: base64\r\n"
document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
document_payload << "\r\n"
document_payload << Base64.strict_encode64(document_content)
document_payload << base64_encode(document_content)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are now canonicalizing line-endings after encoding.

this will not be a behavior change for existing configs (which all use RFC-4648, aka .strict_encode) since RFC-4648 base64 will not contain any line endings.


signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
signature.detached = true
Expand Down Expand Up @@ -201,7 +201,7 @@ def format_body_v1(document_content, content_type:, file_name:)
document_payload << "Content-Transfer-Encoding: base64\r\n"
document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
document_payload << "\r\n"
document_payload << Base64.strict_encode64(document_content)
document_payload << base64_encode(document_content)

signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
signature.detached = true
Expand All @@ -211,7 +211,7 @@ def format_body_v1(document_content, content_type:, file_name:)
# strip off the '-----BEGIN PKCS7-----' / '-----END PKCS7-----' delimiters
bare_pem_signature.gsub!(/^-----[^\n]+\n/, '')
# and update to canonical \r\n line endings
bare_pem_signature.gsub!(/(?<!\r)\n/, "\r\n")
bare_pem_signature = As2.canonicalize_line_endings(bare_pem_signature)

# this is a hack until i can determine a better way to get the micalg parameter
# from the pkcs7 signature generated above...
Expand Down Expand Up @@ -315,6 +315,11 @@ def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_bo

private

def base64_encode(content)
encoded = As2.base64_encode(content, scheme: @partner.base64_scheme)
As2.canonicalize_line_endings(encoded)
end

# extract the MDN body from a multipart/signed wrapper & attempt to verify
# the signature
#
Expand Down
12 changes: 11 additions & 1 deletion lib/as2/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ def self.build_certificate(input)
end
end

class Partner < Struct.new :name, :url, :encryption_certificate, :encryption_cipher, :signing_certificate, :tls_verify_mode, :mdn_format, :outbound_format
class Partner < Struct.new :name, :url, :encryption_certificate, :encryption_cipher, :signing_certificate, :tls_verify_mode, :mdn_format, :outbound_format, :base64_scheme
def initialize
# set default.
self.encryption_cipher = 'aes-256-cbc'
self.base64_scheme = 'rfc4648'
end

def base64_scheme=(scheme)
scheme_s = scheme.to_s
valid_schemes = As2.valid_base64_schemes
if !valid_schemes.include?(scheme_s)
raise ArgumentError, "base64_scheme '#{scheme_s}' must be one of #{valid_schemes.inspect}"
end
self['base64_scheme'] = scheme_s
end

def url=(url)
Expand Down
2 changes: 1 addition & 1 deletion lib/as2/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def format_mdn_v1(mdn_text, as2_to:)
# strip off the '-----BEGIN PKCS7-----' / '-----END PKCS7-----' delimiters
bare_pem_signature.gsub!(/^-----[^\n]+\n/, '')
# and update to canonical \r\n line endings
bare_pem_signature.gsub!(/(?<!\r)\n/, "\r\n")
bare_pem_signature = As2.canonicalize_line_endings(bare_pem_signature)

# this is a hack until i can determine a better way to get the micalg parameter
# from the pkcs7 signature generated above...
Expand Down
2 changes: 1 addition & 1 deletion lib/as2/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module As2
VERSION = "0.11.0"
VERSION = "0.12.0"
end
101 changes: 101 additions & 0 deletions test/as2_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,107 @@
end
end

describe '.base64_encode' do
before(:all) do
@ascii = (32..126).map(&:chr).join.freeze
@binary = File.open('test/fixtures/white-box.png', 'rb').read.freeze
@utf8 = "こんにちは".freeze
end

describe 'with rfc2045 rules' do
it 'can encode all printable ascii characters' do
encoded = As2.base64_encode(@ascii, scheme: 'rfc2045')
decoded = Base64.decode64(encoded)
assert_equal @ascii, decoded
end

it 'can encode arbitrary binary data' do
encoded = As2.base64_encode(@binary, scheme: 'rfc2045')
decoded = Base64.decode64(encoded)
assert_equal @binary, decoded
end

it 'can encode UTF-8' do
encoded = As2.base64_encode(@utf8, scheme: 'rfc2045')
decoded = Base64.decode64(encoded)
decoded.force_encoding('UTF-8')
assert_equal @utf8, decoded
end
end

describe 'with rfc4648 rules' do
it 'can encode all printable ascii characters' do
encoded = As2.base64_encode(@ascii, scheme: 'rfc4648')
decoded = Base64.strict_decode64(encoded)
assert_equal @ascii, decoded
end

it 'can encode arbitrary binary data' do
encoded = As2.base64_encode(@binary, scheme: 'rfc4648')
decoded = Base64.strict_decode64(encoded)
assert_equal @binary, decoded
end

it 'can encode UTF-8' do
encoded = As2.base64_encode(@utf8, scheme: 'rfc4648')
decoded = Base64.decode64(encoded)
decoded.force_encoding('UTF-8')
assert_equal @utf8, decoded
end
end

it 'raises if the given encoding scheme is not recognized' do
error = assert_raises(ArgumentError) do
As2.base64_encode(@binary, scheme: 'blah')
end
assert_equal "unsupported scheme 'blah'. choose one of: [\"rfc2045\", \"rfc4648\"]", error.message
end

it 'defaults to RFC-4648 for backwards-compatibility' do
expected = As2.base64_encode(@ascii, scheme: 'rfc4648')
assert_equal expected, As2.base64_encode(@ascii)
end
end

describe '.canonicalize_line_endings' do
it 'replaces \n with \r\n' do
input = "a\nb\nc\n"
expected = "a\r\nb\r\nc\r\n"
assert_equal expected, As2.canonicalize_line_endings(input)
end

it 'does not alter existing \r\n sequences' do
input = "a\r\nb\nc\n"
expected = "a\r\nb\r\nc\r\n"
assert_equal expected, As2.canonicalize_line_endings(input)
end

it 'does not add trailing newlines if string does not end with a newline' do
input = "a"
expected = "a"
assert_equal expected, As2.canonicalize_line_endings(input)
end

it 'is compatible with all base64_encode schemes' do
ascii = (32..126).map(&:chr).join.freeze
input = ascii * 10 # long enough to be split onto multiple lines in rfc2045

# if a new scheme is added, this assertion will remind us to test it here also.
valid_schemes = As2.valid_base64_schemes
assert_equal(['rfc2045', 'rfc4648'], valid_schemes)

encoded = As2.base64_encode(input, scheme: 'rfc2045')
canonicalized = As2.canonicalize_line_endings(encoded)
decoded = Base64.decode64(encoded)
assert_equal input, decoded

encoded = As2.base64_encode(input, scheme: 'rfc4648')
canonicalized = As2.canonicalize_line_endings(encoded)
decoded = Base64.strict_decode64(encoded)
assert_equal input, decoded
end
end

describe '.choose_mic_algorithm' do
it 'returns nil if no algorithm is found' do
assert_nil As2.choose_mic_algorithm(nil)
Expand Down
17 changes: 17 additions & 0 deletions test/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@
end
end

describe '#base64_scheme=' do
it 'accepts a valid scheme value' do
@partner_config.base64_scheme = 'rfc2045'
assert_equal 'rfc2045', @partner_config.base64_scheme

@partner_config.base64_scheme = 'rfc4648'
assert_equal 'rfc4648', @partner_config.base64_scheme
end

it 'raises if given an invalid format value' do
error = assert_raises(ArgumentError) do
@partner_config.base64_scheme = 'invalid'
end
assert_equal "base64_scheme 'invalid' must be one of [\"rfc2045\", \"rfc4648\"]", error.message
end
end

describe '#url=' do
it 'accepts a string' do
@partner_config.url = 'http://test.com'
Expand Down
Binary file added test/fixtures/white-box.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.