Introducing fashion command-line tool

fashion is a Swift command-line interface to traverse a file hierarchy and compute or match popular hash digests.

The project is hosted on Codeberg, mirrored on GitHub, and natively supports:

  • CryptoKit hash functions: SHA-2 (SHA-256 by default) and insecure SHA-1 or MD5
  • Fuzzy hash functions: ssdeep and TLSH as submodules with Swift C bridging
  • Searching for multiple digests with mixed algorithms and a similarity / distance score for fuzzy hash
  • CDHash of Mach-O binaries, the identity macOS and iOS use: embedded when signed or synthesized like syspolicyd
  • symhash of Mach-O binaries, with any algorithm, separator, and optional sort
  • XAR archives (macOS packages) table of contents (TOC) checksum with any algorithm and optional decompress (zlib)
  • Git blob object
  • Thin universal Mach-O binaries into architecture slices with exact, unsign and unpack options
  • Unsign Mach-O binaries and restore __LINKEDIT vmsize: samples signed by different identities hash the same
  • Unpack UPX Mach-O binaries natively, past the signature, a crafted marker or XORed code
  • Verify Mach-O files for anomalies, as the kernel, dyld, Security, Gatekeeper, AMFI or Rosetta parse them
  • Multithreading, and zero runtime dependencies

With --jobs 0 or the -J shortcut, fashion is blazing fast and stays well under 100 MB of memory without --sort:

Machine Jobs App count File count SHA-256 duration TLSH duration
Mac Studio M2 Ultra 24 274 apps 1,268,657 files 1 min 6 secs 1 min 25 secs
MacBook Air M4 10 213 apps 813,217 files 54 secs 1 min 42 secs

Install

A signed and notarized binary is available via Homebrew, cheers 🍻

brew tap melomac/tap https://codeberg.org/melomac/homebrew-tap
brew trust melomac/tap
brew install melomac/tap/fashion

With melomac/tap mirror on GitHub, you can also just run: brew install melomac/tap/fashion

Build from source

fashion builds with Xcode and Swift 6.2 for macOS 13 and later, its C and C++ dependencies as submodules:

git clone --recurse-submodules https://codeberg.org/melomac/fashion
cd fashion
make

make test runs the tests twice, once per --exact ending. The packed fixtures require upx installed, and are skipped without it.

Usage

fashion provides subcommands to directly access major features: thin, unpack, etc. Run fashion help to list them:

fashion help
OVERVIEW: Compute and match file cryptographic or fuzzy hash digests for threat hunting and binary triage.

USAGE: fashion <subcommand>

OPTIONS:
  --version               Show the version.

SUBCOMMANDS:
  hash, h (default)       Compute and match file cryptographic or fuzzy hash digests for threat hunting and binary triage.
  thin, t, thinr          Scan Mach-O files for artifacts, each architecture and each state the flags take it through.
  unsign, i               Write and restore a Mach-O image, removing the code signature.
  unpack, p, upx          Write and restore a UPX-packed Mach-O image, unpacked without UPX.
  verify, v               Verify Mach-O files for anomalies.

  See 'fashion help <subcommand>' for detailed help.

hash is the default command, fashion alone is fashion hash:

fashion --help
OVERVIEW: Compute and match file cryptographic or fuzzy hash digests for threat hunting and binary triage.

USAGE: fashion [hash] [<options>] [<paths> ...]

ARGUMENTS:
  <paths>                 Paths to scan. (default: .)

HASH MODE:
  -a, --algo <algorithm>  Hash algorithm
        md5               - MD5 insecure hash
        sha1              - SHA-1 insecure hash
        sha256            - SHA-256 hash
        sha384            - SHA-384 hash
        sha512            - SHA-512 hash
        git               - Git blob SHA-1 hash
        git256            - Git blob SHA-256 hash
        ssdeep            - Spam Sum Deep fuzzy hash
        tlsh              - Trend Micro Locality Sensitive Hash
        cdhash            - CodeDirectory hash
  -t, --thin, --slices    Hash individual Mach-O architectures.
  -e, --exact, --strict   Hash an exact Mach-O image, dropping appended bytes.
  -i, --unsign            Hash a Mach-O image, removing the code signature.
  -p, --unpack            Hash a UPX-packed Mach-O image, unpacked without UPX.
  -v, --verify            Verify Mach-O files for anomalies

MATCH MODE:
  -m, --match <match>     Digests to match.
  -s, --score <score>     Minimum similarity (ssdeep) or maximum distance (TLSH) threshold (default: 40).
  -x, --xlen              Compare TLSH digests without the recorded file length.
  -n, --negate            Print only not matched files.

SYMBOL MODE:
  --symhash, --sym        Compute SymHash (external symbol hash) for Mach-O slices.
  --separator, --sep <char>
                          Symbol separator (default: ,).
  -o, --order/--no-order  Sort symbols before hashing (default: sorted).

XAR MODE:
  --xar-toc, --xar        Hash XAR table of contents.
  -d, --dec, --decompress Decompress table of contents before hashing.

OPTIONS:
  -j, --jobs <jobs>       Number of concurrent workers (default: 1, 0 = all CPUs).
  -J                      All CPUs, short for --jobs 0.
  -L, --follow            Follow symlinks.
  -q, --quiet             Quiet mode: digest(s) or matched path(s) only.
  -S, --sort/--no-sort    Sort file paths, numbers by value. (default: --no-sort)
  --version               Show the version.
  -h, --help              Show help information.

Hasher

fashion walks the given paths, or the current directory by default, and prints one line per regular file with the digest then the path, as the files are found or in order with --sort / -S.

--algo picks the hash function: md5, sha1, sha256, sha384, sha512, ssdeep, tlsh, cdhash, git or git256, and defaults to SHA-256.

--thin hashes each architecture of a universal Mach-O as a file of its own.

--exact, --unsign and --unpack print the initial file first, unlabeled, then each artifact thin, unsign or unpack finds. The digests are those of the artifacts. You can match any artifact with any algorithm.

When an edit cannot be made, the file is hashed as it is and reported to standard error. Files existing at the destination of an artifact are compared: one holding different bytes is hashed too and reported to standard error with both sizes and SHA-256s.

Match mode

Use --match to search for files matching one or more digests.

Without the --algo option, we target several algorithms in a single pass: every file is read once and hashed with each hash function the targets need. The shape of each target picks the hash function:

Shape Algorithm Shape conflict
32 hex digits md5
40 hex digits sha1 cdhash, git
64 hex digits sha256 cdhash, git256
96 hex digits sha384
128 hex digits sha512
T1 and 70 hex digits, or 70 alone tlsh
blocksize:chunk:chunk, base64 chunks ssdeep

A fuzzy match line shows the digest that matched with its score:

$ fashion --match $(fashion --quiet --algo tlsh /usr/bin/true) /usr/bin/
T1B683F9DB67586C65EC98A97412CEE6237F33E7950FA2401760A1C4E93E437B67E3980C   40  /usr/bin/update_dyld_shared_cache
T10483D9DF1B582C51ED4C987012CEA6677F33E7950F92422B60A1C4E92E437BB6E3984C    0  /usr/bin/true
T16083DADB57582C64EC989C7412CEA727BF33E7550B92412B60A1C4EA3E437B67E3584C   26  /usr/bin/false

With the -q / --quiet flag, we only print matching paths. We print non-satisfied paths with the -n / --negate flag. --score sets the threshold a fuzzy match must pass, a minimum similarity for ssdeep or a maximum distance for TLSH, 40 by default.

A Code Directory hash and a Git object hash have the shape of a SHA-1 or SHA-256 digest, so matching one requires the --algo option. A symhash or a XAR TOC checksum is matched in the respective mode, --symhash or --xar-toc.

Symbol hash mode

Following the convention used by ImpHash, symhash, and tools like VirusTotal, --symhash mode aka --sym, defaults to the insecure MD5 hash of the ordered external symbol list joined by the , separator. You can actually pick any algorithm (ex: --algo ssdeep), separator, and keep the symbol list as is with --no-order.

An image with no external symbol table reports to standard error.

With --unpack, the symhash goes through each artifact, one line labeled per state, and compared the same way. --unsign and --exact change no symbol but are accepted, so one set of flags serves every mode.

XAR TOC mode

Installation packages on macOS are eXtended ARchives. According to xar(1):

xar is no longer under active development by Apple. Clients of xar should pursue alternative archive formats.

The header is straightforward so, instead of bridging to the libxar shared library, we implement our own parser to read the table of contents (TOC).

Just like xar --dump-toc-cksum and syspolicyd, --xar-toc mode defaults to the insecure SHA-1 of the compressed TOC. You can choose any algorithm and even have the TOC decompressed (zlib) with --decompress / --dec / -d.

Thinner

fashion thin aka thinr takes each architecture of a universal Mach-O as a slice, named as lipo spells the architecture. The fat table alone decides the bytes of each slice, as lipo -thin.

--exact, --unsign and --unpack take universal and thinned Mach-O files through the edits hash mode makes in memory, every state on the way: the slice, then cut where the image ends, then without the code signature, then unpacked, then the unpacked image without the signature it kept.

Nothing is written, all edits happen in memory, so a tree can be looked over first: each artifact is listed as hash labels the state.

With --write, each artifact is named after the file, the architecture and what was done, ex: ls_x86_64_unsigned_unpacked. A different file already under an artifact’s name is kept as a backup, renamed with the suffix ~ and the current timestamp. The backup’s path is printed after the artifact’s. Anything that is not a regular file there is never replaced.

With -q / --quiet, a file that has artifacts is printed once.

Unsigner

A sample signed again gets a new digest, though the code remains identical. With the --unsign flag, we hash a Mach-O without its code signature, so samples signed by different identities hash the same. fashion unsign writes that file beside the sample, sample_unsigned, whole: each slice of a universal file unsigned, and the file laid out again as lipo lays it out.

codesign leaves __LINKEDIT with the 16 KiB the signature needed, and a signed file holds no trace of what was there. We restore vmsize back to its original value, as the tool that wrote the segment:

  • the linker’s page for an image with its symbols
  • the segment’s file size for an image strip(1) cleared the symbols
  • or 16 KiB for a sample stripped since Xcode 15.3, according to ld64, cctools and Security.

Unpacker

Malware was long packed with UPX on macOS, until UPX stopped packing by default the Darwin platform during milestone 4.2.0 (2023). upx -d still unpacks, but gives up on samples signed after packing: code signature hides the 16-byte __LINKEDIT UPX left.

With the --unpack flag, we hash the Mach-O file as it was before packing, natively, with UPX’s own UCL and LZMA decoders, and looking past any code signature. fashion unpack writes that file beside the sample, sample_unpacked, whole, where thin --unpack writes it split. With --unsign, the signature the code carried before packing comes out too.

When the marker UPX! was modified, or the usual section/segment was renamed, heuristics look for the PackHeader structure and checksum to unpack the file anyway, and report the marker we found in place.

When the packer also XORed the decompressed code before unfiltering, like OSX/Keydnap, the image fails its checksum: we brute-force a single byte key until it matches, and report the key. When no key fits, the image is unpacked under none, and the sums are reported.

Unlike upx, we compare what comes out of the decompressor with the two Adler-32 sums the PackHeader carries, of the packed bytes and of the unpacked ones, which neither upx -d nor the stub reads: a sum that doesn’t match is reported to standard error, and the file is unpacked all the same. What refuses a file is: a header that doesn’t hold, a block that doesn’t decode, or an image that doesn’t parse.

A PackHeader wiped out to defeat upx -d leaves nothing to find for us: the file is hashed as it is, unless it keeps the 16-byte __LINKEDIT only UPX writes, which upx -d and we refuse as corrupted. UPX’s canUnpack comments…

// Pirates might overwrite the UPX! marker.  Try harder.

…and falls back to emulating the entry stub and scanning for the packed data. Anything we’d recover that way is unverifiable.

When the decompressed data isn’t a Mach-O image, we hash the file like any other file. fashion unpack --keep writes the decompressed data for further investigation.

We implement our own unpacker, supporting UPX 3.91 and onward:

  • arm64, arm, x86_64, i386 and ppc executables
  • x86_64, i386, and ppc dylibs
  • compressed with NRV or LZMA

Verify

fashion verify lists anomalies in each Mach-O file, one line each after the file’s path, and nothing about a file with nothing to report.

