package main import ( "encoding/asn1" "fmt" "math/big") type dsaSignature struct { R, S *big.Int} const size = 32 // 32 for P-256, 48 for P-384, 66 for P-521 func derToRaw(der []byte) ([]byte, error) { var sig dsaSignature rest, err := asn1.Unmarshal(der, &sig) if err != nil { return nil, err } if len(rest) != 0 { return nil, fmt.Errorf("%d trailing bytes after signature", len(rest)) } raw := make([]byte, 2*size) sig.R.FillBytes(raw[:size]) // FillBytes left-pads with zeros sig.S.FillBytes(raw[size:]) return raw, nil} func rawToDer(raw []byte) ([]byte, error) { half := len(raw) / 2 return asn1.Marshal(dsaSignature{ R: new(big.Int).SetBytes(raw[:half]), S: new(big.Int).SetBytes(raw[half:]), })}How it works
asn1.Unmarshalmaps the DERSEQUENCE { INTEGER, INTEGER }straight onto a struct of two*big.Intfields — field order is what binds R and S.- Check the
restreturn value: bytes after the SEQUENCE mean the input was not just a signature (or was concatenated with something). FillByteswrites the integer right-aligned into a zero-filled slice — the safe way to get fixed-width output.
Gotchas
Bytes()instead ofFillBytesdrops leading zeros and shifts the layout roughly 1 time in 256 — always useFillBytesfor raw signature output.sizeis per-curve. Hard-coding 32 while handling P-384 signatures truncates integers viaFillBytespanicking — treat the panic as the safety net it is, not as a crash to suppress.- For verification you rarely need this file at all:
ecdsa.VerifyASN1takes DER directly, andecdsa.Verifytakes the big.Ints — convert only at true format boundaries (JWS, WebCrypto, hardware).