Help us shape the future of Go! Take the 2026 Go Developer Survey

identity

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

abstraction-identity

Let a local service verify which account and executable connected before it reads a protected request. Each fact records how strongly the operating system proved it. A service that needs executable-level proof receives no identity when the platform can prove only the user or process.

The shared native IPC transport composes this package before parsing a capability request. Applications normally consume the resulting authorization and attribution through capability clients. Service and transport authors use Peer, Proof, CanEver and the native listener directly.

In development. Windows and Linux provide the Program proof used by current protected service tests. The macOS Unix peer path cannot bind an executable to the connection and protected calls fail closed. No tagged release exists yet.

The problem

A privileged service that grants narrow permissions to named applications — "allow LogViewer to read the System log" — is only as good as its ability to tell that the caller really is LogViewer. If it cannot, every grant is theatre: any process on the machine claims to be LogViewer and the permission model is a list of names nobody checks.

This is not hypothetical. A shipped Windows tool exposes a "hold the machine awake on my behalf" API over a named pipe that any interactive user can write to, takes an owner string in the message, and never verifies it. Two callers can hold the same lock under the same name; either can release the other's.

The operating system already knows the answer. On Windows, ImpersonateNamedPipeClient plus OpenThreadToken hands the service the caller's own access token, and GetNamedPipeClientProcessId gives a pid the kernel wrote onto the pipe. What is missing is not the syscall — it is a way to say how much each part of the answer is worth, so that a service needing a verified signature can find out it cannot have one instead of quietly accepting something weaker.

Words

word meaning
peer the program on the other end: five attributes, each with a proof
proof how hard the value would be to fake, weakest first: none, claimed, invalid, unsigned, unmet, pid, bound, kernel, signed. This is an assurance level in the sense of NIST SP 800-63, and diverges from one: three rungs are verdicts on a check that failed rather than claims about strength; CONTRACT.md says why they sit where they do
claimed the peer said so; this package never produces it and has no API that accepts it
bound read through something that ties the value to the process that connected, so no successor can take its place
ceiling the best each attribute can reach on the running platform, known before any connection exists
binding the answer taken once at accept and refused afterwards rather than re-derived

No rule on this page carries a tag; the contract is CONTRACT.md, and the attack tests named there are what hold an implementation to it.

Obtain

  • Go. go get github.com/openabstractions/abstraction-identity. The module is at the repository root. One dependency, golang.org/x/sys. No tag yet; go get resolves a pseudo-version of main.
  • Other languages. See generated protocol and shared transport/client packages in this repository and the facade. Native provider support is separate.

What it does

peer, err := identity.OfHandle(identity.Handle(h), &identity.Options{ConnectedAt: at})

peer carries five attributes — user, process, path, package, code — and every one of them is welded to a Proof:

windows/npipe
  user=WORKSTATION\alice (S-1-5-21-…-1001) medium-integrity     [kernel]
  process=pid 34828 started 2026-09-05T14:24:02.234+03:00       [kernel]
  path=C:\Program Files\LogViewer\LogViewer.exe                 [bound]
  package=unknown(the peer is an ordinary executable…)
  code=LogViewer Ltd (issued by Some CA)                        [bound]

There is no way to read a value without being handed its proof at the same time:

path, err := peer.Path.AtLeast(identity.ProofBound) // err, and no value, if weaker

The ladder, weakest first: none, claimed, invalid, unsigned, unmet, pid, bound, kernel, signed. claimed means the peer said so — this package never produces it, and has no API that accepts it. It exists so that a service tempted to read an owner field out of the request payload has somewhere to look and be told no. invalid, unsigned and unmet are verdicts, not claims: the platform examined a signature and did not accept it, and only Code ever lands on them. CONTRACT.md says why they sit below pid and in that order among themselves.

Ask before you serve, not per connection

if err := identity.CanEver(policy); err != nil {
    return fmt.Errorf("this build cannot enforce its own permission model: %w", err)
}

CanEver compares a policy against what the running platform can prove at best. A policy demanding a signed code identity fails on all three, for three different reasons — Authenticode verifies a file rather than a running image; macOS verifies the running image but a socket cannot bind it to the peer; Linux has no signature to verify. On Linux it also fails if the kernel is older than 6.5 and the policy wants a bound path, because without SO_PEERPIDFD there is nothing to pin the pid. Better to fail to start than to discover that once per connection and be tempted to relax it.

Using it

// Windows will not identify the peer until the server has read from the pipe.
// This is not optional and there is no way round it; see CONTRACT.md.
h := acceptPipeInstance()
at := time.Now()                 // the line after accept
frame := readOneBoundedFrame(h)  // do not parse it yet

peer, err := identity.OfHandle(identity.Handle(h), &identity.Options{ConnectedAt: at})
if err != nil { reject(h); return }
if err := peer.Check(policy); err != nil { reject(h); return }

handle(peer, frame) // now the bytes are a request, and never a claim about
                    // who is making it

On Unix the same loop is shorter, because the credentials are on the socket at accept and no read is needed first:

c, err := listener.AcceptUnix()
at := time.Now()                 // the line after accept

peer, err := identity.OfConn(c, &identity.Options{ConnectedAt: at})
if err != nil { c.Close(); return }
if err := peer.Check(policy); err != nil { c.Close(); return }

Options.ConnectedAt is worth setting on every platform, and on macOS it is doing more work than anywhere else. It is what bounds pid reuse: nothing is read out of the peer's pid unless the process behind it can be shown to have existed before the connection did. On macOS this check does not prevent substitution by a process that existed before connection; see the measured limitation below and CONTRACT.md.

