The Update Framework (TUF) security model over COSE_Sign1/CBOR
0

Configure Feed

Select the types of activity you want to include in your feed.

tuf: add TUF threat-model / CVE attack-vector tests

Cover the named TUF attacks and a real CVE against the verification logic:

- CVE-2021-41131 (go-tuf): a threshold must be met by distinct keys -- a
role listing the same key id twice, and repeated signatures from one key,
must not satisfy a 2-of-2 threshold.
- indefinite freeze: a validly signed but expired snapshot is rejected.
- endless data: the launch check pins the target length.
- cross-role signature replay: a document signed by one role's key is not
accepted for another role (both via the COSE path and against a real
python-tuf snapshot offered where a timestamp is expected).
- delegation containment: a delegated role cannot authorize a path outside
its delegated paths.

+174
+13
test/interop/python-tuf/test.ml
··· 53 53 | Error _ -> () 54 54 | Ok _ -> Alcotest.fail "a tampered timestamp must not verify" 55 55 56 + (* Cross-file replay: a real, correctly signed python-tuf snapshot offered where 57 + a timestamp is expected is rejected -- the timestamp role's key did not sign 58 + the snapshot document, so a valid signature for one role cannot stand in for 59 + another. *) 60 + let test_cross_file_replay () = 61 + let pinned = ok (Tuf_json.Codec.pin_root (read "traces/root.json")) in 62 + let t = Client.of_root pinned in 63 + match Client.update_timestamp ~now t (read "traces/snapshot.json") with 64 + | Error _ -> () 65 + | Ok _ -> Alcotest.fail "a snapshot must not be accepted as a timestamp" 66 + 56 67 let () = 57 68 Alcotest.run "tuf-interop-python-tuf" 58 69 [ ··· 61 72 Alcotest.test_case "verify metadata chain" `Quick test_chain; 62 73 Alcotest.test_case "tampered metadata rejected" `Quick 63 74 test_tamper_rejected; 75 + Alcotest.test_case "cross-file replay rejected" `Quick 76 + test_cross_file_replay; 64 77 ] ); 65 78 ]
+161
test/test_client.ml
··· 202 202 "unrelated path not delegated" true 203 203 (ok_client (Client.lookup ~resolve ~now t "images/other/app") = None) 204 204 205 + (* --- TUF threat model / CVE attack vectors --- *) 206 + 207 + let set_role name role (root : Meta.root) = 208 + { root with roles = (name, role) :: List.remove_assoc name root.roles } 209 + 210 + let load_to_snapshot f = 211 + let t = Client.of_root f.root in 212 + let t = 213 + ok_client (Client.update_timestamp ~now t (env_timestamp f f.timestamp)) 214 + in 215 + ok_client (Client.update_snapshot ~now t (env_snapshot f f.snapshot)) 216 + 217 + (* CVE-2021-41131 (go-tuf): a threshold must be met by that many *distinct* 218 + keys. A role listing the same key id twice must not let one signature satisfy 219 + a threshold of two. *) 220 + let test_cve_2021_41131_distinct_keys () = 221 + let f = fixture () in 222 + let gkey = List.nth f.root.keys 3 in 223 + let root = 224 + set_role "targets" 225 + { Meta.keyids = [ Meta.keyid gkey; Meta.keyid gkey ]; threshold = 2 } 226 + f.root 227 + in 228 + let t = load_to_snapshot { f with root } in 229 + match Client.update_targets ~now t (env_targets f f.targets) with 230 + | Error (Client.Below_threshold { have = 1; need = 2; _ }) -> () 231 + | _ -> 232 + Alcotest.fail 233 + "the same key listed twice must not satisfy a 2-of-2 threshold" 234 + 235 + (* Closely related: repeating one key's signature must not inflate the count 236 + toward a threshold of two distinct keys. *) 237 + let test_duplicate_signatures () = 238 + let f = fixture () in 239 + let pa, ka = gen () in 240 + let _pb, kb = gen () in 241 + let root = 242 + { 243 + (set_role "targets" 244 + { Meta.keyids = [ Meta.keyid ka; Meta.keyid kb ]; threshold = 2 } 245 + f.root) 246 + with 247 + keys = ka :: kb :: f.root.keys; 248 + } 249 + in 250 + let t = load_to_snapshot { f with root } in 251 + (* two signatures, both from key A *) 252 + let env = sign [ pa; pa ] (Codec.value_of_targets f.targets) in 253 + match Client.update_targets ~now t env with 254 + | Error (Client.Below_threshold { have = 1; need = 2; _ }) -> () 255 + | _ -> 256 + Alcotest.fail 257 + "two signatures from one key must not meet a 2-of-2 threshold" 258 + 259 + (* Indefinite freeze attack: a validly signed but expired snapshot is rejected, 260 + so a stale snapshot cannot be served forever. *) 261 + let test_freeze_expired_snapshot () = 262 + let f = fixture () in 263 + let t = Client.of_root f.root in 264 + let t = 265 + ok_client (Client.update_timestamp ~now t (env_timestamp f f.timestamp)) 266 + in 267 + let stale = { f.snapshot with expires = past } in 268 + match Client.update_snapshot ~now t (env_snapshot f stale) with 269 + | Error (Client.Expired "snapshot") -> () 270 + | _ -> Alcotest.fail "an expired snapshot must be rejected (freeze attack)" 271 + 272 + (* Endless-data attack: the launch check pins the length, so bytes longer (or 273 + shorter) than the signed target are rejected even with a matching hash. *) 274 + let test_endless_data_length () = 275 + let f = fixture () in 276 + Alcotest.(check bool) 277 + "right hash but wrong length is rejected" false 278 + (Client.matches f.target 279 + ~length:(String.length f.blob + 1) 280 + ~sha256_hex:f.digest) 281 + 282 + (* Cross-role signature replay: a document signed by one role's key must not be 283 + accepted for another role. A timestamp (signed by the timestamp key) offered 284 + where a snapshot is expected fails the snapshot role's check. *) 285 + let test_cross_role_replay () = 286 + let f = fixture () in 287 + let t = Client.of_root f.root in 288 + let t = 289 + ok_client (Client.update_timestamp ~now t (env_timestamp f f.timestamp)) 290 + in 291 + match Client.update_snapshot ~now t (env_timestamp f f.timestamp) with 292 + | Error _ -> () 293 + | Ok _ -> 294 + Alcotest.fail "a timestamp signature must not be accepted as a snapshot" 295 + 296 + (* Delegation containment: a delegated role's authority is bounded by its 297 + delegated paths. A target it lists outside those paths is never honored. *) 298 + let test_delegation_containment () = 299 + let f = fixture () in 300 + let vpriv, vkey = gen () in 301 + let deleg = 302 + { 303 + Meta.name = "vendor"; 304 + keyids = [ Meta.keyid vkey ]; 305 + threshold = 1; 306 + paths = [ "images/vendor/*" ]; 307 + terminating = false; 308 + } 309 + in 310 + let top = 311 + { 312 + f.targets with 313 + targets = []; 314 + delegations = Some { Meta.keys = [ vkey ]; roles = [ deleg ] }; 315 + } 316 + in 317 + let digest = Digestif.SHA256.(to_hex (digest_string "evil")) in 318 + let evil = 319 + { 320 + Meta.path = "evil/path"; 321 + length = 4; 322 + hashes = [ { Meta.algorithm = "sha256"; digest } ]; 323 + } 324 + in 325 + let vtargets = 326 + { 327 + Meta.version = 2; 328 + expires = future; 329 + targets = [ evil ]; 330 + delegations = None; 331 + } 332 + in 333 + let snapshot = 334 + { 335 + f.snapshot with 336 + meta = 337 + [ 338 + ("targets", { Meta.version = 5; meta = None }); 339 + ("vendor", { Meta.version = 2; meta = None }); 340 + ]; 341 + } 342 + in 343 + let f = { f with targets = top; snapshot } in 344 + let t = loaded f in 345 + let resolve name = 346 + if String.equal name "vendor" then 347 + Some (sign [ vpriv ] (Codec.value_of_targets vtargets)) 348 + else None 349 + in 350 + Alcotest.(check bool) 351 + "a delegated role cannot authorize a path outside its delegation" true 352 + (ok_client (Client.lookup ~resolve ~now t "evil/path") = None) 353 + 205 354 let suite = 206 355 ( "client", 207 356 [ ··· 225 374 Alcotest.test_case "delegation wrong key" `Quick test_delegation_wrong_key; 226 375 Alcotest.test_case "delegation path not matched" `Quick 227 376 test_delegation_path_not_matched; 377 + Alcotest.test_case "CVE-2021-41131 distinct-key threshold" `Quick 378 + test_cve_2021_41131_distinct_keys; 379 + Alcotest.test_case "duplicate signatures not counted" `Quick 380 + test_duplicate_signatures; 381 + Alcotest.test_case "freeze: expired snapshot rejected" `Quick 382 + test_freeze_expired_snapshot; 383 + Alcotest.test_case "endless-data length pin" `Quick 384 + test_endless_data_length; 385 + Alcotest.test_case "cross-role signature replay" `Quick 386 + test_cross_role_replay; 387 + Alcotest.test_case "delegation containment" `Quick 388 + test_delegation_containment; 228 389 ] )