让我们在Golang中实现Stripe Webhook签名
#go #webhooks

注意:本文最初出现在GetConvoy Blog

构建Webhook发布基础架构需要提供一种验证消息完整性的方法,以使消费者能够验证Webhook事件原点。生成Webhook签名将要求我们实现某些重要功能,其中包括:

  1. 防止重播攻击
  2. 正向兼容性。
  3. 零停机键旋转。

稍后再进行更多内容。但是这些属性存在于Stripe的Webhook签名实现中,请参见下文:

Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39

在本文中,我们将在Golang中复制此实施,并需要另外一项要求;我们希望我们的实施与共同实现相兼容,如下:

Stripe-Signature: 
5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

这种向后兼容性使新的API消费者可以选择进入此新系统。出于本文的目的,我们将后者的签名规范定义为简单的签名,而前者则将后者定义为高级签名。实施简单的签名是很琐碎的,但很常见,但是高级签名很常见。

我们首先分解要求进一步讨论高级签名的要求。

防止重播攻击

Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39

当攻击者拦截有效的有效载荷及其签名时,就会发生重播攻击,然后重新传输它们。目的是利用毫无戒心的Webhook消费者多次执行动作。对这样的攻击的态度足够,因为webhook是瞬态数据,假设消费者在一定时间段内清除其webhook日志,这意味着重新传输清除的webhook事件突然变得有效。

为了减轻这种情况,我们会生成一个时间戳并将其包含在签名有效载荷中,因此可以与签名旁边进行验证,因此攻击者在不使签名无效的情况下无法更改时间戳。这确保了给定阈值后的事件被认为是无效的。

重试事件时,每次交付尝试都应重新生成时间戳,这确保时间戳是新鲜的。

正向兼容性

Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39

Webhooks实现会随着时间的推移而演变,提供商可以确定是否要切换到hexbase64进行编码或更改签名的有效载荷的模板或更改有效载荷的模板。为了启用用于消费者的平滑升级,我们在上面的示例中版本签名。这使消费者可以在方便的情况下至少验证至少一个签名并迁移到最新版本。

零停机时间钥匙旋转

Stripe-Signature:
t=1492774577,
v1=xdz+2j9aMVQUUjSy0KUz/CsjD4jaD6wHJGGf1c3eZzrWxHTf1cAjZ3aL07O9NZXMhg5gajfi+TYuBU1aoU18xA==,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=cvt+CsjD4jaD6wHJGGf1/2j9aMVQUUjSy0KUzc3eZzrWxHTf1cTYuBU1aoU18xAAjZ3aL07O9+NZXMhg5gajfi==
v0=df51a848684dac3901d2b8bd17e5c8d2d971b15c544fa923493232df1fe0fbad

webhooks依靠一个共享的秘密,需要定期旋转以确保安全。建立有效的关键旋转机制是伟大的网络钩实现的主要实施。在上面的示例中,您可以看到v1v0出现两次,我们使用了两个秘密来生成两个不同的方案。

核心实施

核心实现因此:

type Scheme struct {
    // Secret represents a list of active secrets used for
    // a scheme. It is used to implement rolled secrets.
    // Its order is irrelevant.
    Secret []string

    Hash     string
    Encoding string
}

type Signature struct {
    Payload json.RawMessage

    // The order of these Schemes is a core part of this API.
    // We use the index as the version number. That is:
    // Index 1 = v1, Index 2 = v2
    Schemes []Scheme

    // This flag allows for backward-compatible implementation
    // of this type. You're either generating a simple header
    // or a complex header.
    Advanced bool

    // This function is used to generate a timestamp for signing
    // your payload. It was only added to aid testing.
    generateTimestampFn func() string
}

func (s *Signature) ComputeHeaderValue() (string, error) {
    // Encode Payload
    tBuf, err := s.encodePayload()
    if err != nil {
        return "", err
    }

    // Generate Simple Signatures
    if !s.Advanced {
        sch := s.Schemes[len(s.Schemes)-1]
        sec := sch.Secret[len(sch.Secret)-1]

        sig, err := s.generateSignature(sch, sec, tBuf)
        if err != nil {
            return "", err
        }

        return sig, nil
    }

    // Generate Advanced Signatures
    var signedPayload strings.Builder
    var hStr strings.Builder
    var ts string

    // Add timestamp.
    if s.generateTimestampFn != nil {
        ts = s.generateTimestampFn()
    } else {
        ts = fmt.Sprintf("%d", time.Now().Unix())
    }

    // Generate Payload
    signedPayload.WriteString(ts)
    signedPayload.WriteString(",")
    signedPayload.WriteString(string(tBuf))

    // Generate Header
    tPrefix := fmt.Sprintf("t=%s", ts)
    hStr.WriteString(tPrefix)

    for k, sch := range s.Schemes {
        v := fmt.Sprintf(",v%d=", k+1)

        var hSig string
        for _, sec := range sch.Secret {
            sig, err := s.generateSignature(sch, sec, []byte(signedPayload.String()))
            if err != nil {
                return "", err
            }

            hSig = fmt.Sprintf("%s%s", v, sig)
            hStr.WriteString(hSig)
        }
    }

    return hStr.String(), nil
}

让我们分解上述代码列表:

  1. 我们使用Advanced标志来确定要生成哪种类型的签名。
  2. 我们使用Scheme类型封装所有版本,并在确定其版本中通过的顺序。我们映射到索引0到v1等,类似于我们如何将API版本定义为/api/v0
  3. ComputeHeaderValue将生成一个简单的签名字符串或基于Advanced标志的高级签名字符串。

此库的其他方面是为了简洁而被删除的,您可以找到完整的代码here

SDK

为了启用易于迁移,我们将Webhook验证逻辑添加到我们的RubyPythonGolang SDK中以解析和验证这种格式。此验证将自动识别简单或高级签名并分别验证它们。

与Webhook Secrets单独的API键

高级签名的另一个有用的好处是,我们可以停止使用API​​密钥作为零停机时间的Webhooks秘密。这很好,因为受损的Webhook秘密并不等于受损的API键,反之亦然。这也被称为特权的原则。要实现这一目标,请执行以下操作:

  1. 创建端点,并将Webhook Secret设置为API键。
  2. 更新您的应用程序以验证高级签名。
  3. 在有期的时间内滚动当前的Webhook Secret。
  4. 在您的应用程序中设置新的Webhook秘密。 ð

结论

在本文中,我们展示了如何实现类似条纹的网络钩,并以您当前的实现方式以向后兼容的方式构建它们。我们将此功能运送到车队Oss和Cloud。您可以注册here开始!