nx_printer_security.nx source
↩ module page · 148 lines · 7112 B
1// nx_printer_security.nx -- honest printer security posture auditor (the #1 measurable exceed axis).
2//
3// WHY: the worst real-world printing failure isn't paper jams -- it's security. Sept 2024 cups-browsed
4// (CVE-2024-47176, CVSS 9.9): bind UDP *:631, trust any packet, fetch attacker IPP, install malicious PPD,
5// shell-out via foomatic -> unauthenticated RCE; 75k+ exposed, 42k unauthenticated. IPP also defaults to
6// CLEARTEXT on 631 (no TLS) and printers frequently ship with NO authentication -- so anyone on the LAN
7// can read your documents and submit jobs. NO vendor UI tells you this. This organ does.
8//
9// It reads the printer's own IPP-reported security companions to printer-uri-supported (RFC 8011
10// ยง5.4.2/5.4.3, verified in knowledge/fetched/ipp_rfc8011.raw):
11// uri-security-supported (1setOf keyword) values incl 'none','tls','ssl3'
12// uri-authentication-supported (1setOf keyword) values incl 'none','requesting-user-name','basic',
13// 'digest','certificate','negotiate'
14// and classifies the posture into a sealed verdict + a findings bitmask.
15//
16// NEVER-BRICK (#26): pure read-only model; no syscalls, no writes. We do NOT auto-install PPDs and NEVER
17// shell out -- so this whole stack is structurally immune to the cups-browsed RCE class by construction.
18// Sovereign: imports only nx_ipp_codec.nx; nx_cc -> nxasm.
19// genealogy_id: project-printer-management-ipp-sclass-2026-06-20 ; license_tier: ORIGINAL
20
21import "nx_ipp_codec.nx"
22
23// ---- sealed verdict ----
24const NX_PSEC_SECURE: i64 = 1 // TLS available AND real auth AND no cleartext/no-auth offered
25const NX_PSEC_WEAK: i64 = 2 // partial: downgrade possible / ssl3 / auth-but-also-none, etc.
26const NX_PSEC_EXPOSED: i64 = 3 // cleartext + no auth, no TLS -> anyone reads & submits (the Brother)
27const NX_PSEC_UNKNOWN: i64 = 4 // printer reported no security attributes
28const NX_PSEC_VERDICT_N: i64 = 5
29
30// ---- findings bitmask ----
31const PSEC_F_CLEARTEXT: i64 = 1
32const PSEC_F_NOAUTH: i64 = 2
33const PSEC_F_TLS: i64 = 4
34const PSEC_F_SSL3: i64 = 8
35const PSEC_F_AUTH: i64 = 16
36
37func nx_psec_verdict_is_valid(v: i64) -> i64 {
38 if v <= 0 { return 0 }
39 if v >= NX_PSEC_VERDICT_N { return 0 }
40 return 1
41}
42
43// 1 if the 1setOf keyword attribute `name` contains the value `target`. Walks the named attribute and all
44// its additional-values (name-length 0). Bounded + bounds-checked; malformed -> 0 (treated as "not advertised").
45func nx_psec_set_contains(body: *u8, n: i64, name: *u8, target: *u8) -> i64 {
46 if n < 8 { return 0 }
47 let nm_len: i64 = nx_ipp_strlen(name)
48 let tlen: i64 = nx_ipp_strlen(target)
49 var off: i64 = 8
50 var in_set: i64 = 0
51 var found: i64 = 0
52 var keep: i64 = 1
53 while keep == 1 {
54 if off >= n { keep = 0 }
55 else {
56 let tag: i64 = body[off] as i64
57 if tag == NX_IPP_TAG_END { keep = 0 }
58 else {
59 if tag <= 0x0f { off = off + 1; in_set = 0 }
60 else {
61 if (off + 3) > n { keep = 0 }
62 else {
63 let nlen: i64 = nx_ipp_get_u16(body, off + 1)
64 let name_off: i64 = off + 3
65 if (name_off + nlen + 2) > n { keep = 0 }
66 else {
67 let vlen_off: i64 = name_off + nlen
68 let vlen: i64 = nx_ipp_get_u16(body, vlen_off)
69 let val_off: i64 = vlen_off + 2
70 if (val_off + vlen) > n { keep = 0 }
71 else {
72 if nlen > 0 {
73 var nm_match: i64 = 0
74 if nlen == nm_len {
75 if nx_ipp_name_eq(body, name_off, name, nlen) == 1 { nm_match = 1 }
76 }
77 in_set = nm_match
78 }
79 if in_set == 1 {
80 if vlen == tlen {
81 if nx_ipp_name_eq(body, val_off, target, tlen) == 1 { found = 1 }
82 }
83 }
84 off = val_off + vlen
85 }
86 }
87 }
88 }
89 }
90 }
91 }
92 return found
93}
94
95// 1 if the attribute `name` is present at all.
96func nx_psec_has(body: *u8, n: i64, name: *u8, scratch3: *i64) -> i64 {
97 let p_tag: *i64 = scratch3
98 let p_voff: *i64 = ((scratch3 as i64) + 8) as *i64
99 let p_vlen: *i64 = ((scratch3 as i64) + 16) as *i64
100 if nx_ipp_find(body, n, name, p_tag, p_voff, p_vlen) == NX_IPP_OK { return 1 }
101 return 0
102}
103
104// Classify the printer's security posture. Writes the findings bitmask via out_flags. scratch3 = i64[3].
105func nx_printer_security(body: *u8, n: i64, scratch3: *i64, out_flags: *i64) -> i64 {
106 out_flags[0] = 0
107 let sec_present: i64 = nx_psec_has(body, n, "uri-security-supported", scratch3)
108 let auth_present: i64 = nx_psec_has(body, n, "uri-authentication-supported", scratch3)
109 if sec_present == 0 {
110 if auth_present == 0 { return NX_PSEC_UNKNOWN }
111 }
112
113 let has_tls: i64 = nx_psec_set_contains(body, n, "uri-security-supported", "tls")
114 let has_ssl3: i64 = nx_psec_set_contains(body, n, "uri-security-supported", "ssl3")
115 let has_clear: i64 = nx_psec_set_contains(body, n, "uri-security-supported", "none")
116 let has_noauth: i64 = nx_psec_set_contains(body, n, "uri-authentication-supported", "none")
117
118 var has_auth: i64 = 0
119 if nx_psec_set_contains(body, n, "uri-authentication-supported", "digest") == 1 { has_auth = 1 }
120 if nx_psec_set_contains(body, n, "uri-authentication-supported", "basic") == 1 { has_auth = 1 }
121 if nx_psec_set_contains(body, n, "uri-authentication-supported", "certificate") == 1 { has_auth = 1 }
122 if nx_psec_set_contains(body, n, "uri-authentication-supported", "negotiate") == 1 { has_auth = 1 }
123 if nx_psec_set_contains(body, n, "uri-authentication-supported", "requesting-user-name") == 1 { has_auth = 1 }
124
125 var flags: i64 = 0
126 if has_clear == 1 { flags = flags | PSEC_F_CLEARTEXT }
127 if has_noauth == 1 { flags = flags | PSEC_F_NOAUTH }
128 if has_tls == 1 { flags = flags | PSEC_F_TLS }
129 if has_ssl3 == 1 { flags = flags | PSEC_F_SSL3 }
130 if has_auth == 1 { flags = flags | PSEC_F_AUTH }
131 out_flags[0] = flags
132
133 // SECURE: real TLS + real auth, and the printer is NOT also offering an open downgrade path.
134 if has_tls == 1 {
135 if has_auth == 1 {
136 if has_clear == 0 {
137 if has_noauth == 0 { return NX_PSEC_SECURE }
138 }
139 }
140 }
141 // EXPOSED: no TLS at all, cleartext, and no authentication -> wide open.
142 if has_tls == 0 {
143 if has_clear == 1 {
144 if has_noauth == 1 { return NX_PSEC_EXPOSED }
145 }
146 }
147 return NX_PSEC_WEAK
148}