Decoding csreq Code Requirements in TCC.db
How to read the csreq and indirect_object_code_identity blobs in TCC.db: compiled code requirements, anchors, identifiers and cdhash-only ad-hoc clients.
TL;DR. The csreq column of a TCC.db row is a compiled code requirement: a small binary blob starting with the magic 0xFADE0C00, which encodes a rule in Apple's Code Signing Requirement Language such as identifier "com.apple.Terminal" and anchor apple. tccd honours the row for any code that satisfies that rule. indirect_object_code_identity holds the same kind of blob for the target of an Automation grant. A requirement that is only cdhash H"..." pins one exact build and is typical of ad-hoc signed code. On a Mac, csreq -r- -t decodes a blob; TCC Parser decodes both columns to text in the browser.
Why the requirement matters more than the name
The client column says who the row is for: a bundle ID such as com.example.app, or an absolute path such as /usr/local/bin/tool. But a name is easy to reuse. Anyone can build an app that claims the bundle ID com.example.app. What stops that impostor from inheriting the real app's permissions is the stored requirement: when a program with that identifier asks for the service, tccd checks whether the program's code signature satisfies csreq.
For an investigator, that has two consequences:
- A bundle-ID row is honoured for any code that satisfies the requirement. If the requirement is strict (Apple anchor, or a specific Team ID), only properly signed builds from that developer qualify. If it is loose or absent, the protection is weaker.
- The binary on disk today may not be the one that was approved. Updates, replacements and deleted tools all break the link between "row" and "file". The requirement is the piece of evidence that describes what was approved.
The blob format
A compiled requirement, as stored in csreq, is a big-endian structure:
| Part | Content |
|---|---|
| Magic | 0xFADE0C00 (requirement) |
| Length | Total length of the blob |
| Kind | 1: expression form |
| Expression | A prefix expression: an operator followed by its operands (strings, hashes, certificate fields, nested expressions) |
You rarely need to walk the expression by hand. The point of knowing the layout is to recognize the blob (it starts with FA DE 0C 00 in a hex view) and to trust a decoder's text output only after checking that it consumed the whole blob.
Decoding on a Mac
The macOS-native way is the csreq tool. Extract the blob as bytes and feed it on standard input:
sqlite3 TCC.db "SELECT hex(csreq) FROM access
WHERE client = 'com.apple.Terminal'
AND service = 'kTCCServiceSystemPolicyAllFiles';" \
| xxd -r -p | csreq -r- -t
To compare with a binary on disk, print that binary's designated requirement:
codesign -d -r- /System/Applications/Utilities/Terminal.app
If you are not on a Mac, or you have hundreds of rows, TCC Parser decodes every csreq and indirect_object_code_identity blob to requirement text and shows it alongside the row.
Reading the decoded text
Requirements are boolean expressions. The clauses you will see most:
| Clause | Meaning |
|---|---|
identifier "com.apple.Terminal" | The code's signing identifier must be exactly this |
anchor apple | Signed by Apple itself (Apple's own software) |
anchor apple generic | Chains to Apple's root, which includes Developer ID and App Store signing |
certificate leaf[subject.OU] = TEAMID | The signing certificate belongs to this Team ID |
cdhash H"..." | The code directory hash must be exactly this value: one specific build |
and, or, parentheses | Combine clauses |
Three patterns cover most rows:
Apple software. identifier "com.apple.Terminal" and anchor apple. Only Apple-signed code with that identifier qualifies.
Third-party signed software. anchor apple generic and identifier "com.example.app" and certificate leaf[subject.OU] = TEAMID, often with additional certificate-field clauses. Only builds signed by that Team ID qualify. The Team ID is a strong pivot: look it up in your fleet, in other TCC rows and in code-signing data for binaries on disk.
Ad-hoc or pinned code. cdhash H"4b6a..." alone. There is no identity beyond the hash of one build. This is what you typically see for ad-hoc signed binaries (no developer certificate) and for many path-based clients. It is not malicious by itself: developers' own builds, some open-source tools and scripts compiled locally look the same. But it is unusual for a high-impact service, and it means the approval is tied to a single build: if the file on disk has a different cdhash, it is not the build that was approved.
A missing requirement (null or empty csreq) deserves a note too. It can be legitimate for some system-set rows, but for a user-facing grant it removes the identity check the row would normally carry.
indirect_object_code_identity: the other side of Automation
For Apple Events automation (kTCCServiceAppleEvents), a row has two parties: the client that sends events and the target in indirect_object_identifier (for example com.apple.finder or com.apple.systemevents). indirect_object_code_identity is the compiled requirement for that target, in the same format as csreq. Decode it the same way. For Apple targets you expect anchor apple requirements; anything else is worth reading closely.
Worked example
In the fictional FIN-MBP-03 sample that ships with TCC Parser, the system database holds an Accessibility row for a path-based client:
| Field | Value |
|---|---|
service | kTCCServiceAccessibility |
client | /Users/dana.whitlock/Library/Caches/.sync/sync-helper |
client_type | 1 (absolute path) |
csreq (decoded) | cdhash H"..." only |
Read together: a binary in a hidden folder under the user's Caches, identified by path rather than bundle ID, with a requirement that pins one ad-hoc build, holding a capability that can drive the user interface. Each element alone has innocent explanations; together they are a reason to acquire that file, hash it, compare its cdhash with the requirement, and look for how it arrived. Compare that with the Full Disk Access row for com.apple.Terminal in the same database, whose requirement is an Apple anchor: the identity is fine, and the question becomes who used Terminal, and for what. That is the subject of investigating TCC permission abuse.
How TCC Parser uses the requirement
In the Permissions tab, click a row to see its decoded requirement in the detail panel, and the Findings tab flags rows where the requirement is ad-hoc (cdhash-only) or missing. The flag is a prompt to look, not a verdict. The same decoded text appears in the JSON export, so you can keep it with the case.
Pitfalls
- Decoding is not verifying. Reading the requirement tells you what was approved. Whether a given file satisfies it needs the file and
codesign. - Identifiers are not proof of origin.
identifier "com.example.app"without an anchor or Team ID clause is weak. - Updates change cdhashes. A cdhash-only row stops matching after a rebuild, and a new prompt creates a new row state.
- The csreq in
expiredandMDMOverrides.plist. Expired rows keep theircsreq, and MDM entries carryCodeRequirement(text) andCodeRequirementData(blob). Compare them with the active rows.