From d40ad86918b19132e785328fb8ad667310b62fc8 Mon Sep 17 00:00:00 2001 From: Chris Andrews Date: Oct 07 2010 09:55:19 +0000 Subject: Separate out the SOAP binding from Artifact Resolve. Introduce SOAP binding, and ArtifactResolve protocol message. --- diff --git a/lib/Net/SAML2.pm b/lib/Net/SAML2.pm index 8d068a8..48520e2 100644 --- a/lib/Net/SAML2.pm +++ b/lib/Net/SAML2.pm @@ -74,13 +74,14 @@ use Net::SAML2::SP; # bindings use Net::SAML2::Binding::Redirect; -use Net::SAML2::Binding::Artifact; use Net::SAML2::Binding::POST; +use Net::SAML2::Binding::SOAP; # protocol use Net::SAML2::Protocol::AuthnRequest; use Net::SAML2::Protocol::LogoutRequest; use Net::SAML2::Protocol::Assertion; +use Net::SAML2::Protocol::ArtifactResolve; =pod diff --git a/lib/Net/SAML2/Binding/Artifact.pm b/lib/Net/SAML2/Binding/Artifact.pm deleted file mode 100644 index f9925da..0000000 --- a/lib/Net/SAML2/Binding/Artifact.pm +++ /dev/null @@ -1,110 +0,0 @@ -package Net::SAML2::Binding::Artifact; -use strict; -use warnings; - -=head1 NAME - -Net::SAML2::Binding::Artifact - SOAP Artifact binding for SAML2 - -=head1 SYNOPSIS - - my $resolver = Net::SAML2::Binding::Artifact->new( - url => $art_url, - key => 'sign-private.pem', - cert => 'sign-certonly.pem', - issuer => 'http://localhost:3000', - ); - - my $response = $resolver->resolve(params->{SAMLart}); - -=head1 METHODS - -=cut - -use XML::Sig; -use LWP::UserAgent; -use HTTP::Request::Common; - -=head2 new( ... ) - -Constructor. Returns an instance of the Artifact binding configured -for the given SP issuer and IdP resolver service url. - -Arguments: - - * url - the resolver service URL - * key - path to the signing key - * cert - path to the signing certificate - * issuer - the issuing SP's identity URI - -=cut - -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{url} = $args{url}; - $self->{key} = $args{key}; - $self->{cert} = $args{cert}; - $self->{issuer} = $args{issuer}; - - return $self; -} - -=head2 resolve($artifact) - -Resolve the given artifact, which should be an opaque SAML2 artifact id. - -Returns the Artifact, or dies if there was an error. - -=cut - -sub resolve { - my ($self, $artifact) = @_; - - my $saml_req = < - $self->{issuer} - $artifact - -XML - - my $sig = XML::Sig->new({ x509 => 1, key => $self->{key}, cert => $self->{cert} }); - my $signed_saml_req = $sig->sign($saml_req); - - my $ret = $sig->verify($signed_saml_req); - die "failed to sign" unless $ret; - - my $soap_req = <<"SOAP"; - - -$saml_req - - -SOAP - - my $soap_action = 'http://www.oasis-open.org/committees/security'; - - my $req = POST $self->{url}; - $req->header('SOAPAction' => $soap_action); - $req->header('Content-Type' => 'text/xml'); - $req->header('Content-Length' => length $soap_req); - $req->content($soap_req); - - my $ua = LWP::UserAgent->new; - my $res = $ua->request($req); - - my $sig_verify = XML::Sig->new({ x509 => 1 }); - $ret = $sig_verify->verify($res->content); - die "bad artifact response" unless $ret; - - return $res->content; -} - -1; diff --git a/lib/Net/SAML2/Binding/SOAP.pm b/lib/Net/SAML2/Binding/SOAP.pm new file mode 100644 index 0000000..839a1d3 --- /dev/null +++ b/lib/Net/SAML2/Binding/SOAP.pm @@ -0,0 +1,105 @@ +package Net::SAML2::Binding::SOAP; +use strict; +use warnings; + +=head1 NAME + +Net::SAML2::Binding::Artifact - SOAP binding for SAML2 + +=head1 SYNOPSIS + + my $soap = Net::SAML2::Binding::SOAP->new( + url => $idp_url, + key => $key, + cert => $cert, + idp_cert => $idp_cert, + ); + + my $response = $soap->request($req); + +=head1 METHODS + +=cut + +use XML::Sig; +use LWP::UserAgent; +use HTTP::Request::Common; + +=head2 new( ... ) + +Constructor. Returns an instance of the SOAP binding configured for +the given IdP service url. + +Arguments: + + * url - the service URL + * key - the key to sign with + * cert - the corresponding certificate + * idp_cert - the idp's signing certificate + +=cut + +sub new { + my ($class, %args) = @_; + my $self = bless {}, $class; + + $self->{url} = $args{url}; + $self->{key} = $args{key}; + $self->{cert} = $args{cert}; + $self->{idp_cert} = $args{idp_cert}; + + return $self; +} + +=head2 request($req) + +Submit the request to the IdP's service. + +Returns the Response, or dies if there was an error. + +=cut + +sub request { + my ($self, $request) = @_; + + # sign the request + my $sig = XML::Sig->new({ + x509 => 1, + key => $self->{key}, + cert => $self->{cert} + }); + my $signed_req = $sig->sign($request); + + # test verify + my $ret = $sig->verify($signed_req); + die "failed to sign" unless $ret; + + my $soap_req = <<"SOAP"; + + +$signed_req + + +SOAP + + my $soap_action = 'http://www.oasis-open.org/committees/security'; + + my $req = POST $self->{url}; + $req->header('SOAPAction' => $soap_action); + $req->header('Content-Type' => 'text/xml'); + $req->header('Content-Length' => length $soap_req); + $req->content($soap_req); + + my $ua = LWP::UserAgent->new; + my $res = $ua->request($req); + + printf STDERR "res:\n%s\n", $res->content; + + my $sig_verify = XML::Sig->new({ x509 => 1, cert_text => $self->{idp_cert} }); + $ret = $sig_verify->verify($res->content); + die "bad SOAP response" unless $ret; + + return $res->content; +} + +1; diff --git a/lib/Net/SAML2/IdP.pm b/lib/Net/SAML2/IdP.pm index 2b056fd..02a788a 100644 --- a/lib/Net/SAML2/IdP.pm +++ b/lib/Net/SAML2/IdP.pm @@ -53,6 +53,15 @@ sub new { my $xpath = XML::XPath->new( xml => $xml ); $xpath->set_namespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata'); + $xpath->set_namespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + + my ($desc) = $xpath->findnodes('//md:EntityDescriptor'); + if (defined $desc) { + $self->{entityID} = $desc->getAttribute('entityID'); + } + else { + die "can't find entityID in metadata"; + } my @ssos = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService'); for my $sso (@ssos) { @@ -71,7 +80,15 @@ sub new { my $binding = $art->getAttribute('Binding'); $self->{Art}->{$binding} = $art->getAttribute('Location'); } - + + my @keys = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor'); + for my $key (@keys) { + my $use = $key->getAttribute('use'); + my ($text) = $key->findvalue('ds:KeyInfo/ds:X509Data/ds:X509Certificate') =~ /^\s+(.+?)\s+$/s; + $self->{Cert}->{$use} = + sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", $text); + } + return $self; } @@ -111,4 +128,26 @@ sub art_url { return $self->{Art}->{$binding}; } +=head2 cert($use) + +Returns the IdP's certificate for the given use (e.g. 'signing'). + +=cut + +sub cert { + my ($self, $use) = @_; + return $self->{Cert}->{$use}; +} + +=head2 entityID() + +Returns the IdP's entityID, for use as the Destination in requests. + +=cut + +sub entityID { + my ($self) = @_; + return $self->{entityID}; +} + 1; diff --git a/lib/Net/SAML2/Protocol/ArtifactResolve.pm b/lib/Net/SAML2/Protocol/ArtifactResolve.pm new file mode 100644 index 0000000..8d1ede6 --- /dev/null +++ b/lib/Net/SAML2/Protocol/ArtifactResolve.pm @@ -0,0 +1,76 @@ +package Net::SAML2::Protocol::ArtifactResolve; +use strict; +use warnings; + +=head1 NAME + +Net::SAML2::Protocol::ArtifactResolve - ArtifactResolve protocol class. + +=head1 SYNOPSIS + + my $resolver = Net::SAML2::Binding::ArtifactResolve->new( + issuer => 'http://localhost:3000', + ); + + my $response = $resolver->resolve(params->{SAMLart}); + +=head1 METHODS + +=cut + +=head2 new( ... ) + +Constructor. Returns an instance of the ArtifactResolve request for +the given issuer and artifact. + +Arguments: + + * issuer - the issuing SP's identity URI + * artifact - the artifact to be resolved + * issueinstant - a DateTime for "now" + * destination - the IdP's identity URI + +=cut + +sub new { + my ($class, %args) = @_; + my $self = bless {}, $class; + + $self->{issuer} = $args{issuer}; + $self->{artifact} = $args{artifact}; + $self->{destination} = $args{destination}; + $self->{issueinstant} = $args{issueinstant}; + + return $self; +} + +=head2 as_xml + +Returns the ArtifactResolve request as XML. + +=cut + +sub as_xml { + my ($self) = @_; + + my $issueinstant = DateTime::Format::XSD->format_datetime( + $self->{issueinstant} + ); + + my $xml = <<"EOXML"; + + $self->{issuer} + $self->{artifact} + +EOXML + + return $xml; +} + +1; diff --git a/lib/Net/SAML2/SP.pm b/lib/Net/SAML2/SP.pm index 818761a..8a6f5c5 100644 --- a/lib/Net/SAML2/SP.pm +++ b/lib/Net/SAML2/SP.pm @@ -42,7 +42,7 @@ sub new { my $cert = Crypt::OpenSSL::X509->new_from_file($args{cert}); $self->{cert} = $cert->as_string; $self->{cert} =~ s/-----[^-]*-----//gm; - + return $self; } @@ -87,6 +87,27 @@ sub logout_request { return $logout_req; } +=head2 artifact_request($destination, $artifact) + +Returns an ArtifactResolve request object created by this SP, intended +for the given destination, which should be the identity URI of the +IdP. + +=cut + +sub artifact_request { + my ($self, $destination, $artifact) = @_; + + my $artifact_request = Net::SAML2::Protocol::ArtifactResolve->new( + issuer => $self->{id}, + destination => $destination, + artifact => $artifact, + issueinstant => DateTime->now, + ); + + return $artifact_request; +} + =head2 metadata Returns the metadata XML document for this SP.