lyrebird 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b9457c5f260c26c5adf43de4262d67077e1f5729b4506ff0efa92ec61519346
4
- data.tar.gz: 16a5dbf97973cd9c3673c26212fa0be146955ef9ddaf877768d40120416d6e54
3
+ metadata.gz: 54582bda5673179b49a58b405ee52668486162eb785d728cfd98b6dcaed7f1cf
4
+ data.tar.gz: f6e2048c4adb1a6b3690f2517c781cd431f9cc0271217df6effb8944e808fee0
5
5
  SHA512:
6
- metadata.gz: fc83af3b91fb52553dc96733c2a8ea0e89c561fe124bddc1b769cb39f681e03c1e18ff78ec2c83ffe886114b369e4a1630e637e56a1cf2dcee2137a5fe88680b
7
- data.tar.gz: 138bd3e26e223c69dc10e435b21c12e0e1fcf3322e64df65d1775695d8920e6a274caf0d1323c5722244d73a47a09296d4d2a1d2037f2761084f470fcff6202c
6
+ metadata.gz: 5998bda9158b2d8d3f64f6260fcfb9b3b06c62469c923e86776bac8ab541f955945621f799b23ab69cc4388e561d403ab15d8adc763b018f5fad9411409668c8
7
+ data.tar.gz: 9aebec46c7f33d74c5274073dea1a93a347fb363b603bbf3cd8a8dc7ba26f0fbe2c9e5f34ba6fc4afcb67bfe850e8c963d485a2adca3539bd3137173d4675bba
@@ -28,112 +28,80 @@ module Lyrebird
28
28
  end
29
29
 
30
30
  def document
31
- @document ||= Nokogiri::XML::Document.new.tap do |doc|
32
- doc.root = root(doc)
33
- end
31
+ @document ||= Nokogiri::XML::Builder.new { |xml| root(xml) }.doc
34
32
  end
35
33
 
36
34
  private
37
35
 
38
- def root(doc)
39
- doc.create_element("Assertion").tap do |a|
40
- ns = a.add_namespace_definition("saml", SAML_ASSERTION_NS)
41
-
42
- a.namespace = ns
43
- a["ID"] = ID.generate
44
- a["Version"] = "2.0"
45
- a["IssueInstant"] = @issue_instant.iso8601
46
-
47
- a.add_child(doc.create_element("Issuer")).tap do |i|
48
- i.namespace = ns
49
- i.content = @issuer
50
- end
51
-
52
- a.add_child(subject(doc, ns))
53
- a.add_child(conditions(doc, ns))
54
- a.add_child(authn_statement(doc, ns))
55
- a.add_child(attribute_statement(doc, ns)) if @attributes.any?
36
+ def root(xml)
37
+ attrs = {
38
+ "xmlns:saml" => SAML_ASSERTION_NS,
39
+ ID: ID.generate,
40
+ Version: "2.0",
41
+ IssueInstant: @issue_instant.iso8601
42
+ }
43
+
44
+ xml["saml"].Assertion(attrs) do
45
+ xml["saml"].Issuer(@issuer.to_s)
46
+ subject(xml)
47
+ conditions(xml)
48
+ authn_statement(xml)
49
+ attribute_statement(xml) if @attributes.any?
56
50
  end
57
51
  end
58
52
 
59
- def subject(doc, ns)
60
- doc.create_element("Subject").tap do |s|
61
- s.namespace = ns
62
-
63
- s.add_child(doc.create_element("NameID")).tap do |nid|
64
- nid.namespace = ns
65
- nid["Format"] = @name_id_format
66
- nid.content = @name_id
67
- end
68
-
69
- s.add_child(subject_confirmation(doc, ns))
53
+ def subject(xml)
54
+ xml["saml"].Subject do
55
+ xml["saml"].NameID(@name_id.to_s, Format: @name_id_format)
56
+ subject_confirmation(xml)
70
57
  end
71
58
  end
72
59
 
73
- def subject_confirmation(doc, ns)
74
- doc.create_element("SubjectConfirmation").tap do |sc|
75
- sc.namespace = ns
76
- sc["Method"] = CM_BEARER
60
+ def subject_confirmation(xml)
61
+ attrs = {
62
+ NotOnOrAfter: @not_on_or_after.iso8601,
63
+ Recipient: @recipient,
64
+ InResponseTo: @in_response_to
65
+ }.compact
77
66
 