Binding a peer, instead of looking one up

OfHandle answers by looking the peer up when it is called, and a lookup is a race: every platform here resolves a program from a number, and what that number refers to can change. Bind takes the answer once, at accept, and refuses afterwards rather than re-deriving.

b, err := identity.BindConn(c, &identity.Options{ConnectedAt: at}) // on the line after accept
defer b.Close()
if err := b.Check(policy); err != nil { reject(c); return }       // ErrPeerMoved, not a stale answer

Bind returns ErrNoBinding on a machine that has no primitive for it — a Linux kernel below 6.5 — rather than degrading to a lookup. Ceiling().Bindable says so before a connection exists, so a service can refuse to start.

A peer that merely exits does not invalidate a binding: "the process that opened this connection was X" stays true after X is gone, and every primitive here pins the process so no successor can take its place. Binding.Alive is the separate question.

Today

Native Go provider profile. listen/ is the local listener the services above it share.

Windows (npipe) macOS (unix) Linux (unix)
user kernel kernel kernel
process kernel pid † kernel
path bound pid † bound with SO_PEERPIDFD, else pid
package signed (MSIX) pid † (bundle id) bound / pid (sandbox id, advisory)
code bound (Authenticode) pid † (Security framework) none
can bind a peer yes, process handle no yes with SO_PEERPIDFD, else no
identify before reading no yes yes

† macOS is the only one of the three that verifies the signature of running code rather than of a file, and it is still capped at pid, because over a plain AF_UNIX socket the audit token that selects which code to verify is resolved by XNU from the peer socket's most recent writer, not stamped at connect. Options.ConnectedAt was believed to close that and does not: p_starttime survives execve, so a helper forked before the connection and exec'ing /bin/cat after it passes every check and is handed Apple's signature. Measured twice on 15.7.4; see bind_attack_darwin_test.go. XPC is where macOS reaches signed, and this package speaks sockets.

Linux is the mirror image: unforgeable answers about who — uid, gid, and the LSM profile from SO_PEERSEC — and no general answer at all about what. Code is none there permanently.

Two processes running as the same user at the same integrity level are not isolated from each other on Windows, so "allow LogViewer to read the System log" is in practice "allow anything this user runs to read the System log". This package makes the prompt an accurate description; it does not make it a guarantee, and a prompt that implies otherwise is lying to the person clicking it. The same holds on Linux, where a same-uid process can ptrace its neighbour, rewrite its binary, and fabricate both of the sandbox identities this package can report. macOS is the one platform where the program is a meaningful unit — task_for_pid is refused against hardened binaries — and it is also the one where the binding between that program and the connection is weakest over a socket. Neither trade is obvious from the API, which is why CONTRACT.md is long: what Windows will not tell you, and what a permission system built on this must therefore refuse to promise.

Conformance

All three platforms are covered by tests in this repository, run on Windows 11, Linux 6.18 and 4.4, and macOS 15.7.4 arm64 with and without cgo. CONTRACT.md has the full "what was tested where" table, and the reasoning for every cell above. The bind_attack_*_test.go files run this platform's own impersonation attack against Bind, and go run ./cmd/peerid prints what this machine can prove, then runs an honest caller and the attack against a service that uses both paths — two real processes, one real connection, no simulation.

Why macOS is the one that needs a Mac

Windows and Linux are proved by cross-compiling and then running: go vet for the target catches what build tags hide, and a Linux kernel is available to execute against. macOS is different for one reason. codesign_darwin.go calls the Security framework through cgo, and cross-compiling turns cgo off — so every darwin build a non-Mac can perform selects codesign_nocgo_darwin.go instead, and the file that verifies a running program's signature is in no build at all. It is not weakly checked here; it is unread.

What can be checked without a Mac, and is, by TestDarwinCgoBuildTypeChecks: the file parses, and the darwin-plus-cgo build of the package type-checks with a stub standing in for the pseudo-package C. That catches a renamed constant, a struct field that no longer exists, a method gone from *Options, a return type or parameter list drifting from what identity_darwin.go calls, and a syntax error — each demonstrated by reintroducing it. What it cannot catch is anything whose type comes from C: go/types marks those expressions invalid and stops, so a return statement built out of C.GoString calls can lose a value and the check stays green.

What a real check needs, in full: a macOS host with the Xcode command line tools, and CGO_ENABLED=1 go test ./... in this directory — about a minute cold. None of it can move off a Mac: the SDK headers, the framework binaries and a signed running process to ask about are all macOS. Without a macOS runner it stays a deliberate measurement rather than a continuous one, so our own checks report darwin as UNPROVEN by name instead of passing it, and a Mac run is dated and recorded when it happens.

Where it sits

Below: the platform. Above: abstraction-asks and abstraction-rights bind every caller through it before reading a request, and abstraction-download imports it for the same reason.

One layer of openabstractions. Every layer names one thing local tools rebuild on their own; the name means the same in each language that implements it, and the conformance scenarios are what hold an implementation to it.

Requirements

Go 1.25 or newer and golang.org/x/sys. Windows, Linux, macOS.

Licence

Apache-2.0. See LICENSE.

Documentation

Overview

Package identity answers one question for a local service: which program is on the other end of this connection, and how strongly is that proven.

