Security

Supported version: 0.7.x. For the formal security policy and vulnerability reporting, see SECURITY.md.

Threat model

licit operates as a local auditing tool. Its attack surface is limited, but there are risks to consider:

Identified threats

ThreatSeverityMitigation
Provenance store manipulationHighHMAC-SHA256 signing, Merkle tree integrity
Signing key exposureHigh.licit/.signing-key in .gitignore, 600 permissions
Sensitive data in FRIAMedium.gitignore for fria-data.json, do not push to public repos
Contributor data in provenanceMedium.gitignore for provenance.jsonl
Injection via malicious YAMLLowExclusive use of yaml.safe_load() (not yaml.load())
Malicious SARIF/JSONLowjson.loads only, isinstance on all fields
Compromised dependenciesMediumPeriodic auditing, minimum version pinning
Code execution via configsLowConfig content is never executed; only data is parsed
Git command injectionLowsubprocess.run with list (no shell=True), timeouts

What licit does NOT do


Cryptographic signing (provenance)

HMAC-SHA256

When provenance signing is enabled (provenance.sign: true), each record is signed with HMAC-SHA256:

signature = HMAC-SHA256(key, canonical_json(record))

Configuration:

provenance:
  sign: true
  # Optional: external key path (defaults to .licit/.signing-key in the project)
  sign_key_path: ~/.licit/signing-key

Manual key generation:

# Generate a 256-bit key
python3.12 -c "import secrets; print(secrets.token_hex(32))" > ~/.licit/signing-key
chmod 600 ~/.licit/signing-key

Note: If sign_key_path is not specified, licit auto-generates a key at .licit/.signing-key within the project. If you prefer to keep the key outside the project, use sign_key_path with an external path.

Attestation (Merkle tree)

licit implements a Merkle tree over provenance records to detect tampering:

         root_hash
        /         \
    hash_01      hash_23
    /    \       /    \
 hash_0 hash_1 hash_2 hash_3
   |      |      |      |
 rec_0  rec_1  rec_2  rec_3

Any modification of a record invalidates the hash chain from that record up to the root.

Implementation:

from licit.provenance.attestation import ProvenanceAttestor

attestor = ProvenanceAttestor()  # Auto-generates key at .licit/.signing-key

# Sign an individual record
sig = attestor.sign_record({"file": "app.py", "source": "ai"})

# Verify integrity
assert attestor.verify_record({"file": "app.py", "source": "ai"}, sig)

# Sign batch with Merkle tree
root = attestor.sign_batch([record1, record2, record3])

Key management

The signing key is resolved in this order:

  1. Explicit path (sign_key_path in config)
  2. Local fallback (.licit/.signing-key in the project)
  3. Auto-generation (32 random bytes with os.urandom(32))

All filesystem accesses are protected with try/except OSError.


Data protection

Sensitive data generated by licit

FileSensitivityRecommendation
.licit.yamlLowCommit to repo
.licit/provenance.jsonlMediumDo not commit (contains contributor info)
.licit/fria-data.jsonHighDo not commit (rights impact data)
.licit/fria-report.mdMediumSelective commit
.licit/annex-iv.mdLowCommit to repo
.licit/changelog.mdLowCommit to repo
.licit/reports/*LowCommit to repo
Signing keyCriticalNever commit, 600 permissions
# licit — sensitive data
.licit/provenance.jsonl
.licit/fria-data.json

# licit — signing key (if stored in the project)
.licit/.signing-key
*.key

# licit — generated reports (optional, can be committed)
# .licit/reports/

Dependencies

Dependency audit

licit uses 6 runtime dependencies, all widely adopted:

DependencyMin. versionPurposeMaintainer
click8.1+CLI frameworkPallets
pydantic2.0+Config validationSamuel Colvin
structlog24.1+Structured loggingHynek Schlawack
pyyaml6.0+YAML parsingYAML org
jinja23.1+Report templatesPallets
cryptography42.0+HMAC-SHA256PyCA

Recommendations

  1. Pin versions in production: Use a requirements.txt or pip-compile to lock exact versions.

  2. Audit regularly:

    pip audit                    # Scan for known vulnerabilities
    pip install pip-audit && pip-audit  # Alternative
  3. Verify hashes:

    pip install --require-hashes -r requirements.txt

Safe file parsing

YAML

licit always uses yaml.safe_load() to parse YAML. Never yaml.load() (which allows arbitrary Python code execution).

# Correct (what licit does)
data = yaml.safe_load(f.read())

# NEVER (vulnerable to code execution)
# data = yaml.load(f.read(), Loader=yaml.FullLoader)

JSON

For SARIF and other JSON files, standard json.load() is used, which is safe by design.

Agent configuration files

Files like CLAUDE.md, .cursorrules, AGENTS.md are read as plain text. licit does not interpret or execute their content — it only analyzes them to detect changes and extract metadata.


External process execution

licit executes git commands via subprocess.run() with the following protections:

# How licit executes git commands
result = subprocess.run(
    ["git", "rev-list", "--count", "HEAD"],
    capture_output=True,
    text=True,
)

Vulnerability reporting

If you find a security vulnerability in licit:

  1. Do not open a public issue.
  2. Send an email to security@licit.dev (or open a private advisory on GitHub) with:
    • Description of the vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if you have one)
  3. You will receive confirmation within 48 hours.
  4. A fix will be published within a maximum of 7 days for critical issues.

See SECURITY.md in the project root for the complete policy.