跳到主要内容
ecdsa.com
Java · Kotlin

Tink (Java)

Google 的 Java 版 Tink,围绕密钥集和模板构建的防误用密码学 API:默认签名带有其他库都无法识别的 5 字节 Tink 前缀,模板名称也看不出你得到的是哪种编码。
许可证
Apache-2.0
检查的版本
1.24.0 ·
源代码
github.com/tink-crypto/tink-java
文档
developers.google.com/tink

兼容性概览

ECDSAEdDSA

ECDSA 曲线
P-256P-384P-521
EdDSA
Ed25519
公钥格式
spki-pem

Tink 用密钥集取代单个密钥——密钥集是一组带 ID 的密钥,其中一个被标记为主密钥——并用 ECDSA_P256、ED25519 等具名模板取代算法。原语(PublicKeySign、PublicKeyVerify)会透明地轮换密钥,这正是该设计的目的,也是互操作时种种意外的根源。密钥集以 Tink JSON 或二进制 protobuf 格式存储,而不是 PEM。

默认的 ECDSA_P256 模板会在签名前加上输出前缀:0x01 和 4 字节的密钥 ID,后面才是 DER 签名。JDK 拒绝了这样的签名,报错 Invalid encoding for signature,所有非 Tink 的验证方都会如此。以 _WITHOUT_PREFIX 或 _RAW 结尾的模板会去掉前缀,但名称同样看不出编码:ECDSA_P256_RAW 生成的是 64 字节的 IEEE P1363 r‖s,而不是 DER——它用 JDK 的 SHA256withECDSAinP1363Format 验证通过,用 SHA256withECDSA 则验证失败。应按对端要求的确切编码选择模板,就像示例使用 ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX 那样,并用 DER ⇄ 原始格式转换器检查字节。

导入外部公钥要经过 SignaturePemKeysetReader,得到的密钥只能验证 DER 签名:同一个原始签名,Tink 自己的 P1363 密钥接受了它,用从 PEM 导入的密钥验证却失败,直到将其重新编码为 DER。签名使用随机 nonce,s 没有经过规范化,验证时接受 high-S;无效输入会抛出 GeneralSecurityException: invalid signature。

Java 版 Tink 1.24.0 提供 P-256、P-384 和 P-521 上的 ECDSA、Ed25519 以及 RSA(PKCS#1 v1.5 和 PSS)模板;该版本中没有注册任何 ML-DSA 或 SLH-DSA 模板(Name ML_DSA_65 does not exist)。目前要在 JVM 上使用后量子签名,可参阅 Java JCA 或 Bouncy Castle。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/tink/run.sh 于2026年10月20日在容器镜像 maven:3.9.16-eclipse-temurin-25 中运行了 Tink (Java) 1.24.0;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der

    ECDSA_P256 签名在输出前缀之后是 DER。

    检查记录
    fact ecdsa-signature-encoding = der
  • 签名前缀

    tink-5-byte

    默认模板会在签名前加上 5 字节的前缀——0x01 和密钥 ID——JDK 拒绝了这种签名,报错 Invalid encoding for signature。

    检查记录
    fact output-prefix = tink-5-byte
    evidence output-prefix: ECDSA_P256 signature starts with 0x01 followed by the 4-byte key id, then the DER signature
    evidence output-prefix: ECDSA_P256_IEEE_P1363 signature: 69 bytes (5-byte prefix + 64)
    evidence output-prefix: ECDSA_P256 signature given to the JDK with the prefix: SignatureException: Invalid encoding for signature
  • RAW 模板的编码

    ieee-p1363

    ECDSA_P256_RAW 生成 64 字节的 IEEE P1363 r‖s(无前缀,不是 DER);它用 SHA256withECDSAinP1363Format 验证通过。

    检查记录
    fact raw-template-encoding = ieee-p1363
    evidence raw-template-encoding: ECDSA_P256_RAW signature: 64 bytes, parses as DER: false
    evidence raw-template-encoding: ECDSA_P256_RAW signature verifies in the JDK's SHA256withECDSAinP1363Format: true
  • 导入 PEM 后的签名编码

    der

    通过 SignaturePemKeysetReader 从 PEM 导入的公钥只能验证 DER 签名;原始 r‖s 签名会验证失败,直到重新编码为 DER。

    检查记录
    fact pem-import-encoding = der
  • ECDSA 的 nonce

    random

    nonce 随机:同一条消息的两个签名不同。

    检查记录
    fact ecdsa-nonce = random
  • 签名时的 low-S

    either

    s 没有经过规范化:64 个签名中出现了 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = either
  • 验证时的 high-S

    accepts-high-s

    把 s 替换为 n − s 的签名验证通过。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • 签名调用对哪些内容做哈希

    hashes-message

    原语对消息签名;没有对摘要签名的 API。

    检查记录
    fact ecdsa-prehash = hashes-message
  • 格式错误的 ECDSA 签名

    throws

    格式错误的签名会抛出 GeneralSecurityException: invalid signature。

    检查记录
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: GeneralSecurityException: invalid signature
  • ECDSA 曲线

    P-256P-384P-521

    P-256、P-384 和 P-521 模板的签名与验证均成功。

    检查记录
    fact ecdsa-curves = P-256, P-384, P-521
  • EdDSA

    Ed25519

    Ed25519 模板,带输出前缀和不带输出前缀的都有。

    检查记录
    fact eddsa = Ed25519
  • 后量子算法

    none

    Java 版 Tink 1.24.0 中没有注册 ML-DSA 或 SLH-DSA 模板。

    检查记录
    fact pqc = none
    evidence pqc: ML_DSA_65: GeneralSecurityException: Name ML_DSA_65 does not exist
    evidence pqc: SLH_DSA_SHA2_128S: GeneralSecurityException: Name SLH_DSA_SHA2_128S does not exist
  • 公钥格式

    spki-pem

    通过 SignaturePemKeysetReader 读取 SPKI PEM;除此之外,密钥都保存在 Tink 密钥集中。

    检查记录
    fact public-key-formats = spki-pem
    evidence public-key-formats: SPKI PEM through SignaturePemKeysetReader, then verifying the RAW template's raw signature: GeneralSecurityException: invalid signature
    evidence public-key-formats: the same signature re-encoded as DER against the PEM-imported key: ok
    evidence public-key-formats: Tink JSON public keyset round trip: ok
  • 密钥集格式

    tink-jsontink-binary

    密钥集序列化为 Tink JSON 或二进制 protobuf;公开密钥集经 JSON 序列化后能原样读回。

    检查记录
    fact keyset-format = tink-json, tink-binary

用法

比较默认的带前缀签名与不带前缀模板生成的可移植 64 字节签名。
import com.google.crypto.tink.KeyTemplates;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.PublicKeySign;
import com.google.crypto.tink.PublicKeyVerify;
import com.google.crypto.tink.RegistryConfiguration;
import com.google.crypto.tink.signature.SignatureConfig;
import java.nio.charset.StandardCharsets;

public class Usage {
    public static void main(String[] args) throws Exception {
        SignatureConfig.register();
        byte[] message = "order #1042 approved".getBytes(StandardCharsets.UTF_8);

        // Default templates prepend a 5-byte key-id prefix that no other library expects.
        KeysetHandle tinkOnly = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256"));
        byte[] prefixed = tinkOnly.getPrimitive(RegistryConfiguration.get(), PublicKeySign.class).sign(message);

        // For signatures other systems verify, pick a *_WITHOUT_PREFIX / *_RAW template.
        KeysetHandle portable = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256_IEEE_P1363_WITHOUT_PREFIX"));
        byte[] raw = portable.getPrimitive(RegistryConfiguration.get(), PublicKeySign.class).sign(message);
        portable.getPublicKeysetHandle().getPrimitive(RegistryConfiguration.get(), PublicKeyVerify.class).verify(raw, message);

        System.out.println("prefixed first byte: 0x0" + prefixed[0] + ", portable signature: " + raw.length + " bytes, verified");
    }
}

代码片段测试工具会在容器镜像 maven:3.9.16-eclipse-temurin-25 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
mvn -q -B dependency:copy-dependencies -DoutputDirectory=lib >/dev/null
命令
java -cp 'lib/*' Usage.java 2>/dev/null
输出必须匹配(正则表达式)
prefixed first byte: 0x01, portable signature: 64 bytes, verified

库