65.9K
CodeProject 正在变化。 阅读更多。
Home

Halibut:.NET/Mono 的安全通信堆栈

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (7投票s)

2013年2月28日

CPOL

6分钟阅读

viewsIcon

31406

WCF 的安全替代方案。JSON-RPC 结合 SSL 结合 .NET 和 Mono。

引言

Halibut 是一个开源、安全、适用于 .NET 和 Mono 的通信堆栈。它通过 SSL 使用类 JSON-RPC 的协议,是 WCF 的轻量级替代方案。  

本文中的源代码和示例可在 GitHub 上获取

背景  

Halibut 的诞生是为了构建一个 WCF 的安全替代方案,用于 Octopus Deploy,这是一款面向 .NET 开发者的自动化部署产品。

作为一款自动化部署工具,Octopus 需要能够将软件包和配置信息推送到可能位于本地网络或云中的机器上。为了安全地做到这一点,Octopus 的用户建立了一种双向信任关系:

  • 协调部署的 Octopus 服务器需要知道它发送软件包和配置信息的目标机器是它信任的,而不是冒名顶替者。 
  • 接收和安装软件包的 Tentacle 代理需要知道它接收的软件包来自它信任的 Octopus 服务器,而不是冒名顶替者。 

为了实现这一点,Octopus 使用公钥/私钥加密。安装时,Octopus 和 Tentacle 服务器都会生成一个 X.509 证书。然后,设置每台机器的管理员会将每台机器的公钥的指纹粘贴到另一台机器上,从而建立信任。 

在 Octopus Deploy 中,我们使用这些 X.509 证书以及 WCF 的 wsHttpBinding 堆栈。建立连接时,双方会验证对方提供的证书指纹。如果公钥不是我们预期的,我们将拒绝连接。 

(您可以在我们的参考页面上阅读有关此工作原理的更多信息。) 

在未来的版本中,我们希望为 Octopus 添加 Mono 支持,以便我们可以将软件包部署到 Linux 和其他类 Unix 操作系统。但有一个问题:WCF 的 wsHttpBinding 在 Mono 上不受支持!

面对这个问题,我们需要提出一个替代通信堆栈。 

Halibut 的目标  

在构建 Halibut 时,我们考虑了几个目标: 

  • 开源: 我们认为将通信堆栈的代码开放给世界将有助于提高我们架构中如此关键部分的安全性。  
  • 安全: 我们仍然希望公钥/私钥加密成为我们安全通信堆栈的基石。  
  • 简单:我们希望编写的代码尽可能少,并且易于上手。  
  • 友好:错误消息已经够糟糕了,但安全专家尤其擅长编写用户无法理解的晦涩难懂的错误消息。我们希望确保我们堆栈中出现的任何错误都易于理解。  
  • 站在巨人的肩膀上:我们希望利用那些比我们更了解如何实现安全通信堆栈的人的代码。我们的目标只是将一些零散的组件组合成一个易于使用的包。  
  • 适用于 Mono 和 .NET:我们需要选择能在两个平台上都适用的技术。 

 经过大量实验,我们最终构建了 Halibut,并在此基础上进行开发: 

  • TcpListener/TcpClient,用于绑定到 TCP 套接字。我们使用原始 TCP 而不是 HTTP,以避免依赖 Windows 下的 HTTP.sys,因为它需要额外的权限才能使用。   
  • SslStream。此类已内置于 .NET 和 Mono 中,负责使用 SSL 加密两个服务之间的连接。它负责呈现证书和协商要使用的加密级别。我们使用 TLS 1.0。  
  • Json.NET:这已成为 .NET 和 Mono 上 JSON 的标准,因此在序列化要在网络上传输的对象时,它是合乎逻辑的选择。 使用 JSON 也意味着与使用专有二进制格式相比,我们可以更容易地处理向后兼容性和版本控制。  
建立安全连接后,Halibut 使用基于 JSON-RPC 的协议。发送一个请求,其中包含要调用的方法名称和参数。服务器处理请求并返回一个响应对象。如果发生错误,Halibut 会捕获它并返回一个错误对象,并在客户端重新抛出该错误。

展示代码! 

要开始使用 Halibut,首先为您的服务定义一个接口

public interface ICalculatorService
{
    long Add(long a, long b);
    long Subtract(long a, long b);
}  

无需为其添加任何修饰,Halibut 假定接口上的所有方法都可以用于 RPC。在您的服务器上,实现该接口: 

public class CalculatorService : ICalculatorService
{
    public long Add(long a, long b)
    {
        return a + b;
    }

    public long Subtract(long a, long b)
    {
        return a - b;
    }
} 

