Skip to content

feat(email): add GPG/PGP email signing and verification#13

Merged
qasim-nylas merged 2 commits intonylas:mainfrom
mqasimca:feat/gpg-email-signing
Feb 2, 2026
Merged

feat(email): add GPG/PGP email signing and verification#13
qasim-nylas merged 2 commits intonylas:mainfrom
mqasimca:feat/gpg-email-signing

Conversation

@mqasimca
Copy link
Contributor

@mqasimca mqasimca commented Feb 1, 2026

Summary

Add support for GPG/PGP email signing and verification using the PGP/MIME standard (RFC 3156).

Features

  • Sign emails with --sign flag using default or specific GPG key
  • Verify signatures with --verify flag on email read
  • Auto-fetch missing public keys from key servers
  • Configure default key and auto-sign via nylas config
  • List keys with --list-gpg-keys

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    GPG EMAIL SIGNING FLOW                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  $ nylas email send --to user@example.com --sign                │
│                                                                 │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐       │
│  │  CLI Layer  │────▶│ GPG Adapter │────▶│  GPG Binary │       │
│  │  send.go    │     │ service.go  │     │  (system)   │       │
│  └─────────────┘     └─────────────┘     └─────────────┘       │
│         │                   │                                   │
│         ▼                   ▼                                   │
│  ┌─────────────┐     ┌─────────────┐                           │
│  │MIME Builder │     │  Signature  │                           │
│  │ builder.go  │◀────│   (ASCII)   │                           │
│  └─────────────┘     └─────────────┘                           │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────────────────────────────┐                       │
│  │         PGP/MIME Message            │                       │
│  │  ┌─────────────────────────────┐   │                       │
│  │  │ multipart/signed            │   │                       │
│  │  │ ├── Part 1: Email content   │   │                       │
│  │  │ └── Part 2: PGP signature   │   │                       │
│  │  └─────────────────────────────┘   │                       │
│  └─────────────────────────────────────┘                       │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                               │
│  │  Nylas API  │  POST /messages/send?type=mime                │
│  └─────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Verification Flow

┌─────────────────────────────────────────────────────────────────┐
│                 SIGNATURE VERIFICATION                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  $ nylas email read <message-id> --verify                       │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ 1. Fetch raw MIME from Nylas API                         │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ 2. Parse multipart/signed structure                      │  │
│  │    • Extract signed content (exact bytes)                │  │
│  │    • Extract detached signature                          │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ 3. GPG Verification                                      │  │
│  │    • Key ID extracted from signature (NOT From header)   │  │
│  │    • Look up in local keyring                            │  │
│  │    • Auto-fetch from key servers if missing              │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ 4. Display Result                                        │  │
│  │    ✓ Good signature                                      │  │
│  │      Signer: John Doe <john@example.com>                 │  │
│  │      Key ID: 601FEE9B1D60185F                            │  │
│  │      Trust: ultimate                                     │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

New Files

Path Description Lines
internal/adapters/gpg/service.go GPG signing/verification service 518
internal/adapters/gpg/types.go GPG types (KeyInfo, SignResult, VerifyResult) 33
internal/adapters/mime/builder.go RFC 3156 PGP/MIME message builder 426
internal/cli/email/send_gpg.go GPG signing flow for send command 204
internal/cli/email/read_verify.go Signature verification for read command 341
docs/commands/email-signing.md User documentation 405
docs/commands/explain-gpg.md Comprehensive GPG guide 969

Usage Examples

Send Signed Email

# Sign with default key (from git config or nylas config)
nylas email send --to user@example.com --subject "Secure" --body "..." --sign

# Sign with specific key
nylas email send --to user@example.com --subject "Secure" --body "..." \
    --sign --gpg-key 601FEE9B1D60185F

# List available GPG keys
nylas email send --list-gpg-keys

Verify Signature

nylas email read <message-id> --verify

Configuration

# Set default signing key
nylas config set gpg.default_key 601FEE9B1D60185F

# Enable auto-sign for all emails
nylas config set gpg.auto_sign true

Security

Input Validation (Defense against command injection)

┌─────────────────────────────────────────────────────────────────┐
│                  KEY ID VALIDATION                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  isValidGPGKeyID(keyID string) bool                             │
│                                                                 │
│  ✅ Valid:                                                      │
│     • 8-40 hex characters: "601FEE9B1D60185F"                   │
│     • Valid email: "user@example.com"                           │
│                                                                 │
│  ❌ Rejected (command injection attempts):                      │
│     • "KEY; rm -rf /"                                           │
│     • "KEY\`whoami\`"                                           │
│     • "KEY\nmalicious"                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Security Fixes Applied

ID Issue Fix
SEC-001 User-provided keyID passed to exec.Command Added isValidGPGKeyID() validation
SEC-002 Extracted keyID from error used in key fetch Validate with regex before use
SEC-003 Predictable MIME boundaries Use crypto/rand for generation

Test Coverage

  • Unit tests: GPG service, MIME builder, verification parsing
  • Integration tests: End-to-end signing and verification
  • Security tests: Command injection prevention
# Run tests
make test-unit
make test-integration  # Requires NYLAS_TEST_EMAIL

Key Servers

Auto-fetches missing public keys from:

  1. keys.openpgp.org
  2. keyserver.ubuntu.com
  3. pgp.mit.edu
  4. keys.gnupg.net

Test Plan

  • Sign email with --sign flag
  • Sign with specific key via --gpg-key
  • Verify signature with --verify
  • Auto-fetch key from key server
  • List GPG keys with --list-gpg-keys
  • Configure default key via nylas config
  • Enable auto-sign via config
  • Unit tests pass
  • Integration tests pass
  • Security scan clean

Add support for signing outgoing emails with GPG keys and verifying
signatures on incoming emails using the PGP/MIME standard (RFC 3156).

Features:
- Sign emails with --sign flag using default or specific GPG key
- Verify signatures with --verify flag on email read
- Auto-fetch missing public keys from key servers
- Configure default key and auto-sign via nylas config
- List available GPG keys with --list-gpg-keys

New files:
- internal/adapters/gpg/ - GPG service for signing/verification
- internal/adapters/mime/ - RFC 3156 PGP/MIME message builder
- internal/cli/email/send_gpg.go - GPG signing flow
- internal/cli/email/read_verify.go - Signature verification
- docs/commands/email-signing.md - User documentation
- docs/commands/explain-gpg.md - GPG concepts guide

Security:
- Input validation for GPG key IDs (SEC-001, SEC-002)
- Cryptographically random MIME boundaries (SEC-003)
- Comprehensive test coverage including injection tests
@qasim-nylas qasim-nylas self-requested a review February 1, 2026 20:12
The TestHandleListGPGKeys_NoGPG test now accepts multiple valid error
conditions:
- GPG not found (not installed)
- no GPG secret keys found (installed but no keys)
- No GPG signing keys (alternative message)

This fixes CI failure where GPG is installed but has no keys configured.
@qasim-nylas qasim-nylas closed this Feb 2, 2026
@qasim-nylas qasim-nylas reopened this Feb 2, 2026
@qasim-nylas qasim-nylas merged commit 952dd62 into nylas:main Feb 2, 2026
4 checks passed
@mqasimca mqasimca deleted the feat/gpg-email-signing branch February 4, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants