duo_api-1.5.1/0000755000004100000410000000000015075140372013172 5ustar www-datawww-dataduo_api-1.5.1/lib/0000755000004100000410000000000015075140372013740 5ustar www-datawww-dataduo_api-1.5.1/lib/duo_api/0000755000004100000410000000000015075140372015360 5ustar www-datawww-dataduo_api-1.5.1/lib/duo_api/accounts.rb0000644000004100000410000000376415075140372017536 0ustar www-datawww-data# frozen_string_literal: true require_relative 'api_client' require_relative 'api_helpers' require_relative 'admin' class DuoApi ## # Duo Accounts API (https://duo.com/docs/accountsapi) # class Accounts < DuoApi ## # Accounts API # def get_child_accounts post('/accounts/v1/account/list')[:response] end def create_child_account(name:) params = { name: name } post('/accounts/v1/account/create', params)[:response] end def delete_child_account(account_id:) params = { account_id: account_id } post('/accounts/v1/account/delete', params)[:response] end ## # Child Account Admin API Wrapper # def admin_api(child_account_id:) child_account = get_child_accounts.select{ |a| a[:account_id] == child_account_id }.first raise(ChildAccountError, "Child account #{child_account_id} not found") unless child_account client = DuoApi::Admin.new(@ikey, @skey, child_account[:api_hostname], @proxy_str, ca_file: @ca_file, default_params: { account_id: child_account_id }) # Additional Child Account Admin API Methods # # Note: # - These are enabled by support request only # - They can only be called by the DuoApi::Admin instance returned by this wrapper method # - account_id is required for each of these, but it is provided by the client default_params # client.instance_eval do def get_edition get('/admin/v1/billing/edition')[:response] end def set_edition(edition:) params = { edition: edition } post('/admin/v1/billing/edition', params)[:response] end def get_telephony_credits get('/admin/v1/billing/telephony_credits')[:response] end def set_telephony_credits(credits:) params = { credits: credits } post('/admin/v1/billing/telephony_credits', params)[:response] end end client end end end duo_api-1.5.1/lib/duo_api/admin.rb0000644000004100000410000007205515075140372017006 0ustar www-datawww-data# frozen_string_literal: true require_relative 'api_client' require_relative 'api_helpers' class DuoApi ## # Duo Admin API (https://duo.com/docs/adminapi) # class Admin < DuoApi ## # Users # def get_users(**optional_params) # optional_params: username, email, user_id_list, username_list optional_params.tap do |p| p[:user_id_list] = json_serialized_array(p[:user_id_list]) if p[:user_id_list] p[:username_list] = json_serialized_array(p[:username_list]) if p[:username_list] end get_all('/admin/v1/users', optional_params)[:response] end def create_user(username:, **optional_params) # optional_params: alias1, alias2, alias3, alias4, aliases, realname, email, # enable_auto_prompt, status, notes, firstname, lastname optional_params.tap do |p| p[:aliases] = serialized_aliases(p[:aliases]) if p[:aliases] end params = optional_params.merge({ username: username }) post('/admin/v1/users', params)[:response] end def bulk_create_users(users:) # Each user hash in users array requires :username and supports the following # optional keys: realname, emaiml, status, notes, firstname, lastname params = { users: json_serialized_array(users) } post('/admin/v1/users/bulk_create', params)[:response] end def bulk_restore_users(user_id_list:) params = { user_id_list: json_serialized_array(user_id_list) } post('/admin/v1/users/bulk_restore', params)[:response] end def bulk_trash_users(user_id_list:) params = { user_id_list: json_serialized_array(user_id_list) } post('/admin/v1/users/bulk_send_to_trash', params)[:response] end def get_user(user_id:) get("/admin/v1/users/#{user_id}")[:response] end def update_user(user_id:, **optional_params) # optional_params: alias1, alias2, alias3, alias4, aliases, realname, email, # enable_auto_prompt, status, notes, firstname, lastname, # username optional_params.tap do |p| p[:aliases] = serialized_aliases(p[:aliases]) if p[:aliases] end post("/admin/v1/users/#{user_id}", optional_params)[:response] end def delete_user(user_id:) delete("/admin/v1/users/#{user_id}")[:response] end def enroll_user(username:, email:, **optional_params) # optional_params: valid_secs params = optional_params.merge({ username: username, email: email }) post('/admin/v1/users/enroll', params)[:response] end def create_user_bypass_codes(user_id:, **optional_params) # optional_params: count, codes, preserve_existing, reuse_count, valid_secs optional_params.tap do |p| p[:codes] = csv_serialized_array(p[:codes]) if p[:codes] end post("/admin/v1/users/#{user_id}/bypass_codes", optional_params)[:response] end def get_user_bypass_codes(user_id:) get_all("/admin/v1/users/#{user_id}/bypass_codes")[:response] end def get_user_groups(user_id:) get_all("/admin/v1/users/#{user_id}/groups")[:response] end def add_user_group(user_id:, group_id:) params = { group_id: group_id } post("/admin/v1/users/#{user_id}/groups", params)[:response] end def remove_user_group(user_id:, group_id:) delete("/admin/v1/users/#{user_id}/groups/#{group_id}")[:response] end def get_user_phones(user_id:) get_all("/admin/v1/users/#{user_id}/phones")[:response] end def add_user_phone(user_id:, phone_id:) params = { phone_id: phone_id } post("/admin/v1/users/#{user_id}/phones", params)[:response] end def remove_user_phone(user_id:, phone_id:) delete("/admin/v1/users/#{user_id}/phones/#{phone_id}")[:response] end def get_user_hardware_tokens(user_id:) get_all("/admin/v1/users/#{user_id}/tokens")[:response] end def add_user_hardware_token(user_id:, token_id:) params = { token_id: token_id } post("/admin/v1/users/#{user_id}/tokens", params)[:response] end def remove_user_hardware_token(user_id:, token_id:) delete("/admin/v1/users/#{user_id}/tokens/#{token_id}")[:response] end def get_user_webauthn_credentials(user_id:) get_all("/admin/v1/users/#{user_id}/webauthncredentials")[:response] end def get_user_desktop_authenticators(user_id:) get_all("/admin/v1/users/#{user_id}/desktopauthenticators")[:response] end def sync_user(username:, directory_key:) params = { username: username } post("/admin/v1/users/directorysync/#{directory_key}/syncuser", params)[:response] end def send_verification_push(user_id:, phone_id:) params = { phone_id: phone_id } post("/admin/v1/users/#{user_id}/send_verification_push", params)[:response] end def get_verification_push_response(user_id:, push_id:) params = { push_id: push_id } get("/admin/v1/users/#{user_id}/verification_push_response", params)[:response] end ## # Bulk Operations # def bulk_operations(operations:) # Each hash in user_operations array requires :method, :path, and :body # Each :body has the same parameter requirements as the individual operation # Supported operations: # Create User: POST /admin/v1/users # Modify User: POST /admin/v1/users/[user_id] # Delete User: DELETE /admin/v1/users/[user_id] # Add User Group: POST /admin/v1/users/[user_id]/groups # Remove User Group: POST /admin/v1/users/[user_id]/groups/[group_id] operations.each{ |o| o[:body][:aliases] = serialized_aliases(o[:body][:aliases]) if o[:body][:aliases] } params = { operations: json_serialized_array(operations) } post('/admin/v1/bulk', params)[:response] end ## # Groups # def get_groups(**optional_params) # optional_params: group_id_list get_all('/admin/v1/groups', optional_params)[:response] end def create_group(name:, **optional_params) # optional_params: desc, status params = optional_params.merge({ name: name }) post('/admin/v1/groups', params)[:response] end def get_group(group_id:) get("/admin/v2/groups/#{group_id}")[:response] end def get_group_users(group_id:) get_all("/admin/v2/groups/#{group_id}/users")[:response] end def update_group(group_id:, **optional_params) # optional_params: desc, status, name post("/admin/v1/groups/#{group_id}", optional_params)[:response] end def delete_group(group_id:) delete("/admin/v1/groups/#{group_id}")[:response] end ## # Phones # def get_phones(**optional_params) # optional_params: number, extension get_all('/admin/v1/phones', optional_params)[:response] end def create_phone(**optional_params) # optional_params: number, name, extension, type, platform, predelay, postdelay post('/admin/v1/phones', optional_params)[:response] end def get_phone(phone_id:) get("/admin/v1/phones/#{phone_id}")[:response] end def update_phone(phone_id:, **optional_params) # optional_params: number, name, extension, type, platform, predelay, postdelay post("/admin/v1/phones/#{phone_id}", optional_params)[:response] end def delete_phone(phone_id:) delete("/admin/v1/phones/#{phone_id}")[:response] end def create_activation_url(phone_id:, **optional_params) # optional_params: valid_secs, install post("/admin/v1/phones/#{phone_id}/activation_url", optional_params)[:response] end def send_sms_activation(phone_id:, **optional_params) # optional_params: valid_secs, install, installation_msg, activation_msg post("/admin/v1/phones/#{phone_id}/send_sms_activation", optional_params)[:response] end def send_sms_installation(phone_id:, **optional_params) # optional_params: installation_msg post("/admin/v1/phones/#{phone_id}/send_sms_installation", optional_params)[:response] end def send_sms_passcodes(phone_id:) post("/admin/v1/phones/#{phone_id}/send_sms_passcodes")[:response] end ## # Tokens # def get_tokens(**optional_params) # optional_params: type, serial get_all('/admin/v1/tokens', optional_params)[:response] end def create_token(type:, serial:, **optional_params) # optional_params: secret, counter, private_id, aes_key params = optional_params.merge({ type: type, serial: serial }) post('/admin/v1/tokens', params)[:response] end def get_token(token_id:) get("/admin/v1/tokens/#{token_id}")[:response] end def resync_token(token_id:, code1:, code2:, code3:) params = { code1: code1, code2: code2, code3: code3 } post("/admin/v1/tokens/#{token_id}/resync", params)[:response] end def delete_token(token_id:) delete("/admin/v1/tokens/#{token_id}")[:response] end ## # WebAuthn Credentials # def get_webauthncredentials get_all('/admin/v1/webauthncredentials')[:response] end def get_webauthncredential(webauthnkey:) get("/admin/v1/webauthncredentials/#{webauthnkey}")[:response] end def delete_webauthncredential(webauthnkey:) delete("/admin/v1/webauthncredentials/#{webauthnkey}")[:response] end ## # Desktop Authenticators # def get_desktop_authenticators get_all('/admin/v1/desktop_authenticators')[:response] end def get_desktop_authenticator(dakey:) get("/admin/v1/desktop_authenticators/#{dakey}")[:response] end def delete_desktop_authenticator(dakey:) delete("/admin/v1/desktop_authenticators/#{dakey}")[:response] end def get_shared_desktop_authenticators get_all('/admin/v1/desktop_authenticators/shared_device_auth')[:response] end def get_shared_desktop_authenticator(shared_device_key:) get("/admin/v1/desktop_authenticators/shared_device_auth/#{shared_device_key}")[:response] end def create_shared_desktop_authenticator(group_id_list:, trusted_endpoint_integration_id_list:, **optional_params) # optional_params: active, name params = optional_params.merge({ group_id_list: group_id_list, trusted_endpoint_integration_id_list: trusted_endpoint_integration_id_list }) post('/admin/v1/desktop_authenticators/shared_device_auth', params)[:response] end def update_shared_desktop_authenticator(shared_device_key:, **optional_params) # optional_params: active, name, group_id_list, trusted_endpoint_integration_id_list put("/admin/v1/desktop_authenticators/shared_device_auth/#{shared_device_key}", optional_params)[:response] end def delete_shared_desktop_authenticator(shared_device_key:) delete("/admin/v1/desktop_authenticators/shared_device_auth/#{shared_device_key}")[:response] end ## # Bypass Codes # def get_bypass_codes get_all('/admin/v1/bypass_codes')[:response] end def get_bypass_code(bypass_code_id:) get("/admin/v1/bypass_codes/#{bypass_code_id}")[:response] end def delete_bypass_code(bypass_code_id:) delete("/admin/v1/bypass_codes/#{bypass_code_id}")[:response] end ## # Integrations # def get_integrations get_all('/admin/v3/integrations')[:response] end def create_integration(name:, type:, **optional_params) # optional_params: adminapi_admins, adminapi_admins_read, adminapi_allow_to_set_permissions, # adminapi_info, adminapi_integrations, adminapi_read_log, # adminapi_read_resource, adminapi_settings, adminapi_write_resource, # enroll_policy, greeting, groups_allowed, ip_whitelist, # ip_whitelist_enroll_policy, networks_for_api_access, notes, # trusted_device_days, self_service_allowed, sso, username_normalization_policy # # sso params: https://duo.com/docs/adminapi#sso-parameters params = optional_params.merge({ name: name, type: type }) post('/admin/v3/integrations', params)[:response] end def get_integration(integration_key:) get("/admin/v3/integrations/#{integration_key}")[:response] end def update_integration(integration_key:, **optional_params) # optional_params: adminapi_admins, adminapi_admins_read, adminapi_allow_to_set_permissions, # adminapi_info, adminapi_integrations, adminapi_read_log, # adminapi_read_resource, adminapi_settings, adminapi_write_resource, # enroll_policy, greeting, groups_allowed, ip_whitelist, # ip_whitelist_enroll_policy, networks_for_api_access, notes, # trusted_device_days, self_service_allowed, sso, username_normalization_policy, # name, policy_key, prompt_v4_enabled, reset_secret_key # # sso params: https://duo.com/docs/adminapi#sso-parameters post("/admin/v3/integrations/#{integration_key}", optional_params)[:response] end def delete_integration(integration_key:) delete("/admin/v3/integrations/#{integration_key}")[:response] end def get_integration_secret_key(integration_key:) get("/admin/v1/integrations/#{integration_key}/skey")[:response] end def get_oauth_integration_client_secret(integration_key:, client_id:) get("/admin/v2/integrations/oauth_cc/#{integration_key}/client_secret/#{client_id}")[:response] end def reset_oauth_integration_client_secret(integration_key:, client_id:) post("/admin/v2/integrations/oauth_cc/#{integration_key}/client_secret/#{client_id}")[:response] end def get_oidc_integration_client_secret(integration_key:) get("/admin/v2/integrations/oidc/#{integration_key}/client_secret")[:response] end def reset_oidc_integration_client_secret(integration_key:) post("/admin/v2/integrations/oidc/#{integration_key}/client_secret")[:response] end ## # Policies # def get_policies_summary get('/admin/v2/policies/summary')[:response] end def get_policies get_all('/admin/v2/policies')[:response] end def get_global_policy get('/admin/v2/policies/global')[:response] end def get_policy(policy_key:) get("/admin/v2/policies/#{policy_key}")[:response] end def calculate_policy(user_id:, integration_key:) params = { user_id: user_id, integration_key: integration_key } get('/admin/v2/policies/calculate', params)[:response] end def copy_policy(policy_key:, **optional_params) # optional_params: new_policy_names_list params = optional_params.merge({ policy_key: policy_key }) post('/admin/v2/policies/copy', params)[:response] end def create_policy(policy_name:, **optional_params) # optional_params: apply_to_apps, apply_to_groups_in_apps, sections params = optional_params.merge({ policy_name: policy_name }) post('/admin/v2/policies', params)[:response] end def update_policies(policies_to_update:, policy_changes:) # parameter formatting: https://duo.com/docs/adminapi#update-policies params = { policies_to_update: policies_to_update, policy_changes: policy_changes } put('/admin/v2/policies/update', params)[:response] end def update_policy(policy_key:, **optional_params) # optional_params: apply_to_apps, apply_to_groups_in_apps, sections, # policy_name, sections_to_delete params = optional_params.merge({ policy_key: policy_key }) put("/admin/v2/policies/#{policy_key}", params)[:response] end def delete_policy(policy_key:) delete("/admin/v2/policies/#{policy_key}")[:response] end ## # Endpoints # def get_endpoints get_all('/admin/v1/endpoints')[:response] end def get_endpoint(epkey:) get("/admin/v1/endpoints/#{epkey}")[:response] end ## # Registered Devices # def get_registered_devices get_all('/admin/v1/registered_devices')[:response] end def get_registered_device(compkey:) get("/admin/v1/registered_devices/#{compkey}")[:response] end def delete_registered_device(compkey:) delete("/admin/v1/registered_devices/#{compkey}")[:response] end def get_blocked_registered_devices get_all('/admin/v1/registered_devices/blocked')[:response] end def get_blocked_registered_device(compkey:) get("/admin/v1/registered_devices/blocked/#{compkey}")[:response] end def block_registered_devices(registered_device_key_list:) params = { registered_device_key_list: registered_device_key_list } post('/admin/v1/registered_devices/blocked', params)[:response] end def block_registered_device(compkey:) post("/admin/v1/registered_devices/blocked/#{compkey}")[:response] end def unblock_registered_devices(registered_device_key_list:) params = { registered_device_key_list: registered_device_key_list } delete('/admin/v1/registered_devices/blocked', params)[:response] end def unblock_registered_device(compkey:) delete("/admin/v1/registered_devices/blocked/#{compkey}")[:response] end ## # Passport # def get_passport_config get('/admin/v2/passport/config')[:response] end def update_passport_config(disabled_groups:, enabled_groups:, enabled_status:) params = { disabled_groups: disabled_groups, enabled_groups: enabled_groups, enabled_status: enabled_status } post('/admin/v2/passport/config', params)[:response] end ## # Administrators # def get_admins get_all('/admin/v1/admins')[:response] end def create_admin(email:, name:, **optional_params) # optional_params: phone, role, restricted_by_admin_units, send_email, token_id, valid_days params = optional_params.merge({ email: email, name: name }) post('/admin/v1/admins', params)[:response] end def get_admin(admin_id:) get("/admin/v1/admins/#{admin_id}")[:response] end def update_admin(admin_id:, **optional_params) # optional_params: phone, role, restricted_by_admin_units, token_id, name, status post("/admin/v1/admins/#{admin_id}", optional_params)[:response] end def delete_admin(admin_id:) delete("/admin/v1/admins/#{admin_id}")[:response] end def reset_admin_auth_attempts(admin_id:) post("/admin/v1/admins/#{admin_id}/reset")[:response] end def clear_admin_inactivity(admin_id:) post("/admin/v1/admins/#{admin_id}/clear_inactivity")[:response] end def create_existing_admin_activation_link(admin_id:) post("/admin/v1/admins/#{admin_id}/activation_link")[:response] end def delete_existing_admin_activation_link(admin_id:) delete("/admin/v1/admins/#{admin_id}/activation_link")[:response] end def email_existing_admin_activation_link(admin_id:) post("/admin/v1/admins/#{admin_id}/activation_link/email")[:response] end def create_new_admin_activation_link(email:, **optional_params) # optional_params: admin_name, admin_role, send_email, valid_days params = optional_params.merge({ email: email }) post('/admin/v1/admins/activations', params)[:response] end def get_new_admin_pending_activations get_all('/admin/v1/admins/activations')[:response] end def delete_new_admin_pending_activations(admin_activation_id:) delete("/admin/v1/admins/activations/#{admin_activation_id}")[:response] end def sync_admin(directory_key:, email:) params = { email: email } post("/admin/v1/admins/directorysync/#{directory_key}/syncadmin", params)[:response] end def get_admin_password_mgmt_statuses get_all('/admin/v1/admins/password_mgmt')[:response] end def get_admin_password_mgmt_status(admin_id:) get("/admin/v1/admins/#{admin_id}/password_mgmt")[:response] end def update_admin_password_mgmt_status(admin_id:, **optional_params) # optional_params: has_external_password_mgmt, password post("/admin/v1/admins/#{admin_id}/password_mgmt", optional_params)[:response] end def get_admin_allowed_auth_factors get('/admin/v1/admins/allowed_auth_methods')[:response] end def update_admin_allowed_auth_factors(**optional_params) # optional_params: hardware_token_enabled, mobile_otp_enabled, push_enabled, sms_enabled, # verified_push_enabled, verified_push_length, voice_enabled, webauthn_enabled, # yubikey_enabled post('/admin/v1/admins/allowed_auth_methods', optional_params)[:response] end ## # Administrative Units # def get_administrative_units(**optional_params) # optional_params: admin_id, group_id, integration_key get_all('/admin/v1/administrative_units', optional_params)[:response] end def get_administrative_unit(admin_unit_id:) get("/admin/v1/administrative_units/#{admin_unit_id}")[:response] end def create_administrative_unit(name:, description:, restrict_by_groups:, **optional_params) # optional_params: restrict_by_integrations, admins, groups, integrations params = optional_params.merge({ name: name, description: description, restrict_by_groups: restrict_by_groups }) post('/admin/v1/administrative_units', params)[:response] end def update_administrative_unit(admin_unit_id:, **optional_params) # optional_params: restrict_by_integrations, admins, groups, integrations, # name, description, restrict_by_groups post("/admin/v1/administrative_units/#{admin_unit_id}", optional_params)[:response] end def add_administrative_unit_admin(admin_unit_id:, admin_id:) post("/admin/v1/administrative_units/#{admin_unit_id}/admin/#{admin_id}")[:response] end def remove_administrative_unit_admin(admin_unit_id:, admin_id:) delete("/admin/v1/administrative_units/#{admin_unit_id}/admin/#{admin_id}")[:response] end def add_administrative_unit_group(admin_unit_id:, group_id:) post("/admin/v1/administrative_units/#{admin_unit_id}/group/#{group_id}")[:response] end def remove_administrative_unit_group(admin_unit_id:, group_id:) delete("/admin/v1/administrative_units/#{admin_unit_id}/group/#{group_id}")[:response] end def add_administrative_unit_integration(admin_unit_id:, integration_key:) post("/admin/v1/administrative_units/#{admin_unit_id}/integration/#{integration_key}")[:response] end def remove_administrative_unit_integration(admin_unit_id:, integration_key:) delete("/admin/v1/administrative_units/#{admin_unit_id}/integration/#{integration_key}")[:response] end def delete_administrative_unit(admin_unit_id:) delete("/admin/v1/administrative_units/#{admin_unit_id}")[:response] end ## # Logs # def get_authentication_logs(mintime:, maxtime:, **optional_params) # optional_params: applications, users, assessment, detections, event_types, factors, formatter, # groups, phone_numbers, reasons, results, tokens, sort # # more info: https://duo.com/docs/adminapi#authentication-logs params = optional_params.merge({ mintime: mintime, maxtime: maxtime }) data_array_path = %i[response authlogs] metadata_path = %i[response metadata] get_all('/admin/v2/logs/authentication', params, data_array_path: data_array_path, metadata_path: metadata_path).dig(*data_array_path) end def get_activity_logs(mintime:, maxtime:, **optional_params) # optional_params: sort params = optional_params.merge({ mintime: mintime, maxtime: maxtime }) data_array_path = %i[response items] metadata_path = %i[response metadata] get_all('/admin/v2/logs/activity', params, data_array_path: data_array_path, metadata_path: metadata_path).dig(*data_array_path) end def get_admin_logs(**optional_params) # optional_params: mintime get('/admin/v1/logs/administrator', optional_params)[:response] end def get_telephony_logs(mintime:, maxtime:, **optional_params) # optional_params: sort params = optional_params.merge({ mintime: mintime, maxtime: maxtime }) data_array_path = %i[response items] metadata_path = %i[response metadata] get_all('/admin/v2/logs/telephony', params, data_array_path: data_array_path, metadata_path: metadata_path).dig(*data_array_path) end def get_offline_enrollment_logs(**optional_params) # optional_params: mintime get('/admin/v1/logs/offline_enrollment', optional_params)[:response] end ## # Trust Monitor # def get_trust_monitor_events(mintime:, maxtime:, **optional_params) # optional_params: formatter, type params = optional_params.merge({ mintime: mintime, maxtime: maxtime }) params[:limit] = 200 if !params[:limit] || (params[:limit].to_i > 200) data_array_path = %i[response events] metadata_path = %i[response metadata] get_all('/admin/v1/trust_monitor/events', params, data_array_path: data_array_path, metadata_path: metadata_path).dig(*data_array_path) end ## # Settings # def get_settings get('/admin/v1/settings')[:response] end def update_settings(**optional_params) # optional_params: caller_id, duo_mobile_otp_type, email_activity_notification_enabled, # enrollment_universal_prompt_enabled, fraud_email, fraud_email_enabled, # global_ssp_policy_enforced, helpdesk_bypass, helpdesk_bypass_expiration, # helpdesk_can_send_enroll_email, inactive_user_expiration, keypress_confirm, # keypress_fraud, language, lockout_expire_duration, lockout_threshold, # log_retention_days, minimum_password_length, name, # password_requires_lower_alpha, password_requires_numeric, # password_requires_special, password_requires_upper_alpha, # push_activity_notification_enabled, sms_batch, sms_expiration, sms_message, # sms_refresh, telephony_warning_min, timezone, unenrolled_user_lockout_threshold, # user_managers_can_put_users_in_bypass, user_telephony_cost_max post('/admin/v1/settings', optional_params)[:response] end def get_logo get_image('/admin/v1/logo') end def update_logo(logo:) # logo should be raw png data or base64 strict encoded raw png data encoded_logo = base64?(logo) ? logo : Base64.strict_encode64(logo) params = { logo: encoded_logo } post('/admin/v1/logo', params)[:response] end def delete_logo delete('/admin/v1/logo')[:response] end ## # Custom Branding # def get_custom_branding get('/admin/v1/branding')[:response] end def update_custom_branding(**optional_params) # optional_params: background_img, card_accent_color, logo, page_background_color, # powered_by_duo, sso_custom_username_label post('/admin/v1/branding', optional_params)[:response] end def get_custom_branding_draft get('/admin/v1/branding/draft')[:response] end def update_custom_branding_draft(**optional_params) # optional_params: background_img, card_accent_color, logo, page_background_color, # powered_by_duo, sso_custom_username_label, user_ids post('/admin/v1/branding/draft', optional_params)[:response] end def add_custom_branding_draft_user(user_id:) post("/admin/v1/branding/draft/users/#{user_id}")[:response] end def remove_custom_branding_draft_user(user_id:) delete("/admin/v1/branding/draft/users/#{user_id}")[:response] end def publish_custom_branding_draft post('/admin/v1/branding/draft/publish')[:response] end def get_custom_branding_messaging get('/admin/v1/branding/custom_messaging')[:response] end def update_custom_branding_messaging(**optional_params) # optional_params: help_links, help_text, locale post('/admin/v1/branding/custom_messaging', optional_params)[:response] end ## # Account Info # def get_account_info_summary get('/admin/v1/info/summary')[:response] end def get_telephony_credits_used_report(**optional_params) # optional_params: maxtime, mintime get('/admin/v1/info/telephony_credits_used', optional_params)[:response] end def get_authentication_attempts_report(**optional_params) get('/admin/v1/info/authentication_attempts', optional_params)[:response] end def get_user_authentication_attempts_report(**optional_params) get('/admin/v1/info/user_authentication_attempts', optional_params)[:response] end private def serialized_aliases(aliases) case aliases when Array aliases.map.with_index{ |a, i| "alias#{i + 1}=#{a}" }.join('&') when Hash aliases.map{ |k, v| "#{k}=#{v}" }.join('&') else aliases end end end end duo_api-1.5.1/lib/duo_api/api_helpers.rb0000644000004100000410000001472615075140372020212 0ustar www-datawww-data# frozen_string_literal: true require_relative 'api_client' # Extend DuoApi class with some HTTP method helpers class DuoApi # Perform a GET request and parse the response as JSON def get(path, params = {}, additional_headers = nil) resp = request('GET', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], 'application/json') parse_json_to_sym_hash(resp.body) end # Perform a GET request and retrieve all paginated JSON data def get_all(path, params = {}, additional_headers = nil, data_array_path: nil, metadata_path: nil) # Set default paths for returned data array and metadata if not provided data_array_path = if data_array_path.is_a?(Array) && (data_array_path.count >= 1) data_array_path.map(&:to_sym) else [:response] end metadata_path = if metadata_path.is_a?(Array) && (metadata_path.count >= 1) metadata_path.map(&:to_sym) else [:metadata] end # Ensure params keys are symbols and ignore offset parameters params.transform_keys!(&:to_sym) %i[offset next_offset].each do |p| if params[p] warn "Ignoring supplied #{p} parameter for get_all method" params.delete(p) end end # Default :limit to 1000 unless specified to minimize requests params[:limit] ||= 1000 all_data = [] prev_results_count = 0 next_offset = 0 prev_offset = 0 resp_body_hash = {} loop do resp = request('GET', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], 'application/json') resp_body_hash = parse_json_to_sym_hash(resp.body) resp_data_array = resp_body_hash.dig(*data_array_path) unless resp_data_array.is_a?(Array) raise(PaginationError, "Object at data_array_path #{JSON.generate(data_array_path)} is not an Array") end all_data.concat(resp_data_array) resp_metadata = resp_body_hash.dig(*metadata_path) if resp_metadata.is_a?(Hash) && resp_metadata[:next_offset] next_offset = resp_metadata[:next_offset] next_offset = next_offset.to_i if string_int?(next_offset) if next_offset.is_a?(Array) || next_offset.is_a?(String) next_offset = next_offset.join(',') if next_offset.is_a?(Array) raise(PaginationError, 'Paginated response offset error') if next_offset == prev_offset params[:next_offset] = next_offset else raise(PaginationError, 'Paginated response offset error') if next_offset <= prev_offset params[:offset] = next_offset end else next_offset = nil params.delete(:offset) params.delete(:next_offset) end break if !next_offset || (all_data.count <= prev_results_count) prev_results_count = all_data.count prev_offset = next_offset end # Replace the data array in the last returned resp_body_hash with the all_data array data_array_parent_hash = if data_array_path.count > 1 resp_body_hash.dig(*data_array_path[0..-2]) else resp_body_hash end data_array_key = data_array_path.last data_array_parent_hash[data_array_key] = all_data resp_body_hash end # Perform a GET request to retrieve image data and return raw data def get_image(path, params = {}, additional_headers = nil) resp = request('GET', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], %r{^image/}) resp.body end # Perform a POST request and parse the response as JSON def post(path, params = {}, additional_headers = nil) resp = request('POST', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], 'application/json') parse_json_to_sym_hash(resp.body) end # Perform a PUT request and parse the response as JSON def put(path, params = {}, additional_headers = nil) resp = request('PUT', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], 'application/json') parse_json_to_sym_hash(resp.body) end # Perform a DELETE request and parse the response as JSON def delete(path, params = {}, additional_headers = nil) resp = request('DELETE', path, params, additional_headers) raise_http_errors(resp) raise_content_type_errors(resp[:'content-type'], 'application/json') parse_json_to_sym_hash(resp.body) end private # Raise errors for non-successful HTTP responses def raise_http_errors(resp) return if resp.is_a?(Net::HTTPSuccess) raise(RateLimitError, 'Rate limit retry max wait exceeded') if resp.is_a?(Net::HTTPTooManyRequests) raise(ResponseCodeError, "HTTP #{resp.code}: #{resp.body}") end # Validate the content type of the response against the expected type def raise_content_type_errors(received, allowed) valid = false if allowed.is_a?(Regexp) valid = true if received =~ allowed elsif received == allowed valid = true end raise(ContentTypeError, "Invalid Content-Type #{received}, should match #{allowed.inspect}") unless valid end # Check if a value is a Base64 encoded string def base64?(value) value.is_a?(String) and Base64.strict_encode64(Base64.decode64(value)) == value end # Check if a string represents an integer def string_int?(value) value.is_a?(String) and value.to_i.to_s == value end # Parse JSON string to Hash with symbol keys def parse_json_to_sym_hash(json) JSON.parse(json, symbolize_names: true) end # JSON serialize Array def json_serialized_array(value) value.is_a?(Array) ? JSON.generate(value) : value end # CSV serialize Array def csv_serialized_array(value) value.is_a?(Array) ? value.join(',') : value end # Format boolean as 'true' or 'false' def stringified_boolean(value) %w[true 1].include?(value.to_s.downcase) ? 'true' : 'false' end # Format boolean as '1' or '0' def stringified_binary_boolean(value) %w[true 1].include?(value.to_s.downcase) ? '1' : '0' end # Format boolean as 'True' or 'False' def stringified_python_boolean(value) %w[true 1].include?(value.to_s.downcase) ? 'True' : 'False' end end duo_api-1.5.1/lib/duo_api/device.rb0000644000004100000410000000413615075140372017150 0ustar www-datawww-data# frozen_string_literal: true require_relative 'api_client' require_relative 'api_helpers' class DuoApi ## # Duo Device API (https://duo.com/docs/deviceapi) # class Device < DuoApi attr_accessor :mkey def initialize(ikey, skey, host, proxy = nil, mkey:, ca_file: nil, default_params: {}) super(ikey, skey, host, proxy, ca_file: ca_file, default_params: default_params) @mkey = mkey end def create_device_cache(**optional_params) # optional_params: active optional_params.tap do |p| p[:active] = stringified_python_boolean(p[:active]) if p[:active] end post("/device/v1/management_systems/#{@mkey}/device_cache", optional_params)[:response] end def add_device_cache_devices(cache_key:, devices:) params = { devices: devices } post("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}/devices", params)[:response] end def get_device_cache_devices(cache_key:, **optional_params) # optional_params: device_ids data_array_path = %i[response devices_retrieved] metadata_path = %i[response] get_all("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}/devices", optional_params, data_array_path: data_array_path, metadata_path: metadata_path).dig(*data_array_path) end def delete_device_cache_devices(cache_key:, devices:) params = { devices: devices } delete("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}/devices", params)[:response] end def activate_device_cache(cache_key:) post("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}/activate")[:response] end def delete_device_cache(cache_key:) delete("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}")[:response] end def get_device_caches(status:) params = { status: status } get("/device/v1/management_systems/#{@mkey}/device_cache", params)[:response] end def get_device_cache(cache_key:) get("/device/v1/management_systems/#{@mkey}/device_cache/#{cache_key}")[:response] end end end duo_api-1.5.1/lib/duo_api/api_client.rb0000644000004100000410000001425215075140372020020 0ustar www-datawww-data# frozen_string_literal: true require 'base64' require 'erb' require 'json' require 'openssl' require 'net/https' require 'time' require 'uri' ## # A Ruby implementation of the Duo API # class DuoApi attr_accessor :ca_file attr_reader :default_params VERSION = Gem.loaded_specs['duo_api'] ? Gem.loaded_specs['duo_api'].version : '0.0.0' # Constants for handling rate limit backoff MAX_BACKOFF_WAIT_SECS = 32 INITIAL_BACKOFF_WAIT_SECS = 1 BACKOFF_FACTOR = 2 def initialize(ikey, skey, host, proxy = nil, ca_file: nil, default_params: {}) @ikey = ikey @skey = skey @host = host @proxy_str = proxy if proxy.nil? @proxy = [] else proxy_uri = URI.parse proxy @proxy = [ proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password ] end @ca_file = ca_file || File.join(File.dirname(__FILE__), '..', '..', 'ca_certs.pem') @default_params = default_params.transform_keys(&:to_sym) end def default_params=(default_params) @default_params = default_params.transform_keys(&:to_sym) end # Basic authenticated request returning raw Net::HTTPResponse object def request(method, path, params = {}, additional_headers = nil) # Merge default params with provided params params = @default_params.merge(params.transform_keys(&:to_sym)) # Determine if params should be in a JSON request body params_go_in_body = %w[POST PUT PATCH].include?(method) if params_go_in_body body = canon_json(params) params = {} else body = '' end # Construct the request URI uri = request_uri(path, params) # Sign the request current_date, signed = sign(method, uri.host, path, params, body, additional_headers) # Create the HTTP request object request = Net::HTTP.const_get(method.capitalize).new(uri.to_s) request.basic_auth(@ikey, signed) request['Date'] = current_date request['User-Agent'] = "duo_api_ruby/#{VERSION}" # Set Content-Type and request body for JSON requests if params_go_in_body request['Content-Type'] = 'application/json' request.body = body end # Start the HTTP session Net::HTTP.start( uri.host, uri.port, *@proxy, use_ssl: true, ca_file: @ca_file, verify_mode: OpenSSL::SSL::VERIFY_PEER ) do |http| wait_secs = INITIAL_BACKOFF_WAIT_SECS loop do resp = http.request(request) # Check if the response is rate-limited and handle backoff return resp if !resp.is_a?(Net::HTTPTooManyRequests) || (wait_secs > MAX_BACKOFF_WAIT_SECS) random_offset = rand sleep(wait_secs + random_offset) wait_secs *= BACKOFF_FACTOR end end end private # Encode a key-value pair for a URL def encode_key_val(key, val) key = ERB::Util.url_encode(key.to_s) value = ERB::Util.url_encode(val.to_s) "#{key}=#{value}" end # Build a canonical parameter string def canon_params(params_hash = nil) return '' if params_hash.nil? params_hash.transform_keys(&:to_s).sort.map do |k, v| # When value an array, repeat key for each unique value in sorted array if v.is_a?(Array) if v.count.positive? v.sort.uniq.map{ |vn| encode_key_val(k, vn) }.join('&') else encode_key_val(k, '') end else encode_key_val(k, v) end end.join('&') end # Generate a canonical JSON body def canon_json(params_hash = nil) return '' if params_hash.nil? JSON.generate(params_hash.sort.to_h) end # Canonicalize additional headers for signing def canon_x_duo_headers(additional_headers) additional_headers ||= {} unless additional_headers.none?{ |k, v| k.nil? || v.nil? } raise(HeaderError, 'Not allowed "nil" as a header name or value') end canon_list = [] added_headers = [] additional_headers.keys.sort.each do |header_name| header_name_lowered = header_name.downcase header_value = additional_headers[header_name] validate_additional_header(header_name_lowered, header_value, added_headers) canon_list.append(header_name_lowered, header_value) added_headers.append(header_name_lowered) end canon = canon_list.join("\x00") OpenSSL::Digest::SHA512.hexdigest(canon) end # Validate additional headers to ensure they meet requirements def validate_additional_header(header_name, value, added_headers) header_name.downcase! raise(HeaderError, 'Not allowed "Null" character in header name') if header_name.include?("\x00") raise(HeaderError, 'Not allowed "Null" character in header value') if value.include?("\x00") raise(HeaderError, 'Additional headers must start with \'X-Duo-\'') unless header_name.start_with?('x-duo-') raise(HeaderError, "Duplicate header passed, header=#{header_name}") if added_headers.include?(header_name) end # Construct the request URI def request_uri(path, params = nil) u = "https://#{@host}#{path}" u += "?#{canon_params(params)}" unless params.nil? URI.parse(u) end # Create a canonical string for signing requests def canonicalize(method, host, path, params, body = '', additional_headers = nil, options: {}) # options[:date] being passed manually is specifically for tests date = options[:date] || Time.now.rfc2822 canon = [ date, method.upcase, host.downcase, path, canon_params(params), OpenSSL::Digest::SHA512.hexdigest(body), canon_x_duo_headers(additional_headers) ] [date, canon.join("\n")] end # Sign the request with HMAC-SHA512 def sign(method, host, path, params, body = '', additional_headers = nil, options: {}) # options[:date] being passed manually is specifically for tests date, canon = canonicalize(method, host, path, params, body, additional_headers, options: options) [date, OpenSSL::HMAC.hexdigest('sha512', @skey, canon)] end # Custom Error Classes class HeaderError < StandardError; end class RateLimitError < StandardError; end class ResponseCodeError < StandardError; end class ContentTypeError < StandardError; end class PaginationError < StandardError; end class ChildAccountError < StandardError; end end duo_api-1.5.1/lib/duo_api/auth.rb0000644000004100000410000000331215075140372016645 0ustar www-datawww-data# frozen_string_literal: true require_relative 'api_client' require_relative 'api_helpers' class DuoApi ## # Duo Auth API (https://duo.com/docs/authapi) # class Auth < DuoApi def ping get('/auth/v2/ping')[:response] end def check get('/auth/v2/check')[:response] end def logo get_image('/auth/v2/logo') end def enroll(**optional_params) # optional_params: username, valid_secs post('/auth/v2/enroll', optional_params)[:response] end def enroll_status(user_id:, activation_code:) params = { user_id: user_id, activation_code: activation_code } post('/auth/v2/enroll_status', params)[:response] end def preauth(**optional_params) # optional_params: user_id, username, client_supports_verified_push, ipaddr, hostname, # trusted_device_token # # Note: user_id or username must be provided optional_params.tap do |p| if p[:client_supports_verified_push] p[:client_supports_verified_push] = stringified_binary_boolean(p[:client_supports_verified_push]) end end post('/auth/v2/preauth', optional_params)[:response] end def auth(factor:, **optional_params) # optional_params: user_id, username, ipaddr, hostname, async # # Note: user_id or username must be provided optional_params.tap do |p| p[:async] = stringified_binary_boolean(p[:async]) if p[:async] end params = optional_params.merge({ factor: factor }) post('/auth/v2/auth', params)[:response] end def auth_status(txid:) params = { txid: txid } get('/auth/v2/auth_status', params)[:response] end end end duo_api-1.5.1/lib/duo_api.rb0000644000004100000410000000036415075140372015710 0ustar www-datawww-data# frozen_string_literal: true require_relative 'duo_api/api_client' require_relative 'duo_api/api_helpers' require_relative 'duo_api/admin' require_relative 'duo_api/accounts' require_relative 'duo_api/auth' require_relative 'duo_api/device' duo_api-1.5.1/ca_certs.pem0000644000004100000410000006253015075140372015466 0ustar www-datawww-data# Source URL: https://www.amazontrust.com/repository/AmazonRootCA1.cer # Certificate #1 Details: # Original Format: DER # Subject: CN=Amazon Root CA 1,O=Amazon,C=US # Issuer: CN=Amazon Root CA 1,O=Amazon,C=US # Expiration Date: 2038-01-17 00:00:00 # Serial Number: 66C9FCF99BF8C0A39E2F0788A43E696365BCA # SHA256 Fingerprint: 8ecde6884f3d87b1125ba31ac3fcb13d7016de7f57cc904fe1cb97c6ae98196e -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy rqXRfboQnoZsG4q5WTP468SQvvG5 -----END CERTIFICATE----- # Source URL: https://www.amazontrust.com/repository/AmazonRootCA2.cer # Certificate #1 Details: # Original Format: DER # Subject: CN=Amazon Root CA 2,O=Amazon,C=US # Issuer: CN=Amazon Root CA 2,O=Amazon,C=US # Expiration Date: 2040-05-26 00:00:00 # Serial Number: 66C9FD29635869F0A0FE58678F85B26BB8A37 # SHA256 Fingerprint: 1ba5b2aa8c65401a82960118f80bec4f62304d83cec4713a19c39c011ea46db4 -----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg 1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K 8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r 2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR 8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz 7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 +XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI 0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY +gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl 7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE 76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H 9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT 4PsJYGw= -----END CERTIFICATE----- # Source URL: https://www.amazontrust.com/repository/AmazonRootCA3.cer # Certificate #1 Details: # Original Format: DER # Subject: CN=Amazon Root CA 3,O=Amazon,C=US # Issuer: CN=Amazon Root CA 3,O=Amazon,C=US # Expiration Date: 2040-05-26 00:00:00 # Serial Number: 66C9FD5749736663F3B0B9AD9E89E7603F24A # SHA256 Fingerprint: 18ce6cfe7bf14e60b2e347b8dfe868cb31d02ebb3ada271569f50343b46db3a4 -----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM YyRIHN8wfdVoOw== -----END CERTIFICATE----- # Source URL: https://www.amazontrust.com/repository/AmazonRootCA4.cer # Certificate #1 Details: # Original Format: DER # Subject: CN=Amazon Root CA 4,O=Amazon,C=US # Issuer: CN=Amazon Root CA 4,O=Amazon,C=US # Expiration Date: 2040-05-26 00:00:00 # Serial Number: 66C9FD7C1BB104C2943E5717B7B2CC81AC10E # SHA256 Fingerprint: e35d28419ed02025cfa69038cd623962458da5c695fbdea3c22b0bfb25897092 -----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi 9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- # Source URL: https://www.amazontrust.com/repository/SFSRootCAG2.cer # Certificate #1 Details: # Original Format: DER # Subject: CN=Starfield Services Root Certificate Authority - G2,O=Starfield Technologies\, Inc.,L=Scottsdale,ST=Arizona,C=US # Issuer: CN=Starfield Services Root Certificate Authority - G2,O=Starfield Technologies\, Inc.,L=Scottsdale,ST=Arizona,C=US # Expiration Date: 2037-12-31 23:59:59 # Serial Number: 0 # SHA256 Fingerprint: 568d6905a2c88708a4b3025190edcfedb1974a606a13c6e5290fcb2ae63edab5 -----BEGIN CERTIFICATE----- MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk 6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn 0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN sSi6 -----END CERTIFICATE----- # Source URL: https://cacerts.digicert.com/DigiCertHighAssuranceEVRootCA.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=DigiCert High Assurance EV Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US # Issuer: CN=DigiCert High Assurance EV Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US # Expiration Date: 2031-11-10 00:00:00 # Serial Number: 2AC5C266A0B409B8F0B79F2AE462577 # SHA256 Fingerprint: 7431e5f4c3c1ce4690774f0b61e05440883ba9a01ed00ba6abd7806ed3b118cf -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE----- # Source URL: https://cacerts.digicert.com/DigiCertTLSECCP384RootG5.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US # Issuer: CN=DigiCert TLS ECC P384 Root G5,O=DigiCert\, Inc.,C=US # Expiration Date: 2046-01-14 23:59:59 # Serial Number: 9E09365ACF7D9C8B93E1C0B042A2EF3 # SHA256 Fingerprint: 018e13f0772532cf809bd1b17281867283fc48c6e13be9c69812854a490c1b05 -----BEGIN CERTIFICATE----- MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS 7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp 0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 DXZDjC5Ty3zfDBeWUA== -----END CERTIFICATE----- # Source URL: https://cacerts.digicert.com/DigiCertTLSRSA4096RootG5.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=DigiCert TLS RSA4096 Root G5,O=DigiCert\, Inc.,C=US # Issuer: CN=DigiCert TLS RSA4096 Root G5,O=DigiCert\, Inc.,C=US # Expiration Date: 2046-01-14 23:59:59 # Serial Number: 8F9B478A8FA7EDA6A333789DE7CCF8A # SHA256 Fingerprint: 371a00dc0533b3721a7eeb40e8419e70799d2b0a0f2c1d80693165f7cec4ad75 -----BEGIN CERTIFICATE----- MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv /PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ -----END CERTIFICATE----- # Source URL: https://secure.globalsign.com/cacert/rootr46.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=GlobalSign Root R46,O=GlobalSign nv-sa,C=BE # Issuer: CN=GlobalSign Root R46,O=GlobalSign nv-sa,C=BE # Expiration Date: 2046-03-20 00:00:00 # Serial Number: 11D2BBB9D723189E405F0A9D2DD0DF2567D1 # SHA256 Fingerprint: 4fa3126d8d3a11d1c4855a4f807cbad6cf919d3a5a88b03bea2c6372d93c40c9 -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud 316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo 0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE +cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC 4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti 2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP 4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 -----END CERTIFICATE----- # Source URL: https://secure.globalsign.com/cacert/roote46.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=GlobalSign Root E46,O=GlobalSign nv-sa,C=BE # Issuer: CN=GlobalSign Root E46,O=GlobalSign nv-sa,C=BE # Expiration Date: 2046-03-20 00:00:00 # Serial Number: 11D2BBBA336ED4BCE62468C50D841D98E843 # SHA256 Fingerprint: cbb9c44d84b8043e1050ea31a69f514955d7bfd2e2c6b49301019ad61d9f5058 -----BEGIN CERTIFICATE----- MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ 7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 +RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= -----END CERTIFICATE----- # Source URL: https://i.pki.goog/r2.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=GTS Root R2,O=Google Trust Services LLC,C=US # Issuer: CN=GTS Root R2,O=Google Trust Services LLC,C=US # Expiration Date: 2036-06-22 00:00:00 # Serial Number: 203E5AEC58D04251AAB1125AA # SHA256 Fingerprint: 8d25cd97229dbf70356bda4eb3cc734031e24cf00fafcfd32dc76eb5841c7ea8 -----BEGIN CERTIFICATE----- MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY 6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV +3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV 7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl 6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL -----END CERTIFICATE----- # Source URL: https://i.pki.goog/r4.crt # Certificate #1 Details: # Original Format: DER # Subject: CN=GTS Root R4,O=Google Trust Services LLC,C=US # Issuer: CN=GTS Root R4,O=Google Trust Services LLC,C=US # Expiration Date: 2036-06-22 00:00:00 # Serial Number: 203E5C068EF631A9C72905052 # SHA256 Fingerprint: 349dfa4058c5e263123b398ae795573c4e1313c83fe68f93556cd5e8031b3c7d -----BEGIN CERTIFICATE----- MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D 9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD -----END CERTIFICATE----- # Source URL: https://www.identrust.com/file-download/download/public/5718 # Certificate #1 Details: # Original Format: PKCS7-DER # Subject: CN=IdenTrust Commercial Root CA 1,O=IdenTrust,C=US # Issuer: CN=IdenTrust Commercial Root CA 1,O=IdenTrust,C=US # Expiration Date: 2034-01-16 18:12:23 # Serial Number: A0142800000014523C844B500000002 # SHA256 Fingerprint: 5d56499be4d2e08bcfcad08a3e38723d50503bde706948e42f55603019e528ae -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT 3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU +ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH 6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 +wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG 4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A 7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H -----END CERTIFICATE----- # Source URL: https://www.identrust.com/file-download/download/public/5842 # Certificate #1 Details: # Original Format: PKCS7-PEM # Subject: CN=IdenTrust Commercial Root TLS ECC CA 2,O=IdenTrust,C=US # Issuer: CN=IdenTrust Commercial Root TLS ECC CA 2,O=IdenTrust,C=US # Expiration Date: 2039-04-11 21:11:10 # Serial Number: 40018ECF000DE911D7447B73E4C1F82E # SHA256 Fingerprint: 983d826ba9c87f653ff9e8384c5413e1d59acf19ddc9c98cecae5fdea2ac229c -----BEGIN CERTIFICATE----- MIICbDCCAc2gAwIBAgIQQAGOzwAN6RHXRHtz5MH4LjAKBggqhkjOPQQDBDBSMQsw CQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MS8wLQYDVQQDEyZJZGVuVHJ1 c3QgQ29tbWVyY2lhbCBSb290IFRMUyBFQ0MgQ0EgMjAeFw0yNDA0MTEyMTExMTFa Fw0zOTA0MTEyMTExMTBaMFIxCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlJZGVuVHJ1 c3QxLzAtBgNVBAMTJklkZW5UcnVzdCBDb21tZXJjaWFsIFJvb3QgVExTIEVDQyBD QSAyMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBwomiZTgLg8KqEImMmnO5rNPb Oo9sv5w4nJh45CXs9Gcu8YET9ulxsyVBCVSfSYeppdtXFEWYyBi0QRCAlp5YZHQB H675v5rWVKRXvhzsuUNi9Xw0Zy1bAXaikmsrY/J0L52j2RulW4q4WvE7f23VFwZu d82J8k0YG+M4MpmdOho1rsKjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ BAQDAgGGMB0GA1UdDgQWBBQhNGgGrnXhVx/FuQqjXpuH+IlbwzAKBggqhkjOPQQD BAOBjAAwgYgCQgDc9F4WOxAgci2uQWfsX9cjeIvDXaaeVjDz31Ycc+ZdPrK1JKrB f6CuTwWy8VojtGxdM3PJMkJC4LGPuhcvkHLo4gJCAV5h+PXe4bDJ3QxE8hkGFoUW Ak6KtMCIpbLyt5pHrROi+YW9MpScoNGJkg96G1ETvJTWz6dv0uQYjKXt3jlOfQ7g -----END CERTIFICATE----- # Source URL: https://ssl-ccp.secureserver.net/repository/sfroot-g2.crt # Certificate #1 Details: # Original Format: PEM # Subject: CN=Starfield Root Certificate Authority - G2,O=Starfield Technologies\, Inc.,L=Scottsdale,ST=Arizona,C=US # Issuer: CN=Starfield Root Certificate Authority - G2,O=Starfield Technologies\, Inc.,L=Scottsdale,ST=Arizona,C=US # Expiration Date: 2037-12-31 23:59:59 # Serial Number: 0 # SHA256 Fingerprint: 2ce1cb0bf9d2f9e102993fbe215152c3b2dd0cabde1c68e5319b839154dbb7f5 -----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg 8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- duo_api-1.5.1/duo_api.gemspec0000644000004100000410000000310315075140372016154 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: duo_api 1.5.1 ruby lib Gem::Specification.new do |s| s.name = "duo_api".freeze s.version = "1.5.1".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Duo Security".freeze] s.date = "2025-10-14" s.description = "A Ruby implementation of the Duo API.".freeze s.email = "support@duo.com".freeze s.files = ["ca_certs.pem".freeze, "lib/duo_api.rb".freeze, "lib/duo_api/accounts.rb".freeze, "lib/duo_api/admin.rb".freeze, "lib/duo_api/api_client.rb".freeze, "lib/duo_api/api_helpers.rb".freeze, "lib/duo_api/auth.rb".freeze, "lib/duo_api/device.rb".freeze] s.homepage = "https://github.com/duosecurity/duo_api_ruby".freeze s.licenses = ["BSD-3-Clause".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.5".freeze) s.rubygems_version = "3.4.19".freeze s.summary = "Duo API Ruby".freeze s.specification_version = 4 s.add_runtime_dependency(%q.freeze, [">= 0.2.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 2.7.1".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.6.1".freeze]) s.add_development_dependency(%q.freeze, ["~> 13.2.1".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.73.1".freeze]) s.add_development_dependency(%q.freeze, ["~> 3.6.7".freeze]) end