登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go rsa.PSSOptions.SaltLength 该怎么选择

来源:17golang原创

时间:2026-10-04 08:37:17 433浏览 收藏

如果没有外部协议约束,Go 程序内部可以使用默认的 rsa.PSSSaltLengthAuto;如果签名需要被 Java、OpenSSL、硬件设备、网关或某个明确规范验证,通常优先选择 rsa.PSSSaltLengthEqualsHash;只有对端协议明确规定固定字节数时,才填写正整数。真正要固定的不是某个“更安全”的数字,而是签名端和验证端共同遵守的契约。

Go 标准库文档:https://pkg.go.dev/crypto/rsa

先用这张表做选择

场景推荐 SaltLength主要理由
纯 Go、两端都由同一团队控制PSSSaltLengthAuto签名使用可容纳的最大盐长,验证可自动检测
跨语言、跨厂商或协议互操作PSSSaltLengthEqualsHash盐长等于摘要长度,约定清晰,常见实现容易对齐
外部规范明确写了固定字节数对应正整数严格服从协议,不自行替换成 Auto
不知道对端要求先查协议,不猜能在 Go 中签出来不代表外部一定能验证

SaltLength 控制的是 RSASSA-PSS 编码中的随机盐长度。它不是密钥长度,也不是摘要长度本身。不同盐长可以产生合法 PSS 签名,但互操作系统未必接受所有合法取值,因此工程上首先要解决协议一致性。

三个取值分别代表什么

rsa.PSSSaltLengthAuto 的值是 0。签名时它让 Go 使用密钥和哈希允许的尽可能大的盐;验证时表示自动检测签名中的盐长。把 SignPSS 的 opts 传 nil,会使用默认选项,也就是这一类自动行为。

rsa.PSSSaltLengthEqualsHash 的值是 -1,表示盐长等于哈希输出长度。使用 SHA-256 时就是 32 字节,使用 SHA-384 时就是 48 字节。这个选项不会让摘要“更长”,只是把随机盐长度绑定到摘要长度。

正整数表示精确的盐长度字节数。例如 SaltLength: 20 表示 20 字节。这个能力主要用于适配已有协议或设备,不适合随意选择;指定值若超过当前密钥和哈希能容纳的范围,签名会失败,可能返回 rsa.ErrMessageTooLong。

Go RSA PSS 的三种 SaltLength 选项与哈希长度和密钥容量关系
图1:PSS 盐长选项结构图。Auto 取可用最大值,EqualsHash 绑定摘要长度,正整数用于明确的外部协议契约;这是静态说明图,不是运行截图。

最小可用写法:互操作优先用 EqualsHash

下面使用 SHA-256,并把签名端和验证端都设为 PSSSaltLengthEqualsHash。这样盐长明确为 32 字节,配置可以直接写进接口文档和测试向量。

package pssdemo

import (
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "fmt"
)

var pssOptions = &rsa.PSSOptions{
    // 盐长固定为摘要长度,便于和其他语言或设备对齐。
    SaltLength: rsa.PSSSaltLengthEqualsHash,
    Hash:       crypto.SHA256,
}

func Sign(priv *rsa.PrivateKey, message []byte) ([]byte, error) {
    // SignPSS 接收消息摘要,不直接接收原始消息。
    digest := sha256.Sum256(message)
    sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, digest[:], pssOptions)
    if err != nil {
        return nil, fmt.Errorf("sign pss: %w", err)
    }
    return sig, nil
}

func Verify(pub *rsa.PublicKey, message, signature []byte) error {
    // 验证端使用相同哈希和相同盐长契约,拒绝其他盐长。
    digest := sha256.Sum256(message)
    if err := rsa.VerifyPSS(pub, crypto.SHA256, digest[:], signature, pssOptions); err != nil {
        return fmt.Errorf("verify pss: %w", err)
    }
    return nil
}

PSSOptions.Hash 在 SignPSS 中非零时会覆盖函数参数里的哈希;但 VerifyPSS 会忽略 opts.Hash,验证使用的是函数参数 hash。因此不要只改 options 而忘了验证调用中的 crypto.SHA256。

