Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed Jul 11, 2024
1 parent d985988 commit 93b063a
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* [#690](https://github.com/SAML-Toolkits/ruby-saml/pull/690) Remove deprecated `settings.security[:embed_sign]` parameter.
* [#697](https://github.com/SAML-Toolkits/ruby-saml/pull/697) Add deprecation for various parameters in `RubySaml::Settings`.
* [#709](https://github.com/SAML-Toolkits/ruby-saml/pull/709) Allow passing in `Net::HTTP` `:open_timeout`, `:read_timeout`, and `:max_retries` settings to `IdpMetadataParser#parse_remote`.
* [#711](https://github.com/SAML-Toolkits/ruby-saml/pull/711) Standardize how RubySaml reads and formats certificate and private_key PEM values, including the `RubySaml::Util#format_cert` and `#format_private_key` methods.

### 1.17.0
* [#687](https://github.com/SAML-Toolkits/ruby-saml/pull/687) Add CI coverage for Ruby 3.3 and Windows.
Expand Down
28 changes: 27 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The SAML SP request/response message compression behavior is now controlled auto
"compression" is used to make redirect URLs which contain SAML messages be shorter. For POST messages,
compression may be achieved by enabling `Content-Encoding: gzip` on your webserver.

## Settings deprecations
### Settings deprecations

The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 2.1.0:

Expand All @@ -92,6 +92,32 @@ The following parameters in `RubySaml::Settings` are deprecated and will be remo
- `#certificate_new` is deprecated and replaced by `#sp_cert_multi`. Refer to documentation as `#sp_cert_multi`
has a different value type than `#certificate_new`.

### Minor changes to Util#format_cert and #format_private_key

Version 2.0.0 standardizes how RubySaml reads and formats certificate and private key
PEM strings. In general, version 2.0.0 is more permissive than 1.x, and the changes
are not anticipated to affect most users. Please note the change affects parameters
such `#idp_cert` and `#certificate`, as well as the `RubySaml::Util#format_cert`
and `#format_private_key` methods. Specifically:

- In RubySaml 2.0.0, `#format_cert` and `#format_private_key` now return `nil` if the
input is an empty or blank (i.e. full of whitespace) string. In version 1.x, the
empty/blank string itself was returned in these cases.
- In version 1.x, RubySaml skipped PEM formatting if the input string contained the
`\r` character (the carriage return character used for Windows-style line endings.)
RubySaml 2.0.0 strips out all `\r` characters and performs formatting as usual.
- In version 1.x, RubySaml skipped PEM formatting if the input string contained
header values other than `CERTIFICATE` and `PRIVATE KEY`/`RSA PRIVATE KEY`
respectively. RubySaml 2.0.0 accepts any header value that ends in `CERTIFICATE`
or `PRIVATE KEY` respectively. For example, `-----BEGIN X509 CERTIFICATE-----` is now
considered a valid certificate header. As before, if `RSA PRIVATE KEY` is present in
the input string it will be preserved in the output.
- In version 1.x, RubySaml skipped PEM formatting if the input string contained text
outside the header/footer value(s). RubySaml 2.0.0 strips out all text outside the
header/footer value(s) and parse the PEM contents normally. When parsing chained
certificates (i.e. multiple certificates in one string), text present between the
footer and header of two different certificates will also be stripped out.

## Updating from 1.12.x to 1.13.0

Version `1.13.0` adds `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding`, and
Expand Down
72 changes: 39 additions & 33 deletions lib/ruby_saml/pem_formatter.rb
Original file line number Diff line number Diff line change
@@ -1,68 +1,74 @@
# frozen_string_literal: true

module RubySaml
# Formats PEM-encoded X.509 certificates and private keys to
# canonical PEM format with 64-char lines and BEGIN/END headers.
# Formats PEM-encoded X.509 certificates and private keys to canonical
# RFC 7468 PEM format, including 64-char lines and BEGIN/END headers.
#
# @api private
module PemFormatter
extend self

# Formats one or many X.509 certificate(s) to canonical
# PEM format with 64-char lines and BEGIN/END headers.
# Formats one or multiple X.509 certificate(s) to canonical RFC 7468 PEM format.
#
# @param cert [String] The original certificate(s)
# @param multi [true|false] Whether to return multiple keys delimited by newline
# @return [String|nil] The formatted certificate(s)
# @return [String|nil] The formatted certificate(s). Returns nil if the input is nil/blank,
# and returns the original string if it contains non-ASCII characters.
def format_cert(cert, multi: false)
detect_pems(cert, 'CERTIFICATE', multi: multi) do |pem|
format_cert_single(pem)
end
detect_and_format_pems(cert, 'CERTIFICATE', multi: multi)
end

# Formats one or many private key(s) to canonical PEM format
# with 64-char lines and BEGIN/END headers.
# Formats one or multiple private key(s) to canonical RFC 7468 PEM format.
#
# @param key [String] The original private key(s)
# @param multi [true|false] Whether to return multiple keys delimited by newline
# @return [String|nil] The formatted private key(s)
# @return [String|nil] The formatted private key(s). Returns nil if the input is nil/blank,
# and returns the original string if it contains non-ASCII characters.
def format_private_key(key, multi: false)
detect_pems(key, '(?:RSA|DSA|EC|ECDSA) PRIVATE KEY', multi: multi) do |pem|
format_private_key_single(pem)
end
detect_and_format_pems(key, 'PRIVATE KEY', %w[RSA ECDSA EC DSA], multi: multi)
end

private

def detect_pems(str, label, multi: false, &block)
return if str.nil? || str.empty?
return str unless str.ascii_only?
return if str.match?(/\A\s*\z/)
def detect_and_format_pems(str, label, known_prefixes = nil, multi: false)
return str unless str&.ascii_only?

pems = str.scan(/-{5}\s*BEGIN #{label}\s*-{5}.*?-{5}\s*END #{label}\s*-{5}?/m).map(&block)
return if !str || str.strip.empty?

# Try to format the original string if no pems were found
return yield(str) if pems.empty?
# Scan the string for PEMs and format each one
pems = str.scan(pem_scan_regexp(label)).map { |pem| format_pem(pem, label, known_prefixes) }

# Try to format the original string if no PEMs were found
return format_pem(str, label, known_prefixes) if pems.empty?

multi ? pems.join("\n") : pems.first
end

def format_cert_single(cert)
format_pem(cert, 'CERTIFICATE')
def pem_scan_regexp(label)
/-{5}\s*BEGIN (?:[A-Z\d] )*#{label}\s*-{5}.*?-{5}\s*END (?:[A-Z\d] )*#{label}\s*-{5}/m
end

# Given a PEM, a label such as "PRIVATE KEY", and a list of known prefixes
# such as "RSA", "DSA", etc., returns the formatted PEM preserving the known
# prefix if possible.
def format_pem(pem, label, known_prefixes = nil)
detected_prefix = detect_label_prefix(pem, label, known_prefixes)
prefixed_label = "#{detected_prefix}#{label}"
"-----BEGIN #{prefixed_label}-----\n#{format_pem_body(pem)}\n-----END #{prefixed_label}-----"
end

def format_private_key_single(key)
algo = key.match(/((?:RSA|ECDSA|EC|DSA) )PRIVATE KEY/)&.[](1)
label = "#{algo}PRIVATE KEY"
format_pem(key, label)
# Given a PEM, a label such as "PRIVATE KEY", and a list of known prefixes
# such as "RSA", "DSA", etc., detects and returns the known prefix if it exists.
def detect_label_prefix(pem, label, known_prefixes)
return unless known_prefixes && !known_prefixes.empty?

pem.match(/((?:#{Array(known_prefixes).join('|')}) )#{label}/)&.[](1)
end

# Strips all whitespace and the BEGIN/END lines,
# then splits the string into 64-character lines,
# and re-applies BEGIN/END labels
def format_pem(str, label)
str = str.gsub(/\s|-{5}\s*(BEGIN|END) [A-Z\d\s]+-{5}/, '').scan(/.{1,64}/).join("\n")
"-----BEGIN #{label}-----\n#{str}\n-----END #{label}-----"
# Given a PEM, strips all whitespace and the BEGIN/END lines,
# then splits the body into 64-character lines.
def format_pem_body(pem)
pem.gsub(/\s|-{5}\s*(BEGIN|END) [A-Z\d\s]+-{5}/, '').scan(/.{1,64}/).join("\n")
end
end
end
12 changes: 6 additions & 6 deletions lib/ruby_saml/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,22 @@ def parse_duration(duration, timestamp=Time.now.utc)
datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
end

# Formats one or many X.509 certificate(s) to canonical
# PEM format with 64-char lines and BEGIN/END headers.
# Formats one or multiple X.509 certificate(s) to canonical RFC 7468 PEM format.
#
# @param cert [String] The original certificate(s)
# @param multi [true|false] Whether to return multiple keys delimited by newline
# @return [String|nil] The formatted certificate(s)
# @return [String|nil] The formatted certificate(s). Returns nil if the input is nil/blank,
# and returns the original string if it contains non-ASCII characters.
def format_cert(cert, multi: true)
PemFormatter.format_cert(cert, multi: multi)
end

# Formats one or many private key(s) to canonical PEM format
# with 64-char lines and BEGIN/END headers.
# Formats one or multiple private key(s) to canonical RFC 7468 PEM format.
#
# @param key [String] The original private key(s)
# @param multi [true|false] Whether to return multiple keys delimited by newline
# @return [String|nil] The formatted private key(s)
# @return [String|nil] The formatted private key(s). Returns nil if the input is nil/blank,
# and returns the original string if it contains non-ASCII characters.
def format_private_key(key, multi: false)
PemFormatter.format_private_key(key, multi: multi)
end
Expand Down

0 comments on commit 93b063a

Please sign in to comment.