@@ -11,6 +11,8 @@ import (
1111 "time"
1212)
1313
14+ const keyserverFetchTimeout = 5 * time .Second
15+
1416// ListPublicKeys lists all public keys in the keyring.
1517func (s * service ) ListPublicKeys (ctx context.Context ) ([]KeyInfo , error ) {
1618 cmd := exec .CommandContext (ctx , "gpg" , "--list-keys" , "--with-colons" , "--with-fingerprint" )
@@ -47,6 +49,12 @@ func (s *service) FindPublicKeyByEmail(ctx context.Context, email string) (*KeyI
4749 }
4850 }
4951
52+ // Reserved domains are never expected to resolve through public key infrastructure.
53+ // Avoid network-dependent lookups for test-only addresses so CI stays deterministic.
54+ if isReservedLookupEmail (email ) {
55+ return nil , fmt .Errorf ("no public key found for %s (checked local keyring only; skipped remote lookup for reserved domain)" , email )
56+ }
57+
5058 // Step 2: Not found locally - try to fetch from key servers
5159 if fetchErr := s .fetchKeyByEmail (ctx , email ); fetchErr != nil {
5260 return nil , fmt .Errorf ("no public key found for %s (checked local keyring and %d key servers): %w" ,
@@ -71,29 +79,59 @@ func (s *service) FindPublicKeyByEmail(ctx context.Context, email string) (*KeyI
7179// fetchKeyByEmail tries to fetch a public key by email from key servers.
7280func (s * service ) fetchKeyByEmail (ctx context.Context , email string ) error {
7381 // Validate email format
74- if _ , err := mail .ParseAddress (email ); err != nil {
82+ parsed , err := mail .ParseAddress (email )
83+ if err != nil {
7584 return fmt .Errorf ("invalid email format: %q" , email )
7685 }
86+ email = strings .ToLower (parsed .Address )
7787
7888 var lastErr error
7989 for _ , server := range KeyServers {
90+ serverCtx , cancel := context .WithTimeout (ctx , keyserverFetchTimeout )
8091 // Use --auto-key-locate with WKD (Web Key Directory) and keyserver fallback
8192 // #nosec G204 - email is validated by mail.ParseAddress above
82- cmd := exec .CommandContext (ctx , "gpg" , "--auto-key-locate" , "wkd,keyserver" , "--keyserver" , server , "--locate-keys" , email )
93+ cmd := exec .CommandContext (serverCtx , "gpg" , "--auto-key-locate" , "wkd,keyserver" , "--keyserver" , server , "--locate-keys" , email )
8394 var stderr bytes.Buffer
8495 cmd .Stderr = & stderr
8596
8697 if err := cmd .Run (); err != nil {
98+ cancel ()
8799 lastErr = fmt .Errorf ("failed to fetch from %s: %w" , server , err )
88100 continue
89101 }
102+ cancel ()
90103 // Success
91104 return nil
92105 }
93106
94107 return fmt .Errorf ("failed to fetch key for %s from any server: %w" , email , lastErr )
95108}
96109
110+ func isReservedLookupEmail (email string ) bool {
111+ parsed , err := mail .ParseAddress (email )
112+ if err != nil {
113+ return false
114+ }
115+
116+ addr := strings .ToLower (parsed .Address )
117+ at := strings .LastIndex (addr , "@" )
118+ if at == - 1 || at == len (addr )- 1 {
119+ return false
120+ }
121+
122+ return isReservedLookupDomain (addr [at + 1 :])
123+ }
124+
125+ func isReservedLookupDomain (domain string ) bool {
126+ domain = strings .Trim (strings .ToLower (domain ), "." )
127+ for _ , suffix := range []string {"test" , "example" , "invalid" , "localhost" } {
128+ if domain == suffix || strings .HasSuffix (domain , "." + suffix ) {
129+ return true
130+ }
131+ }
132+ return false
133+ }
134+
97135// keyMatchesEmail checks if a key contains the given email in its UIDs.
98136func keyMatchesEmail (key * KeyInfo , email string ) bool {
99137 email = strings .ToLower (email )
0 commit comments