Repository navigation
eIPC foundation: session & secrets hygiene (PR-EIPC-4) #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // Copyright (c) 2026 EoS Project | ||
|
|
||
| package core | ||
|
|
||
| import ( | ||
| "errors" | ||
| "net" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/embeddedos-org/eipc/protocol" | ||
| ) | ||
|
|
||
| func TestServerEndpointSetReadDeadline(t *testing.T) { | ||
| clientConn, serverConn := setupTCPPair(t) | ||
| defer clientConn.Close() | ||
| defer serverConn.Close() | ||
|
|
||
| serverEP := NewServerEndpoint(serverConn, protocol.DefaultCodec(), []byte("test-secret-key-for-hmac-256bit!")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generate the test HMAC key at runtime. This fixture embeds an HMAC key in source code. Generate an ephemeral key for the test and check the generation error. Based on learnings, the backend rule requires “No secret, token or credential in code, logs or fixtures.”
|
||
|
|
||
| // A deadline already in the past: Receive must fail fast with a | ||
| // timeout instead of blocking until the peer speaks, so an idle | ||
| // authenticated peer cannot hold its connection slot forever. | ||
| if err := serverEP.SetReadDeadline(time.Now().Add(-time.Second)); err != nil { | ||
| t.Fatalf("SetReadDeadline: %v", err) | ||
| } | ||
| start := time.Now() | ||
| _, err := serverEP.Receive() | ||
| if err == nil { | ||
| t.Fatal("expected Receive to fail past its read deadline") | ||
| } | ||
| if time.Since(start) > 5*time.Second { | ||
| t.Errorf("Receive blocked %v past an expired deadline", time.Since(start)) | ||
| } | ||
| var ne net.Error | ||
| if !errors.As(err, &ne) || !ne.Timeout() { | ||
| t.Errorf("expected a timeout error, got %T: %v", err, err) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,8 +47,15 @@ type Authenticator struct { | |
| activeSessions map[string]*PeerIdentity | ||
| pendingChallenges map[string]*Challenge // serviceID → pending challenge | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Concurrent handshakes overwrite challenges When two connections for the same service ID request challenges before either response is verified, Track pending challenges separately for concurrent connections instead of overwriting one challenge per service ID. Also at Prompt for AI agentsIn `security/auth/identity.go` at line 48:
**Concurrent handshakes overwrite challenges**
When two connections for the same service ID request challenges before either response is verified, `CreateChallenge` stores only one challenge per service ID, so the second request overwrites the first nonce and `VerifyResponse` checks against and consumes the newer challenge; the handshakes fail intermittently.
Track pending challenges separately for concurrent connections instead of overwriting one challenge per service ID.
Also at `security/auth/identity.go:143`. |
||
| sessionTTL time.Duration | ||
| challengeTTL time.Duration | ||
| allowLegacyAuth bool | ||
| } | ||
|
|
||
| // DefaultChallengeTTL bounds how long a challenge-response round trip may | ||
| // take. A challenge older than this is rejected, so a captured nonce | ||
| // cannot be answered later. | ||
| const DefaultChallengeTTL = 5 * time.Minute | ||
|
|
||
| // NewAuthenticator creates an authenticator with the given shared secret. | ||
| // knownServices maps service IDs to their allowed capability sets. | ||
| func NewAuthenticator(sharedSecret []byte, knownServices map[string][]string) *Authenticator { | ||
|
|
@@ -64,6 +71,7 @@ func NewAuthenticator(sharedSecret []byte, knownServices map[string][]string) *A | |
| activeSessions: make(map[string]*PeerIdentity), | ||
| pendingChallenges: make(map[string]*Challenge), | ||
| sessionTTL: 1 * time.Hour, | ||
| challengeTTL: DefaultChallengeTTL, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -74,6 +82,25 @@ func (a *Authenticator) SetSessionTTL(ttl time.Duration) { | |
| a.sessionTTL = ttl | ||
| } | ||
|
|
||
| // SetChallengeTTL configures how long a challenge stays valid for | ||
| // VerifyResponse. A zero or negative TTL disables expiry checking; the | ||
| // default is DefaultChallengeTTL. | ||
| func (a *Authenticator) SetChallengeTTL(ttl time.Duration) { | ||
| a.mu.Lock() | ||
| defer a.mu.Unlock() | ||
| a.challengeTTL = ttl | ||
| } | ||
|
Comment on lines
+85
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚪ Low · Public method docs omit examples The new exported setters and deadline methods have comments describing behavior, but do not document their parameters and return values or provide examples. Callers lack the API details and examples required by the repository guidance. Add parameter and return-value documentation plus runnable examples for each newly exported method. Also at Prompt for AI agentsIn `security/auth/identity.go` at lines 85-92:
**Public method docs omit examples**
The new exported setters and deadline methods have comments describing behavior, but do not document their parameters and return values or provide examples. Callers lack the API details and examples required by the repository guidance.
Add parameter and return-value documentation plus runnable examples for each newly exported method.
Also at `security/auth/identity.go:94-102`, `core/endpoint.go:316-322`, `transport/transport.go:36-40`, `transport/transport.go:103-108`, `transport/shm/shm.go:204-209`.From Every public function carries a description, its parameters, its return value,
and an example. The example must be one that was actually run. |
||
|
|
||
| // SetAllowLegacyAuth opts in to the legacy no-secret Authenticate path. | ||
| // It is off by default: Authenticate issues a session token on a service | ||
| // ID alone, with no proof of the shared secret. Enable it only while | ||
| // migrating an existing consumer to CreateChallenge/VerifyResponse. | ||
| func (a *Authenticator) SetAllowLegacyAuth(enabled bool) { | ||
| a.mu.Lock() | ||
| defer a.mu.Unlock() | ||
| a.allowLegacyAuth = enabled | ||
| } | ||
|
|
||
| // CreateChallenge generates a 32-byte nonce challenge for the given service ID. | ||
| // Returns error if the service is unknown. | ||
| func (a *Authenticator) CreateChallenge(serviceID string) (*Challenge, error) { | ||
|
|
@@ -110,6 +137,13 @@ func (a *Authenticator) VerifyResponse(serviceID string, response []byte) (*Peer | |
| } | ||
| delete(a.pendingChallenges, serviceID) | ||
|
|
||
| // Challenges are one-shot (deleted above before verifying) and | ||
| // time-bound: an old challenge is rejected even with a correct | ||
| // response, so a captured nonce cannot be answered later. | ||
| if a.challengeTTL > 0 && time.Since(challenge.CreatedAt) > a.challengeTTL { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Expired challenges remain valid When the system clock moves backward after a challenge is created, Preserve and use a monotonic time source for challenge expiry, and ensure the expiry test does not depend on wall-clock time. Also at Prompt for AI agentsIn `security/auth/identity.go` at line 143:
**Expired challenges remain valid**
When the system clock moves backward after a challenge is created, `CreateChallenge` stores `time.Now().UTC()` without its monotonic reading, so `time.Since(challenge.CreatedAt)` uses wall-clock time and accepts an over-TTL response; the expiry test also becomes nondeterministic under a clock adjustment.
Preserve and use a monotonic time source for challenge expiry, and ensure the expiry test does not depend on wall-clock time.
Also at `security/auth/identity_test.go:116`.From Tests are deterministic. No dependence on wall-clock time, network, ordering, filesystem state, or randomness without a fixed seed. |
||
| return nil, fmt.Errorf("%w: challenge expired for %q", core.ErrAuth, serviceID) | ||
| } | ||
|
|
||
| // Compute expected response: HMAC-SHA256(sharedSecret, nonce) | ||
| mac := hmac.New(sha256.New, a.sharedSecret) | ||
| mac.Write(challenge.Nonce) | ||
|
|
@@ -140,11 +174,22 @@ func (a *Authenticator) VerifyResponse(serviceID string, response []byte) (*Peer | |
| } | ||
|
|
||
| // Authenticate validates a peer's service ID and returns a PeerIdentity | ||
| // with a fresh session token. (Legacy simple auth, kept for backward compat.) | ||
| // with a fresh session token. | ||
| // | ||
| // Legacy no-secret auth, kept only for migration: it issues a session on | ||
| // a service ID alone, with no proof of the shared secret, so anyone who | ||
| // knows (or guesses) a service ID can impersonate it. It is DISABLED by | ||
| // default and must be opted into per authenticator with | ||
| // SetAllowLegacyAuth(true). New consumers must use | ||
| // CreateChallenge/VerifyResponse. | ||
| func (a *Authenticator) Authenticate(serviceID string) (*PeerIdentity, error) { | ||
| a.mu.Lock() | ||
| defer a.mu.Unlock() | ||
|
|
||
| if !a.allowLegacyAuth { | ||
| return nil, fmt.Errorf("%w: legacy no-secret authentication is disabled for %q; use CreateChallenge/VerifyResponse", core.ErrAuth, serviceID) | ||
| } | ||
|
Comment on lines
+189
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · API reference gives stale guidance When a caller follows the API reference to authenticate or use the new controls, the reference describes Update the API reference to describe the default Also at Prompt for AI agentsIn `security/auth/identity.go` at lines 189-191:
**API reference gives stale guidance**
When a caller follows the API reference to authenticate or use the new controls, the reference describes `Authenticate` as direct trusted-context authentication but omits its default refusal and opt-in, as well as the expiry and read-deadline APIs, so callers get authentication errors and cannot discover the new controls.
Update the API reference to describe the default `Authenticate` behavior and opt-in, challenge expiry, and read-deadline APIs.
Also at `transport/transport.go:36-40`, `core/endpoint.go:316-322`, `security/auth/identity.go:85-102`.From Documentation updated when behaviour, API or setup changes. |
||
|
|
||
| capsSrc, ok := a.knownServices[serviceID] | ||
| if !ok { | ||
| return nil, fmt.Errorf("%w: unknown service %q", core.ErrAuth, serviceID) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 High · Session tokens appear in server logs
When an authenticated peer sends an intent, chat, or completion request, the intent, chat, and completion handlers log session IDs that carry the session token, so server logs expose credential material during ordinary requests.
Remove session IDs from these logs and log only non-sensitive identifiers or metadata.
Prompt for AI agents