The second half is the point. A privileged service that grants narrow permissions to named applications - "allow LogViewer to read the System log" - is only as good as its ability to tell that the caller really is LogViewer. When it cannot, the honest outcome is a refusal, not a weaker grant made silently. So every attribute of a Peer arrives welded to a Proof, there is no way to read the value without the proof, and a service can ask CanEver at startup whether the policy it intends to enforce is achievable on this operating system at all.

Nothing here ever reads an identity out of a message. There is no API that accepts one. ProofClaimed exists only to name what is being refused.

Usage

// Once, at startup: refuse to run a policy this machine cannot enforce.
if err := identity.CanEver(policy); err != nil {
    return fmt.Errorf("this build cannot enforce its own permission model: %w", err)
}

for {
    h := acceptPipeInstance()
    at := time.Now() // the line after accept; see Options.ConnectedAt

    // Windows will not identify the peer until the server has read.
    frame := readOneBoundedFrame(h) // do not parse it yet

    peer, err := identity.OfHandle(identity.Handle(h), &identity.Options{ConnectedAt: at})
    if err != nil { reject(h); continue }
    if err := peer.Check(policy); err != nil { reject(h); continue }

    handle(peer, frame) // now the bytes may be interpreted, as a request
}                       // and never as a claim about who is making it

Platforms

Windows named pipes, Linux AF_UNIX and macOS AF_UNIX are implemented. Every other GOOS is a seam that returns ErrUnimplemented rather than a weaker answer from a portable fallback.

The three do not answer the same question equally well, and the differences are not details:

  • Windows names the process that opened the pipe and cannot verify the code it is running.
  • Linux names the user unforgeably and has no general answer to what a program is; without SO_PEERPIDFD (Linux 6.5) it cannot even bind a path to the peer, and reports that at ProofPID rather than hiding it.
  • macOS can verify a running program's signature, which neither of the others can, but over a plain socket the audit token that selects which program is resolved from the socket's current owner rather than stamped at connect - so the strong answer is bound to a weak binding.

Read CONTRACT.md before building a permission prompt on any of it. It is the document this package exists to make possible.

Index

Constants

View Source
const PackagedProgramPrefix = "msix:"

PackagedProgramPrefix begins the subject program of a caller with MSIX package identity: msix:<package family name>.

View Source
const TransportLoopback = "tcp-loopback"

TransportLoopback names a loopback TCP connection in Peer.Transport and Limits.Transport.

Variables

View Source
var (
	// ErrNoBinding means this machine has no primitive that ties an
	// identity to the process that opened the connection. It is not a
	// weaker answer; it is the absence of one, and a service that needs a
	// peer bound must refuse the connection. Ceiling().Binding says what is
	// missing.
	ErrNoBinding = errors.New("identity: this machine cannot bind a peer to a connection")

	// ErrPeerGone means there was no live process to bind to. It comes from
	// [Bind] only. A peer that exits after it has been bound does not
	// invalidate the binding: "the process that opened this connection was
	// X" stays true after X exits, and every primitive here pins the process
	// so that no successor can take its place. Ask [Binding.Alive] when
	// liveness is what matters.
	ErrPeerGone = errors.New("identity: there is no live peer to bind to")

	// ErrPeerMoved means the connection no longer answers for the process
	// that was captured when it was accepted.
	ErrPeerMoved = errors.New("identity: the connection has moved to a different program since it was accepted")
)

Errors a binding returns instead of an identity.

View Source
var (
	// ErrRemotePeer means the peer is not on this machine. A Windows named
	// pipe is reachable over SMB unless the server passes
	// PIPE_REJECT_REMOTE_CLIENTS, and a remote peer has no local pid at all.
	ErrRemotePeer = errors.New("identity: peer is not on this machine")

	// ErrNotServerEnd means the handle is not the end a service accepted:
	// the client end of a Windows named pipe, or a listening Unix socket.
	//
	// Both mistakes answer, rather than fail, if they are not caught. On the
	// client end of a pipe every question below has an answer and every
	// answer is about the service itself. SO_PEERCRED on a listening socket
	// returns the credentials captured at listen(2) - also the service's
	// own. A service that identified itself and believed it had identified a
	// caller would authorise everything.
	ErrNotServerEnd = errors.New("identity: handle is not the end of the connection the service accepted")

	// ErrUnsupportedConn means the connection is of a kind that carries no
	// peer credentials, or one whose handle this package cannot reach.
	ErrUnsupportedConn = errors.New("identity: connection carries no peer credentials")

	// ErrMustReadFirst means Windows will not identify a named pipe's client
	// until the server has completed a read on that pipe. It is not enough
	// for the client to have written; the server must have read.
	//
	// The consequence is unpleasant and unavoidable: a service has to accept
	// and read bytes from a caller it cannot yet name. Read one bounded
	// frame, identify the peer, and only then decide what the bytes were
	// allowed to ask for. Do not parse the frame first, do not size a buffer
	// from it, and above all do not read a name out of it - see
	// CONTRACT.md, "You must listen before you can ask who is speaking".
	ErrMustReadFirst = errors.New("identity: windows will not identify a pipe client until the server has read from the pipe")

	// ErrImpersonationStuck means the operating system refused to undo an
	// impersonation. It is returned instead of an identity, because a
	// process that cannot drop a client's token has a privilege escalation,
	// not a naming problem. The thread that held the token is destroyed
	// rather than returned to the scheduler; see impersonate_windows.go.
	ErrImpersonationStuck = errors.New("identity: could not revert impersonation; thread abandoned")

	// ErrUnimplemented means this platform's answer has not been built yet.
	// It is returned instead of a weaker answer from a portable fallback,
	// deliberately: a permission system that silently degrades to "some
	// process on this machine" is worse than one that will not start.
	ErrUnimplemented = errors.New("identity: not implemented on this platform")
)

