You sign in library A and verify in library B — what breaks? Pick the two sides and get a checklist of the concrete mismatches: signature encoding, high-S handling, public key format and deterministic nonces, each with the line of code that fixes it.
Static reference data — no inputs, no network calls, nothing to send anywhere
openssl dgst / openssl pkeyutl
browsers, Deno, Node's webcrypto
OpenSSL CLI produces ASN.1 DER by default and WebCrypto (crypto.subtle) expects raw r‖s. Nothing about the key or the maths is wrong — the verifier simply cannot read the bytes, and a variable-length DER blob is the wrong length for a fixed-width parser.
Fix it on either side — the two encodings carry identical information, so converting is lossless and involves no cryptography.
No built-in option
The CLI has no P1363 output for ECDSA. Take the two INTEGERs out of the DER structure and left-pad each to the curve size (32 bytes on P-256) — `openssl asn1parse -inform DER -in sig.der` prints r and s so you can check the conversion by eye.
No built-in option
There is no DER mode. Split the fixed-length output in half, strip leading zeros from each half, and wrap the two integers in a DER SEQUENCE before handing them to an OpenSSL-style verifier.
† Behaviour varies between versions or builds — verify in your version.
See also: DER ⇄ Raw Converter · ECDSA signature formats: DER vs raw
OpenSSL CLI exports the public key as SPKI PEM, and WebCrypto (crypto.subtle) expects JWK. These are genuinely different containers: SPKI wraps the curve identifier around the point, a JWK spells out the x and y coordinates as base64url fields, and a SEC1 point is nothing but the coordinate bytes. The key material underneath is identical in all three.
OpenSSL CLI: `openssl ec -pubin -in pub.pem -pubout -outform DER` gives the same key as raw SPKI DER; `openssl ec -pubin -in pub.pem -text -noout` prints the uncompressed SEC1 point in hex.
WebCrypto (crypto.subtle): importKey("spki", derBytes, …) takes SPKI *DER*: strip the PEM header and footer and base64-decode first, because PEM text itself is rejected.
See also: ECDSA Signature Verifier
The curve sets do not match: secp256k1 only in OpenSSL CLI. Pick a key on a shared curve and this never comes up.
OpenSSL CLI: Everything the build enables: the NIST prime curves, secp256k1, Brainpool and more — `openssl ecparam -list_curves` is the authority for your binary.
WebCrypto (crypto.subtle): The three NIST curves only. secp256k1 is not part of the specification, so Bitcoin and Ethereum keys need a JavaScript library instead.
The usual signing path uses a random nonce. Deterministic ECDSA (RFC 6979) exists in recent 3.x releases as a signature parameter, but the dgst and pkeyutl commands do not expose it.
This is correct behaviour, not a bug — but it means the same key and message produce different bytes every run, so a test that compares signatures byte-for-byte will fail. Compare verification results instead.
† Behaviour varies between versions or builds — verify in your version.
Defaults only — every row can be changed with a flag or a different call. Scroll sideways for the full set of columns.
| Library | Default signature format | How to switch | Low-S policy | RFC 6979 | Default public key |
|---|---|---|---|---|---|
OpenSSL CLI openssl dgst / openssl pkeyutl signer | ASN.1 DER ASN.1 DER — a SEQUENCE of two INTEGERs, so a P-256 signature is usually 70–72 bytes and its length changes from signature to signature. | No raw r‖s mode in the CLI — convert the DER output externally.† | Accepts both No low-S policy in either direction: signatures come out with s in the upper half roughly half the time, and verification never cares. | Opt-in The usual signing path uses a random nonce. Deterministic ECDSA (RFC 6979) exists in recent 3.x releases as a signature parameter, but the dgst and pkeyutl commands do not expose it.† | SPKI PEM SPKI PEM: the `-----BEGIN PUBLIC KEY-----` block written by `openssl ec -pubout`. Note that `-----BEGIN EC PUBLIC KEY-----` is not a thing — an EC-labelled block is a private key. |
Node.js node:crypto crypto.sign / crypto.verify / KeyObject | ASN.1 DER ASN.1 DER unless you explicitly ask for the other encoding. | `dsaEncoding: "ieee-p1363"` on the key object — set it when signing *and* when verifying. | Accepts both Accepts both halves and normalizes nothing — it is OpenSSL underneath. | No — random nonce Random nonce: the same key and message produce different signature bytes on every call. | SPKI PEM `publicKey.export({ type: "spki", format: "pem" })` is the common form; JWK is one call away with `export({ format: "jwk" })`. |
WebCrypto (crypto.subtle) browsers, Deno, Node's webcrypto verifier | Raw r‖s (P1363) Raw r‖s only, fixed length: 64 bytes on P-256, 96 on P-384, 132 on P-521. The specification defines no other encoding. | Nothing to switch — WebCrypto never speaks DER. Convert outside the API. | Accepts both No low-S rule when signing or verifying. | No — random nonce Random nonce, and the API gives you no way to supply your own. | JWK You always name a format on import and export: "jwk", "spki" (DER bytes) or "raw" (the uncompressed SEC1 point). JWK is the form most web code passes around. |
Go crypto/ecdsa SignASN1 / VerifyASN1, or the big.Int pair | ASN.1 DER `SignASN1` — the path `crypto.Signer` uses — returns ASN.1 DER. The lower-level `Sign` hands back r and s as `*big.Int`, which is neither wire format until you encode it. | `SignASN1`/`VerifyASN1` for DER; `Sign`/`Verify` plus `FillBytes` for fixed-width raw. | Accepts both No low-S handling at all: verification accepts either half of the range. | No — random nonce Not RFC 6979-compatible. Recent versions derive the nonce from the key, the message and fresh randomness together, so signature bytes differ on every run and cannot be pinned in tests.† | SPKI DER `x509.MarshalPKIXPublicKey` produces SPKI DER; a PEM block is one `pem.EncodeToMemory` away. |
Python cryptography (pyca) hazmat.primitives.asymmetric.ec | ASN.1 DER DER — `private_key.sign(...)` returns the ASN.1 SEQUENCE. | `decode_dss_signature` / `encode_dss_signature` move between DER and the integer pair; the fixed-width padding is yours to add. | Accepts both Accepts both halves; nothing normalizes s. | Opt-in Random nonce by default. Recent releases accept a deterministic-signing flag on the ECDSA object, and that path needs a recent OpenSSL underneath — check what your wheel was built against before relying on it.† | SPKI PEM `public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)` — the same PEM block OpenSSL writes. |
Python ecdsa (pure Python) SigningKey / VerifyingKey, sigencode= and sigdecode= | Raw r‖s (P1363) Raw r‖s: `sign()` returns the two values concatenated and zero-padded to the curve size. | `sigencode=` and `sigdecode=` arguments pick the encoding on every sign and verify call. | Accepts both Both halves accepted. The library also ships canonicalizing encoders that force s into the low half — they exist alongside the plain ones and have to be selected explicitly.† | Opt-in Deterministic RFC 6979 signing is available as a separate signing method; the plain sign() call uses randomness. | SEC1 point Keys travel as the bare point: `VerifyingKey.to_string()` returns x‖y with no 0x04 prefix by default, which SPKI-based stacks will not accept as-is.† |
Java JCA (SunEC) java.security.Signature | ASN.1 DER `SHA256withECDSA` produces ASN.1 DER. | Append `inP1363Format` to the algorithm name — same class, different wire format. | Accepts both No low-S policy in either direction. | No — random nonce SunEC signs with a random nonce. Deterministic ECDSA is reachable by adding a third-party provider such as Bouncy Castle.† | SPKI DER `publicKey.getEncoded()` returns X.509 SPKI DER. The JDK has no PEM support — the base64 wrapping is on you. |
Rust p256 / k256 (RustCrypto) ecdsa::Signature, SigningKey, VerifyingKey | Raw r‖s (P1363) The default `Signature` type is the fixed-width pair — 64 bytes on P-256 and secp256k1. DER is a separate, explicit conversion.† | The fixed-width pair is the default type; DER conversion is explicit in both directions.† | Accepts both The secp256k1 crate normalizes s into the low half when signing, matching the Bitcoin convention; the NIST-curve crates do not. Verification accepts both, so normalize on the signing side when the other end is strict.† | Yes, by default Deterministic RFC 6979 nonces by default — the same key and message always produce the same bytes. A randomized variant is opt-in. | SEC1 point A `VerifyingKey` serializes to a SEC1 encoded point — 33 bytes compressed, 65 uncompressed. SPKI DER and PEM are available through the companion SPKI/PKCS#8 traits.† |
libsecp256k1 (Bitcoin Core) secp256k1_ecdsa_sign / _verify | You choose Neither, until you serialize: a signature lives in an opaque internal struct and you pick compact (raw r‖s) or DER on the way out. | Serialize compact for raw r‖s, DER for the Bitcoin wire format — the choice is per call. | Enforces low-S The strict one. Signing always emits low-S, and verification rejects a high-S signature outright — the library exposes a normalization call precisely so you can fix signatures that arrived from a general-purpose signer. | Yes, by default RFC 6979 by default; a different nonce function can be passed in if you need one. | SEC1 point SEC1 points: 33 bytes compressed or 65 bytes uncompressed. No SPKI, no PEM, no JWK anywhere in the API. |
PHP openssl_* openssl_sign / openssl_verify | ASN.1 DER DER — `openssl_sign()` hands back exactly what OpenSSL produced. | No raw mode at all: convert in userland, or use a JOSE library that already does it. | Accepts both No low-S policy — it inherits OpenSSL's behaviour unchanged. | No — random nonce Random nonce; the extension exposes no deterministic mode. | SPKI PEM PEM strings in and out: `openssl_pkey_get_public()` takes a `-----BEGIN PUBLIC KEY-----` block or a whole certificate. |
Ruby OpenSSL OpenSSL::PKey::EC | ASN.1 DER DER — `key.sign(digest, data)` returns the ASN.1 SEQUENCE. | No built-in raw mode: convert by hand through OpenSSL::ASN1, or let a JOSE gem do it. | Accepts both No low-S policy — OpenSSL's behaviour, unchanged. | No — random nonce Random nonce; no deterministic option is exposed. | SPKI PEM SPKI PEM. Recent versions provide a dedicated public-key export method — worth using, because plain `to_pem` on a key that holds a private part writes the private key.† |
† Behaviour varies between versions or builds — verify in your version.
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.