Getting Started
Prerequisites
- Go 1.26+
- A PC/SC stack:
pcscd+libccidon Linux, the built-in stack on macOS and Windows. - An OpenPGP smartcard with at least a signing key configured (a YubiKey 5, Nitrokey 3, etc.).
On Linux, make sure the daemon is up:
sudo systemctl enable --now pcscd.socket
Install
go get github.com/floatpane/go-openpgp-card-hl
Open a card
package main
import (
"fmt"
"log"
cardhl "github.com/floatpane/go-openpgp-card-hl"
)
func main() {
card, err := cardhl.Open()
if err != nil {
log.Fatal(err)
}
defer card.Close()
info, err := card.Info()
if err != nil {
log.Fatal(err)
}
fmt.Print(info)
}
Open connects to the first card exposing the OpenPGP applet. If it can't, the
error is specific and matchable:
card, err := cardhl.Open()
switch {
case errors.Is(err, cardhl.ErrNoPCSC):
// pcscd isn't running
case errors.Is(err, cardhl.ErrNoCard):
// no card on any reader
}
Sign
You need the public half of the card's signing key — it supplies the key
ID, fingerprint, and algorithm written into the signature packet. Export it
once (gpg --armor --export <keyid> > key.asc) and load it:
pub, err := cardhl.LoadPublicKey("key.asc")
if err != nil {
log.Fatal(err)
}
sig, err := card.Sign([]byte("hello, world"), pin, pub)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(sig)) // -----BEGIN PGP SIGNATURE-----
sig is a detached, ASCII-armored signature over the bytes you passed. Verify
it with GnuPG:
gpg --verify sig.asc message.txt
Decrypt (RSA only)
Decryption needs the recipient's public key so the library knows which session key to ask the card to unwrap:
key, err := cardhl.LoadEntity("recipient.asc")
if err != nil {
log.Fatal(err)
}
plain, err := card.Decrypt(ciphertext, pin, key)
if err != nil {
log.Fatal(err)
}
Decrypt supports RSA decryption keys only. An ECDH / Curve25519 key
returns ErrUnsupportedKey. The RSA path works because go-crypto will
accept the card as a crypto.Decrypter; ECDH would require the private
scalar, which the card never releases.
Handling the PIN
The PIN (PW1) is passed per call and never cached by the library. Read it from a secure prompt or the environment — never hard-code it. The card enforces its own retry counter; too many wrong PINs blocks the card until the Admin PIN (PW3) resets it.
For signing, the card uses PW1 in CDS mode; for decryption it uses PW1 in DECIPHER mode. They are the same PIN — the library selects the correct mode for you.