Errors that say the channel itself cannot answer the question, as distinct from answering it weakly.

View Source
var ErrNotProven = errors.New("identity: not proven to the required level")

ErrNotProven is the root of every failure to meet a required proof level. Test for it with errors.Is.

Functions

func CanEver

func CanEver(n Need) error

CanEver reports whether a policy is satisfiable on this platform at all, under the best case for every attribute.

Call it at startup, not per connection. The answer does not depend on who is calling, and a service whose permission model rests on a proof this platform cannot produce should refuse to start rather than fail open once per connection - or, worse, decide it can relax the requirement.

func PackageFamily added in v0.3.0

func PackageFamily(fullName string) (string, bool)

PackageFamily returns the family name of an MSIX package full name, <Name>_<Version>_<Architecture>_<ResourceId>_<PublisherId>, where ResourceId may be empty.

func SubjectProgram added in v0.3.0

func SubjectProgram(p *Peer, pathProof Proof) (string, error)

SubjectProgram names the program a service's rights, credentials and inference decisions are about. A Windows caller whose package identity is proven at ProofSigned, and whose image lies inside that package's installed folder, is msix:<package family name>: the image path of a packaged app changes with every package version, and the family does not. The folder check is needed because any process of the account can start an arbitrary program with an installed package's identity (Invoke-CommandInDesktopPackage), while it cannot write into the package's folder under WindowsApps. Every other caller, including a descendant of a packaged app, which has no identity of its own, is its clean absolute image path at pathProof or better.

func ValidSubjectProgram added in v0.3.0

func ValidSubjectProgram(program string) bool

ValidSubjectProgram reports whether program is a subject program as SubjectProgram names one: msix:<package family name>, or a clean absolute path of at most 4096 bytes without NUL, CR or LF.

Types

type Attr

type Attr[T any] struct {
	// contains filtered or unexported fields
}

Attr is one attribute of a peer together with the strength of the evidence for it. There is deliberately no way to read the value without being handed the proof at the same time: a service that needs a verified signature has to see that it got something weaker instead of silently accepting it.

func (Attr[T]) AtLeast

func (a Attr[T]) AtLeast(min Proof) (T, error)

AtLeast returns the value only if it is proven at least as strongly as min, and otherwise a *ProofError explaining what was available instead.

func (Attr[T]) Get

func (a Attr[T]) Get() (T, Proof)

Get returns the value and its proof together. Use it when the decision depends on the proof; use AtLeast when a fixed minimum is required.

func (Attr[T]) Known

func (a Attr[T]) Known() bool

Known reports whether any value at all was determined.

func (Attr[T]) Name

func (a Attr[T]) Name() string

Name is the attribute's name, as it appears in messages.

func (Attr[T]) Proof

func (a Attr[T]) Proof() Proof

Proof is the strength of the evidence. ProofNone means there is no value.

func (Attr[T]) String

func (a Attr[T]) String() string

String renders the attribute for logs, always including the proof, so that a log line cannot look stronger than it is.

func (Attr[T]) Why

func (a Attr[T]) Why() string

Why explains how the attribute came to be as strong as it is, and no stronger. It is populated whenever there is something a reader would otherwise get wrong - including for values that were resolved successfully.

type Binding

type Binding struct {
	// contains filtered or unexported fields
}

A Binding is a peer identity captured when the connection was accepted, together with whatever handle the platform offers for asking, later, whether that capture is still true.

It exists because OfHandle answers by looking the peer up at the moment it is called, and a lookup is a race by construction: every platform here resolves a program from a number, and the thing that number refers to can change. A Binding takes the answer once, at the only instant the caller controls, and refuses afterwards rather than re-deriving.

When to call it differs by platform, and the difference is not cosmetic:

  • Linux and macOS: at accept, before the service reads a single byte. On macOS this is load-bearing. LOCAL_PEERTOKEN follows the socket's most recent writer, so a byte written by another process before this call is a byte this call believes.
  • Windows: after the first read, the same precondition OfHandle has. Nothing is lost by waiting: the client process id is written onto the pipe instance by the kernel when the client opens it and never moves, and a Windows process cannot replace its own image.

func Bind

func Bind(h Handle, opts *Options) (*Binding, error)

Bind captures the peer identity of an accepted local connection and keeps the platform handle needed to re-check it.

The returned Binding must be closed. See the type documentation for when to call this, which is not the same on all three platforms.

func BindConn

func BindConn(c net.Conn, opts *Options) (*Binding, error)

BindConn is Bind for a net.Conn that is willing to give up its handle.

The returned Binding keeps a reference to that handle and is only valid while c is open: close the Binding before closing c. This is the one place this package holds a descriptor outside raw.Control, and it is unavoidable - a binding that has to be re-checked later cannot re-derive the handle it was taken from.

func BindLoopback added in v0.3.0

func BindLoopback(c net.Conn, opts *Options) (*Binding, error)

BindLoopback binds the process on the other end of an accepted IPv4 loopback TCP connection, through the operating system's socket-owner table.

