Raw JSON
{
"$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json",
"document": {
"category": "csaf_security_advisory",
"csaf_version": "2.1",
"distribution": {
"tlp": {
"label": "CLEAR"
}
},
"lang": "en",
"notes": [
{
"category": "summary",
"text": "The encryption-at-rest cipher held exactly one key and the enc:v1:/ENC1 envelope records no key identity, so there was no supported way to rotate the key. An operator who pointed --encryption-key-file at a new key turned every previously sealed annotation body and image asset into unreadable data, and did so silently: an unopenable value is returned verbatim, which is byte-for-byte indistinguishable from a note that was always stored as plaintext.",
"title": "Summary"
},
{
"category": "details",
"text": "1.6.54 makes rotation survivable. Cipher now holds a sealing key plus any number of retired, opening-only keys (Cipher::from_keys), exposed as the new --retired-encryption-key-file flag (env GSV_RETIRED_ENCRYPTION_KEY_FILE). Opening tries each held key in turn, primary first; Poly1305 rejects a wrong key with overwhelming probability, so no key identifier had to be added to the envelope and the stored format is byte-identical -- rotation runs no migration and cannot corrupt an existing store. A malformed retired key file aborts start-up rather than starting up half-readable, and the error names which key failed. Pinned by rotating_the_key_keeps_values_sealed_by_the_retired_key_readable, every_generation_of_retired_key_is_tried_not_only_the_first (an implementation consulting only the first retired entry passes the former and fails the latter), from_keys_with_no_retired_keys_matches_from_key, and the CLI wiring test load_cipher_wires_the_retired_key_for_reading_and_the_new_key_for_writing, which would catch the two keys being passed in the wrong order -- a mistake that compiles and lints cleanly but would seal new data under the key just retired. SCOPE, STATED PLAINLY: this makes rotation survivable, not revoking. A retired key can still read until its data is re-sealed, so rotation alone does not contain a compromised key; the operator must re-save the affected annotations and then restart without the retired-key flag. A re-encryption sweep remains outstanding and is tracked as the reduced C-D1 residual. One new side channel is accepted: a value sealed under a later-listed key takes marginally longer to open, revealing roughly which generation it belongs to. That is not secret, so it does not justify constant-time trial decryption.",
"title": "Remediation"
},
{
"category": "general",
"text": "Upgrade to 1.6.54. No action is required for a store that has never rotated, and no migration runs on upgrade. To rotate: pass the new key as --encryption-key-file and the previous one as --retired-encryption-key-file, verify a pre-rotation annotation still opens, then re-save the affected annotations and restart without the retired flag to put the old key beyond use. Operators who attempted a rotation on 1.6.53 or earlier and still hold the original key can recover by pointing --encryption-key-file back at it; the ciphertext was never altered.",
"title": "Recommended Action"
},
{
"category": "general",
"text": "Discovered by ndaal's own OWASP ASVS v5.0.0 audit of grundschutz-oscal-viewer on 2026-08-13, which produced the product's first STRIDE threat model; this defect is recorded there as C-D1. No external report, CVE or RUSTSEC advisory exists for it.",
"title": "Discovery"
},
{
"category": "general",
"text": "grundschutz-oscal-viewer is a secure, memory-safe, single-binary offline viewer for the BSI Grundschutz++ OSCAL catalog — embedded catalog data, filters and full-text search served over a TLS 1.3 (rustls) HTTP/1.1 + HTTP/2 listener, with an annotations workspace offering optional XChaCha20-Poly1305 encryption-at-rest and embedded SQLite storage.",
"title": "Product Description"
},
{
"category": "legal_disclaimer",
"text": "THIS DOCUMENT IS PROVIDED ON AN 'AS IS' BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY. ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS INFORMATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.",
"title": "Legal Disclaimer"
}
],
"publisher": {
"category": "vendor",
"contact_details": "security@ndaal.eu",
"issuing_authority": "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG Security Team",
"name": "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG",
"namespace": "https://ndaal.eu/csaf"
},
"references": [
{
"category": "self",
"summary": "This advisory in CSAF 2.1 format",
"url": "https://gitlab.com/vPierre/ndaal_public_csaf_information/-/raw/main/csaf/2026/417/ndaal-sa-2026-417.json"
},
{
"category": "external",
"summary": "grundschutz-oscal-viewer source code",
"url": "https://gitlab.com/vPierre/ndaal_public_bsi_grundschutz_oscal_viewer"
},
{
"category": "external",
"summary": "grundschutz-oscal-viewer on crates.io",
"url": "https://crates.io/crates/grundschutz-oscal-viewer"
}
],
"title": "ndaal Security Advisory: Encryption key rotation was impossible and attempting it silently orphaned every sealed value (grundschutz-oscal-viewer < 1.6.54)",
"tracking": {
"current_release_date": "2026-08-13T12:00:00.000Z",
"generator": {
"engine": {
"name": "ndaal CSAF Generator",
"version": "1.0.0"
}
},
"id": "ndaal-sa-2026-417",
"initial_release_date": "2026-08-13T12:00:00.000Z",
"revision_history": [
{
"date": "2026-08-13T12:00:00.000Z",
"number": "1.0.0",
"summary": "Initial advisory: C-D1 remediated in grundschutz-oscal-viewer 1.6.54."
}
],
"status": "final",
"version": "1.0.0"
}
},
"product_tree": {
"branches": [
{
"category": "vendor",
"name": "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG",
"branches": [
{
"category": "product_name",
"name": "grundschutz-oscal-viewer",
"branches": [
{
"category": "product_version",
"name": "1.6.53",
"product": {
"name": "ndaal Grundschutz++ OSCAL Viewer 1.6.53",
"product_id": "CSAFPID-0001"
}
},
{
"category": "product_version",
"name": "1.6.54",
"product": {
"name": "ndaal Grundschutz++ OSCAL Viewer 1.6.54",
"product_id": "CSAFPID-0002"
}
}
]
}
]
}
]
},
"vulnerabilities": [
{
"ids": [
{
"system_name": "ndaal Grundschutz++ OSCAL Viewer threat model",
"text": "C-D1"
}
],
"notes": [
{
"category": "description",
"text": "Cipher (src/crypto.rs) wrapped a single XChaCha20-Poly1305 key. open_text/open_blob attempt one decryption and, on failure, fall back to returning the stored value unchanged -- the deliberate design that lets plaintext and sealed rows coexist without a migration. With one key that fallback is benign; the moment the key changes it becomes a silent data-loss path, because the fallback cannot distinguish 'sealed with a key I do not hold' from 'never sealed at all'. No error is raised, no log line is emitted, and the UI renders the base64 ciphertext as if it were the note's text. The security consequence is key lifecycle: because rotation was not possible, an operator who suspected the data key had leaked had no way to retire it -- the compromised key remained the only key that could read the store, indefinitely. There is no attacker in this issue and no remote trigger: it is reached by a legitimate administrative action on the host, which is why the vector is AV:L/PR:H with no user interaction and the impact is availability-only. Confidentiality and integrity are unaffected -- the ciphertext is intact and remains readable with the original key. Mapped to CWE-392 because the observable defect is the unreported failure; the underlying gap is key management (CWE-320 is the matching category, which CWE guidance does not permit as a mapping target). Threat model C-D1, which the project itself rated medium for Confidential-tier stores.",
"title": "Description"
},
{
"category": "details",
"text": "1.6.54 makes rotation survivable. Cipher now holds a sealing key plus any number of retired, opening-only keys (Cipher::from_keys), exposed as the new --retired-encryption-key-file flag (env GSV_RETIRED_ENCRYPTION_KEY_FILE). Opening tries each held key in turn, primary first; Poly1305 rejects a wrong key with overwhelming probability, so no key identifier had to be added to the envelope and the stored format is byte-identical -- rotation runs no migration and cannot corrupt an existing store. A malformed retired key file aborts start-up rather than starting up half-readable, and the error names which key failed. Pinned by rotating_the_key_keeps_values_sealed_by_the_retired_key_readable, every_generation_of_retired_key_is_tried_not_only_the_first (an implementation consulting only the first retired entry passes the former and fails the latter), from_keys_with_no_retired_keys_matches_from_key, and the CLI wiring test load_cipher_wires_the_retired_key_for_reading_and_the_new_key_for_writing, which would catch the two keys being passed in the wrong order -- a mistake that compiles and lints cleanly but would seal new data under the key just retired. SCOPE, STATED PLAINLY: this makes rotation survivable, not revoking. A retired key can still read until its data is re-sealed, so rotation alone does not contain a compromised key; the operator must re-save the affected annotations and then restart without the retired-key flag. A re-encryption sweep remains outstanding and is tracked as the reduced C-D1 residual. One new side channel is accepted: a value sealed under a later-listed key takes marginally longer to open, revealing roughly which generation it belongs to. That is not secret, so it does not justify constant-time trial decryption. CVSS v3.1 base score 4.4 (MEDIUM), vector CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H. CVSS v4.0 base score 6.7 (MEDIUM), vector CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N.",
"title": "Remediation Detail"
}
],
"product_status": {
"known_affected": [
"CSAFPID-0001"
],
"fixed": [
"CSAFPID-0002"
]
},
"remediations": [
{
"category": "vendor_fix",
"details": "Upgrade to 1.6.54. No action is required for a store that has never rotated, and no migration runs on upgrade. To rotate: pass the new key as --encryption-key-file and the previous one as --retired-encryption-key-file, verify a pre-rotation annotation still opens, then re-save the affected annotations and restart without the retired flag to put the old key beyond use. Operators who attempted a rotation on 1.6.53 or earlier and still hold the original key can recover by pointing --encryption-key-file back at it; the ciphertext was never altered.",
"product_ids": [
"CSAFPID-0002"
]
}
],
"metrics": [
{
"content": {
"cvss_v3": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H",
"baseScore": 4.4,
"baseSeverity": "MEDIUM",
"attackVector": "LOCAL",
"attackComplexity": "LOW",
"privilegesRequired": "HIGH",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"availabilityImpact": "HIGH"
},
"cvss_v4": {
"version": "4.0",
"vectorString": "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"baseScore": 6.7,
"baseSeverity": "MEDIUM",
"attackVector": "LOCAL",
"attackComplexity": "LOW",
"attackRequirements": "NONE",
"privilegesRequired": "HIGH",
"userInteraction": "NONE",
"vulnConfidentialityImpact": "NONE",
"vulnIntegrityImpact": "NONE",
"vulnAvailabilityImpact": "HIGH",
"subConfidentialityImpact": "NONE",
"subIntegrityImpact": "NONE",
"subAvailabilityImpact": "NONE"
}
},
"products": [
"CSAFPID-0001"
]
}
],
"threats": [
{
"category": "impact",
"details": "Cipher (src/crypto.rs) wrapped a single XChaCha20-Poly1305 key. open_text/open_blob attempt one decryption and, on failure, fall back to returning the stored value unchanged -- the deliberate design that lets plaintext and sealed rows coexist without a migration. With one key that fallback is benign; the moment the key changes it becomes a silent data-loss path, because the fallback cannot distinguish 'sealed with a key I do not hold' from 'never sealed at all'. No error is raised, no log line is emitted, and the UI renders the base64 ciphertext as if it were the note's text. The security consequence is key lifecycle: because rotation was not possible, an operator who suspected the data key had leaked had no way to retire it -- the compromised key remained the only key that could read the store, indefinitely. There is no attacker in this issue and no remote trigger: it is reached by a legitimate administrative action on the host, which is why the vector is AV:L/PR:H with no user interaction and the impact is availability-only. Confidentiality and integrity are unaffected -- the ciphertext is intact and remains readable with the original key. Mapped to CWE-392 because the observable defect is the unreported failure; the underlying gap is key management (CWE-320 is the matching category, which CWE guidance does not permit as a mapping target). Threat model C-D1, which the project itself rated medium for Confidential-tier stores.",
"product_ids": [
"CSAFPID-0001"
]
}
],
"title": "Encryption key rotation was impossible and attempting it silently orphaned every sealed value",
"references": [
{
"category": "external",
"summary": "Threat model entry",
"url": "https://gitlab.com/vPierre/ndaal_public_bsi_grundschutz_oscal_viewer/-/blob/main/documentation/threat-model/stride.md"
}
]
}
]
}