Authen-NZRealMe

 view release on metacpan or  search on metacpan

t/35-xml-sig-units.t  view on Meta::CPAN

#!perl

use strict;
use warnings;

use Test::More;
use FindBin;
use File::Spec;
use lib File::Spec->catdir($FindBin::Bin, 'test-lib');

use AuthenNZRealMeTestHelper;
use Authen::NZRealMe;
use XML::LibXML;


my $dispatcher    = 'Authen::NZRealMe';
my $sig_class     = $dispatcher->class_for('xml_signer');
my $idp_cert_file = test_conf_file('idp-assertion-sign-crt.pem');
my $idp_key_file  = test_conf_file('idp-assertion-sign-key.pem');

my @ns_ds = (ds => 'http://www.w3.org/2000/09/xmldsig#');

my($verifier, $signer, $xml, $xc, $node, $input, $output, $error);


##############################################################################
# Transform methods

$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);
my($tr_by_name, $tr_by_uri, $expected, $parser, $doc, $frag);

ok('1', '===== C14N Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/TR/2001/REC-xml-c14n-20010315');

is(ref($tr_by_name) => 'HASH', 'found c14n by name');
is(ref($tr_by_uri)  => 'HASH', 'found c14n by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== C14N-With-Comments Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n_wc');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments');

is(ref($tr_by_name) => 'HASH', 'found c14n_wc by name');
is(ref($tr_by_uri)  => 'HASH', 'found c14n_wc by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== C14N Fragment Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n');

$input = q{<box:Container xmlns:box="https://example.com/box/"><Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc></box:Container>};

