attest unix peers on darwin, where the kernel has an answer

peerOf was implemented for Linux and stubbed everywhere else, and the stub
says the host "does not hand us a peer credential" — which is not true of
Darwin. It reports the same thing SO_PEERCRED does, split across two socket
options: LOCAL_PEERCRED carries the user and groups, LOCAL_PEERPID the
process.

The consequence was quiet. A caller identity is either attested by the kernel
or asserted by the caller, and every Mac silently had only the second kind, so
a check that fails closed on a developer's machine passes in CI for a reason
unrelated to the code under test.

A credential without a group is refused whole rather than reported with GID 0,
for the same reason the stub returns nil instead of an empty Peer: every field
of a non-nil Peer came from the kernel.

x/sys was already in the module graph and is now a direct requirement at the
version it was already resolved to.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-21 01:02:32 -07:00
co-authored by Hanzo Dev
parent 5112db48a9
commit 8e689ef9a5
4 changed files with 47 additions and 8 deletions
+45
View File
@@ -0,0 +1,45 @@
//go:build darwin
package zip
import (
"net"
"golang.org/x/sys/unix"
)
// peerOf reads the credential the KERNEL recorded for the process at the far
// end of a unix connection — the same attestation SO_PEERCRED gives on Linux,
// and the same guarantee: it is not a value the peer sends, so there is
// nothing for it to forge.
//
// It takes two socket options because Darwin splits what Linux returns at
// once: LOCAL_PEERCRED carries the user and groups, LOCAL_PEERPID the process.
//
// A non-unix conn has no answer: a tcp peer is an address, and an address is
// not an identity. That case returns nil rather than a zero Peer, so "not
// attested" is never mistaken for "attested as root" — which is also why a
// credential arriving without a group is refused whole rather than reported
// with GID 0. Every field of a non-nil Peer came from the kernel.
func peerOf(c net.Conn) *Peer {
uc, ok := c.(*net.UnixConn)
if !ok {
return nil
}
raw, err := uc.SyscallConn()
if err != nil {
return nil
}
var cred *unix.Xucred
var pid int
var sockErr error
if err := raw.Control(func(fd uintptr) {
if cred, sockErr = unix.GetsockoptXucred(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERCRED); sockErr != nil {
return
}
pid, sockErr = unix.GetsockoptInt(int(fd), unix.SOL_LOCAL, unix.LOCAL_PEERPID)
}); err != nil || sockErr != nil || cred == nil || cred.Ngroups < 1 {
return nil
}
return &Peer{PID: pid, UID: int(cred.Uid), GID: int(cred.Groups[0])}
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !linux
//go:build !linux && !darwin
package zip
+1 -1
View File
@@ -51,7 +51,7 @@ require (
github.com/zap-proto/mcp v1.0.5
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/sys v0.45.0
golang.org/x/text v0.37.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
-6
View File
@@ -87,12 +87,6 @@ github.com/zap-proto/fiber/v3 v3.2.1 h1:k45oKyTwySPtGt8sPz2Ao8OUHc7pDEhai8Np2Ym6
github.com/zap-proto/fiber/v3 v3.2.1/go.mod h1:eDm2z+ufJrkuE4MeX0Mea4oc/p7/HpXiZjVB+BXCKOA=
github.com/zap-proto/go v1.3.0 h1:S3rMoawwhH/BbSZ4G8zG05hJoQnMSMDPzIq75diCTqE=
github.com/zap-proto/go v1.3.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
github.com/zap-proto/http v0.3.1 h1:A2rCPWYCX866eAsdiWuns0dvWnBmViZtGm4pwX7jwlY=
github.com/zap-proto/http v0.3.1/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/http v0.3.2 h1:4VCAoGUQWNXuO3cXl20rYi7D+GoCObKy1BoienVXam8=
github.com/zap-proto/http v0.3.2/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/http v0.3.3 h1:waa89Ya26jSHUOeGWhcmak2hecdsnyq4z0oF4JUItFs=
github.com/zap-proto/http v0.3.3/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/http v0.3.5 h1:ezSuUbF05jxFGpfmScBnzIVTeH1ZGhcDSm7d5yL8Byc=
github.com/zap-proto/http v0.3.5/go.mod h1:UYfGhDDCetgxs65XSev8Lpf65COg5vKQK+cWwZGh4zQ=
github.com/zap-proto/mcp v1.0.5 h1:1Hi0KNi4YlWW2zkQUMDK9n3jP3wZaRJ1rseMW9p9F0c=