78
- sc.add_child(doc.create_element("SubjectConfirmationData")).tap do |d|
79
- d.namespace = ns
80
- d["NotOnOrAfter"] = @not_on_or_after.iso8601
81
- d["Recipient"] = @recipient
82
- d["InResponseTo"] = @in_response_to if @in_response_to
83
- end
67
+ xml["saml"].SubjectConfirmation(Method: CM_BEARER) do
68
+ xml["saml"].SubjectConfirmationData(attrs)
84
69
  end
85
70
  end
86
71
 
87
- def conditions(doc, ns)
88
- doc.create_element("Conditions").tap do |c|
89
- c.namespace = ns
90
- c["NotBefore"] = @not_before.iso8601
91
- c["NotOnOrAfter"] = @not_on_or_after.iso8601
72
+ def conditions(xml)
73
+ attrs = {
74
+ NotBefore: @not_before.iso8601,
75
+ NotOnOrAfter: @not_on_or_after.iso8601
76
+ }
92
77
 
93
- c.add_child(doc.create_element("AudienceRestriction")).tap do |ar|
94
- ar.namespace = ns
95
- ar.add_child(doc.create_element("Audience")).tap do |a|
96
- a.namespace = ns
97
- a.content = @audience
98
- end
78
+ xml["saml"].Conditions(attrs) do
79
+ xml["saml"].AudienceRestriction do
80
+ xml["saml"].Audience(@audience.to_s)
99
81
  end
100
82
  end
101
83
  end
102
84
 
103
- def authn_statement(doc, ns)
104
- doc.create_element("AuthnStatement").tap do |as|
105
- as.namespace = ns
106
- as["AuthnInstant"] = @issue_instant.iso8601
107
- as["SessionIndex"] = ID.generate
85
+ def authn_statement(xml)
86
+ attrs = {
87
+ AuthnInstant: @issue_instant.iso8601,
88
+ SessionIndex: ID.generate
89
+ }
108
90
 
109
- as.add_child(doc.create_element("AuthnContext")).tap do |ac|
110
- ac.namespace = ns
111
- ac.add_child(doc.create_element("AuthnContextClassRef")).tap do |cr|
112
- cr.namespace = ns
113
- cr.content = @authn_context
114
- end
91
+ xml["saml"].AuthnStatement(attrs) do
92
+ xml["saml"].AuthnContext do
93
+ xml["saml"].AuthnContextClassRef(@authn_context.to_s)
115
94
  end
116
95
  end
117
96
  end
118
97
 
119
- def attribute_statement(doc, ns)
120
- doc.create_element("AttributeStatement").tap do |as|
121
- as.namespace = ns
122
-
98
+ def attribute_statement(xml)
99
+ xml["saml"].AttributeStatement do
123
100
  @attributes.each do |name, values|
124
101
  list = values.is_a?(Enumerable) ? values.to_a : [values].compact
125
102
 
126
- as.add_child(doc.create_element("Attribute")).tap do |a|
127
- a.namespace = ns
128
- a["Name"] = name
129
- a["NameFormat"] = ATTR_NAME_FORMAT
130
-
131
- list.each do |value|
132
- a.add_child(doc.create_element("AttributeValue")).tap do |av|
133
- av.namespace = ns
134
- av.content = value
135
- end
136
- end
103
+ xml["saml"].Attribute(Name: name, NameFormat: ATTR_NAME_FORMAT) do
104
+ list.each { |value| xml["saml"].AttributeValue(value.to_s) }
137
105
  end
138
106
  end
139
107
  end
@@ -9,6 +9,9 @@ module Lyrebird
9
9
  "keyUsage" => "digitalSignature,keyEncipherment"
10
10
  }.freeze
11
11
 
12
+ private_constant :LOCK
13
+ private_constant :EXTENSIONS
14
+
12
15
  attr_reader :key, :x509
13
16
 
14
17
  def self.default
@@ -16,7 +19,7 @@ module Lyrebird
16
19
  end
17
20
 
18
21
  def self.build(**kwargs)
19
- config = OpenStruct.new(kwargs)
22
+ config = Options.new(kwargs)
20
23
  yield config if block_given?