A loopback socket carries no peer credentials: the kernel stamps nothing onto the connection that names the process at the other end. What it does keep is a table of the sockets each process owns. The binding looks the client's end of this connection up in that table, pins the process it names, and refuses afterwards when the table stops agreeing:

  • Windows: GetExtendedTcpTable with TCP_TABLE_OWNER_PID_CONNECTIONS names the process that created the client socket. The binding opens that process, requires its creation time to predate the connection, and refuses with ErrPeerMoved once the process has exited while the connection is still established, because another process then holds the socket.
  • Linux: /proc/net/tcp names the socket inode and its owning uid, and the binding finds the one same-uid process whose descriptor table holds that inode, pins it with pidfd_open and captures its image path. A socket held by two processes, or later held by a different one, is ErrPeerMoved.
  • macOS: ErrNoBinding. The recorded identity refusal stands; see LoopbackCeiling.

Call it on the line after accept, before the service reads a byte, and re-check with Binding.Peer before acting. c must be the accepted end: this function cannot tell a connection the service dialed from one it accepted, and a dialed connection would name the listening service. The returned Binding must be closed before c.

func BindServer added in v0.2.1

func BindServer(h Handle, opts *Options) (*Binding, error)

BindServer captures the server of an already connected local CLIENT handle, before application bytes are sent. The caller must establish that the handle belongs to the local transport namespace; this raw-handle primitive does not validate the path used to open it. The caller owns the connection and closes the returned binding before that connection. ConnectedAt must describe the successful dial, when supplied. This identifies a process; the caller still needs independent trust evidence to authorize that process as its server.

func BindServerConn added in v0.2.1

func BindServerConn(c net.Conn, opts *Options) (*Binding, error)

BindServerConn is BindServer for a connection exposing syscall.Conn.

func (*Binding) Alive

func (b *Binding) Alive() (bool, error)

Alive reports whether the bound process still exists. It is a separate question from whether the identity is still trustworthy, and services that conflate the two end up refusing honest callers that happened to exit.

func (*Binding) BoundAt

func (b *Binding) BoundAt() time.Time

BoundAt is when the capture was taken.

func (*Binding) Captured

func (b *Binding) Captured() *Peer

Captured returns the identity as it was at bind time without re-checking it. It is for log lines and for tests that need to compare the two answers. A permission decision uses Binding.Peer.

func (*Binding) Check

func (b *Binding) Check(n Need) error

Check is Peer.Check against the re-checked capture.

func (*Binding) Close

func (b *Binding) Close() error

Close releases the platform handle. A Binding that is not closed leaks a pidfd on Linux and a process handle on Windows, and on Linux a leaked pidfd holds the peer's pid reserved for as long as the service runs.

func (*Binding) Peer

func (b *Binding) Peer() (*Peer, error)

Peer returns the captured identity, having first confirmed that nothing has contradicted it. It returns ErrPeerMoved instead of a stale answer.

A peer that has merely exited is not a contradiction and is answered normally; see Binding.Alive.

func (*Binding) Shared added in v0.3.0

func (b *Binding) Shared() *Binding

Shared returns a view of b for one request on a connection that carries several. The view rechecks through b's platform handle exactly as b does, and closing it releases nothing: the connection's owner closes b, after every view is done.

type Code

type Code struct {
	// Subject is the name the signing certificate was issued to: the
	// publisher, as a person reading a consent prompt would understand it.
	Subject string
	// Issuer is the certificate authority that vouched for the subject.
	// Subject alone is not an identity; anyone can obtain a certificate
	// naming themselves anything a CA will accept.
	Issuer string
	// Trusted is true when the platform's own trust policy accepted the
	// signature and the chain, with revocation checking as configured in
	// Options. It is the same verdict the Proof beside this Code carries, as
	// a boolean for logs: a policy compares the Proof, and a Code with
	// Trusted false never sits above ProofUnmet. Status says how it failed.
	Trusted bool
	// Status is the platform's verdict in its own words, always populated:
	// "valid", "no signature", or the failing condition.
	Status string
	// Catalog is true on Windows when the signature came from a system
	// catalog rather than from an embedded signature in the file itself.
	Catalog bool

	// TeamID is the Apple Developer team identifier out of the validated
	// signature on macOS, and empty elsewhere.
	//
	// It is the field to write a rule against on that platform. Subject is a
	// display name a certificate authority was willing to issue and two
	// publishers can share one; a team id is assigned by Apple and is the
	// stable half of the answer. Platform binaries have none: they validate
	// against the system's own anchor instead, which is a different claim
	// and should be checked with a requirement rather than a string compare.
	TeamID string
}

Code is a code signing identity as some part of the operating system verified it - not as a file on disk claims it.

func (Code) String

func (c Code) String() string

type Handle

type Handle uintptr

Handle is an operating-system handle for one accepted local connection: on Windows the server end of a named pipe instance, on Unix the file descriptor of a connected socket.

The handle must still be open, and must be the end the service accepted, not the end the client opened. Ownership stays with the caller: nothing in this package closes it.

type Limits

type Limits struct {
	Platform string
	Best     Need
	Why      map[string]string // "user", "process", "path", "package", "code"

	// Transport names the channel these limits are about. The same platform
	// answers differently on different channels and a ceiling that omits
	// which one is unreadable.
	Transport string

	// Bindable reports whether [Bind] can tie an identity to the process
	// that opened the connection on this machine, as opposed to looking a
	// program up from a number afterwards. It is a property of the running
	// kernel, not of the GOOS: the same Linux binary answers true on 6.5 and
	// false on 4.4.
	Bindable bool

	// Binding names the primitive behind Bindable, or states what is missing
	// when it is false. Always populated.
	Binding string

	// Stronger names the transport that would prove more than this one, and
	// what it would add. Empty when this transport is already the platform's
	// best.
	Stronger string
}

