Skip to content

Commit

Permalink
Merge pull request #23 from spencermaxfield/T135916_cert_pinning
Browse files Browse the repository at this point in the history
Add certificate pinning
  • Loading branch information
AaronAtDuo authored Dec 8, 2022
2 parents aad7505 + 3fdc1e7 commit 9324259
Show file tree
Hide file tree
Showing 8 changed files with 632 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/net-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ jobs:
run: msbuild.exe duo_api_csharp.sln

- name: Run Tests dll
run: vstest.console.exe .\test\bin\Debug\test.dll
run: vstest.console.exe .\test\bin\Debug\DuoApiTest.dll
36 changes: 36 additions & 0 deletions duo_api_csharp/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("duo_api_csharp")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("duo_api_csharp")]
[assembly: AssemblyCopyright("Copyright © 2022")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("b15c44a4-74d6-45b7-8a30-a313c2818083")]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

// Allow tests to access internal methods for easier testing
[assembly: InternalsVisibleTo("DuoApiTest")]
142 changes: 142 additions & 0 deletions duo_api_csharp/CertificatePinnerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2022 Cisco Systems, Inc. and/or its affiliates
* All rights reserved
*/

using System.IO;
using System.Linq;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System;

namespace Duo
{
public class CertificatePinnerFactory
{
private readonly X509CertificateCollection _rootCerts;

public CertificatePinnerFactory(X509CertificateCollection rootCerts)
{
_rootCerts = rootCerts;
}

/// <summary>
/// Get a certificate pinner that ensures only connections to a specific list of root certificates are allowed
/// </summary>
/// <returns>A Duo certificate pinner for use in an HttpWebRequest</returns>
public static RemoteCertificateValidationCallback GetDuoCertificatePinner()
{
return new CertificatePinnerFactory(GetDuoCertCollection()).GetPinner();
}
/// <summary>
/// Get a certificate pinner that ensures only connections to the provided root certificates are allowed
/// </summary>
/// <returns>A certificate pinner for use in an HttpWebRequest</returns>
public static RemoteCertificateValidationCallback GetCustomRootCertificatesPinner(X509CertificateCollection rootCerts)
{
return new CertificatePinnerFactory(rootCerts).GetPinner();
}


/// <summary>
/// Get a certificate "pinner" that effectively disables SSL certificate validation
/// </summary>
/// <returns></returns>
public static RemoteCertificateValidationCallback GetCertificateDisabler()
{
return (httpRequestMessage, certificate, chain, sslPolicyErrors) => true;
}

internal RemoteCertificateValidationCallback GetPinner()
{
return PinCertificate;
}

/// <summary>
/// Pin only to specified root certificates, and reject connections to any other roots.
/// NB that the certificate and chain have already been checked, and the status of that check is available
/// in the chain ChainStatus and overall SslPolicyErrors.
/// </summary>
/// <param name="request">The actual request (unused)</param>
/// <param name="certificate">The server certificate presented to the connection</param>
/// <param name="chain">The full certificate chain presented to the connection</param>
/// <param name="sslPolicyErrors">The current result of the certificate checks</param>
/// <returns>true if the connection should be allowed, false otherwise</returns>
internal bool PinCertificate(object request,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
// If there's no server certificate or chain, fail
if (certificate == null || chain == null)
{
return false;
}

// If the regular certificate checking process failed, fail
// we want everything to be valid, but then just restrict the acceptable root certificates
if (sslPolicyErrors != SslPolicyErrors.None)
{
return false;
}

// Double check everything's valid and grab the root certificate (and double check it's valid)
if (!chain.ChainStatus.All(status => status.Status == X509ChainStatusFlags.NoError))
{
return false;
}
var chainLength = chain.ChainElements.Count;
var rootCert = chain.ChainElements[chainLength - 1].Certificate;
if (!rootCert.Verify())
{
return false;
}

// Check that the root certificate is in the allowed list
if (!_rootCerts.Contains(rootCert))
{
return false;
}

return true;
}

/// <summary>
/// Get the root certificates allowed by Duo in a usable form
/// </summary>
/// <returns>A X509CertificateCollection of the allowed root certificates</returns>
internal static X509CertificateCollection GetDuoCertCollection()
{
var certs = ReadCertsFromFile();

X509CertificateCollection coll = new X509CertificateCollection();
foreach (string oneCert in certs)
{
if (!string.IsNullOrWhiteSpace(oneCert))
{
var bytes = Encoding.UTF8.GetBytes(oneCert);
coll.Add(new X509Certificate(bytes));
}
}
return coll;
}

/// <summary>
/// Read the embedded Duo ca_certs.pem certificates file to get an array of certificate strings
/// </summary>
/// <returns>The Duo root CA certificates as strings</returns>
internal static string[] ReadCertsFromFile()
{
var certs = "";
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("duo_api_csharp.ca_certs.pem"))
using (StreamReader reader = new StreamReader(stream))
{
certs = reader.ReadToEnd();
}
var splitOn = "-----DUO_CERT-----";
return certs.Split(new string[] { splitOn }, int.MaxValue, StringSplitOptions.None);
}
}
}
49 changes: 48 additions & 1 deletion duo_api_csharp/Duo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;