21
24
  new(**config.to_h)
22
25
  end
@@ -58,7 +61,7 @@ module Lyrebird
58
61
  end
59
62
 
60
63
  def base64
61
- Base64.strict_encode64(@x509.to_der)
64
+ [@x509.to_der].pack("m0")
62
65
  end
63
66
 
64
67
  private
@@ -79,7 +82,7 @@ module Lyrebird
79
82
  c.add_extension(OpenSSL::X509::Extension.new(oid, value, true))
80
83
  end
81
84
 
82
- c.sign(@key, OpenSSL::Digest::SHA256.new)
85
+ c.sign(@key, OpenSSL::Digest.new("SHA256"))
83
86
  end
84
87
  end
85
88
 
@@ -3,14 +3,25 @@
3
3
  module Lyrebird
4
4
  SAML_ASSERTION_NS = "urn:oasis:names:tc:SAML:2.0:assertion"
5
5
  SAML_PROTOCOL_NS = "urn:oasis:names:tc:SAML:2.0:protocol"
6
+
7
+ NAMEID_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
8
+ NAMEID_PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
9
+ NAMEID_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
10
+ NAMEID_UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
11
+
12
+ ppt = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
13
+ AUTHN_PASSWORD_PROTECTED_TRANSPORT = ppt
14
+
15
+ CM_BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
16
+ ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
17
+ STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
18
+
6
19
  XMLDSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
7
20
  ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
8
21
  EXC_C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
9
22
  SHA256_DIGEST = "http://www.w3.org/2001/04/xmlenc#sha256"
10
23
  RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
11
- CM_BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
12
- ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
13
- STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
24
+
14
25
  XMLENC_NS = "http://www.w3.org/2001/04/xmlenc#"
15
26
  AES256_CBC = "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
16
27
  RSA_OAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lyrebird
4
- NAMEID_EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
5
- NAMEID_PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
6
- NAMEID_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
7
- NAMEID_UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
8
-
9
4
  class Defaults
10
5
  attr_accessor :issuer
11
6
  attr_accessor :name_id
@@ -26,13 +21,11 @@ module Lyrebird
26
21
  @valid_for = 300 # 5 minutes
27
22
  @audience = "https://sp.example.com"
28
23
 
29
- @authn_context =
30
- "urn:oasis:names:tc:SAML:2.0:ac:classes:" \
31
- "PasswordProtectedTransport"
24
+ @authn_context = AUTHN_PASSWORD_PROTECTED_TRANSPORT
32
25
 
33
26
  @attributes = {
34
27
  first_name: "Test",
35
- last_name: "User",
28
+ last_name: "User"
36
29
  }.freeze
37
30
  end
38
31
  end
@@ -5,92 +5,60 @@ module Lyrebird
5
5
  def initialize(element, certificate)
6
6
  @element = element
7
7
  @certificate = certificate
8
- @doc = element.document
9
8
  @aes_key = SecureRandom.random_bytes(32)
10
9
  end
11
10
 
12
11
  def encrypt
13
- build_encrypted_assertion
12
+ Nokogiri::XML::Builder.new do |xml|
13
+ xml["saml"].EncryptedAssertion("xmlns:saml" => SAML_ASSERTION_NS) do
14
+ encrypted_data(xml)
15
+ end
16
+ end.doc.root
14
17
  end
15
18
 
16
19
  private
17
20
 
18
- def build_encrypted_assertion
19
- @doc.create_element("EncryptedAssertion").tap do |ea|
20
- @saml = ea.add_namespace_definition("saml", SAML_ASSERTION_NS)
21
- ea.namespace = @saml
22
- ea.add_child(build_encrypted_data)
23
- end
24
- end
21
+ def encrypted_data(xml)
22
+ attrs = {
23
+ "xmlns:xenc" => XMLENC_NS,
24
+ Type: "#{XMLENC_NS}Element"
25
+ }
25
26
 
