-
-
Notifications
You must be signed in to change notification settings - Fork 566
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d985988
commit 0815ee9
Showing
5 changed files
with
130 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,103 @@ | ||
# 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. | ||
PEM_SCAN_BASE64 = '[A-Za-z\d+/\s]+=?\s*=?\s*' | ||
|
||
# Formats one or multiple X.509 certificate(s) to an array of strings | ||
# in canonical RFC 7468 PEM format. | ||
# | ||
# @param certs [String] A string containing unformatted certificate(s). | ||
# @return [Array<String>] The formatted certificate(s). | ||
def format_cert_array(certs) | ||
format_pem_array(certs, 'CERTIFICATE') | ||
end | ||
|
||
# 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) | ||
# @param cert [String] A string containing unformatted certificate(s). | ||
# @param multi [true|false] Whether to return multiple certificates delimited by newline. | ||
# Default false. | ||
# @return [String] The formatted certificate(s). Returns nil if the input is blank. | ||
def format_cert(cert, multi: false) | ||
detect_pems(cert, 'CERTIFICATE', multi: multi) do |pem| | ||
format_cert_single(pem) | ||
end | ||
pem_array_to_string(format_cert_array(cert), multi: multi) | ||
end | ||
|
||
# Formats one or multiple X.509 private keys(s) to canonical RFC 7468 PEM format. | ||
# | ||
# @param keys [String] A string containing unformatted private keys(s). | ||
# @return [Array<String>] The formatted private keys(s). | ||
def format_private_key_array(keys) | ||
format_pem_array(keys, 'PRIVATE KEY', %w[RSA ECDSA EC DSA]) | ||
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) | ||
# @param key [String] A string containing unformatted private keys(s). | ||
# @param multi [true|false] Whether to return multiple keys delimited by newline. | ||
# Default false. | ||
# @return [String|nil] The formatted private key(s). Returns nil if the input is blank. | ||
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 | ||
pem_array_to_string(format_private_key_array(key), 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 format_pem_array(str, label, known_prefixes = nil) | ||
str = str&.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') | ||
return [] if !str || str.strip.empty? | ||
|
||
pems = str.scan(/-{5}\s*BEGIN #{label}\s*-{5}.*?-{5}\s*END #{label}\s*-{5}?/m).map(&block) | ||
# 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 | ||
pems = format_pem(str, label, known_prefixes).scan(pem_scan_regexp(label)) if pems.empty? | ||
pems | ||
end | ||
|
||
def pem_array_to_string(pem_array, multi: false) | ||
pem_array = Array(pem_array) | ||
return if pem_array.empty? | ||
multi ? pem_array.join("\n") : pem_array.first | ||
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 | ||
|
||
# Try to format the original string if no pems were found | ||
return yield(str) if pems.empty? | ||
# 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? | ||
|
||
multi ? pems.join("\n") : pems.first | ||
pem.match(/((?:#{Array(known_prefixes).join('|')}) )#{label}/)&.[](1) | ||
end | ||
|
||
def format_cert_single(cert) | ||
format_pem(cert, 'CERTIFICATE') | ||
# 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|#{pem_scan_header}/, '').scan(/.{1,64}/).join("\n") | ||
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) | ||
# Returns a regexp which can be used to loosely match unformatted PEM(s) in a string. | ||
def pem_scan_regexp(label) | ||
/#{pem_scan_header('BEGIN', label)}#{PEM_SCAN_BASE64}#{pem_scan_header('END', label)}/m | ||
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}-----" | ||
# Returns a regexp component string to match PEM headers. | ||
def pem_scan_header(marker = '(BEGIN|END)', label = '[A-Z\d]+') | ||
"-{5}\\s*#{marker} (?:[A-Z\\d]+ )*#{label}\\s*-{5}" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters