mfa: add duo_api_golang dependency to Godeps

This commit is contained in:
Bradley Girardeau 2015-07-28 14:08:17 -07:00
parent 112f98d86f
commit 36d60623b1
7 changed files with 1431 additions and 0 deletions

4
Godeps/Godeps.json generated
View file

@ -58,6 +58,10 @@
"Comment": "v2.0.0-18-gc904d70",
"Rev": "c904d7032a70da6551c43929f199244f6a45f4c1"
},
{
"ImportPath": "github.com/duosecurity/duo_api_golang",
"Rev": "16da9e74793f6d9b97b227a0696fe32bcdaecb42"
},
{
"ImportPath": "github.com/fatih/structs",
"Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f"

View file

@ -0,0 +1,25 @@
Copyright (c) 2015, Duo Security, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,14 @@
# Overview
**duo_client** - Demonstration client to call Duo API methods
with Go.
# Duo Auth API
The Duo Auth API provides a low-level API for adding strong two-factor
authentication to applications that cannot directly display rich web
content.
For more information see the Duo Auth API guide:
<http://www.duosecurity.com/docs/authapi>

View file

@ -0,0 +1,380 @@
package authapi
import(
"github.com/duosecurity/duo_api_golang"
"encoding/json"
"net/url"
"strconv"
)
type AuthApi struct {
api duoapi.DuoApi
}
// Build a new Duo Auth API object.
// api is a duoapi.DuoApi object used to make the Duo Rest API calls.
// Example: authapi.NewAuthApi(*duoapi.NewDuoApi(ikey,skey,host,userAgent,duoapi.SetTimeout(10*time.Second)))
func NewAuthApi(api duoapi.DuoApi) *AuthApi {
return &AuthApi{api: api}
}
// API calls will return a StatResult object. On success, Stat is 'OK'.
// On error, Stat is 'FAIL', and Code, Message, and Message_Detail
// contain error information.
type StatResult struct {
Stat string
Code *int32
Message *string
Message_Detail *string
}
// Return object for the 'Ping' API call.
type PingResult struct {
StatResult
Response struct {
Time int64
}
}
// Duo's Ping method. https://www.duosecurity.com/docs/authapi#/ping
// This is an unsigned Duo Rest API call which returns the Duo system's time.
// Use this method to determine whether your system time is in sync with Duo's.
func (api *AuthApi) Ping() (*PingResult, error) {
_, body, err := api.api.Call("GET", "/auth/v2/ping", nil, duoapi.UseTimeout)
if err != nil {
return nil, err
}
ret := &PingResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// Return object for the 'Check' API call.
type CheckResult struct {
StatResult
Response struct {
Time int64
}
}
// Call Duo's Check method. https://www.duosecurity.com/docs/authapi#/check
// Check is a signed Duo API call, which returns the Duo system's time.
// Use this method to determine whether your ikey, skey and host are correct,
// and whether your system time is in sync with Duo's.
func (api *AuthApi) Check() (*CheckResult, error) {
_, body, err := api.api.SignedCall("GET", "/auth/v2/check", nil, duoapi.UseTimeout)
if err != nil {
return nil, err
}
ret := &CheckResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// Return object for the 'Logo' API call.
type LogoResult struct {
StatResult
png *[]byte
}
// Duo's Logo method. https://www.duosecurity.com/docs/authapi#/logo
// If the API call is successful, the configured logo png is returned. Othwerwise,
// error information is returned in the LogoResult return value.
func (api *AuthApi) Logo() (*LogoResult, error) {
resp, body, err := api.api.SignedCall("GET", "/auth/v2/logo", nil, duoapi.UseTimeout)
if err != nil {
return nil, err
}
if resp.StatusCode == 200 {
ret := &LogoResult{StatResult:StatResult{Stat: "OK"},
png: &body}
return ret, nil
}
ret := &LogoResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// Optional parameter for the Enroll method.
func EnrollUsername(username string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("username", username)
}
}
// Optional parameter for the Enroll method.
func EnrollValidSeconds(secs uint64) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("valid_secs", strconv.FormatUint(secs, 10))
}
}
// Enroll return type.
type EnrollResult struct {
StatResult
Response struct {
Activation_Barcode string
Activation_Code string
Expiration int64
User_Id string
Username string
}
}
// Duo's Enroll method. https://www.duosecurity.com/docs/authapi#/enroll
// Use EnrollUsername() to include the optional username parameter.
// Use EnrollValidSeconds() to change the default validation time limit that the
// user has to complete enrollment.
func (api *AuthApi) Enroll(options ...func(*url.Values)) (*EnrollResult, error) {
opts := url.Values{}
for _, o := range options {
o(&opts)
}
_, body, err := api.api.SignedCall("POST", "/auth/v2/enroll", opts, duoapi.UseTimeout)
if err != nil {
return nil, err
}
ret := &EnrollResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// Response is "success", "invalid" or "waiting".
type EnrollStatusResult struct {
StatResult
Response string
}
// Duo's EnrollStatus method. https://www.duosecurity.com/docs/authapi#/enroll_status
// Return the status of an outstanding Enrollment.
func (api *AuthApi) EnrollStatus(userid string,
activationCode string) (*EnrollStatusResult, error) {
queryArgs := url.Values{}
queryArgs.Set("user_id", userid)
queryArgs.Set("activation_code", activationCode)
_, body, err := api.api.SignedCall("POST",
"/auth/v2/enroll_status",
queryArgs,
duoapi.UseTimeout)
if err != nil {
return nil, err
}
ret := &EnrollStatusResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// Preauth return type.
type PreauthResult struct {
StatResult
Response struct {
Result string
Status_Msg string
Enroll_Portal_Url string
Devices []struct {
Device string
Type string
Name string
Number string
Capabilities []string
}
}
}
func PreauthUserId(userid string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("user_id", userid)
}
}
func PreauthUsername(username string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("username", username)
}
}
func PreauthIpAddr(ip string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("ipaddr", ip)
}
}
func PreauthTrustedToken(trustedtoken string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("trusted_device_token", trustedtoken)
}
}
// Duo's Preauth method. https://www.duosecurity.com/docs/authapi#/preauth
// options Optional values to include in the preauth call.
// Use PreauthUserId to specify the user_id parameter.
// Use PreauthUsername to specify the username parameter. You must
// specify PreauthUserId or PreauthUsername, but not both.
// Use PreauthIpAddr to include the ipaddr parameter, the ip address
// of the client attempting authroization.
// Use PreauthTrustedToken to specify the trusted_device_token parameter.
func (api *AuthApi) Preauth(options ...func(*url.Values)) (*PreauthResult, error) {
opts := url.Values{}
for _, o := range options {
o(&opts)
}
_, body, err := api.api.SignedCall("POST", "/auth/v2/preauth", opts, duoapi.UseTimeout)
if err != nil {
return nil, err
}
ret := &PreauthResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
func AuthUserId(userid string) func(*url.Values) {
return func(opts *url.Values) {
opts.Set("user_id", userid)
}
}
func AuthUsername(username string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("username", username)
}
}
func AuthIpAddr(ip string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("ipaddr", ip)
}
}
func AuthAsync() func (*url.Values) {
return func(opts *url.Values) {
opts.Set("async", "1")
}
}
func AuthDevice(device string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("device", device)
}
}
func AuthType(type_ string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("type", type_)
}
}
func AuthDisplayUsername(username string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("display_username", username)
}
}
func AuthPushinfo(pushinfo string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("pushinfo", pushinfo)
}
}
func AuthPasscode(passcode string) func (*url.Values) {
return func(opts *url.Values) {
opts.Set("passcode", passcode)
}
}
// Auth return type.
type AuthResult struct {
StatResult
Response struct {
// Synchronous
Result string
Status string
Status_Msg string
// Asynchronous
Txid string
}
}
// Duo's Auth method. https://www.duosecurity.com/docs/authapi#/auth
// Factor must be one of 'auto', 'push', 'passcode', 'sms' or 'phone'.
// Use AuthUserId to specify the user_id.
// Use AuthUsername to speicy the username. You must specify either AuthUserId
// or AuthUsername, but not both.
// Use AuthIpAddr to include the client's IP address.
// Use AuthAsync to toggle whether the call blocks for the user's response or not.
// If used asynchronously, get the auth status with the AuthStatus method.
// When using factor 'push', use AuthDevice to specify the device ID to push to.
// When using factor 'push', use AuthType to display some extra auth text to the user.
// When using factor 'push', use AuthDisplayUsername to display some extra text
// to the user.
// When using factor 'push', use AuthPushInfo to include some URL-encoded key/value
// pairs to display to the user.
// When using factor 'passcode', use AuthPasscode to specify the passcode entered
// by the user.
// When using factor 'sms' or 'phone', use AuthDevice to specify which device
// should receive the SMS or phone call.
func (api *AuthApi) Auth(factor string, options ...func(*url.Values)) (*AuthResult, error) {
params := url.Values{}
for _, o := range options {
o(&params)
}
params.Set("factor", factor)
var apiOps []duoapi.DuoApiOption
if _, ok := params["async"]; ok == true {
apiOps = append(apiOps, duoapi.UseTimeout)
}
_, body, err := api.api.SignedCall("POST", "/auth/v2/auth", params, apiOps...)
if err != nil {
return nil, err
}
ret := &AuthResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}
// AuthStatus return type.
type AuthStatusResult struct {
StatResult
Response struct {
Result string
Status string
Status_Msg string
Trusted_Device_Token string
}
}
// Duo's auth_status method. https://www.duosecurity.com/docs/authapi#/auth_status
// When using the Auth call in async mode, use this method to retrieve the
// result of the authentication attempt.
// txid is returned by the Auth call.
func (api *AuthApi) AuthStatus(txid string) (*AuthStatusResult, error) {
opts := url.Values{}
opts.Set("txid", txid)
_, body, err := api.api.SignedCall("GET", "/auth/v2/auth_status", opts)
if err != nil {
return nil, err
}
ret := &AuthStatusResult{}
if err = json.Unmarshal(body, ret); err != nil {
return nil, err
}
return ret, nil
}

View file

@ -0,0 +1,497 @@
package authapi
import (
"testing"
"fmt"
"strings"
"net/http"
"net/http/httptest"
"time"
"github.com/duosecurity/duo_api_golang"
)
func buildAuthApi(url string) *AuthApi {
ikey := "eyekey"
skey := "esskey"
host := strings.Split(url, "//")[1]
userAgent := "GoTestClient"
return NewAuthApi(*duoapi.NewDuoApi(ikey,
skey,
host,
userAgent,
duoapi.SetTimeout(1*time.Second),
duoapi.SetInsecure()))
}
// Timeouts are set to 1 second. Take 15 seconds to respond and verify
// that the client times out.
func TestTimeout(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
time.Sleep(15*time.Second)
}))
duo := buildAuthApi(ts.URL)
start := time.Now()
_, err := duo.Ping()
duration := time.Since(start)
if duration.Seconds() > 2 {
t.Error("Timeout took %d seconds", duration.Seconds())
}
if err == nil {
t.Error("Expected timeout error.")
}
}
// Test a successful ping request / response.
func TestPing(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"time": 1357020061,
"unexpected_parameter" : "blah"
}
}`)
}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
result, err := duo.Ping()
if err != nil {
t.Error("Unexpected error from Ping call" + err.Error())
}
if result.Stat != "OK" {
t.Error("Expected OK, but got " + result.Stat)
}
if result.Response.Time != 1357020061 {
t.Errorf("Expected 1357020061, but got %d", result.Response.Time)
}
}
// Test a successful Check request / response.
func TestCheck(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"time": 1357020061
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
result, err := duo.Check()
if err != nil {
t.Error("Failed TestCheck: " + err.Error())
}
if result.Stat != "OK" {
t.Error("Expected OK, but got " + result.Stat)
}
if result.Response.Time != 1357020061 {
t.Errorf("Expected 1357020061, but got %d", result.Response.Time)
}
}
// Test a successful logo request / response.
func TestLogo(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write([]byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00" +
"\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00" +
"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" +
"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00" +
"\x00\x00\x00IEND\xaeB`\x82"))
}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
_, err := duo.Logo()
if err != nil {
t.Error("Failed TestCheck: " + err.Error())
}
}
// Test a failure logo reqeust / response.
func TestLogoError(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
// Return a 400, as if the logo was not found.
w.WriteHeader(400)
fmt.Fprintln(w, `
{
"stat": "FAIL",
"code": 40002,
"message": "Logo not found",
"message_detail": "Why u no have logo?"
}`)
}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
res, err := duo.Logo()
if err != nil {
t.Error("Failed TestCheck: " + err.Error())
}
if res.Stat != "FAIL" {
t.Error("Expected FAIL, but got " + res.Stat)
}
if res.Code == nil || *res.Code != 40002 {
t.Error("Unexpected response code.")
}
if res.Message == nil || *res.Message != "Logo not found" {
t.Error("Unexpected message.")
}
if res.Message_Detail == nil || *res.Message_Detail != "Why u no have logo?" {
t.Error("Unexpected message detail.")
}
}
// Test a successful enroll request / response.
func TestEnroll(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
if r.FormValue("username") != "49c6c3097adb386048c84354d82ea63d" {
t.Error("TestEnroll failed to set 'username' query parameter:" +
r.RequestURI)
}
if r.FormValue("valid_secs") != "10" {
t.Error("TestEnroll failed to set 'valid_secs' query parameter: " +
r.RequestURI)
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"activation_barcode": "https://api-eval.duosecurity.com/frame/qr?value=8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA",
"activation_code": "duo://8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA",
"expiration": 1357020061,
"user_id": "DU94SWSN4ADHHJHF2HXT",
"username": "49c6c3097adb386048c84354d82ea63d"
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
result, err := duo.Enroll(EnrollUsername("49c6c3097adb386048c84354d82ea63d"), EnrollValidSeconds(10))
if err != nil {
t.Error("Failed TestEnroll: " + err.Error())
}
if result.Stat != "OK" {
t.Error("Expected OK, but got " + result.Stat)
}
if result.Response.Activation_Barcode != "https://api-eval.duosecurity.com/frame/qr?value=8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA" {
t.Error("Unexpected activation_barcode: " + result.Response.Activation_Barcode)
}
if result.Response.Activation_Code != "duo://8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA" {
t.Error("Unexpected activation code: " + result.Response.Activation_Code)
}
if result.Response.Expiration != 1357020061 {
t.Errorf("Unexpected expiration time: %d", result.Response.Expiration)
}
if result.Response.User_Id != "DU94SWSN4ADHHJHF2HXT" {
t.Error("Unexpected user id: " + result.Response.User_Id)
}
if result.Response.Username != "49c6c3097adb386048c84354d82ea63d" {
t.Error("Unexpected username: " + result.Response.Username)
}
}
// Test a succesful enroll status request / response.
func TestEnrollStatus(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
if r.FormValue("user_id") != "49c6c3097adb386048c84354d82ea63d" {
t.Error("TestEnrollStatus failed to set 'user_id' query parameter:" +
r.RequestURI)
}
if r.FormValue("activation_code") != "10" {
t.Error("TestEnrollStatus failed to set 'activation_code' query parameter: " +
r.RequestURI)
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": "success"
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
result, err := duo.EnrollStatus("49c6c3097adb386048c84354d82ea63d", "10")
if err != nil {
t.Error("Failed TestEnrollStatus: " + err.Error())
}
if result.Stat != "OK" {
t.Error("Expected OK, but got " + result.Stat)
}
if result.Response != "success" {
t.Error("Unexpected response: " + result.Response)
}
}
// Test a successful preauth with user id. The client doesn't enforce api requirements,
// such as requiring only one of user id or username, but we'll cover the username
// in another test anyway.
func TestPreauthUserId(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
if r.FormValue("ipaddr") != "127.0.0.1" {
t.Error("TestPreauth failed to set 'ipaddr' query parameter:" +
r.RequestURI)
}
if r.FormValue("user_id") != "10" {
t.Error("TestEnrollStatus failed to set 'user_id' query parameter: " +
r.RequestURI)
}
if r.FormValue("trusted_device_token") != "l33t" {
t.Error("TestEnrollStatus failed to set 'trusted_device_token' query parameter: " +
r.RequestURI)
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"result": "auth",
"status_msg": "Account is active",
"devices": [
{
"device": "DPFZRS9FB0D46QFTM891",
"type": "phone",
"number": "XXX-XXX-0100",
"name": "",
"capabilities": [
"push",
"sms",
"phone"
]
},
{
"device": "DHEKH0JJIYC1LX3AZWO4",
"type": "token",
"name": "0"
}
]
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
res, err := duo.Preauth(PreauthUserId("10"), PreauthIpAddr("127.0.0.1"), PreauthTrustedToken("l33t"))
if err != nil {
t.Error("Failed TestPreauthUserId: " + err.Error())
}
if res.Stat != "OK" {
t.Error("Unexpected stat: " + res.Stat)
}
if res.Response.Result != "auth" {
t.Error("Unexpected response result: " + res.Response.Result)
}
if res.Response.Status_Msg != "Account is active" {
t.Error("Unexpected status message: " + res.Response.Status_Msg)
}
if len(res.Response.Devices) != 2 {
t.Errorf("Unexpected devices length: %d", len(res.Response.Devices))
}
if res.Response.Devices[0].Device != "DPFZRS9FB0D46QFTM891" {
t.Error("Unexpected [0] device name: " + res.Response.Devices[0].Device)
}
if res.Response.Devices[0].Type != "phone" {
t.Error("Unexpected [0] device type: " + res.Response.Devices[0].Type)
}
if res.Response.Devices[0].Number != "XXX-XXX-0100" {
t.Error("Unexpected [0] device number: " + res.Response.Devices[0].Number)
}
if res.Response.Devices[0].Name != "" {
t.Error("Unexpected [0] devices name :" + res.Response.Devices[0].Name)
}
if len(res.Response.Devices[0].Capabilities) != 3 {
t.Errorf("Unexpected [0] device capabilities length: %d", len(res.Response.Devices[0].Capabilities))
}
if res.Response.Devices[0].Capabilities[0] != "push" {
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[0])
}
if res.Response.Devices[0].Capabilities[1] != "sms" {
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[1])
}
if res.Response.Devices[0].Capabilities[2] != "phone" {
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[2])
}
if res.Response.Devices[1].Device != "DHEKH0JJIYC1LX3AZWO4" {
t.Error("Unexpected [1] device name: " + res.Response.Devices[1].Device)
}
if res.Response.Devices[1].Type != "token" {
t.Error("Unexpected [1] device type: " + res.Response.Devices[1].Type)
}
if res.Response.Devices[1].Name != "0" {
t.Error("Unexpected [1] devices name :" + res.Response.Devices[1].Name)
}
if len(res.Response.Devices[1].Capabilities) != 0 {
t.Errorf("Unexpected [1] device capabilities length: %d", len(res.Response.Devices[1].Capabilities))
}
}
// Test preauth enroll with username, and an enroll response.
func TestPreauthEnroll(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
if r.FormValue("username") != "10" {
t.Error("TestEnrollStatus failed to set 'username' query parameter: " +
r.RequestURI)
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"enroll_portal_url": "https://api-3945ef22.duosecurity.com/portal?48bac5d9393fb2c2",
"result": "enroll",
"status_msg": "Enroll an authentication device to proceed"
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
res, err := duo.Preauth(PreauthUsername("10"))
if err != nil {
t.Error("Failed TestPreauthEnroll: " + err.Error())
}
if res.Stat != "OK" {
t.Error("Unexpected stat: " + res.Stat)
}
if res.Response.Enroll_Portal_Url != "https://api-3945ef22.duosecurity.com/portal?48bac5d9393fb2c2" {
t.Error("Unexpected enroll portal URL: " + res.Response.Enroll_Portal_Url)
}
if res.Response.Result != "enroll" {
t.Error("Unexpected response result: " + res.Response.Result)
}
if res.Response.Status_Msg != "Enroll an authentication device to proceed" {
t.Error("Unexpected status msg: " + res.Response.Status_Msg)
}
}
// Test an authentication request / response. This won't work against the Duo
// server, because the request parameters included are illegal. But we can
// verify that the go code sets the query parameters correctly.
func TestAuth(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
expected := map[string]string {
"username" : "username value",
"user_id" : "user_id value",
"factor" : "auto",
"ipaddr" : "40.40.40.10",
"async" : "1",
"device" : "primary",
"type" : "request",
"display_username" : "display username",
}
for key, value := range expected {
if r.FormValue(key) != value {
t.Errorf("TestAuth failed to set '%s' query parameter: " +
r.RequestURI, key)
}
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"result": "allow",
"status": "allow",
"status_msg": "Success. Logging you in..."
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
res, err := duo.Auth("auto",
AuthUserId("user_id value"),
AuthUsername("username value"),
AuthIpAddr("40.40.40.10"),
AuthAsync(),
AuthDevice("primary"),
AuthType("request"),
AuthDisplayUsername("display username"),
)
if err != nil {
t.Error("Failed TestAuth: " + err.Error())
}
if res.Stat != "OK" {
t.Error("Unexpected stat: " + res.Stat)
}
if res.Response.Result != "allow" {
t.Error("Unexpected response result: " + res.Response.Result)
}
if res.Response.Status != "allow" {
t.Error("Unexpected response status: " + res.Response.Status)
}
if res.Response.Status_Msg != "Success. Logging you in..." {
t.Error("Unexpected response status msg: " + res.Response.Status_Msg)
}
}
// Test AuthStatus request / response.
func TestAuthStatus(t *testing.T) {
ts := httptest.NewTLSServer(
http.HandlerFunc(
func (w http.ResponseWriter, r *http.Request) {
expected := map[string]string {
"txid" : "4",
}
for key, value := range expected {
if r.FormValue(key) != value {
t.Errorf("TestAuthStatus failed to set '%s' query parameter: " +
r.RequestURI, key)
}
}
fmt.Fprintln(w, `
{
"stat": "OK",
"response": {
"result": "waiting",
"status": "pushed",
"status_msg": "Pushed a login request to your phone..."
}
}`)}))
defer ts.Close()
duo := buildAuthApi(ts.URL)
res, err := duo.AuthStatus("4")
if err != nil {
t.Error("Failed TestAuthStatus: " + err.Error())
}
if res.Stat != "OK" {
t.Error("Unexpected stat: " + res.Stat)
}
if res.Response.Result != "waiting" {
t.Error("Unexpected response result: " + res.Response.Result)
}
if res.Response.Status != "pushed" {
t.Error("Unexpected response status: " + res.Response.Status)
}
if res.Response.Status_Msg != "Pushed a login request to your phone..." {
t.Error("Unexpected response status msg: " + res.Response.Status_Msg)
}
}

View file

@ -0,0 +1,156 @@
package duoapi
import (
"testing"
"net/url"
"strings"
)
func TestCanonicalize(t *testing.T) {
values := url.Values{}
values.Set("username", "H ell?o")
values.Set("password", "H-._~i")
values.Add("password", "A(!'*)")
params_str := canonicalize("post",
"API-XXX.duosecurity.COM",
"/auth/v2/ping",
values,
"5")
params := strings.Split(params_str, "\n")
if len(params) != 5 {
t.Error("Expected 5 parameters, but got " + string(len(params)))
}
if params[1] != string("POST") {
t.Error("Expected POST, but got " + params[1])
}
if params[2] != string("api-xxx.duosecurity.com") {
t.Error("Expected api-xxx.duosecurity.com, but got " + params[2])
}
if params[3] != string("/auth/v2/ping") {
t.Error("Expected /auth/v2/ping, but got " + params[3])
}
if params[4] != string("password=A%28%21%27%2A%29&password=H-._~i&username=H%20ell%3Fo") {
t.Error("Expected sorted escaped params, but got " + params[4])
}
}
func encodeAndValidate(t *testing.T, input url.Values, output string) {
values := url.Values{}
for key, val := range input {
values.Set(key, val[0])
}
params_str := canonicalize("post",
"API-XXX.duosecurity.com",
"/auth/v2/ping",
values,
"5")
params := strings.Split(params_str, "\n")
if params[4] != output {
t.Error("Mismatch\n" + output + "\n" + params[4])
}
}
func TestSimple(t *testing.T) {
values := url.Values{}
values.Set("realname", "First Last")
values.Set("username", "root")
encodeAndValidate(t, values, "realname=First%20Last&username=root")
}
func TestZero(t *testing.T) {
values := url.Values{}
encodeAndValidate(t, values, "")
}
func TestOne(t *testing.T) {
values := url.Values{}
values.Set("realname", "First Last")
encodeAndValidate(t, values, "realname=First%20Last")
}
func TestPrintableAsciiCharaceters(t *testing.T) {
values := url.Values{}
values.Set("digits", "0123456789")
values.Set("letters", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
values.Set("punctuation", "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
values.Set("whitespace", "\t\n\x0b\x0c\r ")
encodeAndValidate(t, values, "digits=0123456789&letters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&punctuation=%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~&whitespace=%09%0A%0B%0C%0D%20")
}
func TestSortOrderWithCommonPrefix(t *testing.T) {
values := url.Values{}
values.Set("foo", "1")
values.Set("foo_bar", "2")
encodeAndValidate(t, values, "foo=1&foo_bar=2")
}
func TestUnicodeFuzzValues(t *testing.T) {
values := url.Values{}
values.Set("bar", "⠕ꪣ㟏䮷㛩찅暎腢슽ꇱ")
values.Set("baz", "ෳ蒽噩馅뢤갺篧潩鍊뤜")
values.Set("foo", "퓎훖礸僀訠輕ﴋ耤岳왕")
values.Set("qux", "讗졆-芎茚쳊ꋔ谾뢲馾")
encodeAndValidate(t, values, "bar=%E2%A0%95%EA%AA%A3%E3%9F%8F%E4%AE%B7%E3%9B%A9%EC%B0%85%E6%9A%8E%E8%85%A2%EC%8A%BD%EA%87%B1&baz=%E0%B7%B3%E8%92%BD%E5%99%A9%E9%A6%85%EB%A2%A4%EA%B0%BA%E7%AF%A7%E6%BD%A9%E9%8D%8A%EB%A4%9C&foo=%ED%93%8E%ED%9B%96%E7%A4%B8%E5%83%80%E8%A8%A0%E8%BC%95%EF%B4%8B%E8%80%A4%E5%B2%B3%EC%99%95&qux=%E8%AE%97%EC%A1%86-%E8%8A%8E%E8%8C%9A%EC%B3%8A%EA%8B%94%E8%B0%BE%EB%A2%B2%E9%A6%BE")
}
func TestUnicodeFuzzKeysAndValues(t *testing.T) {
values := url.Values{}
values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰",
"ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐")
values.Set("瑉繋쳻姿﹟获귌逌쿑砓",
"趷倢鋓䋯⁽蜰곾嘗ॆ丰")
values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ",
"៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳")
values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头",
"䆪붃萋☕㹮攭ꢵ핫U")
encodeAndValidate(t, values, "%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU")
}
func TestSign(t *testing.T) {
values := url.Values{}
values.Set("realname", "First Last")
values.Set("username", "root")
res := sign("DIWJ8X6AEYOR5OMC6TQ1",
"Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep",
"POST",
"api-XXXXXXXX.duosecurity.com",
"/accounts/v1/account/list",
"Tue, 21 Aug 2012 17:29:18 -0000",
values)
if res != "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5Nzgx" +
"YjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==" {
t.Error("Signature did not produce output documented at " +
"https://www.duosecurity.com/docs/authapi :(")
}
}
func TestV2Canonicalize(t *testing.T) {
values := url.Values{}
values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰",
"ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐")
values.Set("瑉繋쳻姿﹟获귌逌쿑砓",
"趷倢鋓䋯⁽蜰곾嘗ॆ丰")
values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ",
"៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳")
values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头",
"䆪붃萋☕㹮攭ꢵ핫U")
canon := canonicalize(
"PoSt",
"foO.BAr52.cOm",
"/Foo/BaR2/qux",
values,
"Fri, 07 Dec 2012 17:18:00 -0000")
expected := "Fri, 07 Dec 2012 17:18:00 -0000\nPOST\nfoo.bar52.com\n/Foo/BaR2/qux\n%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU"
if canon != expected {
t.Error("Mismatch!\n" + expected + "\n" + canon)
}
}
func TestNewDuo(t *testing.T) {
duo := NewDuoApi("ABC", "123", "api-XXXXXXX.duosecurity.com", "go-client")
if duo == nil {
t.Fatal("Failed to create a new Duo Api")
}
}

View file

@ -0,0 +1,355 @@
package duoapi
import (
"strings"
"net/url"
"sort"
"crypto/hmac"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/base64"
"net/http"
"time"
"io/ioutil"
)
var spaceReplacer *strings.Replacer = strings.NewReplacer("+", "%20")
func canonParams(params url.Values) string {
// Values must be in sorted order
for key, val := range params {
sort.Strings(val)
params[key] = val
}
// Encode will place Keys in sorted order
ordered_params := params.Encode()
// Encoder turns spaces into +, but we need %XX escaping
return spaceReplacer.Replace(ordered_params)
}
func canonicalize(method string,
host string,
uri string,
params url.Values,
date string) string {
var canon [5]string
canon[0] = date
canon[1] = strings.ToUpper(method)
canon[2] = strings.ToLower(host)
canon[3] = uri
canon[4] = canonParams(params)
return strings.Join(canon[:], "\n")
}
func sign(ikey string,
skey string,
method string,
host string,
uri string,
date string,
params url.Values) string {
canon := canonicalize(method, host, uri, params, date)
mac := hmac.New(sha1.New, []byte(skey))
mac.Write([]byte(canon))
sig := hex.EncodeToString(mac.Sum(nil))
auth := ikey + ":" + sig
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}
type DuoApi struct {
ikey string
skey string
host string
userAgent string
apiClient *http.Client
authClient *http.Client
}
type apiOptions struct {
timeout time.Duration
insecure bool
}
// Optional parameter for NewDuoApi, used to configure timeouts on API calls.
func SetTimeout(timeout time.Duration) func(*apiOptions) {
return func(opts *apiOptions) {
opts.timeout = timeout
return
}
}
// Optional parameter for testing only. Bypasses all TLS certificate validation.
func SetInsecure() func(*apiOptions) {
return func(opts *apiOptions) {
opts.insecure = true
}
}
// Build an return a DuoApi struct.
// ikey is your Duo integration key
// skey is your Duo integration secret key
// host is your Duo host
// userAgent allows you to specify the user agent string used when making
// the web request to Duo.
// options are optional parameters. Use SetTimeout() to specify a timeout value
// for Rest API calls.
//
// Example: duoapi.NewDuoApi(ikey,skey,host,userAgent,duoapi.SetTimeout(10*time.Second))
func NewDuoApi(ikey string,
skey string,
host string,
userAgent string,
options ...func(*apiOptions)) (*DuoApi) {
opts := apiOptions{}
for _, o := range options {
o(&opts)
}
// Certificate pinning
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM([]byte(duoPinnedCert))
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
InsecureSkipVerify: opts.insecure,
},
}
return &DuoApi{
ikey: ikey,
skey: skey,
host: host,
userAgent: userAgent,
apiClient: &http.Client{
Timeout: opts.timeout,
Transport: tr,
},
authClient: &http.Client{
Transport: tr,
},
}
}
type requestOptions struct {
timeout bool
}
type DuoApiOption func(*requestOptions)
// Pass to Request or SignedRequest to configure a timeout on the request
func UseTimeout(opts *requestOptions) {
opts.timeout = true
}
func (duoapi *DuoApi) buildOptions(options ...DuoApiOption) (*requestOptions) {
opts := &requestOptions{}
for _, o := range options {
o(opts)
}
return opts
}
// Make an unsigned Duo Rest API call. See Duo's online documentation
// for the available REST API's.
// method is POST or GET
// uri is the URI of the Duo Rest call
// params HTTP query parameters to include in the call.
// options Optional parameters. Use UseTimeout to toggle whether the
// Duo Rest API call should timeout or not.
//
// Example: duo.Call("GET", "/auth/v2/ping", nil, duoapi.UseTimeout)
func (duoapi *DuoApi) Call(method string,
uri string,
params url.Values,
options ...DuoApiOption) (*http.Response, []byte, error) {
opts := duoapi.buildOptions(options...)
client := duoapi.authClient
if opts.timeout {
client = duoapi.apiClient
}
url := url.URL{
Scheme: "https",
Host: duoapi.host,
Path: uri,
RawQuery: params.Encode(),
}
request, err := http.NewRequest(method, url.String(), nil)
if err != nil {
return nil, nil, err
}
resp, err := client.Do(request)
var body []byte
if err == nil {
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
return resp, body, err
}
// Make a signed Duo Rest API call. See Duo's online documentation
// for the available REST API's.
// method is POST or GET
// uri is the URI of the Duo Rest call
// params HTTP query parameters to include in the call.
// options Optional parameters. Use UseTimeout to toggle whether the
// Duo Rest API call should timeout or not.
//
// Example: duo.SignedCall("GET", "/auth/v2/check", nil, duoapi.UseTimeout)
func (duoapi *DuoApi) SignedCall(method string,
uri string,
params url.Values,
options ...DuoApiOption) (*http.Response, []byte, error) {
opts := duoapi.buildOptions(options...)
now := time.Now().UTC().Format(time.RFC1123Z)
auth_sig := sign(duoapi.ikey, duoapi.skey, method, duoapi.host, uri, now, params)
url := url.URL{
Scheme: "https",
Host: duoapi.host,
Path: uri,
RawQuery: params.Encode(),
}
request, err := http.NewRequest(method, url.String(), nil)
if err != nil {
return nil, nil, err
}
request.Header.Set("Authorization", auth_sig)
request.Header.Set("Date", now)
client := duoapi.authClient
if opts.timeout {
client = duoapi.apiClient
}
resp, err := client.Do(request)
var body []byte
if err == nil {
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
}
return resp, body, err
}
const duoPinnedCert string = "subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\n" +
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
"d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\n" +
"b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\n" +
"EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\n" +
"cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\n" +
"MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\n" +
"JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\n" +
"mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\n" +
"wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\n" +
"VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\n" +
"AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\n" +
"AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" +
"BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\n" +
"pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\n" +
"dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\n" +
"fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\n" +
"NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\n" +
"H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\n" +
"+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n" +
"-----END CERTIFICATE-----\n" +
"\n" +
"subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" +
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" +
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" +
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" +
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" +
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" +
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" +
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" +
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" +
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" +
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" +
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" +
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" +
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" +
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" +
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" +
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" +
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" +
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" +
"-----END CERTIFICATE-----\n" +
"\n" +
"subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n" +
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" +
"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n" +
"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n" +
"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n" +
"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n" +
"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n" +
"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" +
"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n" +
"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n" +
"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n" +
"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n" +
"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n" +
"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n" +
"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n" +
"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n" +
"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" +
"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n" +
"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n" +
"+OkuE6N36B9K\n" +
"-----END CERTIFICATE-----\n" +
"\n" +
"subject= /C=US/O=SecureTrust Corporation/CN=SecureTrust CA\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\n" +
"MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\n" +
"FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\n" +
"MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\n" +
"cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\n" +
"AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\n" +
"Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\n" +
"0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\n" +
"wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n" +
"7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\n" +
"8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\n" +
"BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n" +
"/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\n" +
"JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\n" +
"NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\n" +
"6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\n" +
"3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\n" +
"D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\n" +
"CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\n" +
"3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n" +
"-----END CERTIFICATE-----\n" +
"\n" +
"subject= /C=US/O=SecureTrust Corporation/CN=Secure Global CA\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\n" +
"MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\n" +
"GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\n" +
"MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\n" +
"Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\n" +
"SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\n" +
"iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\n" +
"/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\n" +
"jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\n" +
"HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\n" +
"sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\n" +
"gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\n" +
"MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\n" +
"KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\n" +
"AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\n" +
"URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\n" +
"H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\n" +
"I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\n" +
"iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\n" +
"f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n" +
"-----END CERTIFICATE-----\n"