26
- def build_encrypted_data
27
- @doc.create_element("EncryptedData").tap do |ed|
28
- @xenc = ed.add_namespace_definition("xenc", XMLENC_NS)
29
- ed.namespace = @xenc
30
- ed["Type"] = "#{XMLENC_NS}Element"
31
-
32
- ed.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
33
- em.namespace = @xenc
34
- em["Algorithm"] = AES256_CBC
35
- end
36
-
37
- ed.add_child(build_key_info)
38
- ed.add_child(build_cipher_data)
27
+ xml["xenc"].EncryptedData(attrs) do
28
+ xml["xenc"].EncryptionMethod(Algorithm: AES256_CBC)
29
+ key_info(xml)
30
+ cipher_data(xml, ciphertext)
39
31
  end
40
32
  end
41
33
 
42
- def build_key_info
43
- @doc.create_element("KeyInfo").tap do |ki|
44
- @ds = ki.add_namespace_definition("ds", XMLDSIG_NS)
45
- ki.namespace = @ds
46
- ki.add_child(build_encrypted_key)
34
+ def key_info(xml)
35
+ xml["ds"].KeyInfo("xmlns:ds" => XMLDSIG_NS) do
36
+ xml["xenc"].EncryptedKey do
37
+ xml["xenc"].EncryptionMethod(Algorithm: RSA_OAEP)
38
+ cipher_data(xml, wrapped_key)
39
+ end
47
40
  end
48
41
  end
49
42
 
50
- def build_encrypted_key
51
- @doc.create_element("EncryptedKey").tap do |ek|
52
- ek.namespace = @xenc
53
-
54
- ek.add_child(@doc.create_element("EncryptionMethod")).tap do |em|
55
- em.namespace = @xenc
56
- em["Algorithm"] = RSA_OAEP
57
- end
58
-
59
- ek.add_child(build_encrypted_key_cipher_data)
43
+ def cipher_data(xml, value)
44
+ xml["xenc"].CipherData do
45
+ xml["xenc"].CipherValue(value)
60
46
  end
61
47
  end
62
48
 
63
- def build_encrypted_key_cipher_data
49
+ def wrapped_key
64
50
  public_key = @certificate.x509.public_key
65
51
  padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
66
- encrypted_aes_key = public_key.public_encrypt(@aes_key, padding)
67
-
68
- @doc.create_element("CipherData").tap do |cd|
69
- cd.namespace = @xenc
70
-
71
- cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
72
- cv.namespace = @xenc
73
- cv.content = Base64.strict_encode64(encrypted_aes_key)
74
- end
75
- end
52
+ [public_key.public_encrypt(@aes_key, padding)].pack("m0")
76
53
  end
77
54
 
78
- def build_cipher_data
55
+ def ciphertext
79
56
  cipher = OpenSSL::Cipher.new("AES-256-CBC")
80
57
  cipher.encrypt
81
58
  cipher.key = @aes_key
82
59
  iv = cipher.random_iv
83
60
  plaintext = @element.to_xml(save_with: 0)
84
- ciphertext = cipher.update(plaintext) + cipher.final
85
-
86
- @doc.create_element("CipherData").tap do |cd|
87
- cd.namespace = @xenc
88
-
89
- cd.add_child(@doc.create_element("CipherValue")).tap do |cv|
90
- cv.namespace = @xenc
91
- cv.content = Base64.strict_encode64(iv + ciphertext)
92
- end
93
- end
61
+ [iv + cipher.update(plaintext) + cipher.final].pack("m0")
94
62
  end
95
63
  end
96
64
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lyrebird
4
+ class Options
5
+ def initialize(values = {})
6
+ @values = values.transform_keys(&:to_sym)
7
+ end
8
+
9
+ def to_h
10
+ @values
11
+ end
12
+
13
+ def [](key)
14
+ @values[key.to_sym]
15
+ end
16
+
17
+ def []=(key, value)
18
+ @values[key.to_sym] = value
19
+ end
20
+
21
+ private
22
+
23
+ def method_missing(name, *args)
24
+ key = name.to_s
25
+
26
+ if key.end_with?("=")
27
+ @values[key.chomp("=").to_sym] = args.first
28
+ elsif @values.key?(name)
29
+ @values[name]
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def respond_to_missing?(name, include_private = false)
36
+ name.to_s.end_with?("=") || @values.key?(name) || super
37
+ end
38
+ end
39
+ end
@@ -2,17 +2,18 @@
2
2
 
3
3
  module Lyrebird
4
4
  class Response