$expected = q{<Doc xmlns="https://example.com/doc/" xmlns:box="https://example.com/box/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$xc = parse_xml_to_xc($input, 'doc' => 'https://example.com/doc/');
($frag) = $xc->findnodes('//doc:Doc');
isa_ok($frag => 'XML::LibXML::Element', 'fragment node');

$output = $verifier->_apply_transform($tr_by_name, [$xc, $frag]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== C14N11 Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n11');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2006/12/xml-c14n11');

is(ref($tr_by_name) => 'HASH', 'found c14n11 by name');
is(ref($tr_by_uri)  => 'HASH', 'found c14n11 by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== C14N11-With-Comments Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n11_wc');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2006/12/xml-c14n11#WithComments');

is(ref($tr_by_name) => 'HASH', 'found c14n11_wc by name');
is(ref($tr_by_uri)  => 'HASH', 'found c14n11_wc by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== C14N11 Fragment Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('c14n11');

$input = q{<box:Container xmlns:box="https://example.com/box/"><Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc></box:Container>};

$expected = q{<Doc xmlns="https://example.com/doc/" xmlns:box="https://example.com/box/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$xc = parse_xml_to_xc($input, 'doc' => 'https://example.com/doc/');
($frag) = $xc->findnodes('//doc:Doc');
isa_ok($frag => 'XML::LibXML::Element', 'fragment node');

$output = $verifier->_apply_transform($tr_by_name, [$xc, $frag]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== EC14N Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('ec14n');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2001/10/xml-exc-c14n#');

is(ref($tr_by_name) => 'HASH', 'found ec14n by name');
is(ref($tr_by_uri)  => 'HASH', 'found ec14n by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== EC14N-With-Comments Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('ec14n_wc');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2001/10/xml-exc-c14n#WithComments');

is(ref($tr_by_name) => 'HASH', 'found ec14n_wc by name');
is(ref($tr_by_uri)  => 'HASH', 'found ec14n_wc by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, $expected, 'canonical output (from string)');

$xc = parse_xml_to_xc($input);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== EC14N Fragment Canonicalisation =====');

$tr_by_name = $verifier->_find_transform('ec14n');

$input = q{<box:Container xmlns:box="https://example.com/box/"><Doc ccc="three"
bbb="tw&#111;"
aaa="one"
xmlns="https://example.com/doc/">
  <Title>Example Document</Title><!-- a comment -->
</Doc></box:Container>};

$expected = q{<Doc xmlns="https://example.com/doc/" aaa="one" bbb="two" ccc="three">
  <Title>Example Document</Title>
</Doc>};

$xc = parse_xml_to_xc($input, 'doc' => 'https://example.com/doc/');
($frag) = $xc->findnodes('//doc:Doc');
isa_ok($frag => 'XML::LibXML::Element', 'fragment node');

$output = $verifier->_apply_transform($tr_by_name, [$xc, $frag]);
is($output, $expected, 'canonical output (from DOM fragment)');

ok('1', '===== Enveloped Signature =====');

$tr_by_name = $verifier->_find_transform('env_sig');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2000/09/xmldsig#enveloped-signature');

is(ref($tr_by_name) => 'HASH', 'found env-sig by name');
is(ref($tr_by_uri)  => 'HASH', 'found env-sig by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$input = q{<Doc><dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
  <dsig:SignedInfo>
    <content>Random stuff goes here</content>
    <!-- Nobody would put a comment in their <Signature> -->
  </dsig:SignedInfo>
</dsig:Signature>
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$expected = q{<Doc>
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$output = $verifier->_apply_transform($tr_by_name, $input);
isa_ok($output => 'ARRAY', 'fragment node');
$output = $output->[1];
isa_ok($output => 'XML::LibXML::Element', 'transformed document node');
is($output->toStringEC14N(1), $expected, 'env-sig output (from string)');

$xc = parse_xml_to_xc($input, @ns_ds);
$output = $verifier->_apply_transform($tr_by_name, [$xc, $xc->getContextNode]);
isa_ok($output => 'ARRAY', 'fragment node');
$output = $output->[1];
isa_ok($output => 'XML::LibXML::Element', 'transformed document node');
is($output->toStringEC14N(1), $expected, 'env-sig output (from DOM fragment)');

ok('1', '===== SHA1 Digest =====');

$input = q{<Doc>
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$tr_by_name = $verifier->_find_transform('sha1');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2000/09/xmldsig#sha1');

is(ref($tr_by_name) => 'HASH', 'found sha1 by name');
is(ref($tr_by_uri)  => 'HASH', 'found sha1 by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, 'zCGTIejOvqGvd6KSmlk4aFOW4Ro=', 'sha1 digest output (from string)');

# No test for sha1 digest with a DOM fragment as input - since any sane
# implementation would use a c14n transform to provide an input string.

ok('1', '===== SHA256 Digest =====');

$input = q{<Doc>
  <Title>Example Document</Title><!-- a comment -->
</Doc>};

$tr_by_name = $verifier->_find_transform('sha256');
$tr_by_uri  = $verifier->_find_transform('http://www.w3.org/2001/04/xmlenc#sha256');

is(ref($tr_by_name) => 'HASH', 'found sha256 by name');
is(ref($tr_by_uri)  => 'HASH', 'found sha256 by URI');
is($tr_by_name->{uri} => $tr_by_uri->{uri}, 'same transform URI');
is($tr_by_name->{method} => $tr_by_uri->{method}, 'same transform method name');

$output = $verifier->_apply_transform($tr_by_name, $input);
is($output, 'WjnmbezTqKqqU7dyvyFO46FwLTa3KBOsklKGLYK4Ge4=',
    'sha256 digest output (from string)'
);

# No test for sha256 digest with a DOM fragment as input - since any sane
# implementation would use a c14n transform to provide an input string.


##############################################################################
# Raw signature methods

my $plaintext       = 'This is some plain text';
my $mismatched_text = 'This is some different plain text';
$signer = $sig_class->new(
    key_text  => slurp_file($idp_key_file),
);
$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);
my($sig_alg, $b64_sig);

ok('1', '===== RSA-SHA1 Signature =====');

$sig_alg = $signer->_find_sig_alg('rsa_sha1');
$b64_sig = $signer->_create_signature($sig_alg, $plaintext);
$b64_sig =~ s/\s+//g;
is(
    $b64_sig,
    'eUrNfCVSosr1PsyofbeC4PyNQZrNuxHE3iw5YBWL8Q39TSvera9Ef6wYSSETSE6j'
    . 'xSXq4JCapAyaj3EcMeu4ksngLmZ+pfrJX/f71gOUAefHCyvr8KNKG4QuUYeL0X'
    . 'Qw0NDnttmfAt4pduVBIkvFMiX6SfFOGz+pmLIZaZg7wIDQkovOEtmpsg/IL4zy'
    . 'v05z52XTMZdXQ+4RAL/YOdzJZ3ow7l6R/q/yZjakMGWIkqWwa7AcL6YPt/Awyw'
    . '50fMtcGii4GVGbOsVUUjHbld4SG1uffzCpOCqqFKKrQWRPjUJuAs9c3L6aqkKf'
    . '80aODokUFhftgM5sMmg3iyNsao2WLw==',
    'created RSA-SHA1 hash signature'
);
ok(
    $verifier->_verify_signature($sig_alg, $plaintext, $b64_sig),
    'verified RSA-SHA1 hash signature'
);
ok(
    !$verifier->_verify_signature($sig_alg, $mismatched_text, $b64_sig),
    'failed to verify mismatched RSA-SHA1 hash signature'
);

ok('1', '===== RSA-SHA256 Signature =====');

$sig_alg = $signer->_find_sig_alg('rsa_sha256');
$b64_sig = $signer->_create_signature($sig_alg, $plaintext);
$b64_sig =~ s/\s+//g;
is(
    $b64_sig,
    'YeR7ga5hEXBD7BL3NTUjReKG09hSp0sWFNs5WpOD3td0nFARedv5Bn6uy1zf2zuW'
    . 'qZos6cyenUERRypZN0QnD5O7M1OmlV/Kpv40UkcMdFqPT/wQP2OHe+YaKXO3b1'
    . 'V9gq1eGhk5wqW51y3Uu6GawKqJj9VZNPD20cDGvTeegtoNjiY3wrFu4G/v6Ro2'
    . 'OaWXOkyQrXN0Ql2TheK4qMV2fklknKsv87H+BCK75+IWHn63LBvavY5kUyvM/2'
    . 'i2n9aoPLkrlVv32dOGFPdN5PE12B8ujcRiywXAHNffYo7s24rbMk/hKAXCG4Ot'
    . '/sh7T+WbBs2Ny8VPq0lgmfnN6o3x0A==',
    'created RSA-SHA256 hash signature'
);
ok(
    $verifier->_verify_signature($sig_alg, $plaintext, $b64_sig),
    'verified RSA-SHA256 hash signature'
);
ok(

t/35-xml-sig-units.t  view on Meta::CPAN

my $id_attr = $verifier->id_attr;
is($id_attr => undef, 'no default attribute name for references');

$node = $ref1->{xml_node};
my $node_xml = $node->toString();
like($node_xml => qr{\A<Assertion\b}, '  top level tag is <Assertion>');
like($node_xml => qr{\A<[^>]+ ID="Idd02c7c2232759874e1c205587017bed"},
    '  ID attribute value'
);
like($node_xml => qr{<Identity\b}, '  included child tag <Identity>');
like($node_xml => qr{<!-- no surname -->}, '  included comment');

# Apply first transform listed above
my($assertion) = $xc->findnodes('/Container/Assertion');
$input = [ $xc, $assertion ];
$output = $verifier->_apply_transform($t1, $input);
is(ref($output) => 'ARRAY', 'transform 1 output');
my $x1_node = $output->[1];
isa_ok($x1_node => 'XML::LibXML::Element', 'transformed output node');
my $x1_xml = $x1_node->toString();
like($x1_xml => qr{\A<Assertion\b}, '  top level tag is <Assertion>');
like($x1_xml => qr{<Identity\b}, '  included child tag <Identity>');
like($x1_xml => qr{<!-- no surname -->}, '  included comment');
unlike($x1_xml => qr{<dsig:Signature\b}, '  child tag <Signature> not included');

# Apply second transform listed above
$input = $output;
$output = $verifier->_apply_transform($t2, $input);
is(ref($output) => '', 'transform returned a string');
like($output => qr{\A<Assertion\b}, '  top level tag is <Assertion>');
like($output => qr{<Identity\b}, '  included child tag <Identity>');
unlike($output => qr{<!-- no surname -->}, '  comment omitted');
unlike($output => qr{<dsig:Signature\b}, '  child tag <Signature> not included');
is($output => q{<Assertion ID="Idd02c7c2232759874e1c205587017bed">
  <Identity>
    <Name>Bob</Name>
    <DateOfBirth>1954-03-21</DateOfBirth>
  </Identity>
</Assertion>}, '  canonical form');

# Apply digest method
$input = $output;
$output = $verifier->_apply_transform($digm, $input);
is(ref($output) => '', 'digest transform returned a string');
is($output => 'Wgb21Ak30ZPTzFKw5uPlRzVd7zo=', '  calculated digest');
is($output => $ref1->{digest_value}, '  expected digest (from signature block)');


##############################################################################
# Use the verify method (which does all of the above) on the same XML document

ok('1', '===== verify() method =====');

$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);

eval { $verifier->verify($xml); };
is($@, '', 'signature verification was successful');

my @frags = $verifier->_signed_fragment_paths;
is(scalar(@frags), 1, 'one signed fragment was found');
is($frags[0], '/Container/Assertion', 'XPath for signed fragment');

$xc = parse_xml_to_xc($xml);

$node = eval {
    $verifier->find_verified_element($xc, '/Assertion');
};
like($@, qr{No element matches: '/Assertion'}, 'XPath did not match');

$node = eval {
    $verifier->find_verified_element($xc, '/Container/Unsafe/Assertion');
};
like($@,
    qr{Element matching '/Container/Unsafe/Assertion' is not in a signed fragment},
    'XPath matched unsigned element'
);

$node = eval { $verifier->find_verified_element($xc, '/Container/Assertion'); };
is($@, '', 'found a verified element');
isa_ok($node => 'XML::LibXML::Element', 'fragment node');
is($node->{ID} => 'Idd02c7c2232759874e1c205587017bed', 'got the assertion node');
my $name = $xc->findvalue('./Identity/Name', $node);
is($name => 'Bob', 'fragment includes <Name> element');


##############################################################################
# Try again but with referenced content that does not match the signature
# i.e.: tamper with the signed content.

$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);

$xml =~ s{1954-03-21}{1954-03-22};

eval { $verifier->verify($xml); };
$error = $@;
like($@, qr{Signature verification failed.}, 'signature verification failed');
like($@, qr{Digest.*differs.*from.*reference}, 'due to digest mismatch');


##############################################################################
# Try again but alter the expected digest to match the altered content
# i.e.: tamper with the contents of the signed info block too.

$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);

$xml =~ s{1954-03-21}{1954-03-22};
$xml =~ s{Wgb21Ak30ZPTzFKw5uPlRzVd7zo=}{iIixkNtguqdVy8HcCFQSIbeMwMo=};

eval { $verifier->verify($xml); };
$error = $@;
like($@, qr{Signature verification failed.}, 'signature verification failed');
like($@, qr{signature does not match}, 'due to signature mismatch');


##############################################################################
# Try again but with a document that is not signed.

$verifier = $sig_class->new(
    pub_cert_text  => slurp_file($idp_cert_file),
);

eval { $verifier->verify('<Doc>Content Here</Doc>'); };
like($@, qr{XML document contains no signatures}, 'no signature to verify');



##############################################################################
# Test creation of signature

ok('1', '===== _make_sig_xml() method =====');

$xml =  q{<Container><Assertion ID="Idd02c7c2232759874e1c205587017bed">
  <Identity>
    <Name>Bob</Name><!-- no surname -->
    <DateOfBirth>1954-03-21</DateOfBirth>
  </Identity>
</Assertion>
</Container>
};



( run in 0.601 second using v1.01-cache-2.11-cpan-364913b4093 )