package main import ( "crypto/ecdsa" "crypto/x509" "encoding/pem" "os") func main() { pemBytes, _ := os.ReadFile("key.pem") block, _ := pem.Decode(pemBytes) key, err := x509.ParsePKCS8PrivateKey(block.Bytes) // "BEGIN PRIVATE KEY" if err != nil { panic(err) } priv := key.(*ecdsa.PrivateKey) spki, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) if err != nil { panic(err) } out, err := os.Create("pub.pem") if err != nil { panic(err) } defer out.Close() pem.Encode(out, &pem.Block{Type: "PUBLIC KEY", Bytes: spki})}How it works
priv.PublicKeyis an embedded struct field — pass its address toMarshalPKIXPublicKey, which accepts*ecdsa.PublicKeyamong other types.- "PKIX" in the function name is SPKI: the output DER is the body of a standard
BEGIN PUBLIC KEYblock. pem.Encodehandles the 64-column base64 layout and header/footer lines; the blockTypestring becomes the PEM label verbatim.- A quick cross-tool self-check after writing the file:
openssl pkey -pubin -in pub.pem -nooutexits 0 only when the SPKI parses cleanly.
Gotchas
- Passing the struct by value (
priv.PublicKeyinstead of&priv.PublicKey) fails marshalling — the function switches on pointer types. - A legacy
BEGIN EC PRIVATE KEYinput needsx509.ParseECPrivateKeyat the load step; everything after the parse is identical. - Misspelling the block type (e.g.
"EC PUBLIC KEY") produces a file OpenSSL and every other stack rejects — the SPKI container's label is exactlyPUBLIC KEY.