namespace Duo
Expand All @@ -34,6 +36,8 @@ public class DuoApi
private string user_agent;
private SleepService sleepService;
private RandomService randomService;
private bool sslCertValidation = true;
private X509CertificateCollection customRoots = null;

/// <param name="ikey">Duo integration key</param>
/// <param name="skey">Duo secret key</param>
Expand Down Expand Up @@ -71,6 +75,32 @@ protected DuoApi(string ikey, string skey, string host, string user_agent, strin
}
}

/// <summary>
/// Disables SSL certificate validation for the API calls the client makes.
/// Incomptible with UseCustomRootCertificates since certificates will not be checked.
///
/// THIS SHOULD NEVER BE USED IN A PRODUCTION ENVIRONMENT
/// </summary>
/// <returns>The DuoApi</returns>
public DuoApi DisableSslCertificateValidation()
{
sslCertValidation = false;
return this;
}

/// <summary>
/// Override the set of Duo root certificates used for certificate pinning. Provide a collection of acceptable root certificates.
///
/// Incompatible with DisableSslCertificateValidation - if that is enabled, certificate pinning is not done at all.
/// </summary>
/// <param name="customRoots">The custom set of root certificates to trust</param>
/// <returns>The DuoApi</returns>
public DuoApi UseCustomRootCertificates(X509CertificateCollection customRoots)
{
this.customRoots = customRoots;
return this;
}

public static string FinishCanonicalize(string p)
{
// Signatures require upper-case hex digits.
Expand Down Expand Up @@ -244,6 +274,7 @@ private HttpWebRequest PrepareHttpRequest(String method, String url, String auth
String cannonParams, int timeout)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.ServerCertificateValidationCallback = GetCertificatePinner();
request.Method = method;
request.Accept = "application/json";
request.Headers.Add("Authorization", auth);
Expand Down Expand Up @@ -271,6 +302,22 @@ private HttpWebRequest PrepareHttpRequest(String method, String url, String auth
return request;
}

private RemoteCertificateValidationCallback GetCertificatePinner()
{
if (!sslCertValidation)
{
// Pinner that effectively disables cert pinning by always returning true
return CertificatePinnerFactory.GetCertificateDisabler();
}

if (customRoots != null)
{
return CertificatePinnerFactory.GetCustomRootCertificatesPinner(customRoots);
}

return CertificatePinnerFactory.GetDuoCertificatePinner();
}

private HttpWebResponse AttemptRetriableHttpRequest(
String method, String url, String auth, String date, String cannonParams, int timeout)
{
Expand Down Expand Up @@ -643,7 +690,7 @@ private static extern int WinHttpOpen([MarshalAs(UnmanagedType.LPWStr)] string p
private static extern bool WinHttpCloseHandle(int hInternet);
#endregion Private DllImport
}

[Serializable]
public class DuoException : Exception
{
Expand Down
Loading

0 comments on commit 9324259

Please sign in to comment.