Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

DocuSign SSL/TLS Configuration

examples/ssl_configuration.livemd

DocuSign SSL/TLS Configuration

Mix.install([
  {:docusign, "~> 2.2.1"},
  {:kino, "~> 0.14.0"}
])

Introduction

This LiveBook demonstrates how to configure SSL/TLS options for secure connections to DocuSign’s API. This includes:

  • Custom CA certificates
  • Client certificate authentication (mutual TLS)
  • SSL verification options
  • Per-request SSL configuration

Basic Setup

First, let’s set up our basic DocuSign configuration:

# These would normally come from environment variables
client_id = System.get_env("DOCUSIGN_CLIENT_ID") || "your-client-id"
user_id = System.get_env("DOCUSIGN_USER_ID") || "your-user-id"
account_id = System.get_env("DOCUSIGN_ACCOUNT_ID") || "your-account-id"

# For demo purposes, we'll show the config but not actually use it
demo_config = %{
  client_id: client_id,
  user_id: user_id,
  account_id: account_id
}

Kino.Markdown.new("""
## Configuration Status

Client ID: `#{client_id}`
User ID: `#{user_id}`
Account ID: `#{account_id}`

⚠️ **Note**: This example demonstrates SSL configuration without making actual API calls.
""")

SSL Configuration Examples

1. Global SSL Configuration

Configure SSL options at the application level:

# Example 1: Basic SSL configuration with custom CA certificate
ssl_config_basic = [
  verify: :verify_peer,
  cacertfile: "/etc/ssl/certs/ca-certificates.crt",
  depth: 3
]

# Example 2: Client certificate authentication (mutual TLS)
ssl_config_mutual_tls = [
  verify: :verify_peer,
  cacertfile: "/path/to/ca-bundle.crt",
  certfile: "/path/to/client-cert.pem",
  keyfile: "/path/to/client-key.pem",
  password: "keypassword"  # If the key is encrypted
]

# Example 3: Advanced SSL configuration
ssl_config_advanced = [
  verify: :verify_peer,
  versions: [:"tlsv1.2", :"tlsv1.3"],
  ciphers: [
    "ECDHE-RSA-AES256-GCM-SHA384",
    "ECDHE-RSA-AES128-GCM-SHA256"
  ],
  customize_hostname_check: [
    match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
  ]
]

