Skip to content

Commit d69cebd

Browse files
committed
Bug 1199089 - add support for duo-security
1 parent 07791e2 commit d69cebd

File tree

23 files changed

+684
-51
lines changed

23 files changed

+684
-51
lines changed

Bugzilla/Config.pm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ sub update_params {
216216
}
217217
}
218218

219+
# Generate unique Duo integration secret key
220+
if ($param->{duo_akey} eq '') {
221+
require Bugzilla::Util;
222+
$param->{duo_akey} = Bugzilla::Util::generate_random_password(40);
223+
}
224+
219225
$param->{'utf8'} = 1 if $new_install;
220226

221227
# --- REMOVE OLD PARAMS ---

Bugzilla/Config/Auth.pm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,27 @@ sub get_param_list {
148148
type => 'b',
149149
default => 0,
150150
},
151+
152+
{
153+
name => 'duo_host',
154+
type => 't',
155+
default => '',
156+
},
157+
{
158+
name => 'duo_akey',
159+
type => 't',
160+
default => '',
161+
},
162+
{
163+
name => 'duo_ikey',
164+
type => 't',
165+
default => '',
166+
},
167+
{
168+
name => 'duo_skey',
169+
type => 't',
170+
default => '',
171+
},
151172
);
152173
return @param_list;
153174
}

Bugzilla/DuoWeb.pm

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# https://github.com/duosecurity/duo_perl
2+
#
3+
# Copyright (c) 2012, Duo Security, Inc.
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions
8+
# are met:
9+
#
10+
# 1. Redistributions of source code must retain the above copyright
11+
# notice, this list of conditions and the following disclaimer.
12+
# 2. Redistributions in binary form must reproduce the above copyright
13+
# notice, this list of conditions and the following disclaimer in the
14+
# documentation and/or other materials provided with the distribution.
15+
# 3. The name of the author may not be used to endorse or promote products
16+
# derived from this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21+
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
29+
package Bugzilla::DuoWeb;
30+
31+
use strict;
32+
use warnings;
33+
34+
use MIME::Base64;
35+
use Digest::HMAC_SHA1 qw(hmac_sha1_hex);
36+
37+
my $DUO_PREFIX = 'TX';
38+
my $APP_PREFIX = 'APP';
39+
my $AUTH_PREFIX = 'AUTH';
40+
41+
my $DUO_EXPIRE = 300;
42+
my $APP_EXPIRE = 3600;
43+
44+
my $IKEY_LEN = 20;
45+
my $SKEY_LEN = 40;
46+
my $AKEY_LEN = 40;
47+
48+
our $ERR_USER = 'ERR|The username passed to sign_request() is invalid.';
49+
our $ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.';
50+
our $ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.';
51+
our $ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters.";
52+
our $ERR_UNKNOWN = 'ERR|An unknown error has occurred.';
53+
54+
55+
sub _sign_vals {
56+
my ($key, $vals, $prefix, $expire) = @_;
57+
58+
my $exp = time + $expire;
59+
60+
my $val = join '|', @{$vals}, $exp;
61+
my $b64 = encode_base64($val, '');
62+
my $cookie = "$prefix|$b64";
63+
64+
my $sig = hmac_sha1_hex($cookie, $key);
65+
66+
return "$cookie|$sig";
67+
}
68+
69+
70+
sub _parse_vals {
71+
my ($key, $val, $prefix, $ikey) = @_;
72+
73+
my $ts = time;
74+
75+
if (not defined $val) {
76+
return '';
77+
}
78+
79+
my @parts = split /\|/, $val;
80+
if (scalar(@parts) != 3) {
81+
return '';
82+
}
83+
my ($u_prefix, $u_b64, $u_sig) = @parts;
84+
85+
my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key);
86+
87+
if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) {
88+
return '';
89+
}
90+
91+
if ($u_prefix ne $prefix) {
92+
return '';
93+
}
94+
95+
my @cookie_parts = split /\|/, decode_base64($u_b64);
96+
if (scalar(@cookie_parts) != 3) {
97+
return '';
98+
}
99+
my ($user, $u_ikey, $exp) = @cookie_parts;
100+
101+
if ($u_ikey ne $ikey) {
102+
return '';
103+
}
104+
105+
if ($ts >= $exp) {
106+
return '';
107+
}
108+
109+
return $user;
110+
}
111+
112+
=pod
113+
Generate a signed request for Duo authentication.
114+
The returned value should be passed into the Duo.init() call!
115+
in the rendered web page used for Duo authentication.
116+
117+
Arguments:
118+
119+
ikey -- Duo integration key
120+
skey -- Duo secret key
121+
akey -- Application secret key
122+
username -- Primary-authenticated username
123+
=cut
124+
125+
sub sign_request {
126+
my ($ikey, $skey, $akey, $username) = @_;
127+
128+
if (not $username) {
129+
return $ERR_USER;
130+
}
131+
132+
if (index($username, '|') != -1) {
133+
return $ERR_USER;
134+
}
135+
136+
if (not $ikey or length $ikey != $IKEY_LEN) {
137+
return $ERR_IKEY;
138+
}
139+
140+
if (not $skey or length $skey != $SKEY_LEN) {
141+
return $ERR_SKEY;
142+
}
143+
144+
if (not $akey or length $akey < $AKEY_LEN) {
145+
return $ERR_AKEY;
146+
}
147+
148+
my $vals = [ $username, $ikey ];
149+
150+
my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE);
151+
my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE);
152+
153+
if (not $duo_sig or not $app_sig) {
154+
return $ERR_UNKNOWN;
155+
}
156+
157+
return "$duo_sig:$app_sig";
158+
}
159+
160+
=pod
161+
162+
Validate the signed response returned from Duo.
163+
164+
Returns the username of the authenticated user, or '' (empty
165+
string) if secondary authentication was denied.
166+
167+
Arguments:
168+
169+
ikey -- Duo integration key
170+
skey -- Duo secret key
171+
akey -- Application secret key
172+
sig_response -- The signed response POST'ed to the server
173+
174+
=cut
175+
176+
sub verify_response {
177+
my ($ikey, $skey, $akey, $sig_response) = @_;
178+
179+
if (not defined $sig_response) {
180+
return '';
181+
}
182+
183+
my ($auth_sig, $app_sig) = split /:/, $sig_response;
184+
my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey);
185+
my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey);
186+
187+
if ($auth_user ne $app_user) {
188+
return '';
189+
}
190+
191+
return $auth_user;
192+
}
193+
1;

Bugzilla/Install/Requirements.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ use constant FEATURE_FILES => (
448448
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
449449
updates => ['Bugzilla/Update.pm'],
450450
memcached => ['Bugzilla/Memcache.pm'],
451-
mfa => ['Bugzilla/MFA/TOTP.pm'],
451+
mfa => ['Bugzilla/MFA/*.pm'],
452452
);
453453

454454
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff

Bugzilla/MFA.pm

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,38 @@ use strict;
1010

1111
use Bugzilla::RNG qw( irand );
1212
use Bugzilla::Token qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token );
13-
use Bugzilla::Util qw( trick_taint);
13+
use Bugzilla::Util qw( trick_taint );
1414

1515
sub new {
1616
my ($class, $user) = @_;
1717
return bless({ user => $user }, $class);
1818
}
1919

20+
sub new_from {
21+
my ($class, $user, $mfa) = @_;
22+
$mfa //= '';
23+
if ($mfa eq 'TOTP') {
24+
require Bugzilla::MFA::TOTP;
25+
return Bugzilla::MFA::TOTP->new($user);
26+
}
27+
elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) {
28+
require Bugzilla::MFA::Duo;
29+
return Bugzilla::MFA::Duo->new($user);
30+
}
31+
else {
32+
require Bugzilla::MFA::Dummy;
33+
return Bugzilla::MFA::Dummy->new($user);
34+
}
35+
}
36+
2037
# abstract methods
2138

22-
# api call, returns required data to user-prefs enrollment page
39+
# called during enrollment
2340
sub enroll {}
2441

42+
# api call, returns required data to user-prefs enrollment page
43+
sub enroll_api {}
44+
2545
# called after the user has confirmed enrollment
2646
sub enrolled {}
2747

@@ -31,6 +51,10 @@ sub prompt {}
3151
# throws errors if code is invalid
3252
sub check {}
3353

54+
# if true verifcation can happen inline (during enrollment/pref changes)
55+
# if false then the mfa provider requires an intermediate verification page
56+
sub can_verify_inline { 0 }
57+
3458
# verification
3559

3660
sub verify_prompt {

Bugzilla/MFA/Dummy.pm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::MFA::Dummy;
9+
use strict;
10+
use parent 'Bugzilla::MFA';
11+
12+
# if a user is configured to use a disabled or invalid mfa provider, we return
13+
# this dummy provider.
14+
#
15+
# it provides no 2fa protection at all, but prevents crashing.
16+
17+
sub prompt {
18+
my ($self, $vars) = @_;
19+
my $template = Bugzilla->template;
20+
21+
print Bugzilla->cgi->header();
22+
$template->process('mfa/dummy/verify.html.tmpl', $vars)
23+
|| ThrowTemplateError($template->error());
24+
}
25+
26+
1;

Bugzilla/MFA/Duo.pm

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::MFA::Duo;
9+
use strict;
10+
use parent 'Bugzilla::MFA';
11+
12+
use Bugzilla::DuoWeb;
13+
use Bugzilla::Error;
14+
15+
sub can_verify_inline {
16+
return 0;
17+
}
18+
19+
sub enroll {
20+
my ($self, $params) = @_;
21+
22+
$self->property_set('user', $params->{username});
23+
}
24+
25+
sub prompt {
26+
my ($self, $vars) = @_;
27+
my $template = Bugzilla->template;
28+
29+
$vars->{sig_request} = Bugzilla::DuoWeb::sign_request(
30+
Bugzilla->params->{duo_ikey},
31+
Bugzilla->params->{duo_skey},
32+
Bugzilla->params->{duo_akey},
33+
$self->property_get('user'),
34+
);
35+
36+
print Bugzilla->cgi->header();
37+
$template->process('mfa/duo/verify.html.tmpl', $vars)
38+
|| ThrowTemplateError($template->error());
39+
}
40+
41+
sub check {
42+
my ($self, $params) = @_;
43+
44+
return if Bugzilla::DuoWeb::verify_response(
45+
Bugzilla->params->{duo_ikey},
46+
Bugzilla->params->{duo_skey},
47+
Bugzilla->params->{duo_akey},
48+
$params->{sig_response}
49+
);
50+
ThrowUserError('mfa_bad_code');
51+
}
52+
53+
1;

0 commit comments

Comments
 (0)