Documentation
¶
Overview ¶
Package directory answers three questions about the people a server serves: who is here, what proves them, and who is in which group.
The answers come from wherever a site already keeps them — a database, an LDAP server, a file, or the program's own configuration — and every source answers the same three questions, so the code that USES an identity never learns where it came from.
What proves somebody is not one thing ¶
This is the whole reason the package exists, and it is a fact about protocols rather than a design choice:
NTLMv2 (SMB, Windows file sharing) is a CHALLENGE-RESPONSE. The client never sends the password, so the server must compute MD4(UTF16LE(password)) itself. An LDAP bind cannot answer it. Neither can a bcrypt. What answers it is the password, or that MD4 — the "NT hash", which is exactly what Samba's sambaNTPassword attribute holds. HTTP Basic, and anything else that hands the server the password, can be answered by ANY verifier: a comparison, a hash, or a bind against a directory that holds the secret and will not give it up. SSH authenticates with a public key, or a certificate from an authority. No password is involved at all, and a directory publishes the keys — OpenSSH's convention is the sshPublicKey attribute.
So an Identity carries what its source could give, each protocol uses what it can, and Identity.Can says which is which. A server can then tell a person "you can use WebDAV and SFTP, and not SMB, because this directory holds a bcrypt" — before they meet a refusal at a mount.
Reading is done once ¶
A source is read when the server starts, and a change to it is picked up by a restart. That is a deliberate limit rather than an oversight: the alternative is asking the directory on every connection, which is a different design with different failure modes (a server that stops authenticating when the database blinks), and this package says which one it is rather than implying.
The exception is a PASSWORD CHECK: Identity.Verify may reach the source every time, because a bind is the only way an LDAP directory will answer "is this the right password" and it cannot be cached without holding the secret this package went out of its way not to hold.
Index ¶
- Constants
- Variables
- func CheckAttributeName(name string) error
- func Expand(names []string, src Source) ([]string, error)
- func IsGroup(name string) bool
- func NTHashOf(password string) []byte
- func ParseNTHash(s string) ([]byte, error)
- func ParseTOTPSecret(s string) ([]byte, error)
- func ReservedAttributes() map[string]string
- type Attribute
- type Attributes
- type Credential
- type GroupAttributer
- type GroupLister
- type Identity
- func (i *Identity) Attributes() Attributes
- func (i *Identity) Can(c Credential) bool
- func (i *Identity) Derive(derive func(password string) ([]byte, error)) ([]byte, error)
- func (i *Identity) Groups() []string
- func (i *Identity) KerberosKey(derive func(password string) ([]byte, error)) ([]byte, error)
- func (i *Identity) Keys() []string
- func (i *Identity) NTKey() ([]byte, error)
- func (i *Identity) Name() string
- func (i *Identity) TOTPSecret() []byte
- func (i *Identity) Verify(password string) error
- func (i *Identity) Where() string
- type Option
- func From(where string) Option
- func WithAttributes(a Attributes) Option
- func WithGroups(groups ...string) Option
- func WithNTHash(hash []byte) Option
- func WithPassword(password string) Option
- func WithPublicKeys(keys ...string) Option
- func WithTOTPSecret(secret []byte) Option
- func WithVerifier(verify func(password string) error) Option
- type Set
- func (s *Set) Add(src Source)
- func (s *Set) Close() error
- func (s *Set) Describe() string
- func (s *Set) GroupAttributes(group string) (Attributes, error)
- func (s *Set) GroupNames() ([]string, error)
- func (s *Set) Identities() ([]*Identity, error)
- func (s *Set) Members(group string) ([]string, error)
- func (s *Set) Sources() []Source
- type Source
- type Static
Constants ¶
const GroupPrefix = "@"
GroupPrefix is how a group is written where a person could be: @staff. It is Samba's spelling (`valid users = @staff`) and what anybody administering a file server types without being told.
Variables ¶
var ErrAttributeName = errors.New("directory: not an attribute this package carries")
ErrAttributeName is a name that cannot be an attribute here: not an RFC 4512 attribute description, or one this package refuses to carry. The message says which, and why.
var ErrAttributeValue = errors.New("directory: not a value an attribute can hold")
ErrAttributeValue is an attribute with no value, or an empty one.
var ErrEmptyGroup = errors.New("directory: the group has no members")
ErrEmptyGroup is what Expand returns for a group a source HAS, with nobody in it: not ErrNoSuchGroup, because the remedy differs -- a typo or a deleted group on one side, a group to fill on the other. A caller that tolerates a group that grants nothing tests for both.
var ErrNoCredential = errors.New("directory: this identity has no such credential")
ErrNoCredential says this identity cannot answer that question at all, which is a different thing from answering it wrongly: one is a configuration to fix, the other is somebody typing the wrong password.
var ErrNoSuchGroup = errors.New("directory: no such group")
ErrNoSuchGroup is what a source returns for a name it does not have.
var ErrWrongPassword = errors.New("directory: wrong password")
ErrWrongPassword is what a failed check returns.
Functions ¶
func CheckAttributeName ¶ added in v0.13.0
CheckAttributeName says whether name can be an attribute: nil, or an error wrapping ErrAttributeName that says why not.
It is exported so that a configuration can be refused before anything is read -- ldapdir checks the attributes it is told to copy when it is built, rather than at the first search.
A name is an attribute description (RFC 4512 §2.5), restricted to the descriptor form:
attributedescription = descr *( ";" option ) descr = ALPHA *( ALPHA / DIGIT / "-" ) ; §1.4 keystring option = 1*( ALPHA / DIGIT / "-" )
The numeric form RFC 4512 also allows (2.5.4.35) is refused, because the refusals below are by NAME: 2.5.4.35 is userPassword, and a list of names cannot recognise it. Every attribute a server would publish has a descriptor.
Then, whatever the case and whatever the options, it refuses:
- anything holding a secret: a password, a hash of one, a key, a one-time code secret, a trust secret -- the list is in ReservedAttributes, and any name containing password, passwd, pwd, secret, credential, hash or privatekey is refused with it, because a list of names only knows the schemas somebody read;
- sshPublicKey, which has its own path through WithPublicKeys, so that Identity.Can keeps telling the truth about who can use SSH;
- uid, cn and objectClass (and their aliases userid and commonName), which a server derives from the name it publishes an entry under;
- member, uniqueMember, memberUid and memberOf, which are membership -- Source.Members answers that, and a second answer would disagree;
- the operational attributes a server maintains (RFC 4512 §3.4, entryDN, entryUUID), which a configuration could only forge.
func Expand ¶
Expand turns a list of names — people and groups — into the people in it, keeping the order and dropping repeats.
A group with nobody in it is refused rather than expanded to nothing: a configuration that grants nothing to nobody reads exactly like one that works.
func NTHashOf ¶
NTHashOf is MD4(UTF16LE(password)), the value Samba stores as sambaNTPassword and Windows calls the NT hash.
MD4 is broken as a hash function and is used here anyway, because NTLMv2 is DEFINED over it: this is not a choice about security, it is the arithmetic the protocol specifies, and a server that used anything else would refuse every correct password.
func ParseNTHash ¶
ParseNTHash reads the hex a directory publishes.
A hash that is not 16 bytes is refused rather than padded or truncated: a mangled one would fail every login with "wrong password", which is the least useful thing a server could say.
func ParseTOTPSecret ¶ added in v0.4.0
ParseTOTPSecret reads the base32 a directory stores a one-time-code secret as: upper or lower case, spaces and padding optional, which is how people copy them out of an authenticator.
github.com/go-authn/totp reads the same shape and neither package imports the other: this one is about who somebody IS, that one is about one way of checking, and a dependency either way would make every consumer of one carry the other. Eight lines of encoding/base32 is the cheaper of the two prices, and it is written down here so that the next reader knows it was a choice.
func ReservedAttributes ¶ added in v0.13.0
ReservedAttributes are the names CheckAttributeName refuses outright, whatever their case, each with the reason it gives. Names containing a secret-looking fragment are refused besides, without being listed.
It returns a copy: the list is what this package enforces, and changing a copy changes nothing.
Types ¶
type Attribute ¶ added in v0.13.0
An Attribute is one name and its values, as given to NewAttributes.
type Attributes ¶ added in v0.13.0
type Attributes struct {
// contains filtered or unexported fields
}
Attributes are what a source says about somebody, or about a group, that is not proof of anything: a uidNumber, a home directory, a mail address, a display name.
They are kept apart from credentials on purpose. A credential decides who may log in and is guarded accordingly -- Identity.NTKey and Identity.TOTPSecret say so in their own documentation. An attribute is published: a server turns it into an LDAP entry that anybody allowed to search may read. So the one thing this type will not hold is a secret, and CheckAttributeName is where that is refused.
The names are LDAP attribute descriptions (RFC 4512 §2.5): a descriptor and optional options, such as "mail" or "description;lang-fr". They are case-insensitive, as LDAP's are, and the order of the options does not matter. Values are strings and are not interpreted -- "1000" is a uidNumber because a server's schema says so, not because this package parsed it.
An Attributes is immutable once built, so one can be shared and copied freely. The zero value holds nothing.
func NewAttributes ¶ added in v0.13.0
func NewAttributes(attrs ...Attribute) (Attributes, error)
NewAttributes checks these attributes and puts them together.
The order is kept. A name given twice -- or twice in different case, or with its options in a different order -- is one attribute, spelled the first way it was written, holding the values of both. A value repeated exactly is kept once: an LDAP attribute may not hold two equal values (RFC 4512 §2.2), and an exact repeat is equal under every matching rule there is. Values that are equal only under a schema's rule ("A" and "a" under caseIgnoreMatch) are kept, because this package does not know the schema.
Refused, with an error naming the attribute:
- a name that is not an RFC 4512 attribute description, or that is an OID rather than a descriptor (see CheckAttributeName);
- a name that holds a credential, or that a server derives itself;
- an attribute with no values, or with an empty one: an LDAP attribute has at least one value, and nearly every syntax refuses an empty string.
func (Attributes) All ¶ added in v0.13.0
func (a Attributes) All() iter.Seq2[string, []string]
All is every attribute, in order, with a copy of its values.
func (Attributes) Get ¶ added in v0.13.0
func (a Attributes) Get(name string) []string
Get is the values of one attribute, found whatever the case of name and the order of its options, or nil when there is no such attribute.
A name that could never be an attribute here -- a refused one, or one that is not an attribute description -- is simply not found: asking is not configuring, and the question has an answer.
func (Attributes) Len ¶ added in v0.13.0
func (a Attributes) Len() int
Len is how many attributes there are.
func (Attributes) Names ¶ added in v0.13.0
func (a Attributes) Names() []string
Names are the attributes, in order, each spelled the first way it was given.
type Credential ¶
type Credential int
A Credential is one way of proving somebody. They are named so that a server can SAY which it has and which a protocol needs, rather than discovering the mismatch when somebody fails to log in.
const ( // Password is the password itself, which this process then holds. It // answers everything, and is the only thing that does. Password Credential = iota // NTHash is MD4(UTF16LE(password)) — Samba's sambaNTPassword. It answers // NTLMv2 and nothing else: it cannot be compared against a password a // client sent, because it IS the thing derived from it. NTHash // Verifier answers "is this the right password" without this process // knowing it: a hash comparison, or a bind against the directory that // holds it. Verifier // PublicKeys are SSH public keys, in authorized_keys spelling. PublicKeys // TOTPSecret is the shared secret behind the six digits on a phone (RFC // 6238). It is a SECOND factor and never a first: it proves the person // holds the thing it was enrolled into, and says nothing about who they // are. A server asking for two factors needs to know who has one — which // is the same question Can() answers for every other credential. TOTPSecret )
func (Credential) String ¶
func (c Credential) String() string
type GroupAttributer ¶ added in v0.13.0
type GroupAttributer interface {
GroupAttributes(group string) (Attributes, error)
}
A GroupAttributer is a source that can say what a group IS beyond who is in it: a gidNumber, a description.
Optional for the reason GroupLister is: a source that implements nothing new still compiles and still answers Members, and a Set asks it only whether it has the group.
It answers like Members: ErrNoSuchGroup for a group the source does not have, and an empty Attributes, with no error, for one it has and says nothing about.
type GroupLister ¶ added in v0.3.0
A GroupLister is a source that can say WHICH groups it has, rather than only answering about one by name.
It is a separate, optional interface because not every source can. Members asks a question with the name in hand -- an LDAP filter, a WHERE clause -- and a source that answers those need not be able to enumerate: a query that lists people says nothing about groups at all, and a directory may permit one and refuse the other.
A server that PUBLISHES groups needs this; one that only checks membership does not, which is why adding it did not change the Source interface.
type Identity ¶
type Identity struct {
// contains filtered or unexported fields
}
An Identity is one person, and what a source could give to prove them.
The zero value is nobody. Build one with NewIdentity or take one from a Source.
func NewIdentity ¶
NewIdentity names somebody. Credentials are added with the With… options, and a person with none can be named but proved by nothing — which is a legitimate state (a directory listing everybody, with the secrets somewhere else) and one Identity.Can reports honestly.
func (*Identity) Attributes ¶ added in v0.13.0
func (i *Identity) Attributes() Attributes
Attributes is what the source said about this person beyond their name and credentials. They describe and prove nothing, and none of them is a secret: see CheckAttributeName.
func (*Identity) Can ¶
func (i *Identity) Can(c Credential) bool
Can reports whether this identity carries a given credential — which is what lets a server say "you can use WebDAV and not SMB" before somebody finds out the hard way.
func (*Identity) Derive ¶ added in v0.6.0
Derive runs a caller's key derivation over this identity's password and returns only the result.
This is what KerberosKey always was: its body never mentioned Kerberos, and the shape -- hand the secret to a function the caller owns, keep the secret here -- is the one every protocol needs that must POSSESS the password rather than merely check it.
⛔ THE NAME MATTERED. go-fileshare needs exactly this to verify an AWS SigV4 signature, whose signing key is
HMAC(HMAC(HMAC(HMAC("AWS4"+secret, date), region), service), "aws4_request")
-- a derivation, and nothing to do with Kerberos. Calling KerberosKey to build an S3 key would have read as a mistake to everybody who met it later, and the protocol after that would have added a third name for one function. KerberosKey stays and delegates, because it is published and its own name is right at its own call sites.
It answers only for an identity that carries the PASSWORD. A Verifier -- a bind against somebody else's directory, or a hash comparison -- cannot serve these protocols at all: they need the secret itself, and "is this the right password" does not produce one. That is a property of the protocols, and a server should say so at configuration time rather than at the first request.
func (*Identity) KerberosKey ¶ added in v0.5.0
KerberosKey derives this identity's Kerberos long-term key.
The derivation is the CALLER's, and that is the whole design. A Kerberos key is string2key(password, salt, enctype) — arithmetic this package has no business carrying, since it would drag a Kerberos library into everything that merely wants to list users. Passing the function in means the password never leaves here and the enctype table stays where enctypes are understood.
It answers only for an identity that carries the PASSWORD. A Verifier — a bind against somebody else's directory, or a hash comparison — cannot serve Kerberos at all: a KDC has to DECRYPT the client's pre-authentication with this key, and "is this the right password" does not produce one. That is a property of Kerberos, not a limitation here, and a server should say so at configuration time rather than at the first kinit.
func (*Identity) NTKey ¶
NTKey is the key NTLMv2 is computed with: MD4(UTF16LE(password)).
It comes from the hash when the source published one, and is derived from the password when the source gave that instead. A source with neither cannot serve SMB, and this says so rather than returning something that will fail every login with "wrong password".
func (*Identity) TOTPSecret ¶ added in v0.4.0
TOTPSecret is the shared secret behind this person's one-time codes, or nil.
⛔ Like Identity.NTKey, this hands out a credential: whoever holds it produces every future code. It is here because a server that must CHECK a code has to have it, and a copy is returned so that the caller cannot change what a directory said.
func (*Identity) Verify ¶
Verify answers "is this the right password".
It prefers the verifier a source gave — that is the one that may reach an LDAP server, and the one that exists precisely so this process need not hold the secret. Falling back to a held password is a constant-time comparison: the difference between "wrong at byte 1" and "wrong at byte 12" is measurable over a network. Of the SHA-256 digests, not the strings: subtle.ConstantTimeCompare returns at once when the lengths differ, which tells a caller how long the password is.
type Option ¶
type Option func(*Identity)
An Option adds something to an identity.
func WithAttributes ¶ added in v0.13.0
func WithAttributes(a Attributes) Option
WithAttributes says what is known about this person that proves nothing: a uidNumber, a home directory, a mail address. Given twice, the second set is added to the first, by the rules of NewAttributes.
It takes an Attributes rather than names and values because an Option cannot fail, and a name that holds a secret must: NewAttributes is where it is refused, so an Attributes that exists has already been checked.
func WithGroups ¶
WithGroups says which groups this person is in, when the source knows without being asked.
func WithNTHash ¶
WithNTHash gives MD4(UTF16LE(password)) — 16 bytes, as a directory publishes it. Anybody holding this can authenticate as that person exactly as if they held the password: it is not a password hash in the sense a login form means, and storing it does not make a leak less bad.
func WithPassword ¶
WithPassword gives the password itself, which answers every protocol.
func WithPublicKeys ¶
WithPublicKeys gives SSH public keys, each in authorized_keys spelling.
func WithTOTPSecret ¶ added in v0.4.0
WithTOTPSecret gives the shared secret behind a one-time code (RFC 6238), as the raw bytes — github.com/go-authn/totp's ParseSecret reads the base32 spelling a directory usually stores.
⛔ It is the credential, not a hash of one: anybody holding it produces every future code, and it does not expire. A source that publishes it has published the second factor, exactly as with an NT hash.
func WithVerifier ¶
WithVerifier gives a way to check a password without holding it: a hash comparison, or a bind. The error it returns is reported to the SERVER, never to the client — a client is told only that authentication failed, which is the right amount to tell somebody who has not proved who they are.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
A Set is several sources read as one.
A site with its people in LDAP and one service account written down locally should not have to put the service account in LDAP. So the sources are asked in the order they were given and the FIRST one that knows a name owns it — which is the rule a person can hold in their head, and the one that lets a local file override a directory rather than the other way round.
func (*Set) GroupAttributes ¶ added in v0.13.0
func (s *Set) GroupAttributes(group string) (Attributes, error)
GroupAttributes asks every source about one group and puts the answers together, one attribute at a time: the first source to give an attribute owns it, so a gidNumber written in a local file overrides the directory's, and a description only the directory has still comes through. Values are never merged across sources -- two sources giving a group two gidNumbers is a disagreement, and taking the union would publish both.
A source that cannot describe groups (no GroupAttributer) is asked Members instead, to learn whether it HAS the group: a group that exists only there is answered with no attributes, not with ErrNoSuchGroup.
⛔ Unlike Members, a broken source is an error even when another answered. Members can return the half it has, because more people is the only thing the missing half could add. Here the missing source may be the one whose gidNumber should have WON, and answering without it would publish a different group id -- which decides who may read which files -- with nothing saying so.
func (*Set) GroupNames ¶ added in v0.3.0
GroupNames is every group the sources can name, sorted and without repeats.
Sources that cannot list groups are skipped rather than refused, so this can legitimately return FEWER groups than Set.Members would answer for: an LDAP directory that will not enumerate still answers about a group somebody names. A caller publishing a list should say where it came from, and a caller checking membership should keep asking Members.
func (*Set) Identities ¶
type Source ¶
type Source interface {
// Identities is everybody this source knows, with whatever credentials it
// has for them.
Identities() ([]*Identity, error)
// Members expands one group. A group the source has never heard of is an
// ERROR, not an empty list: an empty list quietly grants nothing to
// nobody, which reads exactly like a working configuration.
Members(group string) ([]string, error)
// Describe says what this is, for a server to print: "a postgres
// database", "ldaps://ldap.example.org", "the configuration file".
Describe() string
}
A Source is somewhere people are kept.
The three questions are the whole interface, and they are the three a file server actually asks: who is here, who is in this group, and where did that come from.
type Static ¶
type Static struct {
Name string
People []*Identity
Groups map[string][]string
// GroupAttrs describe the groups in Groups. A group is one of this
// source's because it is in Groups: an entry here for a group that is not
// there describes nothing, and is answered as no such group.
GroupAttrs map[string]Attributes
}
Static is a source held in memory: what a program's own configuration file becomes once it is read, and what a test uses.
func (*Static) GroupAttributes ¶ added in v0.13.0
func (s *Static) GroupAttributes(group string) (Attributes, error)
GroupAttributes is what this list says about a group.
func (*Static) GroupNames ¶ added in v0.3.0
GroupNames is the groups this list holds, which it knows exactly.
func (*Static) Identities ¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package hcldir is the `users` block: a directory named in a configuration file, opened.
|
Package hcldir is the `users` block: a directory named in a configuration file, opened. |
|
Package ldapdir reads people and groups from an LDAP directory.
|
Package ldapdir reads people and groups from an LDAP directory. |
|
Package ldaptest is an LDAP directory to test against.
|
Package ldaptest is an LDAP directory to test against. |
|
Package sqldir reads people and groups from a database.
|
Package sqldir reads people and groups from a database. |