line
stmt
bran
cond
sub
pod
time
code
1
# Copyright (c) 2013 Manni Heumann. All rights reserved.
2
#
3
# This program is free software; you can redistribute it and/or
4
# modify it under the same terms as Perl itself.
5
#
6
7
package Google::SAML::Response;
8
9
=head1 NAME
10
11
Google::SAML::Response - Generate signed XML documents as SAML responses for
12
Google's SSO implementation
13
14
=head1 VERSION
15
16
You are currently reading the documentation for version 0.14
17
18
=head1 DESCRIPTION
19
20
Google::SAML::Response can be used to generate a signed XML document that is
21
needed for logging your users into Google using SSO.
22
23
You have some sort of web application that can identify and authenticate users.
24
You want users to be able to use some sort of Google service such as Google mail.
25
26
When using SSO with your Google partner account, your users will send a request
27
to a Google URL. If the user isn't already logged in to Google, Google will
28
redirect him to a URL that you can define. Behind this URL, you need to have
29
a script that authenticates users in your original framework and generates a
30
SAML response for Google that you send back to the user whose browser will then
31
submit it back to Google. If everything works, users will then be logged into
32
their Google account and they don't even have to know their usernames or
33
passwords.
34
35
=head1 SYNOPSIS
36
37
use Google::SAML::Response;
38
use CGI;
39
40
# get SAMLRequest parameter:
41
my $req = CGI->new->param('SAMLRequest');
42
43
# authenticate user
44
...
45
46
# find our user's login for Google
47
...
48
49
# Generate SAML response
50
my $saml = Google::SAML::Response->new( {
51
key => $key,
52
login => $login,
53
request => $req
54
} );
55
my $xml = $saml->get_response_xml;
56
57
# Alternatively, send a HTML page to the client that will redirect
58
# her to Google. You have to extract the RelayState param from the cgi
59
# environment first.
60
61
print $saml->get_google_form( $relayState );
62
63
=head1 PREREQUISITES
64
65
You will need the following modules installed:
66
67
=over
68
69
=item * L
70
71
=item * L
72
73
=item * L
74
75
=item * L
76
77
=item * L
78
79
=item * L
80
81
=back
82
83
=head1 RESOURCES
84
85
=over
86
87
=item XML-Signature Syntax and Processing
88
89
L
90
91
=item Google-Documentation on SSO and SAML
92
93
L
94
95
=item XML Security Library
96
97
L
98
99
=back
100
101
=head1 METHODS
102
103
=cut
104
105
5
5
649946
use strict;
5
13
5
196
106
5
5
28
use warnings;
5
12
5
183
107
108
5
5
5769
use Crypt::OpenSSL::RSA;
5
59646
5
223
109
5
5
6549
use MIME::Base64;
5
5149
5
432
110
5
5
9485
use Digest::SHA qw/ sha1 /;
5
22927
5
528
111
5
5
4893
use Date::Format;
5
68414
5
474
112
5
5
6869
use Compress::Zlib;
5
539505
5
2045
113
5
5
3042
use Google::SAML::Request;
0
0
114
use Carp;
115
use HTML::Entities;
116
117
118
our $VERSION = '0.14';
119
120
=head2 new
121
122
Creates a new object and needs to have all parameters needed to generate
123
the signed xml later on. Parameters are passed in as a hash-reference.
124
125
=head3 Required parameters
126
127
=over
128
129
=item * request
130
131
The SAML request, base64-encoded and all, just as retrieved from the GET
132
request your user contacted you with (make sure that it's not url-encoded, though)
133
134
=item * key
135
136
The path to your private key that will be used to sign the response. Currently,
137
only RSA and DSA keys without pass phrases are supported. B: To handle DSA keys,
138
the module L needs to be installed. However,
139
it is not listed as a requirement in the Makefile for Google::SAML::Response, so make
140
sure it really is installed before using DSA keys.
141
142
=item * login
143
144
Your user's login name with Google
145
146
=back
147
148
=head3 Optional parameters
149
150
=over
151
152
=item * ttl
153
154
Time to live: Number of seconds your response should be valid. Default is two minutes.
155
156
=item * canonicalizer
157
158
The name of the module that will be used to canonicalize parts of our xml. Currently,
159
L and L are
160
supported. L is the default.
161
162
=back
163
164
=cut
165
166
167
sub new {
168
my $class = shift;
169
170
my $params = shift;
171
my $self = {};
172
173
foreach my $required ( qw/ request key login / ) {
174
if ( exists $params->{ $required } ) {
175
$self->{ $required } = $params->{ $required };
176
}
177
else {
178
confess "You need to provide the $required parameter!";
179
}
180
}
181
182
bless $self, $class;
183
184
my $request = Google::SAML::Request->new_from_string( $self->{ request } );
185
186
if ( $request && $self->_load_key ) {
187
$self->{ service_url } = $request->AssertionConsumerServiceURL;
188
$self->{ request_id } = $request->ID;
189
$self->{ ttl } = ( exists $params->{ ttl } ) ? $params->{ ttl } : 60*2;
190
$self->{ canonicalizer } = exists $params->{ canonicalizer }
191
? $params->{ canonicalizer }
192
: 'XML::CanonicalizeXML';
193
194
return $self;
195
}
196
else {
197
return;
198
}
199
200
}
201
202
203
sub _load_dsa_key {
204
my $self = shift;
205
my $key_text = shift;
206
207
eval {
208
require Crypt::OpenSSL::DSA;
209
};
210
211
confess "Crypt::OpenSSL::DSA needs to be installed so that we can handle DSA keys." if $@;
212
213
my $dsa_key = Crypt::OpenSSL::DSA->read_priv_key_str( $key_text );
214
215
if ( $dsa_key ) {
216
$self->{ key_obj } = $dsa_key;
217
my $g = encode_base64( $dsa_key->get_g, '' );
218
my $p = encode_base64( $dsa_key->get_p, '' );
219
my $q = encode_base64( $dsa_key->get_q, '' );
220
my $y = encode_base64( $dsa_key->get_pub_key, '' );
221
222
$self->{ KeyInfo } = "$p
$q $g $y ";
223
$self->{ key_type } = 'dsa';
224
}
225
else {
226
confess "did not get a new Crypt::OpenSSL::RSA object";
227
}
228
}
229
230
231
sub _load_rsa_key {
232
my $self = shift;
233
my $key_text = shift;
234
235
my $rsaKey = Crypt::OpenSSL::RSA->new_private_key( $key_text );
236
237
if ( $rsaKey ) {
238
$rsaKey->use_pkcs1_padding;
239
$self->{ key_obj } = $rsaKey;
240
241
my $bigNum = ( $rsaKey->get_key_parameters )[ 1 ];
242
my $bin = $bigNum->to_bin;
243
my $exp = encode_base64( $bin, '' );
244
245
$bigNum = ( $rsaKey->get_key_parameters )[ 0 ];
246
$bin = $bigNum->to_bin;
247
my $mod = encode_base64( $bin, '' );
248
$self->{ KeyInfo } = "$mod $exp ";
249
$self->{ key_type } = 'rsa';
250
}
251
else {
252
confess "did not get a new Crypt::OpenSSL::RSA object";
253
}
254
}
255
256
257
sub _load_key {
258
my $self = shift;
259
my $file = $self->{ key };
260
261
if ( open my $KEY, '<', $file ) {
262
my $text = '';
263
local $/ = undef;
264
$text = <$KEY>;
265
close $KEY;
266
267
if ( $text =~ m/BEGIN ([DR]SA) PRIVATE KEY/ ) {
268
my $key_used = $1;
269
270
if ( $key_used eq 'RSA' ) {
271
$self->_load_rsa_key( $text );
272
}
273
else {
274
$self->_load_dsa_key( $text );
275
}
276
277
return 1;
278
}
279
else {
280
confess "Could not detect type of key $file.";
281
}
282
}
283
else {
284
confess "Could not load key $file: $!";
285
}
286
287
return;
288
}
289
290
291
=head2 get_response_xml
292
293
Generate the signed response xml and return it as a string
294
295
The method does what the w3c tells us to do (L):
296
297
=over
298
299
3.1.1 Reference Generation
300
301
For each data object being signed:
302
303
1. Apply the Transforms, as determined by the application, to the data object.
304
305
2. Calculate the digest value over the resulting data object.
306
307
3. Create a Reference element, including the (optional) identification of the data object, any (optional) transform elements, the digest algorithm and the DigestValue. (Note, it is the canonical form of these references that are signed in 3.1.2 and validated in 3.2.1 .)
308
309
3.1.2 Signature Generation
310
311
1. Create SignedInfo element with SignatureMethod, CanonicalizationMethod and Reference(s).
312
313
2. Canonicalize and then calculate the SignatureValue over SignedInfo based on algorithms specified in SignedInfo.
314
315
3. Construct the Signature element that includes SignedInfo, Object(s) (if desired, encoding may be different than that used for signing), KeyInfo (if required), and SignatureValue.
316
317
=back
318
319
=cut
320
321
sub get_response_xml {
322
my $self = shift;
323
324
# This is the xml response without any signatures or digests:
325
my $xml = $self->_response_xml;
326
327
# We now calculate the SHA1 digest of the canoncial response xml
328
my $canonical = $self->_canonicalize_xml( $xml );
329
330
my $bin_digest = sha1( $canonical );
331
my $digest = encode_base64( $bin_digest, '' );
332
333
# Create a xml fragment containing the digest:
334
my $digest_xml = $self->_reference_xml( $digest );
335
336
# create a xml fragment consisting of the SignedInfo element
337
my $signed_info = $self->_signedinfo_xml( $digest_xml );
338
339
# We now calculate a signature over the canonical SignedInfo element
340
341
$canonical = $self->_canonicalize_xml( $signed_info );
342
my $signature;
343
344
if ( $self->{ key_type } eq 'dsa' ) {
345
my $sig = $self->{ key_obj }->do_sign( sha1( $canonical ) );
346
$signature = encode_base64( $sig->get_r . $sig->get_s );
347
}
348
else {
349
my $bin_signature = $self->{ key_obj }->sign( $canonical );
350
$signature = encode_base64( $bin_signature, "\n" );
351
}
352
353
# With the signature value and the signedinfo element, we create
354
# a Signature element:
355
my $signature_xml = $self->_signature_xml( $signed_info, $signature );
356
357
# Now insert the signature xml into our response xml
358
$xml =~ s//$signature_xml/;
359
360
return "\n" . $xml;
361
}
362
363
364
sub _signature_xml {
365
my $self = shift;
366
my $signed_info = shift;
367
my $signature_value = shift;
368
369
return qq{
370
$signed_info
371
$signature_value
372
$self->{ KeyInfo }
373
};
374
}
375
376
377
sub _signedinfo_xml {
378
my $self = shift;
379
my $digest_xml = shift;
380
381
return qq{
382
383
384
$digest_xml
385
};
386
}
387
388
389
sub _reference_xml {
390
my $self = shift;
391
my $digest = shift;
392
393
return qq{
394
395
396
397
398
$digest
399
};
400
}
401
402
403
sub _canonicalize_xml {
404
my $self = shift;
405
my $xml = shift;
406
407
if ( $self->{ canonicalizer } eq 'XML::Canonical' ) {
408
require XML::Canonical;
409
my $xmlcanon = XML::Canonical->new( comments => 1 );
410
return $xmlcanon->canonicalize_string( $xml );
411
}
412
elsif ( $self->{ canonicalizer } eq 'XML::CanonicalizeXML' ) {
413
require XML::CanonicalizeXML;
414
my $xpath = '(//. | //@* | //namespace::*) ';
415
return XML::CanonicalizeXML::canonicalize( $xml, $xpath, [], 0, 0 );
416
}
417
else {
418
confess "Unknown XML canonicalizer module.";
419
}
420
}
421
422
423
sub _response_xml {
424
my $self = shift;
425
426
# A 160-bit string containing a set of randomly generated characters.
427
# The ID MUST start with a character
428
my $response_id = sprintf 'GOSAML0d%04d', time, rand(10000);
429
430
# A timestamp indicating the date and time that the SAML response was generated
431
# Bsp: 2006-08-17T10:05:29Z
432
# All SAML time values have the type xs:dateTime, which is built in to the W3C XML Schema Datatypes
433
# specification [Schema2], and MUST be expressed in UTC form, with no time zone component.
434
my $issue_instant = time2str( "%Y-%m-%dT%XZ", time, 'UTC' );
435
436
# A 160-bit string containing a set of randomly generated characters.
437
my $assertion_id = sprintf 'GOSAML%010d%04d', time, rand(10000);
438
439
# The acs url
440
my $assertion_url = $self->{ service_url };
441
442
# The username for the authenticated user.
443
my $username = $self->{ login };
444
445
# A timestamp identifying the date and time after which the SAML response is deemed invalid.
446
my $best_before = time2str( "%Y-%m-%dT%XZ", time + $self->{ ttl }, 'UTC' );
447
448
# A timestamp indicating the date and time that you authenticated the user.
449
my $authn_instant = $issue_instant;
450
451
my $request_id = $self->{ request_id };
452
453
return
454
qq{
455
456
457
458
459
https://www.opensaml.org/IDP
460
461
$username
462
463
464
Recipient="$assertion_url"
465
NotOnOrAfter="$best_before"
466
InResponseTo="$request_id"
467
/>
468
469
470
471
472
$assertion_url
473
474
475
476
477
urn:oasis:names:tc:SAML:2.0:ac:classes:Password
478
479
480
481
482
};
483
}
484
485
486
=head2 get_google_form
487
488
This function will give you a complete HTML page that you can send to clients
489
to have them redirected to Google. Note that former versions of this module
490
also included a Content-Type HTTP header. Fortunately, this is no longer the
491
case and you will have to send a "Content-Type: text/html" yourself using
492
whatever method your framework provides.
493
494
After all the hi-tec stuff Google wants us to do to parse their request and
495
generate a response, this is where it gets low-tec and messy. We are supposed
496
to give clients a html page that contains a hidden form that uses Javascript
497
to post that form to Google. Ugly, but it works. The form will contain a textarea
498
containing the response xml and a textarea containing the relay state.
499
500
Hence the only required argument: the RelayState parameter from the user's GET request
501
502
=cut
503
504
sub get_google_form {
505
my $self = shift;
506
my $rs = shift;
507
508
my $url = $self->{ service_url };
509
my $output = "\n";
510
$output .= "\n";
511
512
my $xml = $self->get_response_xml;
513
my $encoded_rs = encode_entities( $rs );
514
515
$output .= qq|
516
517
518
519
520
521
522
523
|;
524
525
$output .= "\n";
526
527
return $output;
528
}
529
530
531
532
=head1 REMARKS
533
534
Coming up with a valid response for a SAML-request is quite tricky. The simplest
535
way to go is to use the xmlsec1 program distributed with the XML Security Library.
536
Google seems to use that program itself. However, I wanted to have a perlish way
537
of creating the response. Testing your computed response is best done
538
against xmlsec1: If your response is stored in the file test.xml, you can simply do:
539
540
xmlsec1 --verify --store-references --store-signatures test.xml > debug.txt
541
542
This will give you a file debug.txt with lots of information, most importantly it
543
will give you the canonical xml versions of your response and the 'References'
544
element. If your canonical xml of these two elements isn't exactly like the one
545
in debug.txt, your response will not be valid.
546
547
This brings us to another issue: XML-canonicalization. There are currently two
548
modules on CPAN that promise to do the work for you:
549
L and L.
550
Both can be used with Google::SAML::Response, however the default is to use the former
551
because it is much easier to install. However, the latter's interface is much
552
cleaner and Perl-like than the interface of the former.
553
554
L uses L which has a
555
Makefile.PL that begs to be hacked because it insists on using the version
556
of gdome that was available when Makefile.PL was written (2003) and then it still doesn't
557
install without force. L is much easier
558
to install, you just have to have the libxml development files installed so it will
559
compile.
560
561
=head1 TODO
562
563
=over
564
565
=item * Add support for encrypted keys
566
567
=back
568
569
=head1 SOURCE CODE
570
571
This module has a github repository:
572
573
https://github.com/mannih/Google-SAML-Response/
574
575
=head1 AUTHOR
576
577
Manni Heumann (saml at lxxi dot org)
578
579
with the help of Jeremy Smith and Thiago Damasceno. Thank you!
580
581
=head1 LICENSE
582
583
Copyright (c) 2008-2013 Manni Heumann. All rights reserved.
584
585
This program is free software; you can redistribute it and/or
586
modify it under the same terms as Perl itself.
587
588
=cut
589
590
591
1;