NAME

XML::Sig::OO - Modern XML Signatured validation

SYNOPSIS

  use XML::Sig::OO;

  # Sign our xml
  my $s=new XML::Sig::OO(
    xml=>'<?xml version="1.0" standalone="yes"?><data><test ID="A" /><test ID="B" /></data>',
    key_file=>'rsa_key.pem'
    cert_file=>'cert.pem',
  );
  my $result=$s->sign;
  die "Failed to sign the xml, error was: $result" unless $result;

  my $xml=$result->get_data;
  # Example checking a signature
  my $v=new XML::Sig::OO(xml=>$xml);

  # validate our xml
  my $result=$v->validate;

  if($result) {
    print "everything checks out!\n";
  } else {
    foreach my $chunk (@{$result->get_data}) {
      my ($nth,$signature,$digest)=@{$chunk}{qw(nth signature digest)};

      print "Results for processing chunk $nth\n";
      print "Signature State: ".($signature ? "OK\n" : "Failed, error was $signature\n");
      print "Digest State: ".($digest ? "OK\n" : "Failed, error was $digest\n");
    }
  }

DESCRIPTION

XML::Sig::OO is a project to create a stand alone perl module that does a good job creating and validating xml signatures. At its core This module is written around libxml2 better known as XML::LibXML.

Multiple signatures and keys

In the case of signing multiple //@ID elements, it is possible to sign each chunk with a different key, in fact you can even use completly different key types.

  use Modern::Perl;
  use XML::Sig::OO;
  use File::Spec;
  use FindBin qw($Bin);
  use Crypt::OpenSSL::DSA;
  use Crypt::OpenSSL::RSA;

  # create our signign object
  my $s=new XML::Sig::OO(
    xml=>'<?xml version="1.0" standalone="yes"?><data><test ID="A" /><test ID="B" /></data>',
  );

  my $x=$s->build_xpath;

  # sign our first xml chunk with our rsa key!
  my $rsa_str=join '',IO::File->new(File::Spec->catfile($Bin,'x509_key.pem'))->getlines;
  my $rsa=Crypt::OpenSSL::RSA->new_private_key($rsa_str);
  $rsa->use_pkcs1_padding();
  my $cert_str=join '',IO::File->new(File::Spec->catfile($Bin,'x509_cert.pem'))->getlines;
  $s->sign_cert($rsa);
  $s->key_type('rsa');
  $s->cert_string($cert_str);
  my $result=$s->sign_chunk($x,1);
  die $result unless $result;

  # Sign our 2nd chunk with our dsa key
  my $dsa = Crypt::OpenSSL::DSA->read_priv_key(File::Spec->catfile($Bin,'dsa_priv.pem'));
  $s->cert_string(undef);
  $s->sign_cert($dsa);
  $s->key_type('dsa');
  $result=$s->sign_chunk($x,2);
  die $result unless $result;

  my ($node)=$x->findnodes($s->xpath_Root);
  my $xml=$node->toString;

  print "Our Signed XML IS: \n",$xml,"\n";
  # Example checking a signature
  my $v=new XML::Sig::OO(xml=>$xml);

  $result=$v->validate;
  die $result unless $result;

  print "Our signed and xml passes validation\n";

Working with Net::SAML2

Net::SAML2 has many problems when it comes to signature validation of xml strings. This section documents how to use this module in place of the Net::SAML2 built ins.

  use Net::SAML2::Protocol::Assertion;
  use XML::Sig::OO;
  use MIME::Base64;

  # Lets assume we have a post binding response
  my $saml_response=.....

  my $xml=decode_base64($saml_response);

  my $v=XML::Sig::OO->new(xml=>$xml,cacert=>'idp_cert.pem');
  my $result=$v->validate;
  die $result unless $result;

  # we can now use the asertion knowing it was from our idp
  my $assertion=Net::SAML2::Protocol::Assertion->new_from_xml(xml=>$xml)

Encrypted keys

Although this package does not directly support encrypted keys, it is possible to use encrypted keys by loading and exporting them with the Crypt::PK::RSA and Crypt::PK::DSA packages.