Each rule comes from a loader that refuses what the parser accepts: the kernel, dyld, Security, Gatekeeper, AMFI or Rosetta, stated the way that loader sees it. The rest catch a file that two readers parse differently, which is how a sample hides from a tool. verify prints one line per finding, in these groups:

  • load commands: a count that hides the last ones, a size not a multiple of 4, a command found twice, pairs that exclude each other;
  • segments and sections: overlapping or out of order, a file offset that disagrees with the address, __TEXT not first or __LINKEDIT not last, protections no linker writes;
  • entry point: outside __TEXT, in no section, or in a thread state of another architecture;
  • linking: a dylinker other than /usr/lib/dyld, LC_DYLD_ENVIRONMENT, no LC_UUID, no dylib, no symbol table, a static or non-PIE arm64 executable;
  • unaccounted bytes: past the image, inside __LINKEDIT with no table over them, non-zero padding;
  • edits: a __LINKEDIT vmsize rounded by a signature the file no longer has, UPX plain, modified or signed after packing;
  • the signature: pages that do not hash to the code directory, a code limit short of the file, sealed blobs that do not hash to their slots, entitlements that opt out of hardening;
  • the signer: a CMS blob that does not verify, a chain outside Apple’s roots, a certificate Gatekeeper refuses, entitlements AMFI reserves to Apple;
  • universal files: a slice hidden past the table’s count, two slices of one architecture, overlapping or misaligned slices, non-zero padding.

What no loader would accept, the parser refuses: red and counted. Most of the other findings survive an ad-hoc signature, so codesign still calls the file “valid on disk” while it carries hidden bytes, an inserted library, a desynced code section, or a hidden slice.

The reasoning behind each rule, the reader it comes from, the evasion it catches, and the measurements over the OS, third-party software, and a malware corpus that kept or dropped it are in VERIFY-STRATEGY.md.

fashion hash --verify and fashion thin --verify run the same rules, and leave the lines in the unified log, under the verify category of the fashion subsystem, so a scan for another purpose records what is odd without printing it:

log show --style compact --predicate 'subsystem == "fashion" AND category == "verify"' --last 10m

Common options

Exact

Bytes appended past a Mach-O image change its digest and keep a signature from coming out. The --exact / --strict flag cuts a slice where the image ends, so the digest is the image’s with the appended bytes dropped: the file’s own digest prints first, then the cut image’s, labeled (exact). Where the image ends is read as Security’s strict validation reads it, at the end of the __LINKEDIT segment or of the LC_SYMTAB string table, whichever command comes first; built with SECURITY_STRICT_VALIDATION=NO, at the farthest byte any segment or table reaches instead, and no --strict alias.

Quiet

With the -q / --quiet flag, we only print file digests, or matching paths in match mode.

Concurrency

The -j / --jobs flag controls parallel workers. Set -j 0 or -J to use all CPU cores.

The --sort flag prints files in the natural order of paths: a run of digits sorts as a number, so file-2 comes before file-10, and any other byte as itself, so the order holds in every locale. The walk sorts each directory as it reads it, so hashing starts right away, and a file’s lines wait until every file before it has printed.

The -L / --follow flag follows symlinks while walking; by default they are skipped. A symlink named directly on the command line is always followed, like find -H. Only regular files are hashed: a FIFO, device, or socket is skipped, whether named on the command line or in a directory.

Colors

On a terminal, fashion colors lines by what they tell:

🟢 Green

  • a full match with any algorithm: a digest, a symhash, a cdhash, ADHOC included;
  • a fuzzy match, darker as the distance grows toward the --score threshold;
  • TLSH distance from 300, where false positives pass 79%, is dimmed whatever the threshold, unless --xlen leaves out the length those rates count;

đź”´ Red: a malformed file in a mode that has nothing to print without parsing it: a Mach-O under --algo cdhash, verify, unsign, unpack or the edits of thin, a XAR under --xar-toc, a UPX pack unpack can’t undo;

🟡 Yellow: anything else on standard error: a file that can’t be read, a Mach-O or a slice that can’t be parsed, an edit that can’t be made, a signature that doesn’t hold, a packer’s marker replaced or its code XORed, a sum that doesn’t match, an image with no symbol table, a file in place of an artifact that differs, or a file TLSH can’t hash;

⚪️ Dimmed: synthesized ADHOC lines, for unsigned code.

A terminal with 16 colors, by TERM and COLORTERM, gets its own green, dimmed as soon as a match is not full. Set NO_COLOR=1 to turn this off.

Exit status

fashion follows the grep/codesign convention so it can be scripted:

