diff --git a/Makefile.PL b/Makefile.PL index a331521..aa0f60c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -30,6 +30,7 @@ my %WriteMakefileArgs = ( }, "TEST_REQUIRES" => { "Crypt::OpenSSL::Guess" => 0, + "CryptX" => 0, "Exporter" => 0, "File::Slurper" => 0, "File::Which" => 0, @@ -53,6 +54,7 @@ my %FallbackPrereqs = ( "Crypt::OpenSSL::X509" => 0, "Crypt::PK::RSA" => 0, "Crypt::PRNG" => 0, + "CryptX" => 0, "Exporter" => 0, "File::Slurper" => 0, "File::Which" => 0, diff --git a/README b/README index d81b339..b8d9e6c 100644 --- a/README +++ b/README @@ -103,6 +103,37 @@ METHODS * mgf1sha512 + oaep_params + Specify the OAEPparams value to use as part of the mask generation + function (MGF). It is optional but can be a specified for rsa-oaep + and rsa-oaep-mgf1p EncryptionMethods. + + It is base64 encoded and stored in the XML as OAEPparams. + + If specified you MAY specify the oaep_label_hash that should be + used. You should note that not all implementations support an + oaep_label_hash that differs from that of the MGF secified in the + xenc11:MGF element or the default MGF1 with SHA1. + + The oaep_label_hash is stored in the DigestMethod child element of + the EncryptionMethod. + + oaep_label_hash + Specify the Hash Algorithm to use for the rsa-oaep Label. Supported + algorithms are: + + The default is sha1. + + * sha1 + + * sha224 + + * sha256 + + * sha384 + + * sha512 + decrypt( ... ) Main decryption function. diff --git a/cpanfile b/cpanfile index 00c3e5b..48f72d0 100644 --- a/cpanfile +++ b/cpanfile @@ -15,6 +15,7 @@ requires "warnings" => "0"; on 'test' => sub { requires "Crypt::OpenSSL::Guess" => "0"; + requires "CryptX" => "0"; requires "Exporter" => "0"; requires "File::Slurper" => "0"; requires "File::Which" => "0"; diff --git a/lib/XML/Enc.pm b/lib/XML/Enc.pm index 34fe668..ad15622 100644 --- a/lib/XML/Enc.pm +++ b/lib/XML/Enc.pm @@ -167,6 +167,39 @@ Used in encryption. Optional. Default method: mgf1sha1 =back +=item B + +Specify the OAEPparams value to use as part of the mask generation function (MGF). +It is optional but can be specified for rsa-oaep and rsa-oaep-mgf1p EncryptionMethods. + +It is base64 encoded and stored in the XML as OAEPparams. + +If specified you MAY specify the oaep_label_hash that should be used. You should note +that not all implementations support an oaep_label_hash that differs from that of the +MGF specified in the xenc11:MGF element or the default MGF1 with SHA1. + +The oaep_label_hash is stored in the DigestMethod child element of the EncryptionMethod. + +=item B + +Specify the Hash Algorithm to use for the rsa-oaep label as specified by oaep_params. + +The default is sha1. Supported algorithms are: + +=over + +=item * L + +=item * L + +=item * L + +=item * L + +=item * L + +=back + =back =cut @@ -196,8 +229,12 @@ sub new { my $key_method = exists($params->{'key_transport'}) ? $params->{'key_transport'} : 'rsa-oaep-mgf1p '; $self->{'key_transport'} = $self->_setKeyEncryptionMethod($key_method); - my $oaep_mgf_alg = exists($params->{'oaep_mgf_alg'}) ? $params->{'oaep_mgf_alg'} : 'http://www.w3.org/2009/xmlenc11#mgf1sha1'; - $self->{'oaep_mgf_alg'} = $self->_setOAEPAlgorithm($oaep_mgf_alg); + if (exists $params->{'oaep_mgf_alg'}) { + $self->{'oaep_mgf_alg'} = $self->_setOAEPAlgorithm($params->{'oaep_mgf_alg'}); + } + if (exists $params->{'oaep_label_hash'} ) { + $self->{'oaep_label_hash'} = $self->_setOAEPDigest($params->{'oaep_label_hash'}); + } $self->{'oaep_params'} = exists($params->{'oaep_params'}) ? $params->{'oaep_params'} : ''; @@ -233,6 +270,7 @@ sub decrypt { my $xpc = XML::LibXML::XPathContext->new($doc); $xpc->registerNs('dsig', 'http://www.w3.org/2000/09/xmldsig#'); $xpc->registerNs('xenc', 'http://www.w3.org/2001/04/xmlenc#'); + $xpc->registerNs('xenc11', 'http://www.w3.org/2009/xmlenc11#'); $xpc->registerNs('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); my $data; @@ -383,6 +421,8 @@ sub _setOAEPAlgorithm { my $self = shift; my $method = shift; + return if ! defined $method; + my %methods = ( 'mgf1sha1' => 'http://www.w3.org/2009/xmlenc11#mgf1sha1', 'mgf1sha224' => 'http://www.w3.org/2009/xmlenc11#mgf1sha224', @@ -398,6 +438,8 @@ sub _getOAEPAlgorithm { my $self = shift; my $method = shift; + return if ! defined $method; + my %methods = ( 'http://www.w3.org/2009/xmlenc11#mgf1sha1' => 'SHA1', 'http://www.w3.org/2009/xmlenc11#mgf1sha224' => 'SHA224', @@ -409,6 +451,41 @@ sub _getOAEPAlgorithm { return exists($methods{$method}) ? $methods{$method} : $methods{'http://www.w3.org/2009/xmlenc11#mgf1sha1'}; } +sub _setOAEPDigest { + my $self = shift; + my $method = shift; + + return if ! defined $method; + + my %methods = ( + 'sha1' => 'http://www.w3.org/2000/09/xmldsig#sha1', + 'sha224' => 'http://www.w3.org/2001/04/xmldsig-more#sha224', + 'sha256' => 'http://www.w3.org/2001/04/xmlenc#sha256', + 'sha384' => 'http://www.w3.org/2001/04/xmldsig-more#sha384', + 'sha512' => 'http://www.w3.org/2001/04/xmlenc#sha512', + ); + + return exists($methods{$method}) ? $methods{$method} : $methods{'sha256'}; +} + +sub _getParamsAlgorithm { + my $self = shift; + my $method = shift; + + return if ! defined $method; + + my %methods = ( + 'http://www.w3.org/2000/09/xmldsig#sha1' => 'SHA1', + 'http://www.w3.org/2001/04/xmldsig-more#sha224' => 'SHA224', + 'http://www.w3.org/2001/04/xmlenc#sha256' => 'SHA256', + 'http://www.w3.org/2001/04/xmldsig-more#sha384' => 'SHA384', + 'http://www.w3.org/2001/04/xmlenc#sha512' => 'SHA512', + ); + + return exists($methods{$method}) ? $methods{$method} : + $methods{'http://www.w3.org/2000/09/xmldsig#sha1'}; +} + sub _getKeyEncryptionMethod { my $self = shift; my $xpc = shift; @@ -422,19 +499,45 @@ sub _getKeyEncryptionMethod { $id =~ s/#//g; my $keyinfo = $xpc->find('//*[@Id=\''. $id . '\']', $context); + $keyinfo->[0]->setNamespace('http://www.w3.org/2000/09/xmldsig#', 'dsig', 0); + $keyinfo->[0]->setNamespace('http://www.w3.org/2001/04/xmlenc#', 'xenc', 0); + $keyinfo->[0]->setNamespace('http://www.w3.org/2009/xmlenc11#', 'xenc11', 0); if (! $keyinfo ) { die "Unable to find EncryptedKey"; } $method{Algorithm} = $keyinfo->[0]->findvalue('//xenc:EncryptedKey/xenc:EncryptionMethod/@Algorithm', $context); $method{KeySize} = $keyinfo->[0]->findvalue('//xenc:EncryptedKey/xenc:EncryptionMethod/xenc:KeySize', $context); $method{OAEPparams} = $keyinfo->[0]->findvalue('//xenc:EncryptedKey/xenc:EncryptionMethod/xenc:OAEPparams', $context); - $method{MGF} = $keyinfo->[0]->findvalue('//xenc:EncryptedKey/xenc:EncryptionMethod/xenc:MGF/@Algorithm', $context); + + if ($method{Algorithm} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep') { + $method{MGF} = $keyinfo->[0]->findvalue( + '//xenc:EncryptedKey/xenc:EncryptionMethod/xmlenc11:MGF/@Algorithm' + , $context); + } + if ($method{Algorithm} eq 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p') { + $method{MGF} = undef; + } + + $method{oaep_label_hash} = $keyinfo->[0]->findvalue( + 'xenc:EncryptedKey/xenc:EncryptionMethod/dsig:DigestMethod/@Algorithm', + $context) || _setOAEPDigest('sha1'); return \%method; } $method{Algorithm} = $xpc->findvalue('dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/@Algorithm', $context); $method{KeySize} = $xpc->findvalue('dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/xenc:KeySize', $context); $method{OAEPparams} = $xpc->findvalue('dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/xenc:OAEPparams', $context); - $method{MGF} = $xpc->findvalue('dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/xenc:MGF/@Algorithm', $context); + if ($method{Algorithm} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep') { + $method{MGF} = $xpc->findvalue( + 'dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/xenc11:MGF/@Algorithm', + $context); + } + if ($method{Algorithm} eq 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p') { + $method{MGF} = undef; + } + $method{oaep_label_hash} = $xpc->findvalue( + 'dsig:KeyInfo/xenc:EncryptedKey/xenc:EncryptionMethod/dsig:DigestMethod/@Algorithm', + $context) || _setOAEPDigest('sha1'); + return \%method; } @@ -534,10 +637,30 @@ sub _DecryptKey { return $self->{key_obj}->decrypt($encryptedkey, 'v1.5'); } elsif ($keymethod->{Algorithm} eq 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p') { - return $self->{key_obj}->decrypt($encryptedkey, 'oaep', 'SHA1', decode_base64($keymethod->{OAEPparams})); + my $oaep_params = defined $keymethod->{OAEPparams} ? + decode_base64(_trim($keymethod->{OAEPparams}) ) : undef; + my $oaep_label_hash = defined $keymethod->{oaep_label_hash} ? + $self->_getParamsAlgorithm($keymethod->{oaep_label_hash}) : 'SHA1'; + + if ($CryptX::VERSION le 0.077) { + return $self->{key_obj}->decrypt($encryptedkey, 'oaep', 'SHA1', $oaep_params); + } else { + return $self->{key_obj}->decrypt($encryptedkey, 'oaep', 'SHA1', $oaep_params, $oaep_label_hash); + } } elsif ($keymethod->{Algorithm} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep') { - return $self->{key_obj}->decrypt($encryptedkey, 'oaep', $self->_getOAEPAlgorithm($keymethod->{MGF}), decode_base64($keymethod->{OAEPparams})); + my $oaep_params = defined $keymethod->{OAEPparams} ? + decode_base64(_trim($keymethod->{OAEPparams}) ) : undef; + my $mgf_hash = defined $keymethod->{MGF} ? $self->_getOAEPAlgorithm($keymethod->{MGF}) : undef; + my $oaep_label_hash = defined $keymethod->{oaep_label_hash} ? + $self->_getParamsAlgorithm($keymethod->{oaep_label_hash}) : + $mgf_hash; + + if ($CryptX::VERSION le 0.077) { + return $self->{key_obj}->decrypt($encryptedkey, 'oaep', $mgf_hash, $oaep_params); + } else { + return $self->{key_obj}->decrypt($encryptedkey, 'oaep', $mgf_hash, $oaep_params, $oaep_label_hash); + } } else { die "Unsupported Key Encryption Method"; } @@ -557,10 +680,24 @@ sub _EncryptKey { ${$key} = $rsa_pub->encrypt(${$key}, 'v1.5'); } elsif ($keymethod eq 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p') { - ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', 'SHA1', $self->{oaep_params}); + my $oaep_label_hash = (defined $self->{oaep_label_hash} && $self->{oaep_label_hash} ne '') ? + $self->_getParamsAlgorithm($self->{oaep_label_hash}) : 'SHA1'; + if ($CryptX::VERSION le 0.077) { + ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', 'SHA1', $self->{oaep_params}); + } else { + ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', 'SHA1', $self->{oaep_params}, $oaep_label_hash); + } } elsif ($keymethod eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep') { - ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', $self->_getOAEPAlgorithm($self->{oaep_mgf_alg}), $self->{oaep_params}); + my $mgf_hash = defined $self->{oaep_mgf_alg} ? + $self->_getOAEPAlgorithm($self->{oaep_mgf_alg}) : undef; + my $oaep_label_hash = (defined $self->{oaep_label_hash} && $self->{oaep_label_hash} ne '') ? + $self->_getParamsAlgorithm($self->{oaep_label_hash}) : $mgf_hash; + if ($CryptX::VERSION le 0.077) { + ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', $mgf_hash, $self->{oaep_params}); + } else { + ${$key} = $rsa_pub->encrypt(${$key}, 'oaep', $mgf_hash, $self->{oaep_params}, $oaep_label_hash); + } } else { die "Unsupported Key Encryption Method"; } @@ -861,6 +998,7 @@ sub _create_encrypted_data_xml { my $doc = XML::LibXML::Document->new(); my $xencns = 'http://www.w3.org/2001/04/xmlenc#'; + my $xenc11ns = 'http://www.w3.org/2009/xmlenc11#'; my $dsigns = 'http://www.w3.org/2000/09/xmldsig#'; my $encdata = $self->_create_node($doc, $xencns, $doc, 'xenc:EncryptedData', @@ -914,12 +1052,27 @@ sub _create_encrypted_data_xml { ); }; - if ($self->{key_transport} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep') { + if ($self->{key_transport} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep' || + $self->{key_transport} eq 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' && + $self->{oaep_label_hash}) { + my $digestmethod = $self->_create_node( + $doc, + $dsigns, + $kencmethod, + 'dsig:DigestMethod', + { + Algorithm => $self->{oaep_label_hash}, + } + ); + }; + + if ($self->{key_transport} eq 'http://www.w3.org/2009/xmlenc11#rsa-oaep' && + $self->{oaep_mgf_alg}) { my $oaepmethod = $self->_create_node( $doc, - $xencns, + $xenc11ns, $kencmethod, - 'xenc:MGF', + 'xenc11:MGF', { Algorithm => $self->{oaep_mgf_alg}, } diff --git a/t/06-test-encryption-methods.t b/t/06-test-encryption-methods.t index c802a2a..8baafaf 100644 --- a/t/06-test-encryption-methods.t +++ b/t/06-test-encryption-methods.t @@ -1,6 +1,6 @@ use strict; use warnings; -use Test::More tests => 126; +use Test::More tests => 896; use Test::Lib; use Test::XML::Enc; use XML::Enc; @@ -15,10 +15,12 @@ XML my @key_methods = qw/rsa-1_5 rsa-oaep-mgf1p/; my @data_methods = qw/aes128-cbc aes192-cbc aes256-cbc tripledes-cbc aes128-gcm aes192-gcm aes256-gcm/; -my @oaep_mgf_algs = qw/mgf1sha1 mgf1sha224 mgf1sha256 mgf1sha384 mgf1sha512/; +my @oaep_mgf_algs = qw/rsa-oaep-mgf1p mgf1sha1 mgf1sha224 mgf1sha256 mgf1sha384 mgf1sha512/; +my @oaep_label_hashes = qw/sha1 sha224 sha256 sha384 sha512/; my $xmlsec = get_xmlsec_features(); my $lax_key_search = $xmlsec->{lax_key_search} ? '--lax-key-search': ''; +my $cryptx = get_cryptx_features(); foreach my $km (@key_methods) { foreach my $dm (@data_methods) { @@ -39,10 +41,6 @@ foreach my $km (@key_methods) { SKIP: { skip "xmlsec1 not installed", 2 unless $xmlsec->{installed}; - my $version; - if (`xmlsec1 version` =~ m/(\d+\.\d+\.\d+)/) { - $version = $1; - }; skip "xmlsec version 1.2.27 minimum for GCM", 2 if ! $xmlsec->{aes_gcm}; ok( open XML, '>', 'tmp.xml' ); print XML $encrypted; @@ -56,22 +54,45 @@ foreach my $km (@key_methods) { } foreach my $om (@oaep_mgf_algs) { - foreach my $dm (@data_methods) { - my $encrypter = XML::Enc->new( - { - key => 't/sign-private.pem', - cert => 't/sign-certonly.pem', - data_enc_method => $dm, - key_transport => 'rsa-oaep', - oaep_mgf_alg => $om, - no_xml_declaration => 1 + foreach my $omdig (@oaep_label_hashes) { + SKIP: { + if (! $cryptx->{oaem_mgf_digest} && ($om ne $omdig)) { + my $skip = (scalar @data_methods) * 4; + skip "CryptX $cryptx->{version} does not support rsa-oaep MGF: $om and digest $omdig", $skip; } - ); - my $encrypted = $encrypter->encrypt($xml); - ok($encrypted =~ /EncryptedData/, "Successfully Encrypted: Key Method 'rsa-oaep' with $om Data Method $dm"); + my $km = ( $om eq 'rsa-oaep-mgf1p') ? 'rsa-oaep-mgf1p' : 'rsa-oaep'; + foreach my $dm (@data_methods) { + my $encrypter = XML::Enc->new( + { + key => 't/sign-private.pem', + cert => 't/sign-certonly.pem', + data_enc_method => $dm, + key_transport => $km, + oaep_mgf_alg => $om, + oaep_label_hash => $omdig, + oaep_params => 'encrypt', + no_xml_declaration => 1, + } + ); - ok($encrypter->decrypt($encrypted) =~ /XML-SIG_1/, "Successfully Decrypted with XML::Enc"); + my $encrypted = $encrypter->encrypt($xml); + ok($encrypted =~ /EncryptedData/, "Successful Encrypted: Key Method:$km MGF:$om, param:$omdig Data Method:$dm"); + + SKIP: { + skip "xmlsec1 not installed", 2 unless $xmlsec->{installed}; + skip "xmlsec version 1.2.27 minimum for GCM", 2 if ! $xmlsec->{aes_gcm}; + ok( open XML, '>', "$km-$omdig-$dm-tmp.xml" ); + print XML $encrypted; + close XML; + my $verify_response = `xmlsec1 --decrypt $lax_key_search --privkey-pem t/sign-private.pem $km-$omdig-$dm-tmp.xml 2>&1`; + ok( $verify_response =~ m/XML-SIG_1/, "Successfully decrypted with xmlsec1" ) + or warn "calling xmlsec1 failed: '$verify_response'\n"; + unlink "$km-$omdig-$dm-tmp.xml"; + } + ok($encrypter->decrypt($encrypted) =~ /XML-SIG_1/, "Successfully Decrypted with XML::Enc"); + } + } } } done_testing; diff --git a/t/lib/Test/XML/Enc/Util.pm b/t/lib/Test/XML/Enc/Util.pm index eafe539..3d71283 100644 --- a/t/lib/Test/XML/Enc/Util.pm +++ b/t/lib/Test/XML/Enc/Util.pm @@ -9,6 +9,7 @@ our @ISA = qw(Exporter); our @EXPORT = qw( get_xmlsec_features get_openssl_features + get_cryptx_features ); our @EXPORT_OK; @@ -87,6 +88,35 @@ sub get_openssl_features { return \%openssl; } +######################################################################### +# get_cryptx_features +# +# Parameter: none +# +# Returns a hash of the version and any features that are needed +# if proper the version is installed +# +# Response: hash +# +# %features = ( +# version => '0.077', +# oaem_mgf_digest => 0, +# ); +########################################################################## +sub get_cryptx_features { + + require CryptX; + + my $version = $CryptX::VERSION; + + my %cryptx = ( + version => $version, + oaem_mgf_digest => ($version gt '0.077') ? 1 : 0, + ); + + return \%cryptx; +} + 1; __END__