Constructor options

  • xml=>'...'

    The base xml string to validate or sign. This option is always required.

  • cacert=>'/path/to/your/cacert.pem'

    Optional, used to validate X509 certs.

  • build_parser=>sub { return XML::LibXML->new() }

    Callback that returns a new XML Parser

  • namespaces=>{ ds=>'http://www.w3.org/2000/09/xmldsig#', ec=>'http://www.w3.org/2001/10/xml-exc-c14n#'}

    Contains the list of namespaces to set in our XML::LibXML::XPathContext object.

  • digest_cbs=>{ ... }

    Contains the digest callbacks. The default handlers can be found in %XML::SIG::OO::DIGEST.

  • digest_method=>'http://www.w3.org/2000/09/xmldsig#sha1'

    Sets the digest method to be used when signing xml

  • key_type=>'rsa'

    The signature method we will use

  • signature_method=>'http://www.w3.org/2000/09/xmldsig#rsa-sha1'

    Sets the signature method.

  • tune_cert_cbs=>{ ...}

    A collection of callbacks to tune a certificate object for signing

  • mutate_cbs=>{....}

    Transform and Canonization callbacks. The default callbacks are defined in %XML::Sig::OO::MUTATE.

    Callbacks are usied in the following context

      $cb->($self,$xpath_element);

Xpaths

The xpaths in this package are not hard coded, each xpath can be defined as an argument to the constructor. Since xml can contain multiple elements with signatures or multiple id elements to sign, most xpaths are prefixed with the $nth signature

Some cases the xpaths are used in the following context:

  (/xpath)[$nth]

