xml-kit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3a289752cbc49e2c74959f38b5b104ecbd02b4b8
4
+ data.tar.gz: 1475f47273a9d9483645c629517b3bf087f686c9
5
+ SHA512:
6
+ metadata.gz: 0576d38d7792ee436cfe335809516219730d817d5fc0a0d605affc897861e248ea07cc8ffe7e5312b77f2f1a85dd87b034a6d06e05a2b4c4f82afd8a655d8e0c
7
+ data.tar.gz: f7c764c6ff671e16b4992d0bea126819238ae766368d5c8b6215ca2cecbaa30890a5fad7ee2ba802be74de4f29bcfe63476cc6c0520a390c4c36bcf205a6e269
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,15 @@
1
+ image: ruby:2.2
2
+
3
+ before_script:
4
+ - apt-get update && apt-get install -y locales
5
+ - echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
6
+ - locale-gen
7
+ - export LC_ALL=en_US.UTF-8
8
+ - ruby -v
9
+ - which ruby
10
+ - gem install bundler --no-ri --no-rdoc
11
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
12
+
13
+ rspec:
14
+ script:
15
+ - bundle exec rspec
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.3
5
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in xml-kit.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 mo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Xml::Kit
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/xml/kit`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'xml-kit'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install xml-kit
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/saml-kit/xml-kit.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "xml/kit"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/xml/kit.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "active_model"
2
+ require "active_support/core_ext/numeric/time"
3
+ require "base64"
4
+ require "builder"
5
+ require "logger"
6
+ require "nokogiri"
7
+ require "openssl"
8
+ require "tilt"
9
+ require "xmldsig"
10
+
11
+ require "xml/kit/namespaces"
12
+
13
+ require "xml/kit/builders/encryption"
14
+ require "xml/kit/builders/signature"
15
+ require "xml/kit/certificate"
16
+ require "xml/kit/crypto"
17
+ require "xml/kit/decryption"
18
+ require "xml/kit/document"
19
+ require "xml/kit/fingerprint"
20
+ require "xml/kit/id"
21
+ require "xml/kit/key_pair"
22
+ require "xml/kit/self_signed_certificate"
23
+ require "xml/kit/signatures"
24
+ require "xml/kit/templatable"
25
+ require "xml/kit/template"
26
+ require "xml/kit/version"
27
+
28
+ module Xml
29
+ module Kit
30
+ class << self
31
+ def logger
32
+ @logger ||= Logger.new(STDOUT)
33
+ end
34
+
35
+ def logger=(logger)
36
+ @logger = logger
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Xml
2
+ module Kit
3
+ module Builders
4
+ class Encryption
5
+ attr_reader :public_key
6
+ attr_reader :key, :iv, :encrypted
7
+
8
+ def initialize(raw_xml, public_key)
9
+ @public_key = public_key
10
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
11
+ cipher.encrypt
12
+ @key = cipher.random_key
13
+ @iv = cipher.random_iv
14
+ @encrypted = cipher.update(raw_xml) + cipher.final
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,34 @@
1
+ module Xml
2
+ module Kit
3
+ module Builders
4
+ class Signature
5
+ SIGNATURE_METHODS = {
6
+ SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
7
+ SHA224: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224",
8
+ SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
9
+ SHA384: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
10
+ SHA512: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
11
+ }.freeze
12
+ DIGEST_METHODS = {
13
+ SHA1: "http://www.w3.org/2000/09/xmldsig#SHA1",
14
+ SHA224: "http://www.w3.org/2001/04/xmldsig-more#sha224",
15
+ SHA256: "http://www.w3.org/2001/04/xmlenc#sha256",
16
+ SHA384: "http://www.w3.org/2001/04/xmldsig-more#sha384",
17
+ SHA512: "http://www.w3.org/2001/04/xmlenc#sha512",
18
+ }.freeze
19
+
20
+ attr_reader :certificate
21
+ attr_reader :digest_method
22
+ attr_reader :reference_id
23
+ attr_reader :signature_method
24
+
25
+ def initialize(reference_id, signature_method: :SH256, digest_method: :SHA256, certificate:)
26
+ @certificate = certificate
27
+ @digest_method = DIGEST_METHODS[digest_method]
28
+ @reference_id = reference_id
29
+ @signature_method = SIGNATURE_METHODS[signature_method]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ xml.KeyDescriptor use: use do
2
+ xml.KeyInfo "xmlns": ::Xml::Kit::Namespaces::XMLDSIG do
3
+ xml.X509Data do
4
+ xml.X509Certificate stripped
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ xml.EncryptedData xmlns: ::Xml::Kit::Namespaces::XMLENC do
2
+ xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
3
+ xml.KeyInfo xmlns: ::Xml::Kit::Namespaces::XMLDSIG do
4
+ xml.EncryptedKey xmlns: ::Xml::Kit::Namespaces::XMLENC do
5
+ xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
6
+ xml.CipherData do
7
+ xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
8
+ end
9
+ end
10
+ end
11
+ xml.CipherData do
12
+ xml.CipherValue Base64.encode64(iv + encrypted)
13
+ end
14
+ end
File without changes
@@ -0,0 +1,20 @@
1
+ xml.Signature "xmlns" => ::Xml::Kit::Namespaces::XMLDSIG do
2
+ xml.SignedInfo do
3
+ xml.CanonicalizationMethod Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
4
+ xml.SignatureMethod Algorithm: signature_method
5
+ xml.Reference URI: "##{reference_id}" do
6
+ xml.Transforms do
7
+ xml.Transform Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
8
+ xml.Transform Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
9
+ end
10
+ xml.DigestMethod Algorithm: digest_method
11
+ xml.DigestValue ""
12
+ end
13
+ end
14
+ xml.SignatureValue ""
15
+ xml.KeyInfo do
16
+ xml.X509Data do
17
+ xml.X509Certificate certificate.stripped
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,96 @@
1
+ module Xml
2
+ module Kit
3
+ # {include:file:spec/xml/certificate_spec.rb}
4
+ class Certificate
5
+ BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
6
+ END_CERT=/-----END CERTIFICATE-----/
7
+ # The use can be `:signing` or `:encryption`
8
+ attr_reader :use
9
+
10
+ def initialize(value, use:)
11
+ @value = value
12
+ @use = use.downcase.to_sym
13
+ end
14
+
15
+ # @return [Xml::Kit::Fingerprint] the certificate fingerprint.
16
+ def fingerprint
17
+ Fingerprint.new(value)
18
+ end
19
+
20
+ # Returns true if this certificate is for the specified use.
21
+ #
22
+ # @param use [Symbol] `:signing` or `:encryption`.
23
+ # @return [Boolean] true or false.
24
+ def for?(use)
25
+ self.use == use.to_sym
26
+ end
27
+
28
+ # Returns true if this certificate is used for encryption.
29
+ #
30
+ # return [Boolean] true or false.
31
+ def encryption?
32
+ for?(:encryption)
33
+ end
34
+
35
+ # Returns true if this certificate is used for signing.
36
+ #
37
+ # return [Boolean] true or false.
38
+ def signing?
39
+ for?(:signing)
40
+ end
41
+
42
+ # Returns the x509 form.
43
+ #
44
+ # return [OpenSSL::X509::Certificate] the OpenSSL equivalent.
45
+ def x509
46
+ self.class.to_x509(value)
47
+ end
48
+
49
+ # Returns the public key.
50
+ #
51
+ # @return [OpenSSL::PKey::RSA] the RSA public key.
52
+ def public_key
53
+ x509.public_key
54
+ end
55
+
56
+ def ==(other)
57
+ self.fingerprint == other.fingerprint
58
+ end
59
+
60
+ def eql?(other)
61
+ self == other
62
+ end
63
+
64
+ def hash
65
+ value.hash
66
+ end
67
+
68
+ def to_s
69
+ value
70
+ end
71
+
72
+ def to_h
73
+ { use: @use, fingerprint: fingerprint.to_s }
74
+ end
75
+
76
+ def inspect
77
+ to_h.inspect
78
+ end
79
+
80
+ def stripped
81
+ value.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
82
+ end
83
+
84
+ def self.to_x509(value)
85
+ OpenSSL::X509::Certificate.new(Base64.decode64(value))
86
+ rescue OpenSSL::X509::CertificateError => error
87
+ ::Xml::Kit.logger.warn(error)
88
+ OpenSSL::X509::Certificate.new(value)
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :value
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,17 @@
1
+ require 'xml/kit/crypto/oaep_cipher'
2
+ require 'xml/kit/crypto/rsa_cipher'
3
+ require 'xml/kit/crypto/simple_cipher'
4
+ require 'xml/kit/crypto/unknown_cipher'
5
+
6
+ module Xml
7
+ module Kit
8
+ module Crypto
9
+ DECRYPTORS = [ SimpleCipher, RsaCipher, OaepCipher, UnknownCipher ]
10
+
11
+ # @!visibility private
12
+ def self.decryptor_for(algorithm, key)
13
+ DECRYPTORS.find { |x| x.matches?(algorithm) }.new(algorithm, key)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module Xml
2
+ module Kit
3
+ module Crypto
4
+ class OaepCipher
5
+ ALGORITHMS = {
6
+ 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' => true,
7
+ }
8
+ def initialize(algorithm, key)
9
+ @key = key
10
+ end
11
+
12
+ def self.matches?(algorithm)
13
+ ALGORITHMS[algorithm]
14
+ end
15
+
16
+ def decrypt(cipher_text)
17
+ @key.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Xml
2
+ module Kit
3
+ module Crypto
4
+ class RsaCipher
5
+ ALGORITHMS = {
6
+ 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' => true,
7
+ }
8
+
9
+ def initialize(algorithm, key)
10
+ @key = key
11
+ end
12
+
13
+ def self.matches?(algorithm)
14
+ ALGORITHMS[algorithm]
15
+ end
16
+
17
+ def decrypt(cipher_text)
18
+ @key.private_decrypt(cipher_text)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module Xml
2
+ module Kit
3
+ module Crypto
4
+ class SimpleCipher
5
+ ALGORITHMS = {
6
+ 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' => 'DES-EDE3-CBC',
7
+ 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' => 'AES-128-CBC',
8
+ 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' => 'AES-192-CBC',
9
+ 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' => 'AES-256-CBC',
10
+ }
11
+
12
+ def initialize(algorithm, private_key)
13
+ @algorithm = algorithm
14
+ @private_key = private_key
15
+ end
16
+
17
+ def self.matches?(algorithm)
18
+ ALGORITHMS[algorithm]
19
+ end
20
+
21
+ def decrypt(cipher_text)
22
+ cipher = OpenSSL::Cipher.new(ALGORITHMS[@algorithm])
23
+ cipher.decrypt
24
+ iv = cipher_text[0..cipher.iv_len-1]
25
+ data = cipher_text[cipher.iv_len..-1]
26
+ #cipher.padding = 0
27
+ cipher.key = @private_key
28
+ cipher.iv = iv
29
+ cipher.update(data) + cipher.final
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module Xml
2
+ module Kit
3
+ module Crypto
4
+ class UnknownCipher
5
+ def initialize(algorithm, key)
6
+ end
7
+
8
+ def self.matches?(algorithm)
9
+ true
10
+ end
11
+
12
+ def decrypt(cipher_text)
13
+ cipher_text
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ module Xml
2
+ module Kit
3
+ # {include:file:spec/saml/xml_decryption_spec.rb}
4
+ class Decryption
5
+ # The list of private keys to use to attempt to decrypt the document.
6
+ attr_reader :private_keys
7
+
8
+ def initialize(private_keys:)
9
+ @private_keys = private_keys
10
+ end
11
+
12
+ # Decrypts an EncryptedData section of an XML document.
13
+ #
14
+ # @param data [Hash] the XML document converted to a [Hash] using Hash.from_xml.
15
+ def decrypt(data)
16
+ encrypted_data = data['EncryptedData']
17
+ symmetric_key = symmetric_key_from(encrypted_data)
18
+ cipher_text = Base64.decode64(encrypted_data["CipherData"]["CipherValue"])
19
+ to_plaintext(cipher_text, symmetric_key, encrypted_data["EncryptionMethod"]['Algorithm'])
20
+ end
21
+
22
+ private
23
+
24
+ def symmetric_key_from(encrypted_data)
25
+ encrypted_key = encrypted_data['KeyInfo']['EncryptedKey']
26
+ cipher_text = Base64.decode64(encrypted_key['CipherData']['CipherValue'])
27
+ attempts = private_keys.count
28
+ private_keys.each do |private_key|
29
+ begin
30
+ attempts -= 1
31
+ return to_plaintext(cipher_text, private_key, encrypted_key["EncryptionMethod"]['Algorithm'])
32
+ rescue OpenSSL::PKey::RSAError => error
33
+ ::Xml::Kit.logger.error(error)
34
+ raise if attempts.zero?
35
+ end
36
+ end
37
+ end
38
+
39
+ def to_plaintext(cipher_text, symmetric_key, algorithm)
40
+ Crypto.decryptor_for(algorithm, symmetric_key).decrypt(cipher_text)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,75 @@
1
+ module Xml
2
+ module Kit
3
+ # {include:file:spec/saml/xml_spec.rb}
4
+ class Document
5
+ include ActiveModel::Validations
6
+ NAMESPACES = { "ds": ::Xml::Kit::Namespaces::XMLDSIG }.freeze
7
+
8
+ validate :validate_signatures
9
+ validate :validate_certificates
10
+
11
+ def initialize(raw_xml, namespaces: NAMESPACES)
12
+ @raw_xml = raw_xml
13
+ @namespaces = namespaces
14
+ @document = ::Nokogiri::XML(raw_xml)
15
+ end
16
+
17
+ # Returns the first XML node found by searching the document with the provided XPath.
18
+ #
19
+ # @param xpath [String] the XPath to use to search the document
20
+ def find_by(xpath)
21
+ document.at_xpath(xpath, namespaces)
22
+ end
23
+
24
+ # Returns all XML nodes found by searching the document with the provided XPath.
25
+ #
26
+ # @param xpath [String] the XPath to use to search the document
27
+ def find_all(xpath)
28
+ document.search(xpath, namespaces)
29
+ end
30
+
31
+ # Return the XML document as a [String].
32
+ #
33
+ # @param pretty [Boolean] return the XML string in a human readable format if true.
34
+ def to_xml(pretty: true)
35
+ pretty ? document.to_xml(indent: 2) : raw_xml
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :raw_xml, :document, :namespaces
41
+
42
+ def validate_signatures
43
+ invalid_signatures.flat_map(&:errors).uniq.each do |error|
44
+ errors.add(error, "is invalid")
45
+ end
46
+ end
47
+
48
+ def invalid_signatures
49
+ signed_document = Xmldsig::SignedDocument.new(document, id_attr: 'ID=$uri or @Id')
50
+ signed_document.signatures.find_all do |signature|
51
+ x509_certificates.all? do |certificate|
52
+ !signature.valid?(certificate)
53
+ end
54
+ end
55
+ end
56
+
57
+ def validate_certificates(now = Time.current)
58
+ return if find_by('//ds:Signature').nil?
59
+
60
+ x509_certificates.each do |certificate|
61
+ inactive = now < certificate.not_before
62
+ errors.add(:certificate, "Not valid before #{certificate.not_before}") if inactive
63
+
64
+ expired = now > certificate.not_after
65
+ errors.add(:certificate, "Not valid after #{certificate.not_after}") if expired
66
+ end
67
+ end
68
+
69
+ def x509_certificates
70
+ xpath = "//ds:KeyInfo/ds:X509Data/ds:X509Certificate"
71
+ find_all(xpath).map { |item| Certificate.to_x509(item.text) }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ module Xml
2
+ module Kit
3
+ # This generates a fingerprint for an X509 Certificate.
4
+ #
5
+ # certificate, _ = Xml::Kit::SelfSignedCertificate.new("password").create
6
+ #
7
+ # puts Xml::Kit::Fingerprint.new(certificate).to_s
8
+ # # B7:AB:DC:BD:4D:23:58:65:FD:1A:99:0C:5F:89:EA:87:AD:F1:D7:83:34:7A:E9:E4:88:12:DD:46:1F:38:05:93
9
+ #
10
+ # {include:file:spec/saml/fingerprint_spec.rb}
11
+ class Fingerprint
12
+ # The OpenSSL::X509::Certificate
13
+ attr_reader :x509
14
+
15
+ def initialize(raw_certificate)
16
+ @x509 = Certificate.to_x509(raw_certificate)
17
+ end
18
+
19
+ # Generates a formatted fingerprint using the specified hash algorithm.
20
+ #
21
+ # @param algorithm [OpenSSL::Digest] the openssl algorithm to use `OpenSSL::Digest::SHA256`, `OpenSSL::Digest::SHA1`.
22
+ # @return [String] in the format of `"BF:ED:C5:F1:6C:AB:F5:B2:15:1F:BF:BD:7D:68:1A:F9:A5:4E:4C:19:30:BC:6D:25:B1:8E:98:D4:23:FD:B4:09"`
23
+ def algorithm(algorithm)
24
+ pretty_fingerprint(algorithm.new.hexdigest(x509.to_der))
25
+ end
26
+
27
+ def ==(other)
28
+ self.to_s == other.to_s
29
+ end
30
+
31
+ def eql?(other)
32
+ self == other
33
+ end
34
+
35
+ def hash
36
+ to_s.hash
37
+ end
38
+
39
+ def to_s
40
+ algorithm(OpenSSL::Digest::SHA256)
41
+ end
42
+
43
+ private
44
+
45
+ def pretty_fingerprint(fingerprint)
46
+ fingerprint.upcase.scan(/../).join(":")
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/xml/kit/id.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Xml
2
+ module Kit
3
+ # This class is used primary for generating ID.
4
+ #https://www.w3.org/2001/XMLSchema.xsd
5
+ class Id
6
+ # Generate an ID that conforms to the XML Schema.
7
+ # https://www.w3.org/2001/XMLSchema.xsd
8
+ def self.generate
9
+ "_#{SecureRandom.uuid}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Xml
2
+ module Kit
3
+ class KeyPair # :nodoc:
4
+ attr_reader :certificate, :private_key, :use
5
+
6
+ def initialize(certificate, private_key, passphrase, use)
7
+ @use = use
8
+ @certificate = ::Xml::Kit::Certificate.new(certificate, use: use)
9
+ @private_key = OpenSSL::PKey::RSA.new(private_key, passphrase)
10
+ end
11
+
12
+ # Returns true if the key pair is the designated use.
13
+ #
14
+ # @param use [Symbol] Can be either `:signing` or `:encryption`.
15
+ def for?(use)
16
+ @use == use
17
+ end
18
+
19
+ # Returns a generated self signed certificate with private key.
20
+ #
21
+ # @param use [Symbol] Can be either `:signing` or `:encryption`.
22
+ # @param passphrase [String] the passphrase to use to encrypt the private key.
23
+ def self.generate(use:, passphrase: SecureRandom.uuid)
24
+ certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new(passphrase).create
25
+ new(certificate, private_key, passphrase, use)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module Xml
2
+ module Kit
3
+ module Namespaces
4
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
5
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
6
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
7
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
8
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
9
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
10
+ SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
11
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
12
+ SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
13
+ XMLDSIG = "http://www.w3.org/2000/09/xmldsig#"
14
+ XMLENC = "http://www.w3.org/2001/04/xmlenc#"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Xml
2
+ module Kit
3
+ class SelfSignedCertificate
4
+ SUBJECT="/C=CA/ST=Alberta/L=Calgary/O=XmlKit/OU=XmlKit/CN=XmlKit"
5
+
6
+ def initialize(passphrase)
7
+ @passphrase = passphrase
8
+ end
9
+
10
+ def create
11
+ rsa_key = OpenSSL::PKey::RSA.new(2048)
12
+ public_key = rsa_key.public_key
13
+ certificate = OpenSSL::X509::Certificate.new
14
+ certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse(SUBJECT)
15
+ certificate.not_before = Time.now.to_i
16
+ certificate.not_after = (Date.today + 30).to_time.to_i
17
+ certificate.public_key = public_key
18
+ certificate.serial = 0x0
19
+ certificate.version = 2
20
+ certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
21
+ [
22
+ certificate.to_pem,
23
+ rsa_key.to_pem(OpenSSL::Cipher.new('AES-256-CBC'), @passphrase)
24
+ ]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ module Xml
2
+ module Kit
3
+ # @!visibility private
4
+ class Signatures # :nodoc:
5
+ attr_reader :key_pair, :signature_method, :digest_method
6
+
7
+ # @!visibility private
8
+ def initialize(key_pair:, signature_method:, digest_method:)
9
+ @digest_method = digest_method
10
+ @key_pair = key_pair
11
+ @signature_method = signature_method
12
+ end
13
+
14
+ # @!visibility private
15
+ def sign_with(key_pair)
16
+ @key_pair = key_pair
17
+ end
18
+
19
+ # @!visibility private
20
+ def build(reference_id)
21
+ return nil if key_pair.nil?
22
+
23
+ ::Xml::Kit::Builders::Signature.new(
24
+ reference_id,
25
+ certificate: key_pair.certificate,
26
+ signature_method: signature_method,
27
+ digest_method: digest_method
28
+ )
29
+ end
30
+
31
+ # @!visibility private
32
+ def complete(raw_xml)
33
+ return raw_xml if key_pair.nil?
34
+
35
+ private_key = key_pair.private_key
36
+ Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
37
+ end
38
+
39
+ # @!visibility private
40
+ def self.sign(xml: ::Builder::XmlMarkup.new, key_pair:, signature_method: :SHA256, digest_method: :SHA256)
41
+ signatures = new(
42
+ key_pair: key_pair,
43
+ signature_method: signature_method,
44
+ digest_method: digest_method,
45
+ )
46
+ yield xml, XmlSignatureTemplate.new(xml, signatures)
47
+ signatures.complete(xml.target!)
48
+ end
49
+
50
+ class XmlSignatureTemplate # :nodoc:
51
+ # @!visibility private
52
+ attr_reader :signatures, :xml
53
+
54
+ # @!visibility private
55
+ def initialize(xml, signatures)
56
+ @signatures = signatures
57
+ @xml = xml
58
+ end
59
+
60
+ # @!visibility private
61
+ def template(reference_id)
62
+ Template.new(signatures.build(reference_id)).to_xml(xml: xml)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,83 @@
1
+ module Xml
2
+ module Kit
3
+ module Templatable
4
+ # Can be used to disable embeding a signature.
5
+ # By default a signature will be embedded if a signing
6
+ # certificate is available.
7
+ attr_accessor :embed_signature
8
+
9
+ # Used to enable/disable encrypting the document.
10
+ attr_accessor :encrypt
11
+
12
+ # The [Xml::Kit::KeyPair] to use for generating a signature.
13
+ attr_accessor :signing_key_pair
14
+
15
+ # The [Xml::Kit::Certificate] that contains the public key to use for encrypting the document.
16
+ attr_accessor :encryption_certificate
17
+
18
+ # Returns the generated XML document with an XML Digital Signature and XML Encryption.
19
+ def to_xml(xml: ::Builder::XmlMarkup.new)
20
+ signatures.complete(render(self, xml: xml))
21
+ end
22
+
23
+ def encryption_for(xml:)
24
+ if encrypt?
25
+ temp = ::Builder::XmlMarkup.new
26
+ yield temp
27
+ signed_xml = signatures.complete(temp.target!)
28
+ xml_encryption = ::Xml::Kit::Builders::Encryption.new(
29
+ signed_xml,
30
+ encryption_certificate.public_key
31
+ )
32
+ render(xml_encryption, xml: xml)
33
+ else
34
+ yield xml
35
+ end
36
+ end
37
+
38
+ def render(model, options)
39
+ ::Xml::Kit::Template.new(model).to_xml(options)
40
+ end
41
+
42
+ def signature_for(reference_id:, xml:)
43
+ return unless sign?
44
+ render(signatures.build(reference_id), xml: xml)
45
+ end
46
+
47
+ # Allows you to specify which key pair to use for generating an XML digital signature.
48
+ #
49
+ # @param key_pair [Xml::Kit::KeyPair] the key pair to use for signing.
50
+ def sign_with(key_pair)
51
+ signatures.sign_with(key_pair)
52
+ end
53
+
54
+ private
55
+
56
+ def sign?
57
+ embed_signature
58
+ end
59
+
60
+ # @!visibility private
61
+ def signatures
62
+ @signatures ||= ::Xml::Kit::Signatures.new(
63
+ key_pair: signing_key_pair,
64
+ digest_method: digest_method,
65
+ signature_method: signature_method,
66
+ )
67
+ end
68
+
69
+ def digest_method
70
+ :SHA256
71
+ end
72
+
73
+ def signature_method
74
+ :SHA256
75
+ end
76
+
77
+ # @!visibility private
78
+ def encrypt?
79
+ encrypt && encryption_certificate
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,32 @@
1
+ module Xml
2
+ module Kit
3
+ class Template
4
+ attr_reader :target
5
+
6
+ def initialize(target)
7
+ @target = target
8
+ end
9
+
10
+ # Returns the compiled template as a [String].
11
+ #
12
+ # @param options [Hash] The options hash to pass to the template engine.
13
+ def to_xml(options = {})
14
+ template.render(target, options)
15
+ end
16
+
17
+ private
18
+
19
+ def template_path
20
+ return target.template_path if target.respond_to?(:template_path)
21
+
22
+ root_path = File.expand_path(File.dirname(__FILE__))
23
+ template_name = "#{target.class.name.split("::").last.underscore}.builder"
24
+ File.join(root_path, "builders/templates/", template_name)
25
+ end
26
+
27
+ def template
28
+ Tilt.new(template_path)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ module Xml
2
+ module Kit
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/xml-kit.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "xml/kit/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "xml-kit"
8
+ spec.version = Xml::Kit::VERSION
9
+ spec.authors = ["mo khan"]
10
+ spec.email = ["mo@mokhan.ca"]
11
+
12
+ spec.summary = %q{A simple toolkit for working with XML.}
13
+ spec.description = %q{A simple toolkit for working with XML.}
14
+ spec.homepage = "http://www.mokhan.ca"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = '>= 2.2.0'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.metadata["yard.run"] = "yri"
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "activemodel", ">= 4.2.0"
27
+ spec.add_dependency "builder", "~> 3.2"
28
+ spec.add_dependency "nokogiri", "~> 1.8"
29
+ spec.add_dependency "tilt", "~> 2.0"
30
+ spec.add_dependency "xmldsig", "~> 0.6"
31
+ spec.add_development_dependency "bundler", "~> 1.16"
32
+ spec.add_development_dependency "ffaker", "~> 2.7"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "simplecov", "~> 0.15.1"
36
+ end
metadata ADDED
@@ -0,0 +1,220 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xml-kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mo khan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-12-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: builder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tilt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: xmldsig
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.6'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.16'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.16'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ffaker
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.7'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.15.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.15.1
153
+ description: A simple toolkit for working with XML.
154
+ email:
155
+ - mo@mokhan.ca
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".gitignore"
161
+ - ".gitlab-ci.yml"
162
+ - ".rspec"
163
+ - ".travis.yml"
164
+ - Gemfile
165
+ - LICENSE.txt
166
+ - README.md
167
+ - Rakefile
168
+ - bin/console
169
+ - bin/setup
170
+ - lib/xml/kit.rb
171
+ - lib/xml/kit/builders/encryption.rb
172
+ - lib/xml/kit/builders/signature.rb
173
+ - lib/xml/kit/builders/templates/certificate.builder
174
+ - lib/xml/kit/builders/templates/encryption.builder
175
+ - lib/xml/kit/builders/templates/nil_class.builder
176
+ - lib/xml/kit/builders/templates/signature.builder
177
+ - lib/xml/kit/certificate.rb
178
+ - lib/xml/kit/crypto.rb
179
+ - lib/xml/kit/crypto/oaep_cipher.rb
180
+ - lib/xml/kit/crypto/rsa_cipher.rb
181
+ - lib/xml/kit/crypto/simple_cipher.rb
182
+ - lib/xml/kit/crypto/unknown_cipher.rb
183
+ - lib/xml/kit/decryption.rb
184
+ - lib/xml/kit/document.rb
185
+ - lib/xml/kit/fingerprint.rb
186
+ - lib/xml/kit/id.rb
187
+ - lib/xml/kit/key_pair.rb
188
+ - lib/xml/kit/namespaces.rb
189
+ - lib/xml/kit/self_signed_certificate.rb
190
+ - lib/xml/kit/signatures.rb
191
+ - lib/xml/kit/templatable.rb
192
+ - lib/xml/kit/template.rb
193
+ - lib/xml/kit/version.rb
194
+ - xml-kit.gemspec
195
+ homepage: http://www.mokhan.ca
196
+ licenses:
197
+ - MIT
198
+ metadata:
199
+ yard.run: yri
200
+ post_install_message:
201
+ rdoc_options: []
202
+ require_paths:
203
+ - lib
204
+ required_ruby_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: 2.2.0
209
+ required_rubygems_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ requirements: []
215
+ rubyforge_project:
216
+ rubygems_version: 2.6.14
217
+ signing_key:
218
+ specification_version: 4
219
+ summary: A simple toolkit for working with XML.
220
+ test_files: []