XML digital signatures and encryption are fundamental to secure document exchange, particularly in SAML, XML-RPC, and enterprise integration scenarios. Let’s explore xml-kit, a Ruby library that simplifies these complex cryptographic operations.
Let’s pretend that I need to generate an XML represenation of some sort
of policy to be distributed to clients. This policy has one piece of
configuration called name.
# policy.rb
class Policy
attr_accessor :id, :name
end
Using this ruby class, I want to generate the following XML representation.
<Policy ID="_baf8cc51-2133-4312-87ee-76ccda7a8fbe">
<Name>Audit</Name>
</Policy>
To accomplish this we can use builder and tilt.
The following changes should help us accomplish the goal of generating an xml document.
# policy.rb
class Policy
attr_accessor :id, :name
def template_path
File.join(__dir__, 'policy.builder')
end
def to_xml(options = {})
Tilt.new(template_path).render(self, options)
end
end
# policy.builder
xml.instruct!
xml.Policy ID: id do
xml.Name name
end
# app.rb
policy = Policy.new
policy.id = SecureRandom.uuid
policy.name = "Audit"
IO.write("policy.xml", policy.to_xml)
XML Digital Signature
Now we need a mechanism for clients to verify that the generated XML file has not been tampered with and was issued by the party that they trust. To accomplish this we can generate a XML digital signature.
We can use xml-kit to help sign our xml document. There are a few changes that we need to make to the code to generate a signed xml document.
- include the
Xml::Kit::Templatablemodule. - use the
signature_forhelper method in the xml builder template. - specify a key pair to use for generating the signature.
# policy.rb
class Policy
include Xml::Kit::Templatable
attr_accessor :id, :name
def template_path
File.join(__dir__, 'policy.builder')
end
end
# policy.builder
xml.instruct!
xml.Policy ID: id do
signature_for reference_id: id, xml: xml
xml.Name name
end
# app.rb
policy = Policy.new
policy.id = Xml::Kit::Id.generate
policy.name = "Audit"
policy.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
IO.write("policy.xml", policy.to_xml)
This will generate the same XML document with an added XML digital signature. The above example code uses xml-kit to generate a self signed certificate and key pair. This is helpful for development but not recommended in a production like environment.
The output of the above code is:
<Policy ID="_329d2b29-6a82-4fc9-927f-5575ecc633e5">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#_329d2b29-6a82-4fc9-927f-5575ecc633e5">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<Name>Audit</Name>
</Policy>
Clients of this policy should receive the X509 certificate via a trusted transport mechanism so that it can verify that the document was signed with a key pair associated with a trusted certificate. The client can verify the signature to ensure that the document was not altered.
The current version of xml-kit only supports enveloped signatures.
XML Encryption
Next we can look at encrypting the <Name>..</Name> element using the
XML Encryption standard.
To do this we can use the encrypt_with method to specify the x509
certificate of the client so that we can encrypt the data for a specific
client. The xml builder template uses the encrypt_data_for helper to
create an XML encryption element.
The revised version of the code looks like:
# policy.rb
class Policy
include Xml::Kit::Templatable
attr_accessor :id, :name
def template_path
File.join(__dir__, 'policy.builder')
end
end
# policy.builder
xml.instruct!
xml.Policy ID: id do
signature_for reference_id: id, xml: xml
encrypt_data_for xml: xml do |xml|
xml.Name name
end
end
# app.rb
policy = Policy.new
policy.id = Xml::Kit::Id.generate
policy.name = "Audit"
policy.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
policy.encrypt_with(Xml::Kit::KeyPair.generate(use: :encryption).certificate)
IO.write("policy.xml", policy.to_xml)
The final version of the signed and encrypted document looks like:
<?xml version="1.0"?>
<Policy ID="_3fe07010-a5d4-4114-b919-4219a2d5bb0c">
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#_3fe07010-a5d4-4114-b919-4219a2d5bb0c">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<CipherData>
<CipherValue>...</CipherValue>
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>...</CipherValue>
</CipherData>
</EncryptedData>
</Policy>
Real-World Implementation: SAML Document Security
Here’s how xml-kit is used in production SAML implementations. This example is from my OAuth2 and SAML server:
SAML Response Signing
# app/models/saml_response.rb - Production SAML response generation
class SamlResponse
include Xml::Kit::Templatable
attr_accessor :id, :destination, :assertion_id, :subject, :attributes
def initialize(authn_request)
@id = Xml::Kit::Id.generate
@assertion_id = Xml::Kit::Id.generate
@destination = authn_request.assertion_consumer_service_url
@issue_instant = Time.current.iso8601
@not_before = Time.current.iso8601
@not_on_or_after = 30.minutes.from_now.iso8601
@authn_instant = Time.current.iso8601
end
def template_path
Rails.root.join('app/templates/saml_response.builder')
end
def to_xml
sign_with(saml_kit_configuration.signing_key_pair)
super
end
private
def saml_kit_configuration
Saml::Kit.configuration
end
end
SAML Response Template
# app/templates/saml_response.builder - Signed SAML response template
xml.instruct!
xml.Response(
'xmlns:saml' => 'urn:oasis:names:tc:SAML:2.0:assertion',
'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol',
ID: id,
InResponseTo: in_response_to,
Version: '2.0',
IssueInstant: issue_instant,
Destination: destination
) do
xml.Issuer 'xmlns' => 'urn:oasis:names:tc:SAML:2.0:assertion' do
xml.text! Saml::Kit.configuration.entity_id
end
# XML digital signature using xml-kit
signature_for reference_id: id, xml: xml
xml.Status do
xml.StatusCode Value: 'urn:oasis:names:tc:SAML:2.0:status:Success'
end
xml.Assertion(
'xmlns' => 'urn:oasis:names:tc:SAML:2.0:assertion',
ID: assertion_id,
Version: '2.0',
IssueInstant: issue_instant
) do
xml.Issuer Saml::Kit.configuration.entity_id
xml.Subject do
xml.NameID(
Format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
NameQualifier: Saml::Kit.configuration.entity_id
) do
xml.text! subject
end
xml.SubjectConfirmation Method: 'urn:oasis:names:tc:SAML:2.0:cm:bearer' do
xml.SubjectConfirmationData(
InResponseTo: in_response_to,
NotOnOrAfter: not_on_or_after,
Recipient: destination
)
end
end
xml.Conditions(
NotBefore: not_before,
NotOnOrAfter: not_on_or_after
) do
xml.AudienceRestriction do
xml.Audience audience
end
end
xml.AuthnStatement(
AuthnInstant: authn_instant,
SessionIndex: session_index
) do
xml.AuthnContext do
xml.AuthnContextClassRef 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'
end
end
# Encrypted attribute statement
if attributes.present?
xml.AttributeStatement do
attributes.each do |name, value|
xml.Attribute Name: name do
xml.AttributeValue value
end
end
end
end
end
end
SAML Service Provider Integration
# app/controllers/saml_controller.rb - SAML response processing
class SamlController < ApplicationController
def acs
# Parse and validate signed SAML response
saml_response = Saml::Kit::Response.deserialize(params[:SAMLResponse])
# Verify XML digital signature
unless saml_response.valid?
Rails.logger.error "Invalid SAML response: #{saml_response.errors.full_messages}"
redirect_to new_session_path, alert: 'Authentication failed'
return
end
# Extract user information from verified assertion
assertion = saml_response.assertion
user_email = assertion.name_id
user_attributes = assertion.attribute_statements.first&.attributes || {}
# Process authenticated user
user = User.find_or_initialize_by(email: user_email)
user.name = user_attributes['name']
user.save!
sign_in(user)
redirect_to root_path, notice: 'Successfully authenticated via SAML'
end
end
XML Encryption for Sensitive Data
# app/models/encrypted_saml_assertion.rb - Encrypted SAML assertions
class EncryptedSamlAssertion
include Xml::Kit::Templatable
attr_accessor :id, :assertion_data, :recipient_certificate
def initialize(assertion_data, recipient_cert)
@id = Xml::Kit::Id.generate
@assertion_data = assertion_data
@recipient_certificate = recipient_cert
end
def template_path
Rails.root.join('app/templates/encrypted_assertion.builder')
end
def to_xml
encrypt_with(recipient_certificate)
super
end
end
# app/templates/encrypted_assertion.builder - XML encryption template
xml.instruct!
xml.EncryptedAssertion 'xmlns' => 'urn:oasis:names:tc:SAML:2.0:assertion' do
# XML encryption using xml-kit
encrypt_data_for xml: xml do |encrypted_xml|
encrypted_xml.Assertion(
'xmlns' => 'urn:oasis:names:tc:SAML:2.0:assertion',
ID: assertion_id,
Version: '2.0',
IssueInstant: Time.current.iso8601
) do
encrypted_xml.Issuer Saml::Kit.configuration.entity_id
encrypted_xml.Subject do
encrypted_xml.NameID assertion_data[:subject]
end
encrypted_xml.AttributeStatement do
assertion_data[:attributes].each do |name, value|
encrypted_xml.Attribute Name: name do
encrypted_xml.AttributeValue value
end
end
end
end
end
end
Certificate and Key Management
# config/initializers/xml_kit.rb - Production XML-Kit configuration
Xml::Kit.configure do |config|
# Configure signing certificates
config.signing_certificate = ENV.fetch('SAML_SIGNING_CERTIFICATE')
config.signing_private_key = ENV.fetch('SAML_SIGNING_PRIVATE_KEY')
# Configure encryption certificates
config.encryption_certificate = ENV.fetch('SAML_ENCRYPTION_CERTIFICATE')
config.encryption_private_key = ENV.fetch('SAML_ENCRYPTION_PRIVATE_KEY')
# Security settings
config.default_digest_algorithm = :sha256
config.default_signature_algorithm = :rsa_sha256
config.default_canonicalization_algorithm = :exclusive
end
# Key pair generation for development
class XmlKitSetup
def self.generate_development_keys!
# Generate signing key pair
signing_key = Xml::Kit::KeyPair.generate(use: :signing)
Rails.application.credentials.xml_kit[:signing_certificate] = signing_key.certificate
Rails.application.credentials.xml_kit[:signing_private_key] = signing_key.private_key
# Generate encryption key pair
encryption_key = Xml::Kit::KeyPair.generate(use: :encryption)
Rails.application.credentials.xml_kit[:encryption_certificate] = encryption_key.certificate
Rails.application.credentials.xml_kit[:encryption_private_key] = encryption_key.private_key
puts "XML-Kit development keys generated successfully"
end
end
Advanced XML Processing Patterns
Custom XML Signature Validation
# app/services/xml_signature_validator.rb - Custom signature validation
class XmlSignatureValidator
def initialize(trusted_certificates)
@trusted_certificates = trusted_certificates
end
def validate_signature(xml_document)
doc = Nokogiri::XML(xml_document)
signature_element = doc.at_xpath('//ds:Signature', 'ds' => 'http://www.w3.org/2000/09/xmldsig#')
return { valid: false, error: 'No signature found' } unless signature_element
# Extract certificate from signature
cert_data = signature_element.at_xpath('.//ds:X509Certificate', 'ds' => 'http://www.w3.org/2000/09/xmldsig#')&.text
return { valid: false, error: 'No certificate in signature' } unless cert_data
certificate = OpenSSL::X509::Certificate.new(Base64.decode64(cert_data))
# Verify certificate is trusted
unless @trusted_certificates.any? { |trusted| trusted.to_pem == certificate.to_pem }
return { valid: false, error: 'Certificate not trusted' }
end
# Verify signature using xml-kit
signature = Xml::Kit::Signature.new(signature_element)
if signature.valid?
{ valid: true, certificate: certificate }
else
{ valid: false, error: 'Invalid signature' }
end
rescue => e
{ valid: false, error: e.message }
end
end
XML Canonicalization and Transformation
# app/services/xml_canonicalizer.rb - XML canonicalization service
class XmlCanonicalizer
def self.canonicalize(xml_content, algorithm = :exclusive)
doc = Nokogiri::XML(xml_content)
case algorithm
when :exclusive
doc.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
when :inclusive
doc.canonicalize(Nokogiri::XML::XML_C14N_1_0)
else
raise ArgumentError, "Unsupported canonicalization algorithm: #{algorithm}"
end
end
def self.transform_for_signature(element)
# Apply enveloped signature transform
transformed = element.dup
signature_elements = transformed.xpath('.//ds:Signature', 'ds' => 'http://www.w3.org/2000/09/xmldsig#')
signature_elements.remove
canonicalize(transformed.to_xml, :exclusive)
end
end
Performance and Security Considerations
XML Processing Security
# app/services/secure_xml_processor.rb - Secure XML processing
class SecureXmlProcessor
MAX_DOCUMENT_SIZE = 10.megabytes
MAX_ENTITY_EXPANSIONS = 1000
def self.parse_safely(xml_content)
# Validate document size
if xml_content.bytesize > MAX_DOCUMENT_SIZE
raise SecurityError, "XML document too large: #{xml_content.bytesize} bytes"
end
# Configure Nokogiri for security
options = Nokogiri::XML::ParseOptions::STRICT |
Nokogiri::XML::ParseOptions::NONET |
Nokogiri::XML::ParseOptions::NOENT
doc = Nokogiri::XML(xml_content, nil, nil, options)
# Check for entity expansion attacks
if doc.to_xml.bytesize > xml_content.bytesize * 10
raise SecurityError, "Potential XML entity expansion attack detected"
end
doc
rescue Nokogiri::XML::SyntaxError => e
raise SecurityError, "Invalid XML syntax: #{e.message}"
end
end
Complete Implementation
The examples above are from a production SAML and OAuth2 server implementation. You can explore the complete codebase:
GitHub Repository: https://github.com/xlgmokha/proof
Key XML-Kit implementation files:
- SAML Response Model - XML signature generation
- SAML Controller - XML signature validation
- XML Templates - Builder templates for signed documents
- XML-Kit Configuration - Certificate management
Run the complete XML-Kit implementation:
git clone https://github.com/xlgmokha/proof.git
cd proof
bundle install
rails db:setup
rails server
# XML endpoints with digital signatures:
# GET /metadata - Signed SAML metadata
# POST /saml/acs - SAML response validation
# GET /sessions/new - Signed authentication requests
Test XML signature operations:
# Get signed SAML metadata
curl -X GET "http://localhost:3000/metadata" | xmllint --format -
# Generate signed SAML authentication request
curl -X GET "http://localhost:3000/sessions/new?SAMLRequest=..."
# Validate signed SAML response
curl -X POST "http://localhost:3000/saml/acs" \
-d "SAMLResponse=base64_encoded_signed_response"
Key Benefits of XML-Kit
- Simplified API - Complex XML cryptographic operations made simple
- Standards Compliance - Full W3C XML Signature and Encryption support
- SAML Integration - Seamless integration with saml-kit library
- Production Ready - Used in enterprise SAML implementations
- Security Focused - Built-in protection against XML attacks
Conclusion
XML is verbose and difficult to read. However, generating a signed and encrypted xml document is a little bit easier with xml-kit.
XML-Kit provides a Ruby-friendly interface to complex XML security standards, making it practical to implement secure document exchange in production applications. Whether you’re building SAML identity providers, secure API endpoints, or enterprise integration systems, xml-kit simplifies the cryptographic heavy lifting while maintaining standards compliance.
The library continues to evolve with new features and security enhancements. Please feel free to contribute to its development.