LEARN LDAP FROM SCRATCH IN C
Learn LDAP From Scratch in C
Goal: Deeply understand directory services and the LDAP protocol—from basic data structures to building a complete directory server, client library, and authentication system. Master the protocol that powers enterprise identity management.
Why LDAP Matters
Every time you log into a corporate network, authenticate to a VPN, or access enterprise resources, LDAP is likely involved. It’s the backbone of:
- Active Directory (Microsoft’s enterprise identity system)
- OpenLDAP (the most widely-deployed open-source directory)
- Corporate authentication (SSO, identity management)
- Email systems (address books, routing)
- Certificate authorities (PKI infrastructure)
Most developers treat LDAP as a black box—they use libraries without understanding the protocol. After completing these projects, you will:
- Understand every byte of an LDAP message on the wire
- Know how directory trees are structured and searched
- Build your own LDAP client that speaks the actual protocol
- Implement a directory server from scratch
- Understand authentication mechanisms (simple bind, SASL, Kerberos)
- Debug LDAP issues that mystify other developers
Core Concept Analysis
The LDAP Protocol Stack
┌─────────────────────────────────────┐
│ Application Layer │
│ (LDAP Operations: Bind, Search, │
│ Add, Modify, Delete, Compare) │
├─────────────────────────────────────┤
│ LDAP Message Format │
│ (ASN.1 BER Encoding) │
├─────────────────────────────────────┤
│ Transport Layer │
│ (TCP port 389, TLS port 636) │
├─────────────────────────────────────┤
│ Security Layer │
│ (StartTLS, SASL, Simple Bind) │
└─────────────────────────────────────┘
The Directory Information Tree (DIT)
dc=com (Domain Component)
│
dc=example
│
┌─────────────┼─────────────┐
│ │ │
ou=people ou=groups ou=services
│ │ │
┌────┴────┐ ┌───┴───┐ ┌────┴────┐
│ │ │ │ │ │
uid=alice uid=bob cn=admin cn=devs cn=ldap cn=mail
Fundamental Concepts
- Distinguished Name (DN): The unique identifier for every entry
- Example:
uid=alice,ou=people,dc=example,dc=com - Read right-to-left: domain “example.com”, organizational unit “people”, user ID “alice”
- Example:
- Relative Distinguished Name (RDN): The leftmost component of a DN
- For
uid=alice,ou=people,dc=example,dc=com, the RDN isuid=alice
- For
- Entries and Attributes: Each entry contains attributes
dn: uid=alice,ou=people,dc=example,dc=com objectClass: inetOrgPerson objectClass: posixAccount uid: alice cn: Alice Smith sn: Smith mail: alice@example.com uidNumber: 1001 gidNumber: 1001 homeDirectory: /home/alice - Object Classes: Define what attributes an entry can/must have
inetOrgPerson: person with internet-style attributesposixAccount: UNIX account informationorganizationalUnit: container for other entries
-
Schema: Defines all object classes, attributes, and their syntax rules
- LDAP Operations:
- Bind: Authenticate to the server
- Search: Query entries with filters
- Add: Create new entries
- Modify: Change attributes
- Delete: Remove entries
- Compare: Test if attribute has specific value
- ModifyDN: Rename or move entries
- Unbind: Disconnect
- LDAP Filters: Query language for searching
(uid=alice) # exact match (cn=*smith) # ends with "smith" (&(objectClass=person)(age>=21)) # AND filter (|(dept=sales)(dept=marketing)) # OR filter (!(status=disabled)) # NOT filter - ASN.1 BER Encoding: How LDAP messages are serialized
- Tag-Length-Value (TLV) format
- Every message is a sequence of BER-encoded elements
Project List
Projects progress from foundational encoding to a complete directory system.
Project 1: ASN.1 BER Encoder/Decoder Library
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 2: Intermediate
- Knowledge Area: Binary Protocols / Data Encoding
- Software or Tool: Pure C, no libraries
- Main Book: A Layman’s Guide to a Subset of ASN.1, BER, and DER by Burton S. Kaliski Jr.
What you’ll build: A library that can encode and decode ASN.1 Basic Encoding Rules (BER)—the wire format used by LDAP. Support BOOLEAN, INTEGER, OCTET STRING, NULL, SEQUENCE, and SET types.
Why it teaches LDAP: Every LDAP message is BER-encoded. Without understanding BER, you can’t read or write LDAP packets. This is the foundation—like learning bytes before learning TCP.
Core challenges you’ll face:
- Tag-Length-Value parsing → maps to understanding BER’s fundamental structure
- Variable-length integers → maps to encoding lengths > 127 bytes
- Constructed vs primitive types → maps to SEQUENCE contains other elements
- Indefinite length encoding → maps to streaming/unknown-size data
Key Concepts:
- BER Tag Encoding: A Layman’s Guide to ASN.1, BER, and DER - RSA Laboratories
- TLV Structure: LDAPv3 Wire Protocol Reference: ASN.1 BER
- X.690 Specification: ITU-T X.690 (official standard)
- C Bit Manipulation: C Programming: A Modern Approach Chapter 20 - K.N. King
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Solid C knowledge (pointers, bit manipulation), understanding of binary data
Real world outcome:
$ ./ber_test
Encoding INTEGER 12345...
Bytes: 02 03 00 30 39
(tag=02 INTEGER, length=03, value=0x003039)
Encoding SEQUENCE { INTEGER 42, OCTET STRING "hello" }...
Bytes: 30 0c 02 01 2a 04 05 68 65 6c 6c 6f
Decoding received packet...
SEQUENCE (12 bytes)
INTEGER: 42
OCTET STRING: "hello"
Parse successful!
Implementation Hints:
BER uses Tag-Length-Value format:
┌──────────┬────────────┬─────────────────┐
│ Tag │ Length │ Value │
│ (1+ bytes)│ (1+ bytes) │ (Length bytes) │
└──────────┴────────────┴─────────────────┘
Tag byte structure:
Bits 7-6: Class (00=Universal, 01=Application, 10=Context, 11=Private)
Bit 5: Constructed (1) or Primitive (0)
Bits 4-0: Tag number (if < 31, otherwise multi-byte)
Common universal tags:
#define BER_BOOLEAN 0x01
#define BER_INTEGER 0x02
#define BER_OCTET_STRING 0x04
#define BER_NULL 0x05
#define BER_ENUMERATED 0x0A
#define BER_SEQUENCE 0x30 // 0x10 | 0x20 (constructed)
#define BER_SET 0x31 // 0x11 | 0x20 (constructed)
Length encoding:
- If length < 128: single byte (the length itself)
-
If length >= 128: first byte = 0x80 num_length_bytes, followed by length in big-endian
// Short form: length 5
0x05
// Long form: length 300 (0x012C)
0x82 0x01 0x2C // 0x82 means "2 bytes follow"
Questions to explore:
- What’s the difference between definite and indefinite length encoding?
- Why does LDAP use BER instead of simpler formats like JSON?
- How do you handle negative integers in BER?
- What’s the maximum value you can encode in a BER INTEGER?
Learning milestones:
- You can encode/decode simple integers → Basic TLV works
- You handle multi-byte lengths → Long form works
- You parse nested SEQUENCE → Constructed types work
- Round-trip encoding is lossless → Implementation is correct
Project 2: DN and RDN Parser
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Parsing / String Processing
- Software or Tool: Pure C
- Main Book: Understanding LDAP by Heinz Johner et al. (IBM Redbook)
What you’ll build: A parser that takes LDAP Distinguished Names (DNs) as strings, validates them, breaks them into RDN components, and can reconstruct them. Handle escaping and special characters.
Why it teaches LDAP: The DN is the fundamental identifier in LDAP. Understanding DN structure is essential before you can search, add, or modify entries.
Core challenges you’ll face:
- Parsing comma-separated RDNs → maps to handling escaped commas
- Multi-valued RDNs → maps to entries like
cn=John+sn=Doe - Special character escaping → maps to RFC 4514 escape rules
- Normalization → maps to case-insensitive comparison
Key Concepts:
- DN String Format: RFC 4514 - String Representation of Distinguished Names
- RDN Structure: DigitalOcean LDAP Tutorial
- String Parsing in C: C Programming: A Modern Approach Chapter 13 - K.N. King
- X.500 Naming: Oracle X.500 Overview
Difficulty: Beginner Time estimate: 3-5 days Prerequisites: Basic C string handling
Real world outcome:
$ ./dn_parser "uid=alice,ou=people,dc=example,dc=com"
DN is valid.
Number of RDNs: 4
RDN[0]: uid=alice
Attribute: uid
Value: alice
RDN[1]: ou=people
Attribute: ou
Value: people
RDN[2]: dc=example
Attribute: dc
Value: example
RDN[3]: dc=com
Attribute: dc
Value: com
Parent DN: ou=people,dc=example,dc=com
$ ./dn_parser "cn=John\, Jr.,ou=people,dc=example,dc=com"
DN is valid.
RDN[0]: cn=John, Jr.
(Note: comma was escaped in input)
Implementation Hints:
DN format (RFC 4514):
dn = rdn ("," rdn)*
rdn = attr "=" value ("+" attr "=" value)*
Characters that must be escaped with backslash:
- Space at beginning or end
#at beginning,+"\<>;- Null character
Data structure suggestion:
typedef struct {
char *type; // "uid", "cn", "ou", "dc", etc.
char *value; // The actual value
} AttributeValue;
typedef struct {
AttributeValue *avs; // Multi-valued RDN (usually just 1)
int count;
} RDN;
typedef struct {
RDN *rdns;
int count;
} DN;
Functions to implement:
DN* dn_parse(const char *dn_string);
char* dn_to_string(const DN *dn);
char* dn_parent(const DN *dn); // Remove first RDN
int dn_compare(const DN *a, const DN *b); // Case-insensitive
void dn_free(DN *dn);
Questions to explore:
- Why are DNs read right-to-left (most specific to least)?
- What’s the difference between
cnanduid? - How do you compare two DNs for equality (case sensitivity)?
- What’s a multi-valued RDN and when would you use one?
Learning milestones:
- Simple DNs parse correctly → Basic structure works
- Escaped characters handled → RFC 4514 compliance
- Parent DN extraction works → You understand hierarchy
- Round-trip (parse → string) works → Implementation complete
Project 3: LDAP Filter Parser and Evaluator
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Parsing / Query Languages
- Software or Tool: Pure C
- Main Book: Compilers: Principles and Practice by Parag H. Dave (for parsing concepts)
What you’ll build: A parser for LDAP search filter syntax (RFC 4515) that builds an AST, plus an evaluator that tests if entries match the filter.
Why it teaches LDAP: Search filters are LDAP’s query language. Every LDAP search uses them. Understanding how filters work—and how to evaluate them efficiently—is core to building or debugging any LDAP system.
Core challenges you’ll face:
- Recursive descent parsing → maps to nested AND/OR/NOT filters
- Different match types → maps to equality, substring, presence, comparison
- Extensible matching → maps to matching rules and OIDs
- Special character escaping → maps to hex-encoded characters in values
Key Concepts:
- Filter Syntax: RFC 4515 - String Representation of Search Filters
- Filter Types: LDAP.com - LDAP Filters
- Recursive Descent Parsing: Compilers: Principles and Practice Chapter 4 - Dave
- Pattern Matching: Algorithms Chapter 5 - Sedgewick & Wayne
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 2 (DN Parser), basic parsing concepts
Real world outcome:
$ ./filter_parser "(&(objectClass=person)(|(uid=alice)(uid=bob)))"
Filter AST:
AND
├── EQUAL: objectClass = person
└── OR
├── EQUAL: uid = alice
└── EQUAL: uid = bob
$ ./filter_eval "(cn=*smith)" --entry "cn: John Smith"
Entry matches filter: YES
$ ./filter_eval "(&(age>=21)(status=active))" --entry "age: 25" --entry "status=disabled"
Entry matches filter: NO (status mismatch)
$ ./filter_parser "(uid=*)"
Filter AST:
PRESENT: uid
(Tests if uid attribute exists)
Implementation Hints:
Filter grammar (simplified):
filter = "(" filtercomp ")"
filtercomp = and / or / not / item
and = "&" filterlist
or = "|" filterlist
not = "!" filter
filterlist = filter+
item = simple / present / substring
simple = attr filtertype value
present = attr "=*"
substring = attr "=" [initial] "*" [any "*"]* [final]
filtertype = "=" / "~=" / ">=" / "<="
AST structure:
typedef enum {
FILTER_AND,
FILTER_OR,
FILTER_NOT,
FILTER_EQUAL,
FILTER_APPROX, // ~=
FILTER_GREATER, // >=
FILTER_LESS, // <=
FILTER_PRESENT, // attr=*
FILTER_SUBSTRING // attr=*value* patterns
} FilterType;
typedef struct Filter {
FilterType type;
char *attribute;
char *value;
struct Filter **children; // For AND/OR/NOT
int child_count;
// For substring:
char *initial; // beginning pattern
char **any; // middle patterns
char *final; // ending pattern
} Filter;
Evaluation is recursive:
bool filter_match(Filter *f, Entry *entry) {
switch (f->type) {
case FILTER_AND:
for each child: if (!filter_match(child, entry)) return false;
return true;
case FILTER_OR:
for each child: if (filter_match(child, entry)) return true;
return false;
case FILTER_EQUAL:
return entry_has_value(entry, f->attribute, f->value);
// etc.
}
}
Questions to explore:
- How do you handle case-insensitive matching?
- What’s an “approximate match” (~=) and how would you implement it?
- How do substring filters like
(cn=J*n*ith)work? - What are extensible match filters and when are they used?
Learning milestones:
- Simple equality filters parse → Basic parsing works
- Nested AND/OR filters work → Recursion works
- Substring matching works → Pattern matching implemented
- Evaluation returns correct results → Complete implementation
Project 4: In-Memory Directory Tree (DIT)
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Data Structures / Trees
- Software or Tool: Pure C
- Main Book: Algorithms by Robert Sedgewick & Kevin Wayne
What you’ll build: An in-memory data structure representing the Directory Information Tree—entries organized hierarchically, with add, delete, modify, and search operations.
Why it teaches LDAP: The DIT is the heart of any directory service. Understanding how entries are organized, indexed, and searched is fundamental to understanding LDAP’s data model.
Core challenges you’ll face:
- Tree structure with arbitrary children → maps to n-ary tree implementation
- DN-based lookup → maps to path traversal in tree
- Attribute indexing → maps to fast search without full scan
- Schema validation → maps to objectClass requirements
Key Concepts:
- X.500 DIT Structure: X.500 Directory Services
- Tree Data Structures: Algorithms Chapter 4 - Sedgewick & Wayne
- Hash Tables for Indexing: Algorithms Chapter 3 - Sedgewick & Wayne
- LDAP Data Model: DigitalOcean LDAP Tutorial
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 2 (DN Parser), Project 3 (Filter Parser), data structures knowledge
Real world outcome:
$ ./dit_test
Creating DIT...
Adding: dc=example,dc=com (organization)
Adding: ou=people,dc=example,dc=com (organizationalUnit)
Adding: uid=alice,ou=people,dc=example,dc=com (inetOrgPerson)
Adding: uid=bob,ou=people,dc=example,dc=com (inetOrgPerson)
Tree structure:
dc=example,dc=com
└── ou=people
├── uid=alice
│ cn: Alice Smith
│ mail: alice@example.com
└── uid=bob
cn: Bob Jones
mail: bob@example.com
Search: base="ou=people,dc=example,dc=com" filter="(cn=*Smith)"
Found 1 entry:
uid=alice,ou=people,dc=example,dc=com
Modifying: uid=alice - add attribute telephoneNumber
Modified successfully.
Deleting: uid=bob,ou=people,dc=example,dc=com
Deleted successfully.
Implementation Hints:
Entry structure:
typedef struct Attribute {
char *type;
char **values;
int value_count;
} Attribute;
typedef struct Entry {
char *dn;
Attribute *attributes;
int attr_count;
struct Entry *parent;
struct Entry **children;
int child_count;
} Entry;
typedef struct DIT {
Entry *root; // The suffix entry
// Indexes for fast lookup
HashTable *dn_index; // DN -> Entry*
HashTable *uid_index; // uid value -> Entry*
// Add more indexes as needed
} DIT;
Operations to implement:
// Core operations
Entry* dit_lookup(DIT *dit, const char *dn);
int dit_add(DIT *dit, Entry *entry);
int dit_delete(DIT *dit, const char *dn);
int dit_modify(DIT *dit, const char *dn, Modification *mods);
// Search
Entry** dit_search(DIT *dit, const char *base_dn,
int scope, Filter *filter, int *count);
// Search scopes
#define SCOPE_BASE 0 // Only the base entry
#define SCOPE_ONE 1 // Immediate children only
#define SCOPE_SUBTREE 2 // Base and all descendants
For searching:
- Find the base entry by DN
- Depending on scope, collect candidate entries
- Apply filter to each candidate
- Return matching entries
Questions to explore:
- How do you ensure a child’s DN starts with parent’s DN?
- What indexes would you need for efficient
(objectClass=person)searches? - How do you handle the root DSE (empty DN)?
- What happens when you delete an entry with children?
Learning milestones:
- Add/lookup by DN works → Basic tree structure works
- Search with filter works → Filter integration works
- All scopes work correctly → Tree traversal works
- Modify operations work → Full CRUD implemented
Project 5: LDAP Message Encoder/Decoder
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Network Protocols / Binary Encoding
- Software or Tool: Your BER library (Project 1)
- Main Book: RFC 4511 - Lightweight Directory Access Protocol (LDAP): The Protocol
What you’ll build: Encode and decode all LDAP protocol messages—BindRequest, BindResponse, SearchRequest, SearchResultEntry, SearchResultDone, AddRequest, ModifyRequest, DeleteRequest, etc.
Why it teaches LDAP: This is where you go from understanding concepts to speaking the actual protocol. After this, you can read raw LDAP packets off the wire.
Core challenges you’ll face:
- Message framing → maps to LDAP messages as BER SEQUENCE
- Context-specific tags → maps to LDAP uses [0], [1], etc. for disambiguation
- Message IDs → maps to matching requests to responses
- Result codes → maps to success, error types, referrals
Key Concepts:
- LDAP Protocol: RFC 4511 - The Protocol
- Message Structure: LDAP.com Wire Protocol Reference
- Context-Specific Tags: A Layman’s Guide to ASN.1 - Kaliski
- Protocol Implementation: TCP/IP Illustrated, Volume 1 - Stevens
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Project 1 (BER Library), RFC reading ability
Real world outcome:
$ ./ldap_decode < captured_packet.bin
LDAPMessage {
messageID: 1
protocolOp: BindRequest {
version: 3
name: "uid=admin,ou=system"
authentication: simple "secret123"
}
}
$ ./ldap_encode --bind --dn "uid=admin,ou=system" --password "secret"
Encoded BindRequest (45 bytes):
30 2b 02 01 01 60 26 02 01 03 04 14 75 69 64 3d
61 64 6d 69 6e 2c 6f 75 3d 73 79 73 74 65 6d 80
06 73 65 63 72 65 74
$ ./ldap_encode --search --base "ou=people,dc=example,dc=com" \
--filter "(uid=alice)" --attrs "cn,mail"
Encoded SearchRequest (78 bytes):
30 4c 02 01 02 63 47 04 1b 6f 75 3d 70 65 6f 70
...
Implementation Hints:
LDAP message structure (from RFC 4511):
LDAPMessage ::= SEQUENCE {
messageID INTEGER (0 .. maxInt),
protocolOp CHOICE {
bindRequest BindRequest,
bindResponse BindResponse,
unbindRequest UnbindRequest,
searchRequest SearchRequest,
searchResEntry SearchResultEntry,
searchResDone SearchResultDone,
searchResRef SearchResultReference,
modifyRequest ModifyRequest,
modifyResponse ModifyResponse,
addRequest AddRequest,
addResponse AddResponse,
delRequest DelRequest,
delResponse DelResponse,
... },
controls [0] Controls OPTIONAL
}
Key message definitions:
BindRequest ::= [APPLICATION 0] SEQUENCE {
version INTEGER (1 .. 127),
name LDAPDN,
authentication AuthenticationChoice
}
SearchRequest ::= [APPLICATION 3] SEQUENCE {
baseObject LDAPDN,
scope ENUMERATED { baseObject(0), singleLevel(1), wholeSubtree(2) },
derefAliases ENUMERATED { neverDerefAliases(0), ... },
sizeLimit INTEGER (0 .. maxInt),
timeLimit INTEGER (0 .. maxInt),
typesOnly BOOLEAN,
filter Filter,
attributes AttributeSelection
}
APPLICATION tags (context-specific):
#define LDAP_TAG_BIND_REQUEST 0x60 // [APPLICATION 0] constructed
#define LDAP_TAG_BIND_RESPONSE 0x61 // [APPLICATION 1] constructed
#define LDAP_TAG_UNBIND_REQUEST 0x42 // [APPLICATION 2] primitive
#define LDAP_TAG_SEARCH_REQUEST 0x63 // [APPLICATION 3] constructed
#define LDAP_TAG_SEARCH_ENTRY 0x64 // [APPLICATION 4] constructed
#define LDAP_TAG_SEARCH_DONE 0x65 // [APPLICATION 5] constructed
// etc.
Questions to explore:
- Why does LDAP use APPLICATION tags instead of UNIVERSAL?
- How do you handle a SearchRequest that returns 1000 entries?
- What’s the difference between SearchResultDone and SearchResultEntry?
- How do controls (like paging) attach to messages?
Learning milestones:
- BindRequest encodes correctly → Basic encoding works
- SearchRequest with filter works → Complex nested encoding
- Decode captured packets correctly → Real-world compatibility
- Round-trip all message types → Complete implementation
Project 6: Simple LDAP Client
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Network Programming / Protocols
- Software or Tool: Your message encoder (Project 5), TCP sockets
- Main Book: The Linux Programming Interface by Michael Kerrisk
What you’ll build: A command-line LDAP client that can connect to a real LDAP server, bind, search, and display results—speaking the actual LDAP protocol over TCP.
Why it teaches LDAP: This is the moment of truth—your code talks to real LDAP servers. You’ll see how your protocol implementation works against OpenLDAP, Active Directory, or any other server.
Core challenges you’ll face:
- TCP connection management → maps to sockets, connection state
- Message ID tracking → maps to asynchronous request/response matching
- Handling large results → maps to streaming SearchResultEntry messages
- Error handling → maps to result codes, referrals, timeouts
Key Concepts:
- Socket Programming: The Linux Programming Interface Chapter 59 - Kerrisk
- TCP Client Implementation: TCP/IP Sockets in C - Donahoo & Calvert
- LDAP Result Codes: RFC 4511 Section 4.1.9
- Connection Lifecycle: OpenLDAP Admin Guide
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 5 (Message Encoder), socket programming knowledge
Real world outcome:
$ ./myldap -H ldap://localhost:389 -D "cn=admin,dc=example,dc=com" -w secret \
-b "ou=people,dc=example,dc=com" "(objectClass=person)" cn mail
Connecting to ldap://localhost:389...
Connected.
Binding as cn=admin,dc=example,dc=com...
Bind successful.
Searching...
Base: ou=people,dc=example,dc=com
Filter: (objectClass=person)
# uid=alice,ou=people,dc=example,dc=com
cn: Alice Smith
mail: alice@example.com
# uid=bob,ou=people,dc=example,dc=com
cn: Bob Jones
mail: bob@example.com
# numEntries: 2
$ ./myldap -H ldap://ldap.forumsys.com -x -b "dc=example,dc=com" "(uid=euler)"
# Testing against public LDAP server
# uid=euler,dc=example,dc=com
cn: Leonhard Euler
mail: euler@ldap.forumsys.com
Implementation Hints:
Client structure:
typedef struct LDAPConnection {
int socket;
int message_id; // Increment for each request
char *bound_dn; // Current bound identity
bool authenticated;
} LDAPConnection;
// Operations
LDAPConnection* ldap_connect(const char *host, int port);
int ldap_bind_simple(LDAPConnection *conn, const char *dn, const char *password);
SearchResult* ldap_search(LDAPConnection *conn, const char *base,
int scope, const char *filter, char **attrs);
void ldap_disconnect(LDAPConnection *conn);
Search returns a stream of messages:
// Server sends:
// 1. SearchResultEntry (0 or more)
// 2. SearchResultEntry ...
// 3. SearchResultDone (exactly 1, signals end)
while (true) {
LDAPMessage *msg = ldap_recv_message(conn);
if (msg->tag == LDAP_TAG_SEARCH_ENTRY) {
process_entry(msg);
} else if (msg->tag == LDAP_TAG_SEARCH_DONE) {
check_result_code(msg);
break;
}
}
Important result codes:
#define LDAP_SUCCESS 0
#define LDAP_OPERATIONS_ERROR 1
#define LDAP_PROTOCOL_ERROR 2
#define LDAP_TIMELIMIT_EXCEEDED 3
#define LDAP_SIZELIMIT_EXCEEDED 4
#define LDAP_AUTH_METHOD_NOT_SUPPORTED 7
#define LDAP_STRONG_AUTH_REQUIRED 8
#define LDAP_NO_SUCH_OBJECT 32
#define LDAP_INVALID_DN_SYNTAX 34
#define LDAP_INVALID_CREDENTIALS 49
#define LDAP_INSUFFICIENT_ACCESS 50
// etc.
Questions to explore:
- How do you handle a server that sends controls you don’t understand?
- What happens if the server returns a referral?
- How would you implement connection pooling?
- What’s the difference between synchronous and asynchronous LDAP APIs?
Learning milestones:
- Connect and bind works → Basic protocol works
- Search returns results → Message flow works
- Works against OpenLDAP → Real-world compatibility
- Error handling is robust → Production-ready
Project 7: LDAP Server - Core Protocol Handling
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: Server Development / Concurrency
- Software or Tool: All previous projects combined
- Main Book: The Linux Programming Interface by Michael Kerrisk
What you’ll build: An LDAP server that accepts connections, handles bind requests, and responds to search queries against your in-memory DIT.
Why it teaches LDAP: Building a server forces you to understand both sides of the protocol—request handling, response generation, and error conditions. You’ll see LDAP from the perspective of OpenLDAP or Active Directory developers.
Core challenges you’ll face:
- Multi-client handling → maps to threads, select/poll/epoll
- Bind state per connection → maps to authenticated vs anonymous
- Access control → maps to who can see/modify what
- Operation limits → maps to size limits, time limits
Key Concepts:
- Server Architecture: The Linux Programming Interface Chapter 60 - Kerrisk
- Concurrent Server Design: Unix Network Programming - Stevens
- LDAP Access Control: RFC 4511 Section 4.1.9
- OpenLDAP Architecture: OpenLDAP Source
Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: All previous projects, server programming experience
Real world outcome:
$ ./myldapd -p 3389 -b "dc=example,dc=com"
Loading directory from ./data/directory.ldif...
Loaded 50 entries.
LDAP server listening on port 3389...
[Client 1 connected from 127.0.0.1]
[Client 1] BindRequest: uid=admin,ou=system
[Client 1] Bind successful
[Client 1] SearchRequest: base=ou=people,dc=example,dc=com filter=(cn=*)
[Client 1] Returning 25 entries
[Client 1] Disconnected
# From another terminal:
$ ldapsearch -H ldap://localhost:3389 -D "uid=admin,ou=system" -w secret \
-b "ou=people,dc=example,dc=com" "(objectClass=*)"
# ... results from YOUR server ...
Implementation Hints:
Server architecture:
typedef struct ClientConnection {
int socket;
char *bound_dn; // NULL if anonymous
bool authenticated;
// Per-connection state
} ClientConnection;
typedef struct LDAPServer {
int listen_socket;
DIT *directory;
ClientConnection *clients;
int max_clients;
// Configuration
int size_limit;
int time_limit;
} LDAPServer;
Main loop (simplified with select):
while (running) {
fd_set readfds;
// Add listen socket and all client sockets to readfds
select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (FD_ISSET(listen_socket, &readfds)) {
accept_new_client();
}
for each client:
if (FD_ISSET(client->socket, &readfds)) {
handle_client_request(client);
}
}
Request handler:
void handle_request(ClientConnection *client, LDAPMessage *msg) {
switch (msg->operation_tag) {
case LDAP_TAG_BIND_REQUEST:
handle_bind(client, msg);
break;
case LDAP_TAG_SEARCH_REQUEST:
handle_search(client, msg);
break;
case LDAP_TAG_UNBIND_REQUEST:
disconnect_client(client);
break;
// etc.
}
}
Questions to explore:
- How do you handle a client that sends requests but never reads responses?
- What’s the difference between anonymous bind and unauthenticated bind?
- How do you implement search size limits?
- What happens if a client disconnects mid-operation?
Learning milestones:
- Accept connections, handle bind → Basic server works
- Search returns correct results → Protocol handling works
- Multiple concurrent clients work → Concurrency works
- Standard LDAP clients work with your server → Compatibility achieved
Project 8: LDIF Parser and Loader
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: File Formats / Data Import
- Software or Tool: Your DIT (Project 4)
- Main Book: RFC 2849 - The LDAP Data Interchange Format (LDIF)
What you’ll build: A parser for LDIF (LDAP Data Interchange Format) files—the standard text format for representing LDAP entries and changes. Load/export your directory from files.
Why it teaches LDAP: LDIF is how LDAP data is exchanged, backed up, and provisioned. Understanding LDIF means you can import from and export to any LDAP system.
Core challenges you’ll face:
- Multi-line values → maps to continuation lines starting with space
- Base64 encoding → maps to binary data and non-ASCII values
- Change records → maps to LDIF can represent modifications, not just entries
- Ordering and dependencies → maps to parent entries must exist before children
Key Concepts:
- LDIF Format: RFC 2849 - The LDAP Data Interchange Format
- Base64 Encoding: RFC 4648
- File Parsing in C: C Programming: A Modern Approach Chapter 22 - K.N. King
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 4 (DIT)
Real world outcome:
$ cat directory.ldif
dn: dc=example,dc=com
objectClass: domain
dc: example
dn: ou=people,dc=example,dc=com
objectClass: organizationalUnit
ou: people
dn: uid=alice,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
uid: alice
cn: Alice Smith
sn: Smith
mail: alice@example.com
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/alice
description:: VGhpcyBpcyBiYXNlNjQgZW5jb2RlZA==
$ ./ldif_load directory.ldif
Loading LDIF...
Entry 1: dc=example,dc=com (1 attributes)
Entry 2: ou=people,dc=example,dc=com (1 attributes)
Entry 3: uid=alice,ou=people,dc=example,dc=com (8 attributes)
Note: decoded base64 value for 'description'
Loaded 3 entries successfully.
$ ./ldif_export --base "dc=example,dc=com" > backup.ldif
Exported 3 entries.
Implementation Hints:
LDIF content record format:
dn: <distinguished name>
<attribute>: <value>
<attribute>: <value>
<attribute>:: <base64-encoded-value>
<attribute>:< <file-url>
# Blank line separates entries
Continuation lines:
description: This is a very long description that continues
on the next line with a leading space
and even continues more
Parser structure:
typedef struct LDIFEntry {
char *dn;
Attribute *attributes;
int attr_count;
} LDIFEntry;
typedef struct LDIFChangeRecord {
char *dn;
enum { LDIF_ADD, LDIF_DELETE, LDIF_MODIFY, LDIF_MODRDN } type;
// Type-specific data
} LDIFChangeRecord;
LDIFEntry* ldif_parse_entry(FILE *fp);
int ldif_write_entry(FILE *fp, Entry *entry);
Handling base64:
// Values with "::" are base64 encoded
// description:: SGVsbG8gV29ybGQ=
// Decode to: "Hello World"
if (line contains "::") {
value = base64_decode(after_double_colon);
}
Questions to explore:
- When must values be base64 encoded? (Hint: RFC 2849 Section 3)
- How do you handle LDIF with Windows line endings (CRLF)?
- What’s the difference between content LDIF and change LDIF?
- How do you verify referential integrity when loading?
Learning milestones:
- Simple entries parse correctly → Basic parsing works
- Base64 values decode → Encoding handled
- Multi-line values work → Continuation handled
- Round-trip (load → export → load) is lossless → Implementation complete
Project 9: Simple Bind and Password Verification
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Security / Authentication
- Software or Tool: Your LDAP server (Project 7)
- Main Book: Serious Cryptography by Jean-Philippe Aumasson
What you’ll build: Implement proper password storage and verification for LDAP simple bind—with password hashing (SSHA, bcrypt), comparison, and secure handling.
Why it teaches LDAP: Authentication is LDAP’s most common use case. Understanding how passwords are stored, hashed, and verified is essential for anyone working with directory services.
Core challenges you’ll face:
- Password hash formats → maps to {SSHA}, {CRYPT}, {BCRYPT} schemes
- Secure comparison → maps to constant-time comparison to prevent timing attacks
- Salt handling → maps to preventing rainbow table attacks
- Password policy → maps to expiration, history, complexity
Key Concepts:
- Password Storage: Serious Cryptography Chapter 11 - Aumasson
- LDAP Password Schemes: OpenLDAP slappasswd documentation
- Secure Comparison: Practical Cryptography - Ferguson & Schneier
- Password Policy: RFC 3112 - LDAP Authentication Methods
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 7 (LDAP Server), basic cryptography concepts
Real world outcome:
$ ./slappasswd -s "mysecret"
{SSHA}d4e8v/4JqRpPblH0MrCg7Tb0dDLXvYkw
$ ./slappasswd -s "mysecret" -h {BCRYPT}
{BCRYPT}$2b$10$N9qo8uLOickgx2ZMRZoMy.MqHGvN2.xJ7JQ3xS3/8h8v
# In your server:
$ ./myldapd -p 3389
[Bind attempt] uid=alice,ou=people,dc=example,dc=com
[Password check] Comparing input against {SSHA}d4e8v/4JqRp...
[Bind result] SUCCESS (correct password)
[Bind attempt] uid=alice,ou=people,dc=example,dc=com
[Password check] Comparing input against {SSHA}d4e8v/4JqRp...
[Bind result] INVALID_CREDENTIALS (wrong password)
Implementation Hints:
LDAP password format in userPassword attribute:
userPassword: {SCHEME}encoded_hash_and_salt
Common schemes:
{SSHA}- Salted SHA-1 (base64 of SHA1(password + salt) + salt){SHA}- Plain SHA-1 (insecure, no salt){CRYPT}- UNIX crypt(){BCRYPT}- bcrypt (modern, secure){CLEARTEXT}- Plain text (never use!)
SSHA verification:
bool verify_ssha(const char *password, const char *stored) {
// stored = base64(sha1(password + salt) + salt)
// 1. Base64 decode the stored value
// 2. Split into hash (first 20 bytes) and salt (remaining bytes)
// 3. Compute SHA1(password + salt)
// 4. Compare computed hash with stored hash
unsigned char *decoded = base64_decode(stored);
unsigned char *stored_hash = decoded; // First 20 bytes
unsigned char *salt = decoded + 20; // Rest is salt
int salt_len = decoded_len - 20;
unsigned char computed[20];
SHA1_CTX ctx;
SHA1_Init(&ctx);
SHA1_Update(&ctx, password, strlen(password));
SHA1_Update(&ctx, salt, salt_len);
SHA1_Final(computed, &ctx);
return secure_compare(computed, stored_hash, 20);
}
Constant-time comparison:
bool secure_compare(const void *a, const void *b, size_t len) {
const unsigned char *ua = a, *ub = b;
unsigned char result = 0;
for (size_t i = 0; i < len; i++) {
result |= ua[i] ^ ub[i];
}
return result == 0;
}
Questions to explore:
- Why is constant-time comparison important?
- What’s the difference between SSHA and SSHA256?
- How do password policies (lockout, history) work in LDAP?
- What’s the “bind DN” and why does it matter?
Learning milestones:
- SSHA verification works → Basic hashing works
- Multiple schemes supported → Flexible implementation
- Wrong passwords correctly rejected → Security works
- Timing attacks prevented → Secure comparison works
Project 10: StartTLS and Secure Connections
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 4: Expert
- Knowledge Area: Security / TLS
- Software or Tool: OpenSSL or LibreSSL
- Main Book: Network Security with OpenSSL by Viega, Messier, Chandra
What you’ll build: Add TLS support to your LDAP server and client—both LDAPS (TLS from start on port 636) and StartTLS (upgrade existing connection).
Why it teaches LDAP: Production LDAP must be encrypted. Understanding StartTLS shows how protocols upgrade from plaintext to encrypted mid-connection—a pattern used in SMTP, FTP, and more.
Core challenges you’ll face:
- TLS handshake integration → maps to upgrading a socket mid-connection
- Certificate handling → maps to loading certs, verification
- StartTLS protocol → maps to LDAP extended operation
- Both client and server side → maps to different TLS contexts
Key Concepts:
- TLS with OpenSSL: Network Security with OpenSSL - Viega et al.
- StartTLS Operation: RFC 4511 Section 4.14
- LDAPS vs StartTLS: RFC 4513 Section 3
- Certificate Verification: Bulletproof SSL and TLS - Ivan Ristić
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Project 6 & 7 (Client and Server), TLS concepts
Real world outcome:
# Generate test certificates
$ ./generate_certs.sh
Created: server.crt, server.key, ca.crt
# Start server with TLS
$ ./myldapd -p 3389 --tls-cert server.crt --tls-key server.key
LDAP server listening on port 3389 (StartTLS supported)
LDAPS server listening on port 3636
# Client with StartTLS
$ ./myldap -H ldap://localhost:3389 -ZZ -D "cn=admin" -w secret \
-b "dc=example,dc=com" "(uid=*)"
StartTLS successful - connection encrypted
Bind successful
...
# Client with LDAPS
$ ./myldap -H ldaps://localhost:3636 --cacert ca.crt \
-D "cn=admin" -w secret -b "dc=example,dc=com" "(uid=*)"
TLS connected - verified server certificate
Bind successful
...
Implementation Hints:
StartTLS is an LDAP Extended Operation:
ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
requestName [0] OID (1.3.6.1.4.1.1466.20037),
requestValue [1] OCTET STRING OPTIONAL
}
ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
resultCode ENUMERATED,
matchedDN LDAPDN,
diagnosticMessage LDAPString,
...
}
Server-side StartTLS:
void handle_extended_operation(Client *client, LDAPMessage *msg) {
if (strcmp(msg->ext_request.oid, OID_START_TLS) == 0) {
// Send success response BEFORE upgrading
send_extended_response(client, LDAP_SUCCESS);
// Now upgrade to TLS
SSL_CTX *ctx = create_server_ssl_context();
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, client->socket);
if (SSL_accept(ssl) <= 0) {
disconnect_client(client);
return;
}
client->ssl = ssl;
client->encrypted = true;
}
}
Client-side StartTLS:
int ldap_start_tls(LDAPConnection *conn) {
// Send Extended Request with StartTLS OID
send_extended_request(conn, OID_START_TLS, NULL);
// Receive response
LDAPMessage *response = recv_message(conn);
if (response->result_code != LDAP_SUCCESS) {
return -1;
}
// Upgrade to TLS
SSL_CTX *ctx = create_client_ssl_context();
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, conn->socket);
if (SSL_connect(ssl) <= 0) {
return -1;
}
conn->ssl = ssl;
return 0;
}
Questions to explore:
- Why is StartTLS preferred over LDAPS?
- What happens if someone sends an unencrypted request after StartTLS?
- How do you verify the server’s certificate hostname?
- What’s the difference between SSL_connect and SSL_accept?
Learning milestones:
- LDAPS works (TLS from start) → Basic TLS integration
- StartTLS upgrades successfully → Mid-connection upgrade
- Certificate verification works → Security complete
- Both client and server work → Full implementation
Project 11: SASL Authentication Framework
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: Security / Authentication Protocols
- Software or Tool: Cyrus SASL library (or implement from scratch)
- Main Book: RFC 4422 - Simple Authentication and Security Layer (SASL)
What you’ll build: Implement SASL authentication in your LDAP server/client, supporting at least PLAIN, EXTERNAL, and optionally DIGEST-MD5 or GSSAPI mechanisms.
Why it teaches LDAP: SASL is how enterprise LDAP does authentication. It’s the bridge to Kerberos (Active Directory), certificate auth, and modern authentication methods. Understanding SASL unlocks enterprise identity management.
Core challenges you’ll face:
- SASL negotiation → maps to mechanism listing, selection, challenge/response
- Multiple mechanism support → maps to PLAIN, EXTERNAL, DIGEST-MD5, GSSAPI
- Security layers → maps to optional encryption after auth
- Authorization identity → maps to authentication vs authorization ID
Key Concepts:
- SASL Framework: RFC 4422 - SASL
- LDAP + SASL: RFC 4513 Section 5
- GSSAPI/Kerberos: RFC 4752 - Kerberos V5 SASL Mechanism
- Cyrus SASL: GNU SASL Manual
Resources for key challenges:
Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: Project 7 (Server), Project 10 (TLS), understanding of auth protocols
Real world outcome:
# Query supported mechanisms
$ ./myldap -H ldap://localhost:3389 -b "" -s base "(objectClass=*)" supportedSASLMechanisms
supportedSASLMechanisms: PLAIN
supportedSASLMechanisms: EXTERNAL
supportedSASLMechanisms: DIGEST-MD5
# SASL PLAIN bind (over TLS)
$ ./myldap -H ldap://localhost:3389 -ZZ -Y PLAIN -U alice -w secret \
-b "dc=example,dc=com" "(uid=*)"
SASL/PLAIN authentication started
SASL username: alice
SASL authentication successful
# SASL EXTERNAL (certificate-based)
$ ./myldap -H ldaps://localhost:3636 -Y EXTERNAL --cert client.crt --key client.key \
-b "dc=example,dc=com" "(uid=*)"
SASL/EXTERNAL authentication started
Using client certificate CN=alice
SASL authentication successful
Implementation Hints:
SASL Bind Request format:
BindRequest ::= [APPLICATION 0] SEQUENCE {
version INTEGER,
name LDAPDN,
authentication CHOICE {
simple [0] OCTET STRING,
sasl [3] SaslCredentials
}
}
SaslCredentials ::= SEQUENCE {
mechanism LDAPSTRING,
credentials OCTET STRING OPTIONAL
}
SASL PLAIN mechanism (RFC 4616):
credentials = [authzid] NUL authcid NUL passwd
Example: "\x00alice\x00secretpassword"
(empty authzid, authcid=alice, password=secretpassword)
SASL negotiation flow:
Client Server
| |
|-- BindRequest (SASL, mechanism) ->|
| |
|<-- BindResponse (saslCreds) ------| (challenge, if needed)
| |
|-- BindRequest (SASL, response) -->| (response to challenge)
| |
|<-- BindResponse (SUCCESS) --------|
EXTERNAL mechanism (certificate-based):
// Get certificate from TLS connection
X509 *cert = SSL_get_peer_certificate(client->ssl);
if (cert) {
// Extract DN from certificate
char *subject = X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0);
// Map certificate DN to LDAP DN
client->bound_dn = map_cert_to_ldap_dn(subject);
}
Questions to explore:
- What’s the difference between authentication identity and authorization identity?
- Why is PLAIN only safe over TLS?
- How does GSSAPI integrate with Kerberos?
- What’s a “security layer” in SASL and when is it used?
Learning milestones:
- PLAIN mechanism works over TLS → Basic SASL works
- EXTERNAL uses certificates → Certificate auth works
- Mechanism negotiation works → Protocol complete
- Standard clients work with your server → Compatibility
Project 12: Schema and Object Class Validation
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Data Modeling / Validation
- Software or Tool: Your LDAP server (Project 7)
- Main Book: RFC 4512 - LDAP: Directory Information Models
What you’ll build: Implement LDAP schema support—parse schema definitions, validate entries against objectClass requirements, and enforce attribute syntax rules.
Why it teaches LDAP: Schema is what makes LDAP a structured data store instead of a free-form database. Understanding schema explains why LDAP entries have specific attributes and how interoperability between systems works.
Core challenges you’ll face:
- Schema definition parsing → maps to attributetype and objectclass syntax
- Inheritance (SUP) → maps to objectClass hierarchy
- Required vs optional attributes → maps to MUST vs MAY
- Attribute syntax validation → maps to Integer, DN, IA5String, etc.
Key Concepts:
- LDAP Schema: RFC 4512 - Directory Information Models
- Core Schema: RFC 4519 - Schema for User Applications
- Attribute Syntax: RFC 4517 - Syntaxes and Matching Rules
- inetOrgPerson: RFC 2798 - Definition of the inetOrgPerson Object Class
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Project 4 (DIT), Project 7 (Server)
Real world outcome:
$ ./schema_load core.schema inetorgperson.schema
Loaded 48 attributeTypes
Loaded 22 objectClasses
$ ./validate_entry test.ldif
Entry: uid=alice,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
Checking required attributes (MUST):
✓ cn present
✓ sn present
✓ (from person) objectClass present
Checking attribute syntax:
✓ mail: valid IA5String
✓ uidNumber: valid INTEGER
✗ telephoneNumber: invalid (contains letters)
Result: INVALID (1 error)
$ ./myldapd --schema core.schema
[Add attempt] uid=invalid,ou=people,dc=example,dc=com
[Schema error] objectClass 'inetOrgPerson' requires 'sn' attribute
[Result] OBJECT_CLASS_VIOLATION
Implementation Hints:
Schema definition format (from .schema files):
attributetype ( 2.5.4.3
NAME 'cn'
DESC 'Common Name'
SUP name
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectclass ( 2.5.6.6
NAME 'person'
DESC 'RFC2256: a person'
SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ description ) )
objectclass ( 2.16.840.1.113730.3.2.2
NAME 'inetOrgPerson'
DESC 'RFC2798: Internet Organizational Person'
SUP organizationalPerson STRUCTURAL
MAY ( audio $ businessCategory $ ... $ uid $ mail $ ... ) )
Data structures:
typedef struct AttributeType {
char *oid;
char **names; // NAME can have multiple values
char *description;
char *sup; // Superior type (inheritance)
char *syntax_oid;
bool single_valued;
// Matching rules
char *equality_rule;
char *ordering_rule;
char *substr_rule;
} AttributeType;
typedef struct ObjectClass {
char *oid;
char **names;
char *description;
char **sup; // Superior classes (can have multiple)
enum { STRUCTURAL, AUXILIARY, ABSTRACT } kind;
char **must_attrs; // Required attributes
char **may_attrs; // Optional attributes
} ObjectClass;
Validation algorithm:
bool validate_entry(Entry *entry, Schema *schema) {
// 1. Get all objectClasses from entry
// 2. For each objectClass, include its superiors (recursively)
// 3. Collect all MUST and MAY attributes from all objectClasses
// 4. Check all MUST attributes are present
// 5. Check no unexpected attributes (not in any MAY or MUST)
// 6. Validate syntax of each attribute value
}
Questions to explore:
- What’s the difference between STRUCTURAL and AUXILIARY objectClasses?
- How does attribute inheritance (SUP) work?
- What are matching rules and why do they matter?
- How do you handle custom schema extensions?
Learning milestones:
- Parse schema files correctly → Syntax parsing works
- Validate required attributes → MUST checking works
- Reject invalid entries on add → Server integration works
- Syntax validation works → Complete implementation
Project 13: Access Control Lists (ACLs)
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Security / Authorization
- Software or Tool: Your LDAP server (Project 7)
- Main Book: OpenLDAP Administrator’s Guide - Access Control chapter
What you’ll build: Implement access control for your LDAP server—define who can read, write, search, and compare entries and attributes based on identity and entry location.
Why it teaches LDAP: ACLs are what make LDAP secure for multi-tenant use. Without them, everyone sees everything. Understanding ACLs shows how enterprise directories protect sensitive data.
Core challenges you’ll face:
- ACL syntax parsing → maps to OpenLDAP-style access rules
- Identity matching → maps to dn patterns, group membership, wildcards
- Attribute-level control → maps to some attributes more sensitive than others
- Inheritance and precedence → maps to which rule wins?
Key Concepts:
- OpenLDAP ACLs: OpenLDAP Access Control
- Authorization Model: RFC 4511 Section 4.1.9
- Rule Evaluation: OpenLDAP slapd.access(5) man page
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 7 (Server), Project 9 (Authentication)
Real world outcome:
$ cat acl.conf
# Everyone can read public info
access to attrs=cn,sn,mail
by * read
# Only self can read own password
access to attrs=userPassword
by self write
by anonymous auth
by * none
# Admins can do anything
access to *
by dn="cn=admin,dc=example,dc=com" write
by users read
by * none
$ ./myldapd --acl acl.conf
[Client: anonymous] Search: (uid=*) attrs=cn,mail
[ACL] Allowed: read cn,mail (public attributes)
[Result] 10 entries returned
[Client: anonymous] Search: (uid=*) attrs=userPassword
[ACL] Denied: userPassword (not authenticated)
[Result] 0 entries returned (filtered by ACL)
[Client: uid=alice] Modify: uid=alice - change userPassword
[ACL] Allowed: self can write own password
[Result] SUCCESS
Implementation Hints:
ACL rule structure:
typedef enum {
ACCESS_NONE = 0,
ACCESS_AUTH = 1, // For password checking only
ACCESS_COMPARE = 2,
ACCESS_SEARCH = 4,
ACCESS_READ = 8,
ACCESS_WRITE = 16,
ACCESS_MANAGE = 32
} AccessLevel;
typedef struct ACLRule {
// What this rule applies to
char *dn_pattern; // "ou=people,dc=example,dc=com"
char **attr_list; // Specific attributes, or "*"
// Who and what access
struct {
char *who; // "self", "anonymous", "users", dn="...", group="..."
AccessLevel level;
} *by_clauses;
int by_count;
} ACLRule;
Evaluation algorithm:
AccessLevel check_access(Entry *entry, char *attr, DN *requester, Operation op) {
for each ACL rule (in order):
if (rule matches entry DN and attribute):
for each "by" clause:
if (requester matches "who"):
return clause.level;
// Default: deny
return ACCESS_NONE;
}
bool matches_who(DN *requester, char *who, Entry *target) {
if (strcmp(who, "*") == 0) return true;
if (strcmp(who, "anonymous") == 0) return requester == NULL;
if (strcmp(who, "users") == 0) return requester != NULL;
if (strcmp(who, "self") == 0) return dn_equal(requester, target->dn);
if (starts_with(who, "dn=")) return dn_matches(requester, who + 3);
if (starts_with(who, "group=")) return is_member_of(requester, who + 6);
}
Applying ACLs to search:
SearchResult* search_with_acl(Client *client, SearchRequest *req) {
// 1. Find all matching entries
Entry **candidates = dit_search(...);
// 2. Filter by ACL (remove entries client can't read)
// 3. For each remaining entry, filter attributes by ACL
// 4. Return filtered results
}
Questions to explore:
- What’s the difference between “read” and “search” access?
- How do you implement “deny” rules (not just “allow”)?
- What happens when multiple rules match?
- How do group-based ACLs work?
Learning milestones:
- Basic read/write ACLs work → Core implementation
- Self and anonymous work → Identity matching
- Attribute-level ACLs work → Fine-grained control
- Standard LDAP clients see filtered results → Integration works
Project 14: Replication (Consumer/Provider)
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Distributed Systems / Replication
- Software or Tool: Your LDAP server (Project 7)
- Main Book: Designing Data-Intensive Applications by Martin Kleppmann
What you’ll build: Implement LDAP replication between two servers—a provider (master) that accepts writes, and a consumer (replica) that syncs changes.
Why it teaches LDAP: Replication is essential for production LDAP—high availability, read scaling, and disaster recovery. Understanding syncrepl shows how distributed directory services work.
Core challenges you’ll face:
- Change tracking → maps to changelog, CSN (Change Sequence Number)
- Initial sync → maps to full copy of DIT
- Incremental updates → maps to delta sync
- Conflict resolution → maps to multi-master scenarios
Key Concepts:
- LDAP Sync (syncrepl): RFC 4533 - LDAP Content Synchronization
- Change Sequence Numbers: OpenLDAP CSN format
- Replication Patterns: Designing Data-Intensive Applications Chapter 5 - Kleppmann
- Eventual Consistency: Designing Data-Intensive Applications Chapter 5 - Kleppmann
Difficulty: Master Time estimate: 4-6 weeks Prerequisites: All previous projects, distributed systems concepts
Real world outcome:
# Start provider
$ ./myldapd -p 3389 --role provider --server-id 001
Provider started on port 3389
# Start consumer pointing to provider
$ ./myldapd -p 3390 --role consumer \
--provider ldap://localhost:3389 \
--bind-dn "cn=replicator" --bind-pw secret
Consumer started on port 3390
Initial sync: fetching 500 entries from provider...
Sync complete. Entering refresh-and-persist mode.
# Add entry on provider
$ ldapadd -H ldap://localhost:3389 -D "cn=admin" -w secret <<EOF
dn: uid=charlie,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
cn: Charlie
sn: Brown
EOF
# Provider logs:
[Provider] Entry added: uid=charlie,ou=people,dc=example,dc=com
[Provider] CSN: 20240115120000.000001Z#000001#001#000000
[Provider] Notifying 1 consumer(s)
# Consumer logs:
[Consumer] Received change notification
[Consumer] Applying: add uid=charlie,ou=people,dc=example,dc=com
[Consumer] Sync complete, CSN: 20240115120000.000001Z#000001#001#000000
# Query consumer - entry is there!
$ ldapsearch -H ldap://localhost:3390 "(uid=charlie)"
dn: uid=charlie,ou=people,dc=example,dc=com
cn: Charlie
sn: Brown
Implementation Hints:
Change Sequence Number (CSN) format:
YYYYMMDDHHmmss.ffffff#count#sid#mod
20240115120000.000001Z#000001#001#000000
timestamp # op #srv# mod
Change log entry:
typedef struct ChangeLogEntry {
char *csn;
enum { CHANGE_ADD, CHANGE_MODIFY, CHANGE_DELETE, CHANGE_MODRDN } type;
char *dn;
Entry *entry; // For add
Modification *mods; // For modify
char *new_rdn; // For modrdn
} ChangeLogEntry;
Sync protocol (simplified):
Consumer Provider
| |
|-- SearchRequest (syncrepl) --->|
| (mode=refresh, cookie="") |
| |
|<-- SearchResultEntry ----------| (all entries)
|<-- SearchResultEntry ----------|
|<-- ... |
|<-- SearchResultDone -----------| (with cookie)
| |
|-- SearchRequest (syncrepl) --->|
| (mode=persist, cookie=xxx) |
| |
|<-- SearchResultEntry ----------| (changes as they happen)
|<-- ... (connection stays open) |
Consumer sync loop:
void consumer_sync_loop(Consumer *c) {
// 1. Send sync request with last cookie (or empty for initial)
// 2. Receive entries until SearchResultDone
// 3. Apply changes to local DIT
// 4. Save cookie
// 5. Enter persist mode (keep connection open for changes)
// 6. When change notification arrives, apply and update cookie
}
Questions to explore:
- What happens if the consumer is offline for a long time?
- How do you handle deletes in replication?
- What’s the difference between refresh-only and refresh-and-persist?
- How does multi-master replication handle conflicts?
Learning milestones:
- Initial sync works → Full copy transfers
- Incremental sync works → Changes propagate
- Consumer survives restarts → Cookie persistence works
- Persist mode works → Real-time updates
Project 15: CLI Tools Suite (ldapsearch, ldapadd, ldapmodify)
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: CLI Tools / User Interface
- Software or Tool: Your LDAP client (Project 6)
- Main Book: The Linux Command Line by William Shotts
What you’ll build: A complete suite of command-line LDAP tools compatible with OpenLDAP’s interfaces—ldapsearch, ldapadd, ldapmodify, ldapdelete, ldapwhoami.
Why it teaches LDAP: These tools are how administrators interact with LDAP daily. Building them shows you the user’s perspective and forces you to handle real-world edge cases.
Core challenges you’ll face:
- Argument parsing → maps to complex flag combinations
- LDIF output format → maps to proper line wrapping, base64
- Interactive password prompts → maps to secure input handling
- Error messages → maps to translating result codes to human messages
Key Concepts:
- OpenLDAP CLI Tools: OpenLDAP ldapsearch(1), ldapadd(1) man pages
- Argument Parsing: getopt_long documentation
- Secure Password Input: The Linux Programming Interface Chapter 8 - Kerrisk
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 6 (LDAP Client)
Real world outcome:
# Full compatibility with OpenLDAP command-line options
$ ./ldapsearch -H ldap://localhost -D "cn=admin" -W \
-b "dc=example,dc=com" "(objectClass=person)" cn mail
Enter LDAP Password: ********
# uid=alice,ou=people,dc=example,dc=com
cn: Alice Smith
mail: alice@example.com
# uid=bob,ou=people,dc=example,dc=com
cn: Bob Jones
mail: bob@example.com
# numEntries: 2
$ ./ldapadd -H ldap://localhost -D "cn=admin" -w secret < new_user.ldif
adding new entry "uid=charlie,ou=people,dc=example,dc=com"
$ ./ldapmodify -H ldap://localhost -D "cn=admin" -w secret <<EOF
dn: uid=charlie,ou=people,dc=example,dc=com
changetype: modify
add: telephoneNumber
telephoneNumber: +1-555-1234
EOF
modifying entry "uid=charlie,ou=people,dc=example,dc=com"
$ ./ldapdelete -H ldap://localhost -D "cn=admin" -w secret \
"uid=charlie,ou=people,dc=example,dc=com"
deleting entry "uid=charlie,ou=people,dc=example,dc=com"
$ ./ldapwhoami -H ldap://localhost -D "cn=admin" -w secret
dn:cn=admin,dc=example,dc=com
Implementation Hints:
Common options across tools:
struct CommonOptions {
char *uri; // -H ldap://host:port
char *bind_dn; // -D dn
char *password; // -w password, or -W for prompt
bool use_tls; // -Z (StartTLS), -ZZ (require)
char *sasl_mech; // -Y mechanism
int debug_level; // -d level
};
// Use getopt_long for parsing
static struct option long_options[] = {
{"host", required_argument, 0, 'H'},
{"bind", required_argument, 0, 'D'},
{"password", required_argument, 0, 'w'},
{"prompt", no_argument, 0, 'W'},
// etc.
};
Secure password prompt:
char* prompt_password(const char *prompt) {
struct termios old, new;
tcgetattr(STDIN_FILENO, &old);
new = old;
new.c_lflag &= ~ECHO; // Disable echo
tcsetattr(STDIN_FILENO, TCSANOW, &new);
fprintf(stderr, "%s", prompt);
char *password = read_line(stdin);
tcsetattr(STDIN_FILENO, TCSANOW, &old); // Restore
fprintf(stderr, "\n");
return password;
}
ldapsearch output format:
void print_entry_ldif(Entry *e, bool wrap_lines) {
printf("# %s\n", e->dn);
printf("dn: %s\n", e->dn);
for each attribute:
for each value:
if (needs_base64(value))
printf("%s:: %s\n", attr, base64_encode(value));
else if (needs_wrap(value) && wrap_lines)
print_wrapped(attr, value, 76);
else
printf("%s: %s\n", attr, value);
printf("\n");
}
Questions to explore:
- How do you handle very long attribute values in output?
- What’s the difference between
-w passwordand-W? - How does
-c(continue on errors) work in ldapmodify? - What exit codes should each tool return?
Learning milestones:
- ldapsearch works with real servers → Basic functionality
- ldapadd creates entries from LDIF → Write operations work
- Password prompting is secure → No echo, memory cleanup
- Tools are drop-in replacements for OpenLDAP → Full compatibility
Project Comparison Table
| # | Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|---|
| 1 | ASN.1 BER Encoder/Decoder | Intermediate | 1-2 weeks | ★★★★☆ | ★★☆☆☆ |
| 2 | DN and RDN Parser | Beginner | 3-5 days | ★★★☆☆ | ★★☆☆☆ |
| 3 | LDAP Filter Parser | Intermediate | 1-2 weeks | ★★★★☆ | ★★★☆☆ |
| 4 | In-Memory Directory Tree | Intermediate | 1-2 weeks | ★★★★☆ | ★★★☆☆ |
| 5 | LDAP Message Encoder/Decoder | Advanced | 2-3 weeks | ★★★★★ | ★★★☆☆ |
| 6 | Simple LDAP Client | Advanced | 2 weeks | ★★★★★ | ★★★★☆ |
| 7 | LDAP Server Core | Expert | 3-4 weeks | ★★★★★ | ★★★★★ |
| 8 | LDIF Parser and Loader | Intermediate | 1 week | ★★★☆☆ | ★★☆☆☆ |
| 9 | Password Verification | Advanced | 1-2 weeks | ★★★★☆ | ★★★☆☆ |
| 10 | StartTLS/Secure Connections | Expert | 2-3 weeks | ★★★★★ | ★★★★☆ |
| 11 | SASL Authentication | Expert | 3-4 weeks | ★★★★★ | ★★★★☆ |
| 12 | Schema Validation | Advanced | 2-3 weeks | ★★★★☆ | ★★★☆☆ |
| 13 | Access Control Lists | Advanced | 2 weeks | ★★★★☆ | ★★★☆☆ |
| 14 | Replication | Master | 4-6 weeks | ★★★★★ | ★★★★★ |
| 15 | CLI Tools Suite | Intermediate | 1-2 weeks | ★★★☆☆ | ★★★★☆ |
Recommended Learning Path
Phase 1: Protocol Foundations (3-4 weeks)
Start here: Projects 1 → 2 → 3
Build the core parsing/encoding infrastructure. After this, you understand how LDAP data is structured and transmitted.
Phase 2: Data Layer (2-3 weeks)
Projects 4 → 8
Build the directory tree and LDIF handling. You now have a working in-memory directory.
Phase 3: Network Protocol (4-5 weeks)
Projects 5 → 6 → 7
Connect your data layer to the network. After this, you have a working LDAP client and server.
Phase 4: Security (4-6 weeks)
Projects 9 → 10 → 11 → 13
Add authentication, encryption, and access control. Your server is now production-quality for security.
Phase 5: Enterprise Features (6-8 weeks)
Projects 12 → 14 → 15
Add schema validation, replication, and professional tools. You’ve now built an enterprise-grade directory service.
Final Capstone Project: Complete Directory Service
- File: LEARN_LDAP_FROM_SCRATCH_IN_C.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 5: Pure Magic
- Business Potential: 5. The “Industry Disruptor”
- Difficulty: Level 5: Master
- Knowledge Area: Full-Stack Directory Services
- Software or Tool: Everything you’ve built
- Main Book: LDAP System Administration by Gerald Carter
What you’ll build: A production-ready LDAP directory service that can replace OpenLDAP for small deployments—with TLS, SASL, replication, schema validation, ACLs, and a full CLI toolset.
Why it teaches LDAP: Integration is where mastery is proven. Making all components work together reveals edge cases textbooks don’t cover. You’ll understand why OpenLDAP is designed the way it is.
Core challenges you’ll face:
- Integration testing → maps to running against real LDAP clients
- Performance tuning → maps to indexing, caching, connection pooling
- Configuration management → maps to slapd.conf-style configuration
- Operational concerns → maps to logging, monitoring, backup/restore
Difficulty: Master Time estimate: 2-3 months Prerequisites: All previous projects
Real world outcome:
$ ./mydirectory --config /etc/mydirectory/config.conf
Directory Service v1.0 starting...
Loading schema: core.schema, inetorgperson.schema
Loading ACL rules
Loading replication config
Starting TLS listener on port 636
Starting LDAP listener on port 389
Ready to accept connections.
# Standard tools work perfectly
$ ldapsearch -H ldaps://localhost -D "cn=admin" -W ...
$ ldapadd -H ldap://localhost -ZZ ...
# PAM/NSS integration works
$ getent passwd alice
alice:x:1001:1001:Alice Smith:/home/alice:/bin/bash
# Replication keeps servers in sync
# ACLs protect sensitive data
# Schema validation prevents bad data
Essential Resources Summary
RFCs (The Authoritative Sources)
| RFC | Title | Purpose |
|---|---|---|
| RFC 4510 | Technical Specification Road Map | Overview of all LDAP RFCs |
| RFC 4511 | The Protocol | Core protocol definition |
| RFC 4512 | Directory Information Models | Schema, DIT, entries |
| RFC 4513 | Authentication Methods | Bind, SASL, TLS |
| RFC 4514 | String Representation of DNs | DN format |
| RFC 4515 | String Representation of Filters | Filter syntax |
| RFC 4517 | Syntaxes and Matching Rules | Attribute types |
| RFC 4519 | Schema for User Applications | Common schema |
| RFC 4533 | Content Synchronization | Replication |
| RFC 2849 | LDIF Format | Data interchange |
Books
| Book | Author | Best For |
|---|---|---|
| LDAP System Administration | Gerald Carter | Operations, practical use |
| Understanding LDAP | Heinz Johner et al. | Concepts, architecture |
| The Linux Programming Interface | Michael Kerrisk | Socket programming, systems |
| Serious Cryptography | Jean-Philippe Aumasson | Password hashing, TLS |
| Designing Data-Intensive Applications | Martin Kleppmann | Replication, distributed systems |
Online Resources
- LDAP.com - Comprehensive LDAP reference
- OpenLDAP Documentation - Implementation reference
- A Layman’s Guide to ASN.1/BER/DER - BER encoding
- DigitalOcean LDAP Tutorial - Beginner-friendly
- OpenLDAP Source Code - Reference implementation
Test Servers
- ForumSys Public LDAP - Free test server
- OpenLDAP in Docker -
docker run -d -p 389:389 osixia/openldap
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | ASN.1 BER Encoder/Decoder Library | C |
| 2 | DN and RDN Parser | C |
| 3 | LDAP Filter Parser and Evaluator | C |
| 4 | In-Memory Directory Tree (DIT) | C |
| 5 | LDAP Message Encoder/Decoder | C |
| 6 | Simple LDAP Client | C |
| 7 | LDAP Server - Core Protocol Handling | C |
| 8 | LDIF Parser and Loader | C |
| 9 | Simple Bind and Password Verification | C |
| 10 | StartTLS and Secure Connections | C |
| 11 | SASL Authentication Framework | C |
| 12 | Schema and Object Class Validation | C |
| 13 | Access Control Lists (ACLs) | C |
| 14 | Replication (Consumer/Provider) | C |
| 15 | CLI Tools Suite (ldapsearch, ldapadd, ldapmodify) | C |
| Final | Complete Directory Service (Capstone) | C |
Sources
- RFC 4511 - LDAP: The Protocol
- RFC 4510 - LDAP Technical Specification Road Map
- RFC 4513 - LDAP Authentication Methods
- LDAP.com - LDAP Filters
- LDAP.com - LDAPv3 Wire Protocol Reference
- LDAP.com - LDAP-Related RFCs
- DigitalOcean - Understanding LDAP Protocol
- Okta - What Is LDAP?
- A Layman’s Guide to ASN.1, BER, and DER
- Let’s Encrypt - A Warm Welcome to ASN.1 and DER
- OpenLDAP Source Repository
- OpenLDAP Administrator’s Guide
- OpenLDAP SASL Guide
- Oracle X.500 Overview
- X.500 Directory Services
- GNU SASL Manual
- RFC 4752 - Kerberos V5 SASL Mechanism