Saltar al contenido
ecdsa.com

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

Firmante
Verificador
Biblioteca
OpenSSL CLI
WebCrypto (crypto.subtle)
Firma
ASN.1 DER
r‖s en bruto (P1363)
Low-S
Acepta ambas formas · firma sin normalizar
Acepta ambas formas · firma sin normalizar
RFC 6979
Opcional
No: nonce aleatorio
Clave pública
SPKI PEM
JWK
Curvas
P-256, P-384, P-521, secp256k1
P-256, P-384, P-521

Firma en OpenSSL CLI → verificación en WebCrypto (crypto.subtle)

2 puntos por corregir antes de que funcione
Falla

Formatos 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.

Lado del firmanteEmite r‖s en bruto desde OpenSSL CLI

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.

Lado del verificadorAcepta ASN.1 DER en WebCrypto (crypto.subtle)

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

Revisar

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

Conviene saberlo

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.

Conviene saberlo

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.

BibliotecaFormato de firma predeterminadoCómo cambiarloPolítica low-SRFC 6979Clave 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

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.