Saltar al contenido
ecdsa.com
Swift

Apple CryptoKit

Apple CryptoKit, el framework criptográfico de Swift integrado en iOS y macOS: firmas ECDSA con forma en bruto y DER a la vez, firmas Ed25519 aleatorizadas, ML-KEM, ML-DSA y X-Wing desde las versiones 26, y claves del Secure Enclave que la API solo expone como referencias opacas (handles).
Licencia
Proprietary (part of Apple's operating systems)
Versión comprobada
macOS 27.0.1 ·
Código fuente
developer.apple.com/documentation/cryptokit
Documentación
developer.apple.com/documentation/cryptokit

Compatibilidad de un vistazo

ECDSAEdDSACriptografía poscuántica

Curvas ECDSA
P-256P-384P-521
EdDSA
Ed25519
Algoritmos poscuánticos
ML-KEMML-DSAX-Wing
Formatos de clave pública
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32
Formatos de clave privada
pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

CryptoKit es el framework que usan las apps de Swift en las plataformas de Apple para hashes, firmas, acuerdo de claves, AEAD y HPKE. Viene con el sistema operativo, así que su versión es la del sistema; el SDK marca P-256 como disponible desde iOS 13 y macOS 10.15, y ML-KEM, ML-DSA y X-Wing desde iOS 26 y macOS 26. Los datos de esta página se comprobaron en macOS 27.0.1 con Swift 6.4 en un Mac con Apple silicon, y los dos ejemplos se compilaron y ejecutaron allí; iOS no se comprobó. En Linux, la misma API la proporciona swift-crypto.

Una firma P-256 es un valor ECDSASignature con dos formas: rawRepresentation, el r‖s de 64 bytes que usan JWS (ES256) y WebCrypto, y derRepresentation, la forma DER que esperan X.509, OpenSSL y Java. Enviar la forma equivocada es un error de interoperabilidad frecuente; el ejemplo convierte de forma explícita, y el conversor DER ⇄ en bruto muestra ambas. El análisis es estricto y lanza errores: los bytes en bruto pasados a init(derRepresentation:) lanzan un CryptoKitASN1Error —truncatedASN1Field con la firma de prueba del RFC 6979, aunque el caso concreto depende de los bytes—, y el DER pasado a init(rawRepresentation:) lanza incorrectParameterSize, mientras que isValidSignature simplemente devuelve false.

Los nonces son aleatorios —dos firmas de un mismo mensaje salieron distintas, y la clave de prueba del RFC 6979 no produjo la r del RFC— y s no se normaliza: aparecen valores high-S, y una firma con s sustituido por n − s se verificó. signature(for: data) calcula el hash con la función que corresponde a la curva (SHA-256, SHA-384, SHA-512), pero signature(for: digest) acepta cualquier tipo de resumen: un resumen SHA-256 se firmó y se verificó con una clave P-384, y un resumen SHA-512 firmado con P-256 se truncó a sus primeros 32 bytes, lo que confirmó Security.framework. Si calculas tú el hash (pre-hash), encárgate de pasar el resumen que corresponde a la curva.

Las firmas Ed25519 (Curve25519.Signing) son aleatorizadas: con la clave de prueba del RFC 8032, la firma de CryptoKit del mensaje de prueba difirió de la del RFC y de su propia firma anterior, y aun así ambas se verificaron, también con Node.js. Por eso, las pruebas que comparan los bytes de la firma con los vectores de prueba del RFC 8032 fallan con CryptoKit en macOS; verifica las firmas en su lugar. Los valores S no canónicos se rechazan, pero el punto identidad se aceptó como clave pública, y una firma con R = identidad y S = 0 se verificó para un mensaje arbitrario. Rechaza tú mismo las claves de orden pequeño cuando una firma deba vincularse a un firmante concreto. Las claves Ed25519 solo existen como 32 bytes en bruto; no tienen forma PEM ni DER.

Los tipos poscuánticos son ML-KEM-768 y ML-KEM-1024, ML-DSA-65 y ML-DSA-87 —no hay ML-KEM-512, ML-DSA-44 ni SLH-DSA— y el KEM híbrido X-Wing (XWingMLKEM768X25519), que también se ofrece como conjunto de cifrado de HPKE. La firma ML-DSA es hedged (reforzada con aleatoriedad) y acepta una cadena de contexto de FIPS 204, que el verificador debe repetir. La desencapsulación ML-KEM de un texto cifrado dañado devuelve una clave distinta en lugar de lanzar un error. Las claves privadas se guardan solo como semillas —64 bytes para ML-KEM, 32 para ML-DSA— o como la semilla con una comprobación de integridad, así que no se puede importar una clave privada expandida exportada por otra biblioteca. Prueba los esquemas en la herramienta de ML-KEM y en la herramienta de firmas poscuánticas.

SecureEnclave.P256.Signing.PrivateKey() crea una clave dentro del Secure Enclave, y en este Mac también lo hicieron los tipos ML-KEM y ML-DSA de SecureEnclave. Estos tipos no exponen ninguna clave privada en bruto, DER ni PEM —solo dataRepresentation, una referencia (handle) con la que se volvió a abrir la clave—, mientras que la clave pública es un P256.Signing.PublicKey o un MLDSA65.PublicKey normal. Las claves P-256 de software se exportan como PKCS#8 (derRepresentation, pemRepresentation) y se importan desde PKCS#8 y SEC1, en PEM o DER, pero no desde PKCS#8 cifrado, y CryptoKit no admite JWK.

Datos y las comprobaciones que los respaldan

Nada de lo que sigue está copiado de la documentación. La comprobación content/libraries/verify/apple-cryptokit/run.sh ejecutó Apple CryptoKit macOS 27.0.1 en macOS 27.0.1 (Swift 6.4) el 20 de octubre de 2026; la página debe coincidir con lo que imprimió, y cada dato cita esas líneas.

  • Codificación de la firma ECDSA

    structured

    ECDSASignature tiene un rawRepresentation de 64 bytes y un derRepresentation; sus bytes (withUnsafeBytes) son la forma en bruto.

    Registrado por la comprobación
    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
  • Nonce de ECDSA

    random

    Dos firmas de un mismo mensaje fueron distintas, y la clave y el mensaje de A.2.5 del RFC 6979 no produjeron la r del RFC.

    Registrado por la comprobación
    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 al firmar

    either

    s no se normaliza: aparecieron valores high-S entre 64 firmas.

    Registrado por la comprobación
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • High-S al verificar

    accepts-high-s

    Se verificó una firma con s sustituido por n − s.

    Registrado por la comprobación
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: isValidSignature with s replaced by n - s: true
  • De qué calcula el hash la llamada de firma

    hashes-message-or-digest

    signature(for: data) calcula el hash; signature(for: SHA256.hash(data: message)) se verificó contra el mensaje.

    Registrado por la comprobación
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: signature(for: SHA256.hash(data: message)) verifies with isValidSignature(_:for: message): yes
  • Hash aplicado a los mensajes

    curve-matched

    Las firmas sobre datos se verificaron como firmas sobre SHA-256 con P-256, SHA-384 con P-384 y SHA-512 con P-521.

    Registrado por la comprobación
    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
  • Hash vinculado a la curva

    none

    Una clave P-384 firmó un resumen SHA-256, y la firma se verificó.

    Registrado por la comprobación
    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
  • Resumen más largo que el orden de la curva

    truncated

    Una clave P-256 firmó un resumen SHA-512; Security.framework verificó la firma sobre los primeros 32 bytes del resumen.

    Registrado por la comprobación
    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
  • Firma ECDSA mal formada

    throws

    init(derRepresentation:) lanzó truncatedASN1Field con una firma r‖s en bruto y con 3 bytes; init(rawRepresentation:) lanzó incorrectParameterSize con DER y con 3 bytes.

    Registrado por la comprobación
    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
  • Curvas ECDSA

    P-256P-384P-521

    Con P-256, P-384 y P-521 se firmó y se verificó; el SDK no declara ninguna otra curva ECDSA.

    Registrado por la comprobación
    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); el SDK no declara ningún tipo Ed448.

    Registrado por la comprobación
    fact eddsa = Ed25519
  • Firmas Ed25519 deterministas

    no

    Dos firmas de un mismo mensaje con la misma clave fueron distintas; ambas se verificaron, también con node:crypto.

    Registrado por la comprobación
    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
  • Firma Ed25519 con S no canónico

    rejected

    Se rechazó la firma de la prueba 1 del RFC 8032 con S sustituido por S + L.

    Registrado por la comprobación
    fact ed25519-non-canonical-s = rejected
    evidence ed25519-non-canonical-s: the RFC 8032 test 1 signature with S replaced by S + L
  • Clave pública Ed25519 de orden pequeño

    accepted

    El punto identidad se importó como clave pública, y una firma con R = identidad y S = 0 se verificó.

    Registrado por la comprobación
    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
  • Firma Ed25519 mal formada

    rejected

    64 bytes de 0xab y una firma de 63 bytes devolvieron false.

    Registrado por la comprobación
    fact ed25519-malformed-signature = rejected
    evidence ed25519-malformed-signature: 64 bytes of 0xab: false; 63 bytes of the RFC signature: false
  • Algoritmos poscuánticos

    ML-KEMML-DSAX-Wing

    Las encapsulaciones ML-KEM y X-Wing superaron la prueba de ida y vuelta y las firmas ML-DSA se verificaron; X-Wing también funcionó a través de HPKE.

    Registrado por la comprobación
    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
  • Conjuntos de parámetros de ML-KEM

    ML-KEM-768ML-KEM-1024

    MLKEM768 y MLKEM1024 superaron la prueba de ida y vuelta; el SDK no declara ningún tipo ML-KEM-512.

    Registrado por la comprobación
    fact ml-kem-parameter-sets = ML-KEM-768, ML-KEM-1024
  • Conjuntos de parámetros de ML-DSA

    ML-DSA-65ML-DSA-87

    Con MLDSA65 y MLDSA87 se firmó y se verificó; el SDK no declara ningún tipo ML-DSA-44.

    Registrado por la comprobación
    fact ml-dsa-parameter-sets = ML-DSA-65, ML-DSA-87
  • Modo de firma de ML-DSA

    hedged

    Dos firmas ML-DSA-65 de un mismo mensaje fueron distintas (firma hedged).

    Registrado por la comprobación
    fact ml-dsa-signing = hedged
    evidence ml-dsa-signing: two ML-DSA-65 signatures of one message are different
  • Cadena de contexto de ML-DSA

    supported

    Una firma con el contexto invoice-v1 se verificó con él y falló con invoice-v2 y sin contexto.

    Registrado por la comprobación
    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
  • Firma ML-DSA mal formada

    returns-false

    Una firma truncada, 3 bytes y un mensaje incorrecto devolvieron false.

    Registrado por la comprobación
    fact ml-dsa-malformed-signature = returns-false
    evidence ml-dsa-malformed-signature: isValidSignature: truncated signature false, 3 bytes false, wrong message false
  • Texto cifrado de ML-KEM modificado

    implicit-rejection

    Con un bit del texto cifrado invertido, decapsulate devolvió una clave distinta de la del remitente.

    Registrado por la comprobación
    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
  • Claves privadas poscuánticas

    seed-only

    seedRepresentation ocupa 64 bytes en ML-KEM-768 y 32 en ML-DSA-65, y las claves reconstruidas a partir de él coincidieron; el único otro inicializador de clave privada recibe la semilla con comprobación de integridad.

    Registrado por la comprobación
    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:
  • Algoritmos de Secure Enclave

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

    Se generaron claves de cada tipo en el Secure Enclave y con ellas se firmó, verificó o desencapsuló en este Mac.

    Registrado por la comprobación
    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
  • Formatos de clave pública

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

    SPKI PEM y DER, SEC1 sin comprimir y comprimido, y x‖y en bruto para P-256, además de 32 bytes en bruto para Ed25519; todos se escribieron y se volvieron a leer.

    Registrado por la comprobación
    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
  • Formatos de clave privada

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

    Lee PKCS#8 y SEC1, en PEM o DER, el escalar P-256 en bruto y la semilla Ed25519; escribe PKCS#8; las claves del Secure Enclave son referencias (handles).

    Registrado por la comprobación
    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

Uso

Firma con una clave P-256, toma la forma en bruto para JOSE y la forma DER para los sistemas X.509, y verifica una firma reconstruida a partir de 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])
Crea una clave P-256 en el Secure Enclave, vuelve a abrirla a partir de su referencia (handle), firma y verifica con la clave pública normal.
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))")

Ninguna imagen de contenedor ofrece esta plataforma; los ejemplos se ejecutaron en macOS 27.0.1 (Swift 6.4) el 20 de octubre de 2026.

Bibliotecas