from cryptography.hazmat.primitives.asymmetric.utils import ( decode_dss_signature, encode_dss_signature,) SIZE = 32 # coordinate bytes: 32 for P-256, 48 for P-384, 66 for P-521 def der_to_raw(der: bytes) -> bytes: r, s = decode_dss_signature(der) return r.to_bytes(SIZE, "big") + s.to_bytes(SIZE, "big") def raw_to_der(raw: bytes) -> bytes: half = len(raw) // 2 return encode_dss_signature( int.from_bytes(raw[:half], "big"), int.from_bytes(raw[half:], "big"), )der = open("sig.der", "rb").read()raw = der_to_raw(der)assert len(raw) == 2 * SIZEassert raw_to_der(raw) == der # round-trips byte-for-byteopen("sig.raw", "wb").write(raw)How it works
decode_dss_signaturereturns plain Python ints — all DER framing, padding and length rules are handled for you.int.to_bytes(SIZE, "big")re-adds the fixed-width, big-endian layout that raw (IEEE P1363) format requires.- The round trip is lossless: converting DER → raw → DER reproduces the input exactly, because DER is a canonical encoding.
Gotchas
- The integers carry no curve information — you must know SIZE from context. If
to_bytesraisesOverflowError, your SIZE is too small for the curve that made the signature. - JWS/JWT ES256 signatures are exactly this raw form (64 bytes) — but base64url-encoded. Decode the text first;
der_to_rawon base64 text fails on the first byte. - This conversion never fixes an invalid signature: if verification failed before converting, and the format was already right, the problem is the key, hash or message — not the encoding.