PGP/MIME
RFC 3156 defines how OpenPGP signatures and encryption are carried inside MIME email messages. This library implements both sides:
SignMIME— wraps any MIME message in amultipart/signedenvelope.DecryptMIME— unwraps amultipart/encryptedenvelope and returns the plaintext.
Both methods handle boundary generation, MIME part structure, and ASCII armor internally, so the caller only needs to provide the raw MIME message and a PIN.
Signing — SignMIME
func (c *Card) SignMIME(payload []byte, pin string, pub *packet.PublicKey) ([]byte, error)
payload— a raw MIME message: transport headers (From,To,Subject, etc.) followed by\r\n\r\nand the body with its own content headers.pin— the user PIN (PW1), authorizes the signing operation.pub— the signing-capable public key, obtained withLoadPublicKeyorParsePublicKey.
What comes back
A complete multipart/signed message conforming to RFC 3156 §3:
From: you@example.com
To: them@example.com
Subject: Hello
MIME-Version: 1.0
Content-Type: multipart/signed;
boundary="----=_Part_…";
micalg=pgp-sha256;
protocol="application/pgp-signature"
------=_Part_…
Content-Type: text/plain
Hello, world.
------=_Part_…
Content-Type: application/pgp-signature; name="signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="signature.asc"
-----BEGIN PGP SIGNATURE-----
…
-----END PGP SIGNATURE-----
------=_Part_…--
How it works
- Transport headers (
From,To,Subject, …) are separated from content headers (Content-Type,MIME-Version, …) at the\r\n\r\nboundary. - A random 16-byte boundary string is generated.
- The signed part is assembled from the original
Content-Typeheader and body — this is the exact byte sequence the signature covers. Signis called with that byte sequence; the card produces a detached signature.- The final message is assembled: transport headers at the top level, the
new
multipart/signedContent-Type, and the two parts.
Example
pub, err := cardhl.LoadPublicKey("key.asc")
if err != nil {
log.Fatal(err)
}
card, err := cardhl.Open()
if err != nil {
log.Fatal(err)
}
defer card.Close()
rawMessage := []byte(
"From: you@example.com\r\n" +
"To: them@example.com\r\n" +
"Subject: Signed mail\r\n" +
"Content-Type: text/plain\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"This message is signed.\r\n",
)
signed, err := card.SignMIME(rawMessage, pin, pub)
if err != nil {
log.Fatal(err)
}
// signed is ready to hand to an SMTP client.
Decryption — DecryptMIME
func (c *Card) DecryptMIME(payload []byte, pin string, key *openpgp.Entity) ([]byte, error)
payload— a completemultipart/encryptedMIME message.pin— the user PIN (PW1), authorizes the decryption operation.key— the recipient's public key entity. Load it withLoadEntityorParseEntity.
Important
DecryptMIME supports RSA decryption keys only, for the same reason as
Decrypt: ECDH requires the private scalar, which the card never releases.
An ECDH key returns ErrUnsupportedKey.
How it works
- The
Content-Typeheader is located and parsed; anything other thanmultipart/encryptedwithprotocol="application/pgp-encrypted"returnsErrDecryptwrappingErrMIME. - The first MIME part (the RFC 3156 control part,
Version: 1) is discarded. - The second part (
application/octet-stream) is read and its ASCII armor is decoded to binary. Decryptis called with the binary ciphertext.
Example
key, err := cardhl.LoadEntity("recipient.asc")
if err != nil {
log.Fatal(err)
}
card, err := cardhl.Open()
if err != nil {
log.Fatal(err)
}
defer card.Close()
plain, err := card.DecryptMIME(encryptedMessage, pin, key)
switch {
case errors.Is(err, cardhl.ErrMIME):
log.Fatal("malformed multipart/encrypted envelope")
case errors.Is(err, cardhl.ErrUnsupportedKey):
log.Fatal("ECDH key — use gpg-agent instead")
case err != nil:
log.Fatal(err)
}
Errors specific to MIME operations
| Error | Meaning |
|---|---|
ErrMIME | The MIME envelope was malformed: missing \r\n\r\n separator, wrong Content-Type, missing boundary parameter, missing or unreadable MIME parts, or invalid PGP armor. |
ErrDecrypt | Wraps ErrMIME when the envelope cannot be parsed; also wraps lower-level decryption errors from the card. Use errors.Is to distinguish. |
if errors.Is(err, cardhl.ErrMIME) {
// envelope problem — log and reject the message
} else if errors.Is(err, cardhl.ErrDecrypt) {
// card-level failure — check PIN, key match, or card presence
}