Limits is what a platform can prove at best, with the reason for each ceiling. Obtained from Ceiling.

func Ceiling

func Ceiling() Limits

Ceiling reports what this platform can prove at best, and why it stops there. It does not depend on any connection.

func LoopbackCeiling added in v0.3.0

func LoopbackCeiling() Limits

LoopbackCeiling reports what this platform can prove at best about the process behind a loopback TCP connection. It does not depend on any connection.

func (Limits) String

func (l Limits) String() string

type Need

type Need struct {
	User    Proof
	Process Proof
	Path    Proof
	Package Proof
	Code    Proof
}

Need is a policy: the minimum proof a service requires for each attribute before it will act. The zero Need requires nothing.

type Options

type Options struct {
	// ConnectedAt is when the connection was accepted.
	//
	// Resolution refuses to report anything read out of the peer's pid
	// unless it can first open the process and see that it was created
	// before this instant. A process that inherited a recycled pid must have
	// been created after the connection already existed, so the check
	// excludes every one of them.
	//
	// Leave it zero and the check uses the moment of resolution instead,
	// which still excludes any reuse after that moment but not reuse between
	// accept and resolution. Set it in the accept loop, on the line after
	// accept returns, and the window closes to the width of one accept.
	ConnectedAt time.Time

	// CheckRevocation asks the platform to contact revocation infrastructure
	// while verifying a signature. It is off by default because it makes
	// identification depend on the network, and a permission prompt that
	// hangs is a permission prompt that gets clicked through.
	CheckRevocation bool

	// SkipCodeSignature skips signature verification entirely. On Windows
	// the first verification for a process costs tens of milliseconds, and
	// more for a large signed image; later connections from the same live
	// process reuse that verdict (CONTRACT.md). A service that never asks
	// about Code should not pay it.
	SkipCodeSignature bool

	// CodeRequirement is a macOS code signing requirement, such as
	//
	//	anchor apple generic and certificate leaf[subject.OU] = "TEAMID"
	//
	// It is handed to SecCodeCheckValidity, so the operating system decides
	// whether the peer satisfies it. That is worth more than reading
	// Code.TeamID and comparing it here: the requirement language covers the
	// anchor, the chain and the leaf together, and a comparison written in
	// Go can only see what this package chose to extract.
	//
	// Empty means "verify the signature, tell me what it says, and let me
	// decide". Ignored on Windows and Linux, neither of which has anything
	// to give it to.
	CodeRequirement string
}

Options tunes resolution. The zero value is safe. ConnectedAt is worth setting, and is the only field most callers need.

type Peer

type Peer struct {
	// Platform is the GOOS the answer came from. The answer differs per
	// platform and a log line that omits which one is unreadable later.
	Platform string
	// Transport names the channel the identity was taken from: "npipe" on
	// Windows, "unix" on Linux and macOS. The same platform gives different
	// answers on different transports - on macOS, dramatically so, since an
	// XPC peer can be identified to a standard a socket peer cannot - so the
	// transport belongs next to the answer.
	Transport string
	// ObservedAt is when the identity was resolved, which is not when the
	// connection was accepted. See Options.ConnectedAt.
	ObservedAt time.Time

	// User is the security principal the peer runs as.
	User Attr[User]
	// Process is which process it is.
	Process Attr[Process]
	// Path is the filesystem path of the running image.
	Path Attr[string]
	// Package is a package identity where the platform has one: an MSIX
	// package full name on Windows, the signing identifier out of a
	// validated signature on macOS, a sandbox application id on Linux
	// ("flatpak:org.gnome.Calculator", "snap:chromium.chromium"). Unset for
	// ordinary executables.
	//
	// The three are not comparable. The Windows one comes out of a
	// certificate the OS validated; the macOS one out of a signature it
	// validated; the Linux one out of a file in the peer's own mount
	// namespace or a cgroup name, neither of which resists a hostile
	// process running as the same user. See CONTRACT.md before writing a
	// rule against this field.
	Package Attr[string]
	// Code is a code signing identity for the image, and never exceeds
	// ProofBound on any platform this package supports.
	//
	// On Windows it is an Authenticode verification of the file the process
	// was launched from, which is a weaker statement than it looks. On macOS
	// it is a Security framework verification of the running code, which is
	// a stronger statement - attached to a weaker binding, because a unix
	// socket cannot prove which process it belongs to. On Linux it is always
	// unset: the platform has nothing to verify. See CONTRACT.md.
	//
	// A signature the platform examined and did not accept is reported below
	// ProofPID, at ProofInvalid, ProofUnsigned or ProofUnmet, so AtLeast
	// refuses it for any policy that implies verification and admits it only
	// for a policy that names the lower rung.
	Code Attr[Code]

	// Notes records everything a reader would otherwise get wrong about this
	// particular answer: every place it came out weaker than this platform's
	// best, and why. It is for humans, for logs and for tests.
	Notes []string
}

Peer is what a service was able to learn about the program on the other end of an accepted local connection, with the strength of the evidence attached to every part of it separately.

Nothing in a Peer was ever spoken by the peer. There is no field a caller can fill in from a request payload, and no constructor that takes one.

func OfConn

func OfConn(c net.Conn, opts *Options) (*Peer, error)

OfConn is OfHandle for a net.Conn that is willing to give up its handle.