Code Description
0 ✅ success — in match mode: at least one file is matched
1 ❌ match mode: no file matched
2 ❌ one or more paths couldn’t be enumerated, hashed, or written
64 ❌ usage: invalid options, or an argument that isn’t UTF-8

Errors and warnings never abort the rest of the scan.

Per-path failures (a missing path, a permission-denied directory, an unreadable file, one that changed or was replaced while hashed, a name that is not UTF-8) are written to standard error and set exit code 2.

Hash functions

Project algorithm choices are driven by interoperability with existing tools and formats.

CryptoKit

We use CryptoKit rather than bridging to the old Common Crypto collection of C functions from libSystem:

Prefer CryptoKit over lower-level interfaces. CryptoKit frees your app from managing raw pointers, and automatically handles tasks that make your app more secure, like overwriting sensitive data during memory deallocation.

CommonCrypto’s own manual page dates it:

$ man 3 CC_SHA | grep HISTORY --after-context 4
HISTORY
     These functions are available in OS X 10.4 and later.

     These functions provide similar functionality to the routines found in
     OpenSSL 0.9.6 and may use the same implementation.

Insecure MD5 and SHA-1

MD5 and SHA-1 both have known collision attacks. CryptoKit files them under Insecure, so every use is visible at the call site:

These algorithms aren’t considered cryptographically secure, but the framework provides them for backward compatibility with older services that require them.

libSystem’s Common Crypto deprecates MD5 instead, which turns each call into a build warning we expressly muted before adopting CryptoKit.

ssdeep

Clang’s -Wshorten-64-to-32 warns where a 64-bit integer is stored in a 32-bit one, in case the high bits would be lost. Xcode and SwiftPM both turn this safeguard on, and ssdeep trips it three times, in edit_dist.c and fuzzy.c, so every build of fashion printed warnings. We silence the warnings in Package.swift for the ssdeep target only.

A spamsum chunk holds at most 64 characters, and ssdeep carries that bound everywhere. The two values in edit_dist.c are positions in a chunk, 0 to 64. The one in fuzzy.c caps the score of a small block size, under 45, at block_size / 3 * minlen, so at most 14 Ă— 64. A 32-bit integer holds over two billion, so the sources stay as upstream ships them.

TLSH

Trend Micro’s library counts the total data length using an unsigned int data_len, i.e. 32-bit, so any file larger than ~4 GB is undefined behavior. Even worse, in Java, the code was using a signed integer (~2 GB).

We follow Trend Micro’s recommendations and backport the Java fix from TLSH 4.6.0: define the TLSH of a file as the TLSH of its first ~4 GB. The tlsh tool reads a file’s size into a 32-bit int instead: it calls a file “too small” when that size modulo 4 GiB is 2 GiB or more, and hashes only that many bytes of any other.

We report on standard error each file TLSH cannot hash: under 50 bytes, with too little variation, and greater than ~4 GB (discussion). Neither changes the exit status, and --quiet leaves them in: it shortens standard output alone.

A TLSH distance counts file length: padding, such as appended null bytes, adds to it even when the content is the same. --xlen leaves the length out of --match, as the tlsh tool’s -xlen does: a 50 KB binary padded with zeros to 2.9 MB is 1 away from itself instead of 505.

CDHash

The CodeDirectory hash is how macOS and iOS identify an executable’s integrity. It’s the hash of the directory that holds the hash of every code page: it changes with any byte of code.

We print the CDHash of Mach-O binaries, one line per code directory, strongest hash type first as the Security framework ranks them: sha512, sha384, sha256, sha256t, then sha1. A binary signed with multiple algorithms gets multiple lines, labeled with the hash type. The Security framework reads the signature by the strongest directory; the kernel knows no SHA-512 directory and refuses a signature that carries one (which verify reports).

We print the CandidateCDHashFull but --match accepts the truncated to 40 hex digits CDHash Apple’s tools usually show.

When syspolicyd meets unsigned code, it has the Security framework sign it in memory and looks up a notarization ticket for the detached ADHOC hash. This is not documented: we mirror the Security framework and syspolicyd (see Reverse engineering), build the same signature, and print its CandidateCDHashFull:

  • the ADHOC, sha256 line is, byte for byte, the hash syspolicyd looks up;
  • the ADHOC, sha1 line is its legacy counterpart.

When Security’s static validation rejects the signature, which codesign reports as “not signed at all”, the file gets its ADHOC lines too, computed up to where the signature starts.

Each signed slice is checked as Security’s static validation checks it, which codesign --verify shows: every code page against its code directory, the sealed blobs against their slots, the CMS signature over the directory. A slice that fails is labeled INVALID, and named on standard error without changing the exit status: “signature rejected, hashed as unsigned”, or “invalid signature (code or signature have been modified)” when a byte of code changed since signing.

Mach-O files that are not code to codesign — object files, dSYMs, core dumps, kernel filesets — have no identity and yield no line, signed or not.

Every rule is checked against codesign: the tests compare fashion’s ADHOC lines with codesign --detached -s - --identifier ADHOC on real and crafted binaries, so a change in the Security framework fails the test rather than silently drift.

Git hash

The git and git256 algorithms compute the hash of a Git blob object: blob \(length)\0 + data.

With the digest, you can look for the object across all branches and commits of a repository:

$ git log --raw --all --format='%h %s' --find-object=$(fashion --algo git --quiet .swiftformat)
7f1a770 initial public release

:000000 100644 0000000 842281a A        .swiftformat

Misc

Completion

swift-argument-parser provides free completion for bash, fish, and zsh:

fashion --generate-completion-script

With fish-shell, you can even source the generated completion script on demand, and it will remain for the session:

echo "fashion --generate-completion-script fish | source" > ~/.config/fish/completions/fashion.fish

The file holds the command rather than its output, so the completions come from the binary each time fish loads them: a flag added to a command is completed in the next shell, with nothing to regenerate. Very convenient when you craft your own tools.

Fuzzer

Every parser takes attacker-controlled bytes: a scan walks whatever tree it is pointed at. Each parser has a libFuzzer harness under AddressSanitizer: the fat and Mach-O headers, the code signature and its directories, the XAR header and table of contents, the symbol tables, the digests --match is given, the byte hashers, UPX’s pack header, decoders and filters, the verifier, the CMS signer and the DER readers.

make fuzz-run builds them outside SwiftPM, seeds their corpora from the test fixtures and runs them all; Fuzz/README.md says what each covers and how to reproduce a finding.

Reverse engineering

Much of what fashion mirrors is not documented, and was read from the binaries with IDA Pro driven through ida-mcp:

  • the Security framework, in the dyld shared cache: how codesign signs, verifies and removes a signature, which code directory it reads, and what its strict validation refuses;
  • syspolicyd: the identity Gatekeeper signs unsigned code with, and the ticket it looks up;
  • dyld: what it refuses of an image it loads, and what it reads past a fat table’s count;
  • the kernel cache: what parse_machfile and load_segment refuse, how cs_validate_csblob reads a signature, what AMFI’s entitlements validator takes, and what the kernel does with a thread state under Rosetta or a segment both writable and executable.

We keep their structure and names, so the two can be read side by side, and every rule taken from them is checked against the tool that enforces it. Thanks to Hex-Rays for IDA Pro, and for the MCP server that turned those reads into a conversation.

History

The original fashion was a workaround for repetitive loading of the Perl command shasum.

The name is a mashup of my original bash function fsha and the David Bowie song, more specifically the duet with Frank Black of the Pixies. As for the thinr command, the name comes from the Nightmares On Wax track, Finer, remixed by Alex Gopher.

Python made it very easy thanks to the built-in hashlib, io, and the python-ssdeep or tlsh / py-tlsh modules were straightforward. But uv started complaining about pkg_resources, and I needed features Python couldn’t give me without significant effort.

So here we are: Swift, native, concurrent, with C and C++ dependencies bridged as submodules, and zero external runtime requirements.

License

fashion is licensed under the GNU General Public License version 3.

The binary includes third-party code, and NOTICE carries its copyright notices and license terms:

  • ssdeep, GPL-2.0-or-later, used under GPL-3.0;
  • TLSH, under its BSD-3-Clause license;
  • UPX’s Mach-O unpacker, translated to Swift, and its UCL decompressor, GPL-2.0-or-later, used under GPL-3.0;
  • the LZMA SDK decoder UPX uses, LZMA, placed in the public domain by Igor Pavlov;
  • swift-argument-parser, Apache-2.0 with the Runtime Library Exception.

The ssdeep, TLSH, UCL and LZMA SDK sources are in the repository’s submodules, with their license files, at the commits a release tag pins.