默认 Auto 为什么会带来跨系统风险

在纯 Go 链路中,Auto 往往很方便:签名端使用最大可用盐长,验证端自动检测,不必额外维护数字。问题出现在签名离开 Go 服务之后。某些协议、证书链、硬件模块或其他语言库会要求盐长等于哈希长度;它们可能拒绝 Go 按密钥容量生成的更长盐,即使该签名在 Go 的自动验证模式下是合法的。

这类故障常呈现为“Go 自测通过、外部验签失败”。修复方向不是反复更换密钥,而是同时核对以下四项:

  • 签名算法是否都是 RSASSA-PSS,而不是一端用了 PKCS #1 v1.5;
  • 消息摘要算法是否一致;
  • MGF1 使用的哈希是否和对端约定一致;
  • 盐长是自动、等于哈希长度,还是一个固定正整数。

如果外部系统的文档明确要求 saltLength = digestLength,Go 端就应使用 PSSSaltLengthEqualsHash,不要把 Auto 当成“自动兼容”。Auto 只描述 Go API 的选择或检测方式,不代表对端协议会接受任意盐长。

跨语言时优先固定契约

签名数据跨服务、跨队列或跨组织流动时,应把“算法、哈希、盐长、密钥标识和签名编码”一起写成协议字段。不要只写“RSA-PSS”,因为这个名字不足以唯一确定盐长策略。

package pssdemo

import "crypto/rsa"

type SignaturePolicy struct {
    Algorithm  string
    Hash       string
    SaltLength int
    KeyID      string
}

var PolicyV1 = SignaturePolicy{
    // 协议记录语义值,业务代码再映射到 Go 常量。
    Algorithm:  "RSASSA-PSS",
    Hash:       "SHA-256",
    SaltLength: 32,
    KeyID:      "signing-key-v1",
}

func VerifyOptions(policy SignaturePolicy) *rsa.PSSOptions {
    // 外部契约写 32 字节时,验证端使用精确值而不是 Auto。
    return &rsa.PSSOptions{SaltLength: policy.SaltLength}
}
Go SignPSS 与外部验证器共享摘要算法盐长契约和 RSA 公钥的静态边界
图2:PSS 互操作边界结构图。两端必须共享摘要算法和盐长契约,验证端是否自动检测应由协议明确决定;这是静态结构图,不是运行证据。

协议中最好记录实际字节数,而不是把 Go 的特殊常量值 0 或 -1 直接传给其他语言。其他实现未必使用相同枚举;写“SHA-256、saltLength 32 bytes”比写“-1”更容易理解和复现。

验证端 Auto 和严格盐长怎么取舍

验证端使用 PSSSaltLengthAuto 时,会自动检测签名里的盐长。这适合兼容一批历史签名,或服务必须接受多个已批准的旧策略。代价是验证规则更宽:只要签名结构合法并通过公钥验证,不会因为盐长和当前推荐值不同而拒绝。

验证端使用 PSSSaltLengthEqualsHash 或精确正整数时,会把盐长纳入严格协议检查。新系统、单一协议版本、合规接口或跨组织签名通常更适合这种方式。若迁移期需要同时接受旧值和新值,建议按可信的协议版本选择对应验证函数,而不是先严格验证、失败后再无条件 Auto 重试。

package pssdemo

import (
    "crypto"
    "crypto/rsa"
    "crypto/sha256"
    "errors"
)

var ErrInvalidSignature = errors.New("invalid signature")

func VerifyV1(pub *rsa.PublicKey, message, signature []byte) error {
    // V1 契约固定 SHA-256 和 32 字节盐长,失败不降级到 Auto。
    digest := sha256.Sum256(message)
    opts := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}
    if err := rsa.VerifyPSS(pub, crypto.SHA256, digest[:], signature, opts); err != nil {
        return ErrInvalidSignature
    }
    return nil
}

对外统一返回签名无效即可,不要根据“盐长错了”“摘要错了”“密钥错了”暴露过细差异。内部日志可以记录协议版本、密钥 ID 和策略 ID,但不要记录私钥或未脱敏的敏感消息。

FIPS、密钥大小和随机源的边界

当前 Go 文档说明:使用 PSSSaltLengthAuto 签名时,普通模式会尽可能使用最大盐长;在 FIPS 140-3 模式中,盐长会被限制为哈希长度。这意味着同一份 Auto 配置在不同运行模式下可能产生不同盐长。若产物需要跨环境保持完全一致的策略,应显式选择 EqualsHash 或协议规定的正整数。

签名还受 RSA 密钥容量和哈希长度限制。更大的正整数不等于更强;当编码空间不足时,SignPSS 会失败。生产代码必须检查错误,不能在失败后退回弱算法或静默缩短盐长。

SignPSS 是随机化签名,官方文档建议大多数应用使用 crypto/rand.Reader。不要用固定字节流追求“每次签名相同”;同一消息产生不同的合法 PSS 签名是正常现象,验证结果才是判断依据。

测试要覆盖策略不匹配

最小测试矩阵应包含:EqualsHash 签名由 EqualsHash 验证成功;Auto 签名由 Auto 验证成功;若 Auto 实际生成的盐长不等于哈希长度,则严格 EqualsHash 验证应失败;消息、摘要、签名或公钥任一变化都应失败。

package pssdemo_test

import (
    "crypto/rand"
    "crypto/rsa"
    "testing"

    "example.com/pssdemo"
)

func TestEqualsHashPolicy(t *testing.T) {
    // 测试使用临时 2048 位密钥,生产密钥应由受控密钥系统管理。
    priv, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        t.Fatal(err)
    }

    message := []byte("release-manifest")
    signature, err := pssdemo.Sign(priv, message)
    if err != nil {
        t.Fatal(err)
    }

    // 同一策略必须验签成功。
    if err := pssdemo.Verify(&priv.PublicKey, message, signature); err != nil {
        t.Fatalf("verify failed: %v", err)
    }

    // 修改消息后必须验签失败,不能复用原签名。
    if err := pssdemo.Verify(&priv.PublicKey, []byte("other-manifest"), signature); err == nil {
        t.Fatal("modified message was accepted")
    }
}

跨语言项目还应保存不含私钥的测试向量:原始消息或其公开样例、哈希算法、盐长、签名、公钥和预期结果。每个消费者在升级密码库或切换 FIPS 模式后都运行同一组向量,能比线上报错更早发现策略漂移。

最终决策清单

  • 只在 Go 内部闭环且没有固定协议:可以选择 Auto。
  • 跨语言、跨厂商、合规或希望配置清晰:优先 EqualsHash。
  • 对端明确规定 N 字节:使用正整数 N,并写入协议和测试。
  • 验证端是否允许 Auto 必须由兼容策略决定,不能把失败后的自动放宽当作通用降级。
  • 哈希、MGF1、盐长、密钥和签名编码需要一起核对。
  • 切换 FIPS 运行模式时,重新确认 Auto 产生的盐长行为。

一句话总结:SaltLength 的默认值适合 Go 自己管理的链路,PSSSaltLengthEqualsHash 更适合需要稳定互操作的协议,正整数只用于明确的外部要求。签名端和验证端把同一策略写进协议、代码和测试,才是最重要的选择标准。

相关问题

SHA-256 一定要使用 32 字节盐吗?

不是密码学实现层面的唯一合法值,但在选择 EqualsHash 或外部协议要求摘要等长盐时就是 32 字节。实际项目仍以双方协议为准。

VerifyPSS 传 nil 会怎样?

会使用默认选项,盐长按 Auto 处理。若你需要严格限制为摘要长度,应显式传入 PSSSaltLengthEqualsHash。

可以为了兼容把验签失败后改用 Auto 吗?

不建议无条件这样做。更安全的做法是依据可信协议版本选择已批准的策略,并为旧策略设置明确的迁移范围和截止时间。

SaltLength 越大签名越安全吗?

不能这样简单判断。盐长受密钥和编码空间约束,还必须满足互操作协议;盲目增大可能只会造成签名失败或对端无法验证。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>