5
- def self.build(**kwargs)
6
- config = OpenStruct.new(kwargs)
7
-
8
- config.define_singleton_method(:attributes) do |&block|
5
+ class Config < Options
6
+ def attributes(&block)
9
7
  current = self[:attributes] || {}
10
8
  return current unless block
11
9
 
12
- fresh = OpenStruct.new.tap(&block).to_h
10
+ fresh = Options.new.tap(&block).to_h
13
11
  self[:attributes] = current.transform_keys(&:to_sym).merge(fresh)
14
12
  end
13
+ end
15
14
 
15
+ def self.build(**kwargs)
16
+ config = Config.new(kwargs)
16
17
  yield config if block_given?
17
18
  new(**config.to_h)
18
19
  end
@@ -39,7 +40,7 @@ module Lyrebird
39
40
  end
40
41
 
41
42
  def mimic
42
- Base64.strict_encode64(to_xml)
43
+ [to_xml].pack("m0")
43
44
  end
44
45
 
45
46
  def to_xml
@@ -47,67 +48,42 @@ module Lyrebird
47
48
  end
48
49
 
49
50
  def document
50
- @document ||= Nokogiri::XML::Document.new.tap do |doc|
51
- doc.root = build_response(doc)
52
- sign_assertion(doc) if @sign_with && !@encrypt_with
53
- sign_response(doc) if @sign_with
51
+ @document ||= build_response.doc.tap do |doc|
52
+ Signature.new(doc.root, @sign_with).sign! if @sign_with
54
53
  end
55
54
  end
56
55
 
57
56
  private
58
57
 
59
- def build_response(doc)
60
- doc.create_element("Response").tap do |r|
61
- @samlp = r.add_namespace_definition("samlp", SAML_PROTOCOL_NS)
62
- @saml = r.add_namespace_definition("saml", SAML_ASSERTION_NS)
63
-
64
- r.namespace = @samlp
65
- r["ID"] = ID.generate
66
- r["Version"] = "2.0"
67
- r["IssueInstant"] = Time.now.utc.iso8601
68
- r["Destination"] = @destination if @destination
69
- r["InResponseTo"] = @in_response_to if @in_response_to
70
-
71
- r.add_child(doc.create_element("Issuer")).tap do |i|
72
- i.namespace = @saml
73
- i.content = @issuer
58
+ def build_response
59
+ attrs = {
60
+ "xmlns:samlp" => SAML_PROTOCOL_NS,
61
+ "xmlns:saml" => SAML_ASSERTION_NS,
62
+ ID: ID.generate,
63
+ Version: "2.0",
64
+ IssueInstant: Time.now.utc.iso8601,
65
+ Destination: @destination,
66
+ InResponseTo: @in_response_to
67
+ }.compact
68
+
69
+ Nokogiri::XML::Builder.new do |xml|
70
+ xml["samlp"].Response(attrs) do
71
+ xml["saml"].Issuer(@issuer.to_s)
72
+
73
+ xml["samlp"].Status do
74
+ xml["samlp"].StatusCode(Value: STATUS_SUCCESS)
75
+ end
76
+
77
+ xml.parent << build_assertion_element
74
78
  end
75
-
76
- r.add_child(build_status(doc))
77
- r.add_child(build_assertion_element)
78
79
  end
79
80
  end
80
81
 
81
82
  def build_assertion_element
82
83
  element = @assertion.document.root
83
-
84
- if @encrypt_with
85
- Signature.new(element, @sign_with).sign! if @sign_with
86
- Encryption.new(element, @encrypt_with).encrypt
87
- else
88
- element
89
- end
90
- end
91
-
92
- def sign_assertion(doc)
93
- ns = { "saml" => SAML_ASSERTION_NS }
94
- assertion = doc.at_xpath("//saml:Assertion", ns)
95
- Signature.new(assertion, @sign_with).sign!
96
- end
97
-
98
- def sign_response(doc)
99
- Signature.new(doc.root, @sign_with).sign!
100
- end
101
-
102
- def build_status(doc)
103
- doc.create_element("Status").tap do |s|
104
- s.namespace = @samlp
105
-
106
- s.add_child(doc.create_element("StatusCode")).tap do |sc|
107
- sc.namespace = @samlp
108
- sc["Value"] = STATUS_SUCCESS
109
- end
110
- end
84
+ Signature.new(element, @sign_with).sign! if @sign_with
85
+ element = Encryption.new(element, @encrypt_with).encrypt if @encrypt_with
86
+ element
111
87
  end