Most named-pipe libraries on Windows are not: they wrap the handle in an unexported struct with no accessor. When that is the case OfConn returns ErrUnsupportedConn and the caller must keep hold of the handle it created the pipe instance with and use OfHandle. This is not a workaround to be embarrassed about - the handle is the object the identity belongs to.

func OfHandle

func OfHandle(h Handle, opts *Options) (*Peer, error)

OfHandle identifies the program on the other end of an accepted local connection.

It never blocks on the peer and never reads from the connection itself.

On Windows there is a precondition that is easy to miss and impossible to work around: the server must already have completed a read on the pipe. Windows refuses to impersonate the client of a pipe nothing has been read from, and it is the read that matters, not the client's write. So the sequence is accept, read one bounded frame, identify, and only then decide what that frame was allowed to ask for. Calling OfHandle before the first read returns ErrMustReadFirst.

A returned error means the channel could not answer the question at all - a remote peer, the wrong end of the pipe, a refused impersonation. An answer that is merely weak comes back as a *Peer with low Proof values and a populated Notes, not as an error, because only the caller's policy knows whether weak is fatal.

func (*Peer) Check

func (p *Peer) Check(n Need) error

Check reports whether this Peer meets the policy. The error names every attribute that fell short, so a service does not learn about its second impossible requirement only after fixing the first.

func (*Peer) Rung added in v0.3.0

func (p *Peer) Rung() string

Rung is the compact proof summary a decision records: the transport, the platform, and the proof of the user, process and path attributes.

func (*Peer) String

func (p *Peer) String() string

type Process

type Process struct {
	// PID is the process id the kernel recorded for the peer when it
	// connected. It names the process that opened the connection, which is
	// not necessarily the process writing to it now: handles can be
	// inherited and duplicated. See CONTRACT.md.
	PID int
	// StartTime is when that process was created. It is the thing that
	// distinguishes a process from a later one that inherited its number,
	// so it belongs in a log next to the pid. Zero when the process could
	// not be opened.
	StartTime time.Time
	// Recycled is true when the pid was proven to belong to a different,
	// later process than the one that connected. Nothing resolved from the
	// pid is reported when it is set.
	Recycled bool

	// Generation distinguishes this process from a later one that inherits
	// its pid, where the platform keeps a counter for exactly that: the
	// pidversion inside a macOS audit token. Zero where the platform has
	// none, which is everywhere else - Windows uses StartTime for the same
	// job and Linux has neither.
	//
	// It is not a bound on its own. A pid and a pidversion read together
	// describe one process instance consistently; whether that instance is
	// the peer is a separate question, and on macOS sockets it is the
	// unanswered one. See CONTRACT.md.
	Generation uint32
}

Process identifies the process on the other end.

func (Process) String

func (p Process) String() string

type Proof

type Proof uint8

Proof says how hard it would be for a hostile caller to make one attribute of a Peer lie. It is a total order because a policy has to compare against a minimum, but the steps are qualitatively different, and the documentation of each one says what it actually rests on. Read those before writing a policy; the number alone is not the argument.

From ProofPID upwards a rung says how the value was bound to the peer. The three rungs below it are verdicts, produced only for Code: the platform examined the peer's signature and did not accept it, and the rung says how far it got. They sit below every rung that implies verification, so a policy that requires a verified signature refuses them by comparison alone, and a policy that knowingly admits an unverified caller names the lower rung.