In special cases like finding a list of transforms or which key, signature, or digest:

  (//ds::Signature)[$nth]/xpath
  • xpath_SignatureValue=>//ds:SignatureValue

    Xpath used to find the signature value.

  • xpath_SignatureMethod=>'//ds:SignatureMethod/@Algorithm'

    Xpath used to find the signature method algorithm.

  • xpath_CanonicalizationMethod=>'//ds:CanonicalizationMethod/@Algorithm'

    Xpath used to find the list of canonicalization method(s).

  • xpath_SignedInfo=>'//ds:SignedInfo'

    Xpath used to find the singed info.

  • xpath_Signature=>'//ds:Signature'

    Xpath used to fetch the signature value

  • xpath_Transforms=>//ds:Transforms

    Xpath Transform path

  • xpath_Transform=>'/ds:Transform/@Algorithm'

    Xpath used to find the transform Algorithm

  • xpath_DigestValue=>'//ds:DigestValue'

    Xpath used to fetch the digest value

  • xpath_DigestMethod=>'//ds:DigestMethod/@Algorithm'

    Xpath used to find the digest method.

  • xpath_DigestId=>'//ds:Reference/@URI'

    Xpath used to find the id of the node that should contain our digest.

  • digest_id_convert_cb=>sub { my ($self,$xpath_object,$id)=@_;$id =~ s/^#//;return "//*[\@ID='$id']" }

    Code ref that converts the xpath_DigestId into the xpath lookup ised to find the digest node

  • xpath_ToSign=>'//[@ID]'

    Xpath used to find what nodes to sign.

  • xpath_IdValue=>'//@ID'

    Xpath used to find the value of the current id.

  • xpath_Root=>'/'

    Root of the document expath

This section documents all xpaths/options related to certs.

  • xpath_x509Data=>'/ds:KeyInfo/ds:X509Data/ds:X509Certificate'

    Xpath used to find the x509 cert value. In reality the nth signature will be prepended to this xpath.

    Actual xpath used:

      (//ds:Signature)[$nth]/ds:KeyInfo/ds:X509Data/ds:X509Certificate
  • xpath_RSAKeyValue=>'/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue'

    Xpath used to find the RSA value tree.

  • xpath_RSA_Modulus=>'/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus'

    Xpath used to find the RSA Modulus.

  • xpath_RSA_Exponent=>'/ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent'

    Xpath used to find the RSA Exponent.

  • xpath_DSAKeyValue=>'/ds:KeyInfo/ds:KeyValue/ds:DSAKeyValue'

    Xpath used for DSA key tree discovery.

  • xpath_DSA_P=>'/ds:KeyInfo/ds:KeyValue/ds:DSAKeyValue/ds:P'

    Xpath used to find DSA_P.

  • xpath_DSA_Q=>''

    Xpath used to find DSA_Q.

  • xpath_DSA_G=>'/ds:KeyInfo/ds:KeyValue/ds:DSAKeyValue/ds:G'

    Xpath used to find DSA_G.

  • xpath_DSA_Y=>'/ds:KeyInfo/ds:KeyValue/ds:DSAKeyValue/ds:Y'

    Xpath used to find DSA_Y

OO Signing Options

The following Signature options can be passed to the constructor object.

  • key_file=>'path/to/my.key'

    Key file only used when signing.

  • envelope_method=>"http://www.w3.org/2000/09/xmldsig#enveloped-signature"

    Sets the envelope method; This value most likely is the only valid value.

  • canon_method=>'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'

    Sets the canonization method used when signing the code

  • tag_namespace=>'ds'

    Default namespace of the tags being created. This must be defined in $self->namespaces.

  • sign_cert=>$cert_object

    Optional: The Certificate object used to sign xml. If this option is set it is recomended that you set the "key_type" option as well.

  • cert_file=>'/path/to/cert.pem'

    The path that contains the cert file used for signing.

  • cert_string=>undef

    This optional argument lets you define the x509 pem text that will be used to generate the x509 portion of the xml.

OO Methods

my $xpath=$self->build_xpath(undef|$xml,{ns=>'url'}|undef);

Creates a new xpath object based on our current object state.

my $result=$self->validate;

Returns a Data::Result Object. When true validation passed, when false it contains why validation failed.

A better use case would be this:

  my $result=$self->validate;

  if($result) {
    print "everything checks out\n";
  } else {
    foreach my $chunk (@{$result->get_data}) {
      my ($nth,$signature,$digest)=@{$chunk}{qw(nth signature digest)};

      print "Results for processing chunk $nth\n";
      print "Signature State: ".($signature ? "OK\n" : "Failed, error was $signature\n";
      print "Digest State: ".($digest ? "OK\n" : "Failed, error was $digest\n";
    }
  }

my $result=$self->verify_digest($nth)

Returns a Data::Result object: when true, the signature was verified, when false it contains why it failed.

my $result=$self->get_transforms($xpath_object,$nth)

Returns a Data::Reslt object, when true it contains an array ref that contains each digest transform, when false it contains why it failed.

Please note, the xpath generate is a concatination of $self->context($self->xpath_Transforms,$nth).$self->xpath_Transform, so keep that in mind when trying to change how transforms are looked up.

my $result=$self->get_digest_node($xpath_object)

Returns a Data::Result Object, when true it contains the Digest Node, when false it contains why it failed.

my $result=$self->get_digest_method($xpath_object,$nth)

Returns a Data::Result Object, when true it contains the Digest Method

my $result=$self->get_digest_value($xpath_object,$nth)

Returns a Data::Result Object, when true it contains the Digest Value.

my $result=$self->verify_signature($nth);

Returns a Data::Result Object, when true the signature was validated, when fails it contains why it failed.

my $result=$self->verify_dsa($x,$string,$nth)

Returns a Data::Result object, when true it validated the DSA signature.

my $xpath_string=$self->context($xpath,$nth)

Returns an xpath wrapped in the nth instance syntax.

Example

  my $xpath="//something"
  my $nth=2;

  my $xpath_string=$self->context($xpath,$nth);

  $xpath_string eq '(//something)[2]';

Note: if nth is not set it defaults to 1

my $result=$self->get_sig_canon($x,$nth)

Returns a Data::Result object, when true it contains the canon xml of the $nth signature node.

my $result=$self->verify_x509_sig($x,$string,$nth)

Returns a Data::Result Object, when true the x509 signature was validated.

my $result=$self->tune_cert_and_get_sig($x,$nth,$cert)

Returns a Data::Result object, when true it contains the following hashref

Structure:

  cert: the tuned cert
  sig:  the binary signature to verify
  xml:  the xml to be verified against the signature

my $result=$self->verify_rsa($x,$nth)

Returns a Data::Result Object, when true the the rsa key verification passed.

my $result=$self->do_transforms($xpath_object,$node_to_transform,$nth_node);

Retruns a Data::Result Object, when true it contains the xml string of the context node.

my $result=$self->do_canon($xpath_object,$node_to_transform,$nth_node);

Returns a Data::Result Object, when true it contains the canonized string.

my $result=$self->get_canon($xpath_object,$nth)

Returns a Data::Result Object, when true it contains an array ref of the canon methods.

Special note, the xpath is generated as follows

  my $xpath=$self->context($self->xpath_SignedInfo,$nth).$self->xpath_CanonicalizationMethod;

my $result=$self->get_signature_value($xpath_object,$nth)

Returns a Data::Result object, when true it contains the base64 decoded signature

my $result=$self->get_signed_info_node($xpath_object,$nth);

Given $xpath_object, Returns a Data::Result when true it will contains the signed info node

my $result=$self->get_signature_method($xpath_object,$nth_node,$cert|undef)

Returns a Data::Result object, when true it contains the SignatureMethod. If $cert is passed in, it will cert the hashing mode for the cert

my $result=$self->tune_cert($cert,$method)

Returns a Data::Result Object, when true Sets the hashing method for the $cert object.

my $x509=$self->clean_x509($string)

Converts a given string to an x509 certificate.

my $result=$self->transform($xpath_object,$node,$transformType,$nth)

Given the $node XML::LibXML::Element and $transformType, returns a Data::Result object. When true the call to $result->get_data will return the xml, when false it will contain a string that shows why it failed.

my $array_ref=$self->transforms

Returns an ArrayRef that contains the list of transform methods we will use when signing the xml.

This list is built out of the following:

  0: $self->envelope_method
  1: $self->canon_method

my $xml=$self->create_digest_xml($id,$digest)

Produces a text xml fragment to be used for an xml digest.

my $xml=$self->create_signedinfo_xml($digest_xml)

Produces text xml fragment to be used for an xml signature

my $xmlns=$self->create_xmlns

Creates our common xmlns string based on our namespaces.

my $xml=$self->create_signature_xml

Creates the signature xml for signing.

my $result=$self->load_cert_from_file($filename)

Returns a Data::Result structure, when true it contains a hasref with the following elements:

  type: 'dsa|rsa|x509'
  cert: $cert_object

my $result=$self->detect_cert($text)

Returns a Data::Result object, when true it contains the following hashref

  type: 'dsa|rsa|x509'
  cert: $cert_object

my $result=$self->load_rsa_string($string)

Returns a Data::Result object, when true it contains the following hashref:

  type: 'rsa'
  cert: $cert_object

my $result=$self->load_x509_string($string)

Returns a Data::Result object, when true it contains the following hashref:

  type: 'x509'
  cert: $cert_object

my $result=$self->load_dsa_string($string)

Returns a Data::Result object, when true it contains the following hashref:

  type: 'dsa'
  cert: $cert_object

my $result=$self->get_xml_to_sign($xpath_object,$nth)

Returns a Data::Result object, when true it contains the xml object to sign.

my $result=$self->get_signer_id($xpath_object,$nth)

Returns a Data::Result object, when true it contains the id value

my $result=$self->sign

Returns a Data::Result Object, when true it contains the signed xml string.

my $result=$self->sign_chunk($xpath_object,$nth)

Returns a Data::Result object, when true, the nth element with //@ID was signed and updated in $xpath_object. This method provides absolute granular control over what node is signed.

my $xml=$self->create_x509_xml($cert)

Creates the xml from the Certificate Object.

my $xml=$self->build_x509_xml($encoded_key)

Given the base64 encoded key, create a block of x509 xml.

my $result=$self->find_key_cert

Returns a Data::Result Object, when true it contains the x509 cert xml.

my $xml=$self->create_rsa_xml($cert)

Creates the xml from the Certificate Object.

my $xml=$self->create_dsa_xml($cert)

Creates the xml for the Key Object.

Limitations

This package currently has some limitations.

Supported Key Types and formats for signing/validation

Currently this module only supports RSA and DSA keys in pem format.

CaCert Validation

Currently CaCert validation only works with RSA keys.

Credits

This code is based on the following modules: XML::Sig, Net::SAML2::XML::Sig, Authen::NZRealMe::XMLSig, and Mojo::XMLSig and would not exist today withot them.

Bugs

Currently there are no known bugs, but if any are found please report them on our github project. Patches and pull requests are welcomed!

https://github.com/akalinux/xml-sig-oo

Author

AKALINUX <AKALINUX@CPAN.ORG>