112
88
  end
113
89
  end
@@ -4,120 +4,76 @@ module Lyrebird
4
4
  class Signature
5
5
  C14N_EXCLUSIVE = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
6
6
 
7
+ private_constant :C14N_EXCLUSIVE
8
+
7
9
  def initialize(element, certificate)
8
10
  @element = element
9
11
  @certificate = certificate
10
- @doc = element.document
11
12
  end
12
13
 
13
14
  def sign!
14
15
  ns = { "saml" => SAML_ASSERTION_NS, "ds" => XMLDSIG_NS }
15
16
  @element.at_xpath("ds:Signature", ns)&.remove
17
+ signature = build_signature
16
18
  issuer = @element.at_xpath("saml:Issuer", ns)
17
- issuer.add_next_sibling(build_signature)
18
- populate_signature_value
19
+ issuer.add_next_sibling(signature)
20
+ populate_signature_value(signature)
19
21
  self
20
22
  end
21
23
 
22
24
  private
23
25
 
24
26
  def build_signature
25
- @doc.create_element("Signature").tap do |sig|
26
- @ds = sig.add_namespace_definition("ds", XMLDSIG_NS)
27
- sig.namespace = @ds
28
-
29
- sig.add_child(build_signed_info)
30
- sig.add_child(build_signature_value)
31
- sig.add_child(build_key_info)
32
- end
33
- end
34
-
35
- def build_signed_info
36
- @doc.create_element("SignedInfo").tap do |si|
37
- @signed_info = si
38
- si.namespace = @ds
39
-
40
- si.add_child(@doc.create_element("CanonicalizationMethod")).tap do |cm|
41
- cm.namespace = @ds
42
- cm["Algorithm"] = EXC_C14N
27
+ Nokogiri::XML::Builder.new do |xml|
28
+ xml["ds"].Signature("xmlns:ds" => XMLDSIG_NS) do
29
+ signed_info(xml)
30
+ xml["ds"].SignatureValue
31
+ key_info(xml)
43
32
  end
44
-
45
- si.add_child(@doc.create_element("SignatureMethod")).tap do |sm|
46
- sm.namespace = @ds
47
- sm["Algorithm"] = RSA_SHA256
48
- end
49
-
50
- si.add_child(build_reference)
51
- end
52
- end
53
-
54
- def build_signature_value
55
- @doc.create_element("SignatureValue").tap do |sv|
56
- sv.namespace = @ds
57
- @signature_value = sv
58
- end
33
+ end.doc.root
59
34
  end
60
35
 
61
- def populate_signature_value
62
- canonical = @signed_info.canonicalize(C14N_EXCLUSIVE)
63
- sig = @certificate.key.sign("SHA256", canonical)
64
- @signature_value.content = Base64.strict_encode64(sig)
65
- end
66
-
67
- def build_key_info
68
- @doc.create_element("KeyInfo").tap do |ki|
69
- ki.namespace = @ds
70
-
71
- ki.add_child(@doc.create_element("X509Data")).tap do |xd|
72
- xd.namespace = @ds
73
-
74
- xd.add_child(@doc.create_element("X509Certificate")).tap do |xc|
75
- xc.namespace = @ds
76
- xc.content = @certificate.base64
77
- end
78
- end
36
+ def signed_info(xml)
37
+ xml["ds"].SignedInfo do
38
+ xml["ds"].CanonicalizationMethod(Algorithm: EXC_C14N)
39
+ xml["ds"].SignatureMethod(Algorithm: RSA_SHA256)
40
+ reference(xml)
79
41
  end
80
42
  end
81
43
 
82
- def build_reference
83
- @doc.create_element("Reference").tap do |ref|
84
- ref.namespace = @ds
85
- ref["URI"] = "##{@element["ID"]}"
86
-
87
- ref.add_child(build_transforms)
88
-
89
- ref.add_child(@doc.create_element("DigestMethod")).tap do |dm|
90
- dm.namespace = @ds
91
- dm["Algorithm"] = SHA256_DIGEST
44
+ def reference(xml)
45
+ xml["ds"].Reference(URI: "##{@element["ID"]}") do
46
+ xml["ds"].Transforms do
47
+ xml["ds"].Transform(Algorithm: ENVELOPED_SIG)
48
+ xml["ds"].Transform(Algorithm: EXC_C14N)
92
49
  end
93
50
 
94
- ref.add_child(@doc.create_element("DigestValue")).tap do |dv|
95
- dv.namespace = @ds
96
- dv.content = compute_digest
97
- end
51
+ xml["ds"].DigestMethod(Algorithm: SHA256_DIGEST)
52
+ xml["ds"].DigestValue(compute_digest)
98
53
  end
99
54
  end
100
55
 
101
- def build_transforms
102
- @doc.create_element("Transforms").tap do |t|
103
- t.namespace = @ds
104
-
105
- t.add_child(@doc.create_element("Transform")).tap do |tr|
106
- tr.namespace = @ds
107
- tr["Algorithm"] = ENVELOPED_SIG
108
- end
109
-
110
- t.add_child(@doc.create_element("Transform")).tap do |tr|
111
- tr.namespace = @ds
112
- tr["Algorithm"] = EXC_C14N
56
+ def key_info(xml)
57
+ xml["ds"].KeyInfo do
58
+ xml["ds"].X509Data do
59
+ xml["ds"].X509Certificate(@certificate.base64)
113
60
  end
114
61
  end
115
62
  end
116
63
 
117
64
  def compute_digest
118
65
  canonical = @element.canonicalize(C14N_EXCLUSIVE)
119
- digest = OpenSSL::Digest::SHA256.digest(canonical)
120
- Base64.strict_encode64(digest)
66
+ digest = OpenSSL::Digest.new("SHA256").digest(canonical)
67
+ [digest].pack("m0")
68
+ end
69
+
70
+ def populate_signature_value(signature)
71
+ ns = { "ds" => XMLDSIG_NS }
72
+ signed_info = signature.at_xpath("ds:SignedInfo", ns)
73
+ canonical = signed_info.canonicalize(C14N_EXCLUSIVE)
74
+ sig = @certificate.key.sign(OpenSSL::Digest.new("SHA256"), canonical)
75
+ value = signature.at_xpath("ds:SignatureValue", ns)
76
+ value.content = [sig].pack("m0")
121
77
  end
122
78
  end
123
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lyrebird
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.1"
5
5
  end
data/lib/lyrebird.rb CHANGED
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
3
  require "nokogiri"
5
4
  require "openssl"
6
- require "ostruct"
7
5
  require "securerandom"
8
6
  require "time"
9
7
 
10
8
  require_relative "lyrebird/assertion"
11
9
  require_relative "lyrebird/certificate"
10
+ require_relative "lyrebird/constants"
12
11
  require_relative "lyrebird/defaults"
13
12
  require_relative "lyrebird/encryption"
14
13
  require_relative "lyrebird/id"
15
- require_relative "lyrebird/namespaces"
14
+ require_relative "lyrebird/options"
16
15
  require_relative "lyrebird/response"
17
16
  require_relative "lyrebird/signature"
18
17
  require_relative "lyrebird/version"
metadata CHANGED
@@ -1,113 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lyrebird
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: base64
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: nokogiri
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '0'
19
+ version: '1.14'
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - ">="
39
25
  - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: ostruct
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: logger
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: minitest
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: ruby-saml
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
26
+ version: '1.14'
111
27
  description:
112
28
  email:
113
29
  executables: []
@@ -119,10 +35,11 @@ files:
119
35
  - lib/lyrebird.rb
120
36
  - lib/lyrebird/assertion.rb
121
37
  - lib/lyrebird/certificate.rb
38
+ - lib/lyrebird/constants.rb
122
39
  - lib/lyrebird/defaults.rb
123
40
  - lib/lyrebird/encryption.rb
124
41
  - lib/lyrebird/id.rb
125
- - lib/lyrebird/namespaces.rb
42
+ - lib/lyrebird/options.rb
126
43
  - lib/lyrebird/response.rb
127
44
  - lib/lyrebird/signature.rb
128
45
  - lib/lyrebird/version.rb