const (
	// ProofNone means the attribute was not determined: either the platform
	// cannot supply it at all, or this particular connection did not carry
	// it. The value is meaningless and must not be used. Attr refuses to
	// hand it out.
	ProofNone Proof = iota

	// ProofClaimed means the peer said so. This package never produces it
	// and has no API that accepts it. The level exists to name the thing
	// being refused, so a service tempted to read an "owner" field out of
	// the request payload has somewhere to look and be told no.
	ProofClaimed

	// ProofInvalid means the platform found a signature and refused it: the
	// code was modified after signing, the certificate is expired or revoked,
	// or the chain reaches a root this machine does not trust. It is the
	// lowest verdict, below ProofUnsigned, because no signature is no claim
	// and a refused signature is a claim the operating system rejected. The
	// value carries the platform's wording in Code.Status and no identity.
	ProofInvalid

	// ProofUnsigned means the platform looked and found no signature at all.
	// A policy that admits unsigned callers asks for this rung by name.
	ProofUnsigned

	// ProofUnmet means the signature is intact and the platform accepted it,
	// but the code does not satisfy the requirement the service asked for in
	// Options.CodeRequirement. It is the strongest verdict short of
	// verification: intact code, signed by somebody other than who was
	// required. Nothing is read out of a signature that failed the check it
	// was given, so the value still carries no identity. Only macOS produces
	// it; Windows has no requirement language.
	ProofUnmet

	// ProofPID means the attribute was read out of the operating system by
	// process id, after the connection was already up, without checking that
	// the process behind that id is still the one that connected. It is racy
	// by construction: pids are reused. Good enough for a log line, never
	// good enough for a permission decision.
	ProofPID

	// ProofBound means the attribute was read through something that ties
	// it to the process that connected, rather than to a number that
	// process once had. What that something is differs per platform, and so
	// does exactly what it excludes:
	//
	//   - Windows: a process handle whose creation time predates the
	//     connection, which excludes every process that could have
	//     inherited the pid. The reuse race is closed.
	//   - Linux: a pidfd the kernel derived from the connection itself
	//     (SO_PEERPIDFD). While it is open the pid cannot be reassigned, so
	//     /proc read behind it answers about the peer or about nothing.
	//   - macOS: a pid and pidversion from the peer socket, cross-checked
	//     against the connect-time credentials and refused if the process
	//     started after the connection. That excludes pid reuse and any
	//     program spawned to impersonate; it does not exclude a process
	//     that already existed and was handed the socket. See CONTRACT.md.
	//
	// The attribute is still only the operating system's record about a
	// running process (an image path, say), not a statement about code -
	// except on macOS, where a verified code signature also lands here,
	// because over a socket the signature is only worth what the binding
	// above is worth.
	ProofBound

	// ProofKernel means the kernel stamped the attribute onto the connection
	// itself, out of its own records, at the moment the peer connected. The
	// peer never spoke it and cannot influence it. The access token behind
	// ImpersonateNamedPipeClient is of this kind, and so is the client
	// process id the kernel recorded on the pipe; on Linux, so are the uid,
	// gid and pid in SO_PEERCRED and the LSM label in SO_PEERSEC.
	//
	// "At the moment the peer connected" is the load-bearing half. A value
	// the kernel produces honestly but resolves when asked - LOCAL_PEERPID
	// and LOCAL_PEERTOKEN on macOS, which follow the peer socket's current
	// owner - is not of this kind, however official its source.
	ProofKernel

	// ProofSigned means the operating system itself validated a code
	// signature and the identity reported here came out of that validation -
	// not out of a filename. On Windows only MSIX package identity reaches
	// this level: the package full name contains a hash of the publisher
	// identity from the certificate the OS checked when it installed and
	// launched the package, and no unpackaged process can be given one.
	//
	// Authenticode on a plain executable does NOT reach this level, however
	// good the signature is. See Peer.Code and CONTRACT.md.
	//
	// Neither does macOS over a unix socket, which is the surprise in this
	// package: the Security framework really does verify running code, but
	// the audit token that selects which code is resolved from the peer
	// socket's current owner rather than stamped at connect. The transport
	// that reaches this level on macOS is XPC, whose messages carry a token
	// the kernel stamped on the sender. This package speaks sockets, so no
	// platform it supports reports ProofSigned for an ordinary program.
	ProofSigned
)

func (Proof) String

func (p Proof) String() string

type ProofError

type ProofError struct {
	Attribute string
	Want, Got Proof
	Why       string
}

ProofError reports an attribute that is either unknown or known less firmly than the caller demanded. Why carries the reason the platform gave, which is usually the only actionable part of the message.

func (*ProofError) Error

func (e *ProofError) Error() string

func (*ProofError) Is

func (e *ProofError) Is(target error) bool

type User

type User struct {
	Kind string // "windows" or "posix"

	// Windows.
	SID      string // e.g. "S-1-5-21-...-1001"
	Name     string // account name, best effort, may be empty
	Domain   string // account domain, best effort, may be empty
	Elevated bool   // the peer's token is elevated

	// Integrity is the peer's mandatory integrity level: "untrusted",
	// "low", "medium", "high", "system", or "unknown".
	//
	// It matters more than it looks. Two processes at the same integrity
	// level running as the same user are not isolated from each other on
	// Windows: either can open the other for full access and write into
	// it. So the answer this package gives about a same-user, same-level
	// peer identifies a program but does not defend against one. The
	// boundary that does hold is a lower level talking to a higher one - a
	// medium-integrity process cannot touch a high-integrity one, and an
	// AppContainer (low or untrusted) cannot touch either. See CONTRACT.md.
	Integrity string
	// IntegrityRID is the raw mandatory label RID behind Integrity, for
	// comparisons the named levels are too coarse for. Zero when unknown.
	IntegrityRID uint32

	// POSIX.
	UID int // -1 when unknown
	GID int // -1 when unknown

	// SecurityContext is the peer's LSM label on Linux: an SELinux context
	// ("unconfined_u:unconfined_r:unconfined_t:s0") or an AppArmor profile
	// ("/usr/bin/evince (enforce)"), read from SO_PEERSEC.
	//
	// It belongs to the principal rather than to the process because that
	// is what it is - a label the kernel attaches to a subject at exec, by
	// local policy - and it is stamped onto the socket at connect, so it
	// carries the same ProofKernel strength as the uid beside it. On an
	// AppArmor system it is worth more than the image path: the path names
	// a file, the profile names the policy the kernel is actually enforcing
	// against the peer. It is not a signature and never becomes one.
	//
	// Empty when no LSM labels processes on this system, and on every
	// platform other than Linux.
	SecurityContext string

	// AuditSessionID is the macOS audit session the peer belongs to, from
	// its audit token. Zero elsewhere. Two processes in the same audit
	// session are, roughly, the same login session.
	AuditSessionID uint32
}

User is a security principal. Which fields are populated depends on the platform; Kind says which.

func (User) String

func (u User) String() string

Directories

Path Synopsis
cmd
peerid command
Command peerid shows what this machine can prove about the program on the other end of a local connection, and then attacks it.
Command peerid shows what this machine can prove about the program on the other end of a local connection, and then attacks it.
go
internal
integrity
Package integrity reads the mandatory integrity level Windows stamps on a token.
Package integrity reads the mandatory integrity level Windows stamps on a token.
Package remote supplies explicitly trusted, mutually authenticated TLS streams for the shared frame protocol.
Package remote supplies explicitly trusted, mutually authenticated TLS streams for the shared frame protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL