跳到主要内容
ecdsa.com
Swift

Apple CryptoKit

Apple CryptoKit,iOS 和 macOS 内置的 Swift 密码学框架:ECDSA 签名兼有原始和 DER 两种形式,Ed25519 签名是随机化的,自 26 版起支持 ML-KEM、ML-DSA 和 X-Wing,Secure Enclave 密钥在 API 中只以不透明句柄的形式出现。
许可证
Proprietary (part of Apple's operating systems)
检查的版本
macOS 27.0.1 ·
源代码
developer.apple.com/documentation/cryptokit
文档
developer.apple.com/documentation/cryptokit

兼容性概览

ECDSAEdDSA后量子

ECDSA 曲线
P-256P-384P-521
EdDSA
Ed25519
后量子算法
ML-KEMML-DSAX-Wing
公钥格式
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32
私钥格式
pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

CryptoKit 是 Apple 平台上的 Swift 应用用于哈希、签名、密钥协商、AEAD 和 HPKE 的框架。它随操作系统一起发布,因此其版本就是系统版本;SDK 标注 P-256 自 iOS 13 和 macOS 10.15 起可用,ML-KEM、ML-DSA 和 X-Wing 自 iOS 26 和 macOS 26 起可用。本页的事实是在一台搭载 Apple 芯片的 Mac 上用 macOS 27.0.1 和 Swift 6.4 检查的,两个示例也都在这台机器上编译并运行过;iOS 未经检查。在 Linux 上,同样的 API 由 swift-crypto 提供。

P-256 签名是一个 ECDSASignature 值,有两种形式:rawRepresentation,即 JWS(ES256)和 WebCrypto 使用的 64 字节 r‖s;derRepresentation,即 X.509、OpenSSL 和 Java 要求的 DER 形式。发送错误的形式是常见的互操作缺陷;示例会显式转换,DER ⇄ 原始格式转换器则能同时显示两种形式。解析很严格,出错时会抛出异常:把原始字节传给 init(derRepresentation:) 会抛出 CryptoKitASN1Error——对于 RFC 6979 的测试签名是 truncatedASN1Field,不过具体是哪种错误取决于字节内容;把 DER 传给 init(rawRepresentation:) 会抛出 incorrectParameterSize,而 isValidSignature 只会返回 false。

nonce 是随机的——同一条消息的两个签名并不相同,用 RFC 6979 的测试密钥也没有得到 RFC 中的 r——而且 s 没有经过规范化:会出现 high-S 值,把 s 替换为 n − s 的签名也能验证通过。signature(for: data) 会用与曲线匹配的哈希算法(SHA-256、SHA-384、SHA-512)计算哈希,但 signature(for: digest) 接受任何摘要类型:在 P-384 密钥上,SHA-256 摘要可以签名并验证通过;在 P-256 上,SHA-512 摘要则被截断为前 32 字节,Security.framework 证实了这一点。预哈希时,应自行传入匹配的摘要。

Ed25519(Curve25519.Signing)签名是随机化的:使用 RFC 8032 的测试密钥时,CryptoKit 对测试消息的签名既不同于 RFC 中的签名,也不同于它自己上一次生成的签名,但两次生成的签名都能验证通过,用 Node.js 验证也是如此。因此,在 macOS 上把签名字节与 RFC 8032 测试向量比较的测试,用 CryptoKit 运行时会失败;应改为验证签名。非规范的 S 值会被拒绝,但单位元被接受为公钥,而且 R 为单位元、S = 0 的签名对任意消息都能验证通过。如果签名必须绑定到某个特定的签名者,就要自行拒绝小阶密钥。Ed25519 密钥只以 32 个原始字节的形式存在,没有 PEM 或 DER 形式。

后量子类型有 ML-KEM-768 和 ML-KEM-1024、ML-DSA-65 和 ML-DSA-87——没有 ML-KEM-512、ML-DSA-44 或 SLH-DSA——以及混合 KEM X-Wing(XWingMLKEM768X25519),后者也作为 HPKE 密码套件提供。ML-DSA 签名采用对冲方式,并接受 FIPS 204 上下文字符串,验证者必须提供相同的上下文字符串。对损坏的密文做 ML-KEM 解封装时,会返回一个不同的密钥,而不是抛出异常。私钥只以种子的形式存储——ML-KEM 为 64 字节,ML-DSA 为 32 字节——或者以带完整性校验的种子形式存储,因此无法导入其他库导出的展开形式私钥。可以在 ML-KEM 工具和后量子签名工具中试用这些方案。

SecureEnclave.P256.Signing.PrivateKey() 会在 Secure Enclave 内部创建密钥;在这台 Mac 上,SecureEnclave 下的 ML-KEM 和 ML-DSA 类型也是如此。这些类型不提供原始、DER 或 PEM 格式的私钥——只提供 dataRepresentation,即一个能重新打开该密钥的句柄——而公钥是普通的 P256.Signing.PublicKey 或 MLDSA65.PublicKey。软件 P-256 密钥可导出为 PKCS#8(derRepresentation、pemRepresentation),也可从 PEM 或 DER 格式的 PKCS#8 和 SEC1 导入,但不能从加密的 PKCS#8 导入;CryptoKit 也不支持 JWK。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/apple-cryptokit/run.sh 于2026年10月20日在 macOS 27.0.1 (Swift 6.4) 上运行了 Apple CryptoKit macOS 27.0.1;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    structured

    ECDSASignature 有 64 字节的 rawRepresentation 和 derRepresentation;它的字节(withUnsafeBytes)就是原始形式。

    检查记录
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: signature(for:) returns P256.Signing.ECDSASignature: rawRepresentation 64 bytes, derRepresentation starts with 0x30 and converts back: yes, withUnsafeBytes yields the raw form: yes
  • ECDSA 的 nonce

    random

    同一条消息的两个签名不同;用 RFC 6979 A.2.5 中的密钥和消息签名,也没有得到 RFC 中的 r。

    检查记录
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two signatures of one message are different; r for the RFC 6979 A.2.5 key and message equals the RFC's r: no
  • 签名时的 low-S

    either

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

    检查记录
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • 验证时的 high-S

    accepts-high-s

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

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: isValidSignature with s replaced by n - s: true
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    signature(for: data) 会先计算哈希;signature(for: SHA256.hash(data: message)) 生成的签名针对原消息验证通过。

    检查记录
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: signature(for: SHA256.hash(data: message)) verifies with isValidSignature(_:for: message): yes
  • 对消息使用的哈希

    curve-matched

    对数据生成的签名可以作为对哈希值的签名验证通过:P-256 对应 SHA-256,P-384 对应 SHA-384,P-521 对应 SHA-512。

    检查记录
    fact ecdsa-message-hash = curve-matched
    evidence ecdsa-message-hash: signature(for: data) verifies as a signature over SHA-256 on P-256: yes, SHA-384 on P-384: yes, SHA-512 on P-521: yes
  • 哈希与曲线绑定

    none

    P-384 密钥对 SHA-256 摘要签名,签名验证通过。

    检查记录
    fact ecdsa-hash-curve-binding = none
    evidence ecdsa-hash-curve-binding: a P-384 key signed a SHA-256 digest, and the signature verifies: yes
  • 摘要长于曲线的阶

    truncated

    P-256 密钥对 SHA-512 摘要签名;Security.framework 把该签名作为对摘要前 32 字节的签名验证通过。

    检查记录
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: a P-256 key signed a SHA-512 digest; Security.framework verifies the signature over the digest's first 32 bytes: yes
  • 格式错误的 ECDSA 签名

    throws

    对于原始 r‖s 签名和 3 个字节的输入,init(derRepresentation:) 抛出 truncatedASN1Field;对于 DER 和 3 个字节的输入,init(rawRepresentation:) 抛出 incorrectParameterSize。

    检查记录
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: ECDSASignature(derRepresentation:) with the RFC 6979 A.2.5 signature as raw r||s: threw CryptoKitASN1Error.truncatedASN1Field; with 3 bytes: threw CryptoKitASN1Error.truncatedASN1Field
    evidence ecdsa-malformed-signature: ECDSASignature(rawRepresentation:) with DER: threw CryptoKitError.incorrectParameterSize; with 3 bytes: threw CryptoKitError.incorrectParameterSize
    evidence ecdsa-malformed-signature: isValidSignature for a valid signature over another message: false
  • ECDSA 曲线

    P-256P-384P-521

    P-256、P-384 和 P-521 的签名与验证均成功;SDK 未声明其他 ECDSA 曲线。

    检查记录
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: SDK 27.0 CryptoKit.swiftinterface top-level key types: Curve25519 MLDSA65 MLDSA87 MLKEM1024 MLKEM768 P256 P384 P521 XWingMLKEM768X25519
  • EdDSA

    Ed25519

    Curve25519.Signing(Ed25519);SDK 未声明 Ed448 类型。

    检查记录
    fact eddsa = Ed25519
  • 确定性 Ed25519 签名

    no

    用同一个密钥对同一条消息生成的两个签名不同;两者都能验证通过,用 node:crypto 验证也一样。

    检查记录
    fact ed25519-deterministic = no
    evidence ed25519-deterministic: RFC 8032 test 1: the derived public key matches: yes, the RFC signature verifies: yes
    evidence ed25519-deterministic: CryptoKit's signature with the same key and message equals the RFC signature: no; two CryptoKit signatures are identical: no; both verify: yes
    evidence ed25519-deterministic: node:crypto (OpenSSL) verifies the CryptoKit signature: yes
  • S 值非规范的 Ed25519 签名

    rejected

    把 RFC 8032 测试 1 的签名中的 S 替换为 S + L 后,该签名被拒绝。

    检查记录
    fact ed25519-non-canonical-s = rejected
    evidence ed25519-non-canonical-s: the RFC 8032 test 1 signature with S replaced by S + L
  • Ed25519 小阶公钥

    accepted

    单位元可以作为公钥导入,R 为单位元且 S = 0 的签名验证通过。

    检查记录
    fact ed25519-small-order-key = accepted
    evidence ed25519-small-order-key: the identity point as public key: import accepted; signature R = identity, S = 0 verifies: yes
  • 格式错误的 Ed25519 签名

    rejected

    由 64 个 0xab 字节构成的签名和 63 字节的签名都返回 false。

    检查记录
    fact ed25519-malformed-signature = rejected
    evidence ed25519-malformed-signature: 64 bytes of 0xab: false; 63 bytes of the RFC signature: false
  • 后量子算法

    ML-KEMML-DSAX-Wing

    ML-KEM 和 X-Wing 封装后都能解封装出相同的密钥,ML-DSA 签名验证通过;X-Wing 经由 HPKE 也能正常工作。

    检查记录
    fact pqc = ML-KEM, ML-DSA, X-Wing
    evidence pqc: ML-KEM-768: public key 1184 bytes, ciphertext 1088 bytes, shared secrets match: yes
    evidence pqc: ML-KEM-1024: public key 1568 bytes, ciphertext 1568 bytes, shared secrets match: yes
    evidence pqc: ML-DSA-65: public key 1952 bytes, signature 3309 bytes, verifies: yes; ML-DSA-87: signature 4627 bytes, verifies: yes
    evidence pqc: X-Wing (XWingMLKEM768X25519): public key 1216 bytes, ciphertext 1120 bytes, shared secrets match: yes
    evidence pqc: HPKE with XWingMLKEM768X25519_SHA256_AES_GCM_256 opens its own message: yes
  • ML-KEM 参数集

    ML-KEM-768ML-KEM-1024

    MLKEM768 和 MLKEM1024 封装后都能解封装出相同的密钥;SDK 未声明 ML-KEM-512 类型。

    检查记录
    fact ml-kem-parameter-sets = ML-KEM-768, ML-KEM-1024
  • ML-DSA 参数集

    ML-DSA-65ML-DSA-87

    MLDSA65 和 MLDSA87 的签名与验证均成功;SDK 未声明 ML-DSA-44 类型。

    检查记录
    fact ml-dsa-parameter-sets = ML-DSA-65, ML-DSA-87
  • ML-DSA 签名模式

    hedged

    同一条消息的两个 ML-DSA-65 签名不同(对冲签名)。

    检查记录
    fact ml-dsa-signing = hedged
    evidence ml-dsa-signing: two ML-DSA-65 signatures of one message are different
  • ML-DSA 上下文字符串

    supported

    使用上下文 invoice-v1 生成的签名,用该上下文验证通过,用 invoice-v2 或不带上下文验证则失败。

    检查记录
    fact ml-dsa-context-string = supported
    evidence ml-dsa-context-string: signed with context "invoice-v1": verifies with it yes, with "invoice-v2" no, without a context no
  • 格式错误的 ML-DSA 签名

    returns-false

    截断的签名、3 个字节的输入和错误的消息都返回 false。

    检查记录
    fact ml-dsa-malformed-signature = returns-false
    evidence ml-dsa-malformed-signature: isValidSignature: truncated signature false, 3 bytes false, wrong message false
  • 被修改的 ML-KEM 密文

    implicit-rejection

    翻转密文中的一位后,decapsulate 返回的密钥与发送方的不同。

    检查记录
    fact ml-kem-modified-ciphertext = implicit-rejection
    evidence ml-kem-modified-ciphertext: ML-KEM-768 with one ciphertext bit flipped: decapsulate returned a key that differs from the sender's
  • 后量子私钥

    seed-only

    seedRepresentation 对 ML-KEM-768 为 64 字节,对 ML-DSA-65 为 32 字节,由它重建的密钥与原密钥一致;唯一的另一个私钥初始化器接受带完整性校验的种子。

    检查记录
    fact pq-private-keys = seed-only
    evidence pq-private-keys: ML-KEM-768 seedRepresentation 64 bytes, integrityCheckedRepresentation 96 bytes; ML-DSA-65 seedRepresentation 32 bytes, integrityCheckedRepresentation 64 bytes
    evidence pq-private-keys: keys rebuilt from the seed alone have the same public key: ML-KEM-768 yes, ML-DSA-65 yes
    evidence pq-private-keys: MLKEM768.PrivateKey initializers in the SDK: init() init<D>(integrityCheckedRepresentation: init<D>(seedRepresentation:
    evidence pq-private-keys: MLDSA65.PrivateKey initializers in the SDK: init() init<D>(integrityCheckedRepresentation: init<D>(seedRepresentation:
  • Secure Enclave 支持的算法

    P-256ML-KEM-768ML-KEM-1024ML-DSA-65ML-DSA-87

    在这台 Mac 上,每种类型的密钥都在 Secure Enclave 中生成,并完成了签名、验证或解封装。

    检查记录
    fact secure-enclave-algorithms = P-256, ML-KEM-768, ML-KEM-1024, ML-DSA-65, ML-DSA-87
    evidence secure-enclave-algorithms: SecureEnclave.isAvailable: true
    evidence secure-enclave-algorithms: P-256: signature verifies with the public key: yes, two signatures differ: yes, a key re-created from dataRepresentation signs for the same public key: yes
    evidence secure-enclave-algorithms: SecureEnclave key types in the SDK: MLDSA65 MLDSA87 MLKEM1024 MLKEM768 P256
  • 公钥格式

    spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32

    P-256 的 SPKI PEM 与 DER、SEC1 未压缩与压缩形式和原始 x‖y,以及 Ed25519 的 32 个原始字节,均能写出并读回。

    检查记录
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, raw-xy, raw-32
    evidence public-key-formats: P-256 public key sizes: raw 64, x963 65, compressed 33, DER 91; PEM header -----BEGIN PUBLIC KEY-----; Ed25519 raw 32
    evidence public-key-formats: written and read back: spki-pem yes, spki-der yes, sec1-uncompressed yes, sec1-compressed yes, raw-xy yes, raw-32 yes
    evidence public-key-formats: key representations declared on the Curve25519 types: rawRepresentation; lines mentioning JWK in the SDK interface: 0
  • 私钥格式

    pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

    可读取 PEM 或 DER 格式的 PKCS#8 和 SEC1、原始 P-256 标量和 Ed25519 种子;写出 PKCS#8;Secure Enclave 密钥是句柄。

    检查记录
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, sec1-der, raw-scalar, seed-32, hardware-handle
    evidence private-key-formats: P-256 private key written as: derRepresentation PKCS#8 (138 bytes), pemRepresentation -----BEGIN PRIVATE KEY-----, rawRepresentation 32 bytes, x963Representation 97 bytes
    evidence private-key-formats: OpenSSL-written P-256 keys read: pkcs8-pem yes, pkcs8-der yes, encrypted-pkcs8 no, sec1-pem yes, sec1-der yes; encrypted PKCS#8: threw CryptoKitASN1Error.invalidPEMDocument
    evidence private-key-formats: key representations declared inside the SecureEnclave types: dataRepresentation

用法

用 P-256 密钥签名,为 JOSE 取原始形式、为使用 X.509 的对端取 DER 形式,并验证从 DER 重建的签名。
import CryptoKit
import Foundation

let key = P256.Signing.PrivateKey()
let message = Data("order #1042 approved".utf8)
let signature = try key.signature(for: message) // SHA-256 for P-256, random nonce

// JWS (ES256) and WebCrypto use the 64-byte r||s form; X.509, OpenSSL and Java use DER.
let jose = signature.rawRepresentation
let der = signature.derRepresentation

// Rebuild the signature from the form that arrived, then verify it.
let received = try P256.Signing.ECDSASignature(derRepresentation: der)
let valid = key.publicKey.isValidSignature(received, for: message)
print("raw bytes: \(jose.count), DER tag: 0x\(String(der[0], radix: 16)), verified: \(valid)")
// Other platforms import the public key as SPKI PEM.
print(key.publicKey.pemRepresentation.split(separator: "\n")[0])
在 Secure Enclave 中创建 P-256 密钥,通过句柄重新打开它,签名,再用普通公钥验证。
import CryptoKit
import Foundation

guard SecureEnclave.isAvailable else { fatalError("this device has no Secure Enclave") }
// The private key is generated inside the Secure Enclave; dataRepresentation is
// an opaque handle to store and reopen it, not the key itself.
let key = try SecureEnclave.P256.Signing.PrivateKey()
let reopened = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: key.dataRepresentation)

let message = Data("order #1042 approved".utf8)
let signature = try reopened.signature(for: message)
// Verifiers need only the ordinary P-256 public key.
let verifier = try P256.Signing.PublicKey(derRepresentation: key.publicKey.derRepresentation)
print("Secure Enclave signature verified: \(verifier.isValidSignature(signature, for: message))")

没有容器镜像提供此平台;这些示例于2026年10月20日在 macOS 27.0.1 (Swift 6.4) 上运行。

库