本文へスキップ
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
公開鍵の形式
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32
秘密鍵の形式
pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

CryptoKit は、Apple プラットフォーム上の Swift アプリがハッシュ、署名、鍵共有、AEAD、HPKE に使うフレームワークです。OS に同梱されているため、そのバージョンは OS のバージョンと同じです。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 型の値で、2 つの形式があります。JWS(ES256)と WebCrypto が使う 64 バイトの r‖s である rawRepresentation と、X.509、OpenSSL、Java が想定する DER 形式の derRepresentation です。誤った形式を送ってしまうのは相互運用でよくあるバグです。サンプルでは明示的に変換しており、DER ⇄ 生形式の変換ツールでは両方の形式を確認できます。解析は厳格で、失敗するとエラーをスローします。生のバイト列を init(derRepresentation:) に渡すと CryptoKitASN1Error がスローされ(RFC 6979 のテスト署名では truncatedASN1Field でしたが、どのケースになるかはバイト列によって変わります)、DER を init(rawRepresentation:) に渡すと incorrectParameterSize がスローされます。一方、isValidSignature は単に false を返します。

nonce はランダムです(同じメッセージの 2 つの署名は異なり、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 でも検証できました。そのため、署名のバイト列を RFC 8032 のテストベクトルと比較するテストは、macOS の CryptoKit では失敗します。代わりに署名を検証してください。正規形でない S の値は拒否されますが、単位元(identity point)は公開鍵として受け入れられ、R = 単位元、S = 0 の署名は任意のメッセージに対して検証を通りました。署名を特定の 1 人の署名者に結び付ける必要がある場合は、小さい位数の鍵を自分で拒否してください。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)として書き出せ、PKCS#8 と SEC1(PEM または DER)から読み込めますが、暗号化された PKCS#8 からは読み込めません。また、CryptoKit は JWK に対応していません。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/apple-cryptokit/run.sh が 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

    同じメッセージへの 2 つの署名は異なり、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

    init(derRepresentation:) は生の r‖s 署名と 3 バイトのデータに対して truncatedASN1Field をスローし、init(rawRepresentation:) は DER と 3 バイトのデータに対して 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

    同じ鍵による同じメッセージへの 2 つの署名は異なりましたが、どちらも検証を通り、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

    単位元(identity point)を公開鍵としてインポートでき、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

    0xab を 64 バイト並べた署名と 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

    同じメッセージへの 2 つの 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

    暗号文の 1 ビットを反転させると、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

    各型の鍵を Secure Enclave 内で生成し、この Mac で署名、検証、デカプセル化を行えました。

    チェックによる記録
    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

    PKCS#8 と SEC1(PEM または DER)、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) 上でサンプルを実行しました。

ライブラリ