Kino.Markdown.new("""
### SSL Configuration Examples

1. **Basic Configuration** - Uses custom CA certificate bundle
2. **Mutual TLS** - Includes client certificate for authentication
3. **Advanced** - Specifies TLS versions and cipher suites

In production, you would add one of these to your config file:

```elixir
config :docusign, :ssl_options, #{inspect(ssl_config_basic, pretty: true)}

“””)


### 2. Building SSL Options

Let's see how the SSL options are built and merged:

```elixir
# Test the SSL options builder
defmodule SSLDemo do
  def show_ssl_options do
    # Default options
    default_opts = DocuSign.SSLOptions.build()

    # With custom options
    custom_opts = DocuSign.SSLOptions.build(
      verify: :verify_none,
      depth: 5,
      validate_files: false  # Don't validate file paths for demo
    )

    # With file paths (validation disabled for demo)
    file_opts = DocuSign.SSLOptions.build(
      cacertfile: "/custom/ca.pem",
      certfile: "/custom/cert.pem",
      keyfile: "/custom/key.pem",
      validate_files: false
    )

    %{
      default: default_opts,
      custom: custom_opts,
      with_files: file_opts
    }
  end

  def show_ca_detection do
    # Show how CA certificates are auto-detected
    opts = DocuSign.SSLOptions.build()

    cond do
      opts[:cacertfile] -> "System CA bundle found at: #{opts[:cacertfile]}"
      opts[:cacerts] -> "Using CAStore or built-in certificates"
      true -> "No CA certificates configured"
    end
  end
end

ssl_examples = SSLDemo.show_ssl_options()
ca_detection = SSLDemo.show_ca_detection()

Kino.Markdown.new("""
### SSL Options Builder Results

**Default Options:**

#{inspect(ssl_examples.default, pretty: true, limit: 10)}


**Custom Options:**

#{inspect(ssl_examples.custom, pretty: true, limit: 10)}


**With File Paths:**

#{inspect(ssl_examples.with_files, pretty: true, limit: 10)}


**CA Certificate Detection:** #{ca_detection}
""")

3. Per-Request SSL Configuration

Demonstrate how to use different SSL options for specific requests:

defmodule RequestDemo do
  def show_request_structure do
    # This shows the structure without making actual requests

    # Standard request
    standard_request = [
      method: :get,
      url: "/v2.1/accounts/#{:account_id}/users"
    ]

    # Request with custom SSL options
    ssl_request = [
      method: :get,
      url: "/v2.1/accounts/#{:account_id}/users",
      ssl_options: [
        verify: :verify_peer,
        cacertfile: "/special/ca-for-this-request.pem"
      ]
    ]

    %{
      standard: standard_request,
      with_ssl: ssl_request
    }
  end

  def show_connection_pooling do
    # Show connection pool configuration
    %{
      pool_size: Application.get_env(:docusign, :pool_size, 10),
      pool_count: Application.get_env(:docusign, :pool_count, 1),
      ssl_configured: Application.get_env(:docusign, :ssl_options) != nil
    }
  end
end

request_examples = RequestDemo.show_request_structure()
pool_config = RequestDemo.show_connection_pooling()

Kino.Markdown.new("""
### Per-Request SSL Options

**Standard Request Structure:**
```elixir
#{inspect(request_examples.standard, pretty: true)}

Request with SSL Options:

#{inspect(request_examples.with_ssl, pretty: true)}

Connection Pooling

Current pool configuration:

  • Pool size: #{pool_config.pool_size}
  • Pool count: #{pool_config.pool_count}
  • SSL configured: #{pool_config.ssl_configured}

You can configure pooling in your config:

config :docusign,
  pool_size: 20,
  pool_count: 2

“””)


## Security Best Practices

```elixir
Kino.Markdown.new("""
## Security Best Practices

### 1. Certificate Verification

**Always use `:verify_peer` in production:**
```elixir
# ✅ Good
ssl_options: [verify: :verify_peer]

# ❌ Bad - only for development/testing
ssl_options: [verify: :verify_none]

2. CA Certificate Management

Keep CA certificates up to date:

  • Use system CA bundles when possible
  • Update regularly to include new root certificates
  • Consider using castore hex package for Elixir apps

3. Client Certificates

Protect private keys:

# Store securely and use environment variables
ssl_options: [
  certfile: System.get_env("CLIENT_CERT_PATH"),
  keyfile: System.get_env("CLIENT_KEY_PATH"),
  password: System.get_env("CLIENT_KEY_PASSWORD")
]

4. TLS Versions

Use modern TLS versions:

ssl_options: [
  versions: [:"tlsv1.2", :"tlsv1.3"]  # No TLS 1.0 or 1.1
]

5. Hostname Verification

Always verify hostnames:

ssl_options: [
  customize_hostname_check: [
    match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
  ]
]

“””)


## Testing SSL Configuration with Live API Calls

Let's test the SSL configuration with actual DocuSign API calls:

```elixir
# Configure your credentials for live testing
user_id_input = Kino.Input.text("User ID")
account_id_input = Kino.Input.text("Account ID")

form = Kino.Layout.grid([user_id_input, account_id_input], columns: 2)

Kino.Layout.grid([
  Kino.Markdown.new("""
  ## Live SSL Configuration Testing

  Enter your DocuSign credentials to test SSL configuration with real API calls.

  ⚠️ **Note**: Make sure you have:
  1. Set up your DocuSign application
  2. Configured JWT authentication
  3. Granted user consent
  """),
  form
])

Now let’s test the SSL configuration with actual API calls:

# Get user inputs
user_id = Kino.Input.read(user_id_input)
account_id = Kino.Input.read(account_id_input)

if user_id == "" or account_id == "" do
  Kino.Markdown.new("⚠️ Please enter both User ID and Account ID above")
else
  # Test 1: Connect with default SSL configuration
  default_result = case DocuSign.Connection.get(user_id) do
    {:ok, conn} ->
      # Make a simple API call to test the connection
      case DocuSign.Api.Accounts.accounts_get_account(conn, account_id) do
        {:ok, account} -> {:ok, "Connected successfully with default SSL config"}
        {:error, error} -> {:error, "API call failed: #{inspect(error)}"}
      end
    {:error, error} -> {:error, "Connection failed: #{inspect(error)}"}
  end

  # Test 2: Test with custom SSL options (if we had a test endpoint)
  # Note: This demonstrates the API but won't actually use different SSL since
  # DocuSign's production servers have valid certificates
  custom_ssl_test = case DocuSign.Connection.get(user_id) do
    {:ok, conn} ->
      # In a real scenario, you might test against a server with:
      # - Self-signed certificates (verify: :verify_none)
      # - Custom CA certificates
      # - Client certificates for mutual TLS

      # For this demo, we'll just show the structure
      {:info, "Custom SSL options would be used like this:

      DocuSign.Connection.request(conn,
        method: :get,
        url: \"/v2.1/accounts/#{account_id}\",
        ssl_options: [
          verify: :verify_peer,
          cacertfile: \"/custom/ca.pem\"
        ]
      )"}
    {:error, _} -> {:error, "Couldn't establish connection"}
  end

  # Display results
  Kino.Markdown.new("""
  ## Test Results

  ### Default SSL Configuration Test
  #{case default_result do
    {:ok, msg} -> "✅ " <> msg
    {:error, msg} -> "❌ " <> msg
  end}

  ### Custom SSL Options
  #{case custom_ssl_test do
    {:info, msg} -> "ℹ️ " <> msg
    {:error, msg} -> "❌ " <> msg
  end}

  ### Current SSL Configuration
  ```elixir
  #{DocuSign.SSLOptions.build() |> inspect(pretty: true, limit: 10)}

Notes

  • DocuSign’s production API uses valid SSL certificates
  • Custom SSL options are most useful for: - Corporate proxies with custom CAs - Client certificate authentication - Development/testing environments “””) end

## Test SSL Error Handling

Let's demonstrate what happens with different SSL configurations:

```elixir
# This cell demonstrates SSL configuration scenarios
# Note: These won't actually fail against DocuSign's valid certificates

scenarios = [
  %{
    name: "Strict certificate validation (default)",
    options: [verify: :verify_peer, depth: 3],
    expected: "Should work with valid certificates"
  },
  %{
    name: "No certificate validation (NOT for production!)",
    options: [verify: :verify_none],
    expected: "Would accept any certificate - INSECURE"
  },
  %{
    name: "Custom CA certificate",
    options: [
      verify: :verify_peer,
      cacertfile: "/path/to/corporate-ca.pem",
      validate_files: false
    ],
    expected: "Would use corporate CA bundle"
  },
  %{
    name: "Client certificate (mutual TLS)",
    options: [
      certfile: "/path/to/client.pem",
      keyfile: "/path/to/client-key.pem",
      validate_files: false
    ],
    expected: "Would authenticate with client certificate"
  }
]

results = Enum.map(scenarios, fn scenario ->
  # Build SSL options for this scenario
  opts = DocuSign.SSLOptions.build(scenario.options)

  """
  ### #{scenario.name}

  **Options:**
  ```elixir
  #{inspect(scenario.options, pretty: true)}

Result: #{scenario.expected}

Built configuration includes:

  • Verify mode: #{opts[:verify]}
  • Max depth: #{opts[:depth]}
  • TLS versions: #{inspect(opts[:versions])} #{if opts[:cacertfile], do: “- CA cert file: #{opts[:cacertfile]}“, else: “”} #{if opts[:certfile], do: “- Client cert: #{opts[:certfile]}“, else: “”}

“”” end)

Kino.Markdown.new(“””

SSL Configuration Scenarios

#{Enum.join(results, “\n”)}

🔒 Security Reminder

Always use :verify_peer in production to ensure you’re connecting to the real DocuSign API! “””)


## Next Steps

```elixir
Kino.Markdown.new("""
## Next Steps

1. **Configure SSL in your application:**
   ```elixir
   # config/prod.exs
   config :docusign, :ssl_options,
     verify: :verify_peer,
     cacertfile: "/etc/ssl/certs/ca-certificates.crt"
  1. Test with your DocuSign sandbox:

    • Set up your credentials
    • Configure appropriate SSL options
    • Make test API calls
  2. Monitor and maintain:

    • Keep CA certificates updated
    • Monitor for SSL errors in logs
    • Test certificate rotation procedures
  3. For production:

    • Use proper certificate storage (not in code)
    • Implement certificate rotation
    • Set up monitoring for certificate expiration

Additional Resources