接下来,加载一个您的服务器将用于标识自身的证书。证书需要有公钥和私钥。为了方便创建证书,示例代码附带了一个可执行文件(Halibut.CertificateGenerator.exe),您可以这样调用它:

Halibut.CertificateGenerator.exe CN=YourApplication YourApplication.pfx  

这会创建一个包含上面示例中YourApplication标识的证书的公钥和私钥的文件。  

在服务器中加载证书

var certificate = new X509Certificate2("HalibutServer.pfx"); 

最后,启动您的服务。这里我们监听端口 8433: 

var endPoint = new IPEndPoint(IPAddress.Any, 8433);

var server = new HalibutServer(endPoint, certificate);
server.Services.Register<ICalculatorService, CalculatorService>();
server.Start();  

现在我们需要连接我们的客户端。再次使用 Halibut.CertificateGenerator.exe 生成证书,并在您的客户端上加载它: 

var certificate = new X509Certificate2("HalibutClient.pfx"); 

现在连接到服务器

var serverThumbprint = "EF3A7A69AFE0D13130370B44A228F5CD15C069BC";
var client = new HalibutClient(certificate);
var calculator = client.Create<ICalculatorService>(new Uri("rpc://" + hostName + ":8433/"), serverThumbprint);
var result = calculator.Add(12, 18); 

请注意,在上面的示例中,我们的客户端指定了服务器公钥的指纹。这样,如果服务器冒充我们不期望的服务器,我们的客户端将中止连接。  

不过,我们的服务器目前接受来自任何有效客户端的连接。我们可以通过在调用 Start() 之前向服务器添加验证回调来更改这一点

server.Options.ClientCertificateValidator = ValidateClientCertificate;

...

static CertificateValidationResult ValidateClientCertificate(X509Certificate2 clientcertificate)
{
    return clientcertificate.Thumbprint == "2074529C99D93D5955FEECA859AEAC6092741205"
        ? CertificateValidationResult.Valid
        : CertificateValidationResult.Rejected;
}  

客户端 

HalibutClient 类是一个工厂,用于创建连接到 Halibut 服务器的代理实例。当您调用

var calculator = halibutClient.Create<ICalculatorService>(...); 

HalibutClient 返回一个代理(使用 RealProxy 实现)。当在代理上调用方法时,Halibut 会连接到服务器,序列化 JSON 请求,并发送它们。它会将服务器返回的结果反序列化,并将结果作为方法调用的结果返回。如果服务器返回错误或连接失败,代理将抛出异常。  

Halibut 不使用会话,也不保持连接打开,因此每次调用都是完全无状态的。 

扩展点  

HalibutServer 类提供了一个 Options 属性,可用于自定义和扩展 Halibut: 

  • Options.ServiceFactory:如果您想控制服务的构造方式(例如,调用 IOC 容器),请实现 IServiceFactory。 构造对象,并将其返回在一个可处置对象中,该对象将在服务调用完成后被处置。  
  • Options.Serializer:返回 Halibut 使用的 Json.NET 序列化器 。使用它来自定义序列化设置。 
  • Options.ServiceInvoker:控制如何在服务上调用方法。   

日志记录

Halibut 使用跟踪源自动记录消息,并且它们与 WCF 服务跟踪查看器兼容。要启用日志记录,请在您的配置文件中添加以下内容

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <system.diagnostics>
    <sources>
      <source name="Halibut.Client" switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
          <add name="console" />
        </listeners>
      </source>
      <source name="Halibut.Server" switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
          <add name="console" />
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="Halibut.Client" value="Verbose" />
      <add name="Halibut.Server" value="Verbose" />
    </switches>
    <sharedListeners>
      <add name="xml" type="System.Diagnostics.XmlWriterTraceListener" initializeData="starling.e2e" />
      <add name="console" type="System.Diagnostics.ConsoleTraceListener" />
    </sharedListeners>
    <trace autoflush="true" />
  </system.diagnostics>
</configuration> 

Halibut 也会将跟踪活动 ID 传输到网络上,因此如果您使用上面的 XML 日志记录器,您可以将客户端和服务器的日志消息都打开到同一个服务跟踪查看器实例中,以查看合并在一起的日志消息,从而获得精美的端到端跟踪。  

总结 

Halibut 是作为 WCF 的 wsHttpBinding 的替代方案而构建的,主要是因为 wsHttpBinding 在 Mono 上不受支持,而我们需要一些东西来使用。它是开源的、简单的、安全的。我希望您也能在您的项目中使用它,或者至少,享受阅读代码的乐趣。谢谢! 

© . All rights reserved.