Guía rápida de interoperabilidad entre bibliotecas
Firmas en la biblioteca A y verificas en la biblioteca B: ¿qué falla? Elige los dos lados y obtén una lista de comprobación de las discrepancias concretas: codificación de la firma, tratamiento de high-S, formato de la clave pública y nonces deterministas, cada una con la línea de código que la corrige.
Datos de referencia estáticos: sin entradas, sin llamadas de red, nada que enviar a ningún sitio
openssl dgst / openssl pkeyutl
navegadores, Deno, webcrypto de Node
Firma en OpenSSL CLI → verificación en WebCrypto (crypto.subtle)
2 puntos por corregir antes de que funcioneFormatos de firma incompatibles: sale ASN.1 DER, se espera r‖s en bruto
OpenSSL CLI produce ASN.1 DER de forma predeterminada y WebCrypto (crypto.subtle) espera r‖s en bruto. No hay ningún problema con la clave ni con las matemáticas: el verificador simplemente no puede leer los bytes, y un bloque DER de longitud variable no tiene la longitud correcta para un analizador de ancho fijo.
Corrígelo en cualquiera de los dos lados: las dos codificaciones contienen exactamente la misma información, así que la conversión no pierde nada ni implica criptografía.
Sin opción integrada
La CLI no ofrece salida P1363 para ECDSA. Extrae los dos INTEGER de la estructura DER y rellena cada uno con ceros a la izquierda hasta el tamaño de la curva (32 bytes en P-256): `openssl asn1parse -inform DER -in sig.der` imprime r y s para que puedas comprobar la conversión a ojo.
Sin opción integrada
No hay modo DER. Divide la salida de longitud fija por la mitad, quita los ceros iniciales de cada mitad y envuelve los dos enteros en una SEQUENCE DER antes de pasárselos a un verificador al estilo de OpenSSL.
† El comportamiento varía entre versiones o compilaciones: compruébalo en tu versión.
Ver también: Conversor DER ⇄ en bruto · Formatos de firma ECDSA: DER frente a formato en bruto
Formato de clave pública incompatible: SPKI PEM frente a JWK
OpenSSL CLI exporta la clave pública como SPKI PEM, y WebCrypto (crypto.subtle) espera JWK. Son contenedores realmente distintos: SPKI envuelve el punto con el identificador de la curva, una JWK expresa las coordenadas x e y como campos base64url, y un punto SEC1 no es más que los bytes de las coordenadas. El material de clave subyacente es idéntico en los tres.
OpenSSL CLI: `openssl ec -pubin -in pub.pem -pubout -outform DER` da la misma clave en SPKI DER binario; `openssl ec -pubin -in pub.pem -text -noout` imprime en hexadecimal el punto sin comprimir en formato SEC1.
WebCrypto (crypto.subtle): importKey("spki", derBytes, …) recibe SPKI *DER*: quita el encabezado y el pie PEM y decodifica antes el base64, porque el texto PEM tal cual se rechaza.
Ver también: Verificador de firmas ECDSA
Limítate a P-256 / P-384 / P-521: las curvas que admiten ambos lados
Los conjuntos de curvas no coinciden: secp256k1 solo en OpenSSL CLI. Elige una clave en una curva compartida y el problema no se plantea.
OpenSSL CLI: Todo lo que habilite la compilación: las curvas NIST sobre cuerpos primos, secp256k1, Brainpool y más; `openssl ecparam -list_curves` es la referencia definitiva para tu binario.
WebCrypto (crypto.subtle): Solo las tres curvas NIST. secp256k1 no forma parte de la especificación, así que las claves de Bitcoin y Ethereum necesitan en su lugar una biblioteca de JavaScript.
OpenSSL CLI usa un nonce aleatorio salvo que actives RFC 6979
La ruta de firma habitual usa un nonce aleatorio. Desde la versión 3.2, el parámetro de firma `nonce-type:1` selecciona el ECDSA determinista (RFC 6979), con un resumen indicado de forma explícita: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` o `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.
Es el comportamiento correcto, no un fallo, pero significa que la misma clave y el mismo mensaje producen bytes distintos en cada ejecución, así que una prueba que compare firmas byte a byte fallará. Compara en su lugar los resultados de la verificación.
† El comportamiento varía entre versiones o compilaciones: compruébalo en tu versión.
Comparativa de todas las bibliotecas
Solo valores predeterminados: cada fila se puede cambiar con una opción o con otra llamada. Desplázate en horizontal para ver todas las columnas.
| Biblioteca | Formato de firma predeterminado | Cómo cambiarlo | Política low-S | RFC 6979 | Clave pública predeterminada |
|---|---|---|---|---|---|
OpenSSL CLI openssl dgst / openssl pkeyutl firmante | ASN.1 DER ASN.1 DER: una SEQUENCE de dos INTEGER, así que una firma P-256 suele ocupar entre 70 y 72 bytes y su longitud cambia de una firma a otra. | La CLI no tiene modo r‖s en bruto: convierte la salida DER externamente.† | Acepta ambas formas Sin política low-S en ninguna dirección: aproximadamente la mitad de las firmas salen con s en la mitad superior, y a la verificación nunca le importa. | Opcional La ruta de firma habitual usa un nonce aleatorio. Desde la versión 3.2, el parámetro de firma `nonce-type:1` selecciona el ECDSA determinista (RFC 6979), con un resumen indicado de forma explícita: `openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` o `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256`.† | SPKI PEM SPKI PEM: el bloque `-----BEGIN PUBLIC KEY-----` que escribe `openssl ec -pubout`. Ten en cuenta que `-----BEGIN EC PUBLIC KEY-----` no existe: un bloque con la etiqueta EC es una clave privada. |
Node.js node:crypto crypto.sign / crypto.verify / KeyObject | ASN.1 DER ASN.1 DER, salvo que pidas explícitamente la otra codificación. | `dsaEncoding: "ieee-p1363"` en el objeto de clave: configúralo al firmar *y* al verificar. | Acepta ambas formas Acepta ambas mitades y no normaliza nada: por debajo es OpenSSL. | No: nonce aleatorio Nonce aleatorio: la misma clave y el mismo mensaje producen bytes de firma distintos en cada llamada. | SPKI PEM `publicKey.export({ type: "spki", format: "pem" })` es la forma habitual; JWK está a una llamada de distancia con `export({ format: "jwk" })`. |
WebCrypto (crypto.subtle) navegadores, Deno, webcrypto de Node verificador | r‖s en bruto (P1363) Solo r‖s en bruto, de longitud fija: 64 bytes en P-256, 96 en P-384 y 132 en P-521. La especificación no define ninguna otra codificación. | No hay nada que cambiar: WebCrypto nunca usa DER. Convierte fuera de la API. | Acepta ambas formas Sin regla low-S al firmar ni al verificar. | No: nonce aleatorio Nonce aleatorio, y la API no te permite proporcionar el tuyo. | JWK Al importar y exportar siempre indicas un formato: "jwk", "spki" (bytes DER) o "raw" (el punto sin comprimir en formato SEC1). JWK es la forma que más circula en el código web. |
Go crypto/ecdsa SignASN1 / VerifyASN1, o el par de big.Int | ASN.1 DER `SignASN1` —la ruta que usa `crypto.Signer`— devuelve ASN.1 DER. La función de nivel más bajo `Sign` devuelve r y s como `*big.Int`, que no es ninguno de los dos formatos de transmisión hasta que lo codificas. | `SignASN1`/`VerifyASN1` para DER; `Sign`/`Verify` más `FillBytes` para el formato en bruto de ancho fijo. | Acepta ambas formas Ningún tratamiento de low-S: la verificación acepta cualquiera de las dos mitades del rango. | No: nonce aleatorio No es compatible con RFC 6979. Las versiones recientes derivan el nonce de la clave, el mensaje y aleatoriedad nueva a la vez, así que los bytes de la firma cambian en cada ejecución y no se pueden fijar en las pruebas.† | SPKI DER `x509.MarshalPKIXPublicKey` produce SPKI DER; un bloque PEM está a un `pem.EncodeToMemory` de distancia. |
Python cryptography (pyca) hazmat.primitives.asymmetric.ec | ASN.1 DER DER: `private_key.sign(...)` devuelve la SEQUENCE ASN.1. | `decode_dss_signature` / `encode_dss_signature` convierten entre DER y el par de enteros; el relleno de ancho fijo lo añades tú. | Acepta ambas formas Acepta ambas mitades; nada normaliza s. | Opcional Nonce aleatorio de forma predeterminada. Las versiones recientes aceptan un indicador de firma determinista en el objeto ECDSA, y esa ruta necesita un OpenSSL reciente por debajo: comprueba contra qué se compiló tu paquete wheel antes de confiar en ello.† | SPKI PEM `public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)`: el mismo bloque PEM que escribe OpenSSL. |
Python ecdsa (Python puro) SigningKey / VerifyingKey, sigencode= y sigdecode= | r‖s en bruto (P1363) r‖s en bruto: `sign()` devuelve los dos valores concatenados y rellenados con ceros hasta el tamaño de la curva. | Los argumentos `sigencode=` y `sigdecode=` eligen la codificación en cada llamada de firma y de verificación. | Acepta ambas formas Acepta ambas mitades. La biblioteca también incluye codificadores que canonicalizan la firma y fuerzan s a la mitad inferior; existen junto a los normales y hay que seleccionarlos explícitamente.† | Opcional La firma determinista según RFC 6979 está disponible como método de firma aparte; la llamada sign() normal usa aleatoriedad. | punto SEC1 Las claves viajan como el punto a secas: `VerifyingKey.to_string()` devuelve x‖y sin el prefijo 0x04 de forma predeterminada, y los entornos basados en SPKI no lo aceptan tal cual.† |
Java JCA (SunEC) java.security.Signature | ASN.1 DER `SHA256withECDSA` produce ASN.1 DER. | Añade `inP1363Format` al nombre del algoritmo: misma clase, distinto formato de transmisión. | Acepta ambas formas Sin política low-S en ninguna dirección. | No: nonce aleatorio SunEC firma con un nonce aleatorio. Para tener ECDSA determinista hay que añadir un proveedor de terceros como Bouncy Castle.† | SPKI DER `publicKey.getEncoded()` devuelve X.509 SPKI DER. El JDK no admite PEM: el envoltorio base64 corre de tu cuenta. |
Rust p256 / k256 (RustCrypto) ecdsa::Signature, SigningKey, VerifyingKey | r‖s en bruto (P1363) El tipo `Signature` predeterminado es el par de ancho fijo: 64 bytes en P-256 y secp256k1. DER es una conversión aparte y explícita.† | El par de ancho fijo es el tipo predeterminado; la conversión a DER es explícita en ambos sentidos.† | Acepta ambas formas El crate de secp256k1 normaliza s a la mitad inferior al firmar, siguiendo la convención de Bitcoin; los crates de las curvas NIST no. La verificación acepta ambas mitades, así que normaliza en el lado del firmante cuando el otro extremo sea estricto.† | Sí, de forma predeterminada Nonces deterministas RFC 6979 de forma predeterminada: la misma clave y el mismo mensaje producen siempre los mismos bytes. La variante aleatorizada hay que activarla explícitamente. | punto SEC1 Una `VerifyingKey` se serializa como punto codificado SEC1: 33 bytes en formato comprimido y 65 sin comprimir. SPKI DER y PEM están disponibles mediante los traits complementarios SPKI/PKCS#8.† |
libsecp256k1 (Bitcoin Core) secp256k1_ecdsa_sign / _verify | Lo eliges tú Ninguno, hasta que serializas: la firma vive en una estructura interna opaca y, al sacarla, eliges el formato compacto (r‖s en bruto) o DER. | Serializa en compacto para r‖s en bruto y en DER para el formato de transmisión de Bitcoin: se elige en cada llamada. | Exige low-S La estricta. Al firmar siempre emite low-S, y la verificación rechaza de plano una firma high-S; la biblioteca ofrece una llamada de normalización precisamente para que puedas corregir las firmas que llegan de un firmante de uso general. | Sí, de forma predeterminada RFC 6979 de forma predeterminada; si lo necesitas, se puede pasar otra función de nonce. | punto SEC1 Puntos SEC1: 33 bytes en formato comprimido o 65 bytes sin comprimir. Ni SPKI, ni PEM, ni JWK en ninguna parte de la API. |
PHP openssl_* openssl_sign / openssl_verify | ASN.1 DER DER: `openssl_sign()` devuelve exactamente lo que produjo OpenSSL. | No hay ningún modo en bruto: convierte en tu propio código o usa una biblioteca JOSE que ya lo haga. | Acepta ambas formas Sin política low-S: hereda tal cual el comportamiento de OpenSSL. | No: nonce aleatorio Nonce aleatorio; la extensión no ofrece ningún modo determinista. | SPKI PEM Cadenas PEM de entrada y de salida: `openssl_pkey_get_public()` acepta un bloque `-----BEGIN PUBLIC KEY-----` o un certificado completo. |
Ruby OpenSSL OpenSSL::PKey::EC | ASN.1 DER DER: `key.sign(digest, data)` devuelve la SEQUENCE ASN.1. | Sin modo en bruto integrado: convierte a mano con OpenSSL::ASN1 o deja que lo haga una gema JOSE. | Acepta ambas formas Sin política low-S: el comportamiento de OpenSSL, sin cambios. | No: nonce aleatorio Nonce aleatorio; no se ofrece ninguna opción determinista. | SPKI PEM SPKI PEM. Las versiones recientes ofrecen un método específico para exportar la clave pública; vale la pena usarlo, porque un `to_pem` simple sobre una clave que contiene la parte privada escribe la clave privada.† |
† El comportamiento varía entre versiones o compilaciones: compruébalo en tu versión.
Para profundizar
- Conversor DER ⇄ en bruto: pega la firma que ya tienes y obtén la otra codificación, con r, s y una comprobación de high-S.
- Verificador de firmas ECDSA: zanja la discusión de si la firma es válida o no, con independencia de ambas bibliotecas.
- Formatos de firma ECDSA: DER frente a formato en bruto (P1363): el contexto a nivel de bytes detrás de cada fila de la tabla.
How it works
A signature that verifies in one library and fails in another is almost never wrong mathematically: the two sides simply disagree about packaging. An ECDSA signature is a pair of integers (r, s), and each ecosystem picked its own way to serialize them — ASN.1 DER in the OpenSSL and X.509 lineage, fixed-width r‖s in WebCrypto, JWS and most blockchain stacks. On top of that sit two more sources of disagreement: whether s must be in the lower half of the range (the low-S rule that Bitcoin enforces and general-purpose libraries ignore), and how the public key itself is wrapped — an SPKI structure, a JWK object, or the bare SEC1 point.
This page compares those defaults for a pair of libraries and lists only the differences that actually cause a failure, together with the switch or conversion that removes each one. Every row describes what a library does when you call the obvious API without extra arguments — not what it is capable of. Nearly all of it is configurable, which is exactly why two teams reading the same documentation end up incompatible, and why the fix is usually one option rather than new code.