(* SPDX-License-Identifier: LGPL-3.0-only WITH OCaml-LGPL-linking-exception *) module U64 = Stdint.Uint64 module U32 = Stdint.Uint32 type sid = { sid_ident_auth : U64.t (* 6 B *) ; sid_sub_auths : U32.t array (* max. 15 × *) } and sub_auths = U32.t array let sizeof_ident_auth = 6 let max_ident_auth = U64.of_string "0x0000_ffff_ffff_ffff" let ident_auth_hexlen = 14 (* 0x, 12 × hex digit *) let ident_auth_hexmin = U64.of_string "0x0001_0000_0000" let sizeof_sub_auth = 4 let max_subauth_count = 15 let create_unsafe ia sa = { sid_ident_auth = ia ; sid_sub_auths = sa } (* There isn’t much to validate to begin with except for the hard cap on the number of subauths. *) let create ia sa = let nsa = Array.length sa in if nsa < 1 || max_subauth_count < nsa then None else if U64.compare ia max_ident_auth > 0 then None else Some (create_unsafe ia sa) let get_ident_auth s = s.sid_ident_auth let get_sub_auths s = s.sid_sub_auths exception Nope let equal_sub_auths saa sab = try Array.iter2 (fun saa sab -> if U32.compare saa sab <> 0 then raise Nope) saa sab; true with Nope -> false let equal a b = U64.compare a.sid_ident_auth b.sid_ident_auth = 0 && Array.length a.sid_sub_auths = Array.length b.sid_sub_auths && equal_sub_auths a.sid_sub_auths b.sid_sub_auths module Util = struct let expect_char s c p = if s.[p] = c then () else raise (Invalid_argument (Printf.sprintf "Invalid SID [%s]: expected ‘%c’ at position %d, found ‘%c’" s c p s.[p])) let is_digit c = '0' <= c && c <= '9' let is_xdigit c = '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' end module type Conformance = sig val name : string val parse_hex_ia : string -> int -> U64.t * int (** [parse_hex_ia raw p] read ia from [raw] at position [p] in hexadecimal, return the value and the next position in the string. This only enforces parsing behavior, not the value range. *) val valid_decimal : string -> bool (** [valid_decimal s] holds if [s] conforms to the constraints for decimal numbers. Assumes the input already is all decimal digits. *) val valid_ia : bool -> U64.t -> bool (** [valid_ia ia] holds if [ia] is a valid identifier authority. *) val valid_sas : U32.t array -> bool (** [valid_sas sas] holds if [sas] is a valid array of subauthorities. *) val pad_hex_ia : int -> int (** [pad n] return the number of padding zeros to prepend to the hex format ident auth. *) end (* Validate and convert as though the spec in [MS-DTYP] were normative. *) module Con_strict = struct let name = "strict" let parse_hex_ia s p = if p > String.length s - ident_auth_hexlen then raise (Invalid_argument (Printf.sprintf "Invalid SID [%s]: conformant string encoding mandates \ exactly 12 hex digits" s)) else let e = p + ident_auth_hexlen - 1 in let b = Bytes.make ident_auth_hexlen '\x2a' in Bytes.set b 0 '0'; (* hex indicator for U64.of_string *) Bytes.set b 1 'x'; for i = p + 2 to e do (* assumption: prefix “0x” checked by caller *) if not (Util.is_xdigit s.[i]) then raise (Invalid_argument (Printf.sprintf "Invalid SID [%s]: expected hexadecimal digit at \ position %d while parsing ident auth, got ‘%c’" s i s.[i])) done; Bytes.blit_string s (p+2) b 2 12; U64.of_string (Bytes.unsafe_to_string b), p + ident_auth_hexlen (* leading zeros forbidden except for zero itself *) let valid_decimal d = String.length d > 0 && (String.length d = 1 || String.get d 0 <> '0') let valid_ia hex v = if U64.compare v max_ident_auth > 0 then false else if not hex && U64.compare v ident_auth_hexmin >= 0 then false else not (hex && U64.compare v ident_auth_hexmin < 0) let valid_sas v = let nsa = Array.length v in 0 < nsa && nsa <= max_subauth_count (* zero-pad to exactly twelve hex digits *) let pad_hex_ia niax = ident_auth_hexlen - niax end (* [module Con_strict] *) (* Validate simulating the empirically determined behavior of MS’s own implementation “ConvertStringSidToSidA()”. The error code it returns when rejecting an input is “ERROR_INVALID_SID”. input | well-formed parsed | round-trip well-formed -------------------------+----------------------+------------------------------------ S-1-0x000000000000-1 | no yes | S-1-0-1 yes S-1-0x0000000000ff-1 | no yes | S-1-255-1 yes S-1-0x0001000000ff-1 | yes yes | S-1-0x1000000FF-1 no S-1-0xffffffffffff-1 | yes yes | S-1-0xFFFFFFFFFFFF-1 yes S-1-0xff00ff00ff-1 | no yes | S-1-0xFF00FF00FF-1 no S-1-0xff00ff00ff00ff-1 | no no | ø - S-1-4294967296-1 | no yes | S-1-0x100000000-1 no S-1-01-1 | no yes | S-1-1-1 yes S-1-08-1 | no yes | S-1-8-1 yes S-1-001-1 | no yes | S-1-1-1 yes S-1-1 | no no | ø - S-1-5 | no no | ø - The inverse operations were modeled after “ConvertSidToStringSidA()”. *) module Con_MS = struct let name = "MS" let parse_hex_ia s p = assert (String.length s - p - 2 > 0); let e = p + ident_auth_hexlen in let b = Buffer.create ident_auth_hexlen in Buffer.add_string b "0x"; (* hex indicator for U64.of_string *) let i = ref (p + 2) in while !i < min (String.length s) e && Util.is_xdigit s.[!i] do Buffer.add_char b s.[!i]; incr i; done; U64.of_string (Buffer.contents b), !i (* MS doesn’t care for leading zeros. *) let valid_decimal d = String.length d > 0 (* MS accepts hex-formatted ias of arbitrary values as long as they fit a u64 *) let valid_ia _hex v = true (* Interestingly, subauths behave as specified. *) let valid_sas v = let nsa = Array.length v in 0 < nsa && nsa <= max_subauth_count (* No leading zeros in the hex output despite the spec mandating this only for decimal formatting. *) let pad_hex_ia _niax = 0 end (* [module Con_strict] *) module type StringFmt_intf = sig val decode : string -> (sid, string) result val encode : sid -> string end module MkStringFmt (Con : Conformance) = struct let read_decimal_string f s p = let n = String.length s in assert (p < n); let p' = ref p in let b = Buffer.create 16 in while !p' < n && Util.is_digit s.[!p'] do Buffer.add_char b s.[!p']; incr p' done; let d = Buffer.contents b in if not (Con.valid_decimal d) then raise (Invalid_argument (Printf.sprintf "Invalid SID [%s]: “%s” is not a valid decimal in mode %s" s d Con.name)) else p + String.length d, f d let read_decimal_u64 = read_decimal_string U64.of_string let read_decimal_u32 = read_decimal_string U32.of_string (* specialized cause only needed for ia *) let read_ident_auth_hex s p = let ia, p' = Con.parse_hex_ia s p in if not (Con.valid_ia true ia) then raise (Invalid_argument (Printf.sprintf "input malformed: hex-encoded identifier authority failed \ “%s” validation (value=%s)" Con.name (U64.to_string ia))) else p', ia let ident_auth_decimal s p = let p, ia = read_decimal_u64 s p in if not (Con.valid_ia false ia) then raise (Invalid_argument (Printf.sprintf "input malformed: decimal identifier authority failed \ “%s” validation (value=%s)" Con.name (U64.to_string ia))) else p, ia (* Below constant determines the lowest length at which it is sensible to test for the “-0x…” component that indicates a hex encoded ia. We require at least one hex digit to allow non-conformant parsing modes. *) let min_hexlen = String.length "0xf" let read_ident_auth s p = let r = String.length s - p in if r < min_hexlen then ident_auth_decimal s p else match s.[p], s.[p+1] with | '0', 'x' -> read_ident_auth_hex s p | _ -> ident_auth_decimal s p (* * The spec ([MS-DTYP]): * * 2.4.2.1: SID= "S-1-" IdentifierAuthority 1*SubAuthority *) let decode s = let n = String.length s in if n <= 4 then Error (Printf.sprintf "Invalid SID: ‘%s’ too short to be a SID in string format" s) else begin try Util.expect_char s 'S' 0; Util.expect_char s '-' 1; Util.expect_char s '1' 2; Util.expect_char s '-' 3; let p = 4 in let p, ia = read_ident_auth s p in let sa = ref [] and p' = ref p in while !p' < n - 1 && List.length !sa < max_subauth_count && s.[!p'] = '-' do let np, d = try read_decimal_u32 s (!p' + 1) with Invalid_argument e -> (* Brr, but Stdint’s error messages aren’t overly instructive. *) raise (Invalid_argument (Printf.sprintf "Invalid SID: error parsing subauth at position %d, \ (err: %s)" (!p' + 1) e)) in sa := d :: !sa; p' := np done; let sas = Array.of_list (List.rev !sa) in if not (Con.valid_sas sas) then Error (Printf.sprintf "input malformed: subauthority list failed “%s” validation \ (count=%n)" Con.name (Array.length sas)) else Ok { sid_ident_auth = ia ; sid_sub_auths = sas } with Invalid_argument e -> Error e; end let ident_auth_blank = "0x000000000000" let fmt_ident_auth b ia = if U64.compare ia ident_auth_hexmin < 0 then Buffer.add_string b (U64.to_string ia) else begin let iax = U64.to_string_hex ia in let niax = String.length iax in let pad = Con.pad_hex_ia niax in if pad = 0 then Buffer.add_string b iax else (Buffer.add_string b (String.sub ident_auth_blank 0 (2+pad)); Buffer.add_string b (String.sub iax 2 (niax - 2))) end let fmt_sub_auths b sas = Array.iter (fun sa -> Buffer.add_char b '-'; Buffer.add_string b (U32.to_string sa)) sas let encode s = let b = Buffer.create 16 in Buffer.add_string b "S-1-"; fmt_ident_auth b s.sid_ident_auth; fmt_sub_auths b s.sid_sub_auths; Buffer.contents b end (* [module MkStringFmt] *) module StringFmt = MkStringFmt (Con_strict) module MSStringFmt = MkStringFmt (Con_MS) module PacketRep = struct (* [MS-DTYP] 2.4.22 *) type endian = Big | Little let wordlen = 4 (* sizeof int *) let min_pktrep_len = 1 + 1 + sizeof_ident_auth let max_pktrep_len = 1 + 1 + sizeof_ident_auth + max_subauth_count * sizeof_sub_auth let pktrep_sa_off = min_pktrep_len let encode ?(endian=Little) s = let nsa = Array.length s.sid_sub_auths in let l = 8 + nsa * sizeof_sub_auth in let b = Bytes.create l in let o = ref 0 in let pushbyte c = char_of_int c |> Bytes.set b !o; incr o in assert (0 <= nsa && nsa <= 15); pushbyte 1; pushbyte nsa; let getia n = pushbyte (U64.to_int (U64.shift_right s.sid_ident_auth n) land 0xff) in (* big endian!, cf. [MS-DTYP] 2.4.1.1 *) getia 5; getia 4; getia 3; getia 2; getia 1; getia 0; let write_u32 = match endian with | Big -> U32.to_bytes_big_endian | Little -> U32.to_bytes_little_endian in Array.iteri (fun i sa -> let o' = !o + i * wordlen in write_u32 sa b o') s.sid_sub_auths; b let decode ?(endian=Little) b = let l = Bytes.length b in if l < min_pktrep_len || max_pktrep_len < l then Error (Printf.sprintf "bad input size: expected %d–%d B, got %d B" min_pktrep_len max_pktrep_len l) else if l mod wordlen <> 0 then Error (Printf.sprintf "bad input size: not divisible by word length (%d)" wordlen) else let v = Bytes.get b 0 |> int_of_char in if v <> 0x01 then Error (Printf.sprintf "input malformed: expected SID version=0x01, got 0x%0.2x" v) else let nsa = Bytes.get b 1 |> int_of_char in if max_subauth_count < nsa then Error (Printf.sprintf "input malformed: up to %d subAuthority elements permitted, \ %d specified" max_subauth_count nsa) else let ia = U64.of_bytes_big_endian b 0 |> U64.logand max_ident_auth in let sas = Array.make nsa (U32.zero) and read_u32 = match endian with | Big -> U32.of_bytes_big_endian | Little -> U32.of_bytes_little_endian in for i = 0 to (nsa - 1) do let off = pktrep_sa_off + i * sizeof_sub_auth in sas.(i) <- read_u32 b off done; Ok { sid_ident_auth = ia ; sid_sub_auths = sas } let from_channel ?(endian=Little) ic = try let v = input_byte ic in if v <> 0x01 then Error (Printf.sprintf "input malformed: expected SID version=0x01, got 0x%0.2x" v) else let nsa = input_byte ic in if max_subauth_count < nsa then Error (Printf.sprintf "input malformed: up to %d subAuthority elements permitted, \ %d specified" max_subauth_count nsa) else let n = sizeof_ident_auth + nsa * sizeof_sub_auth in let b = Bytes.make (2 + n) '\x00' in Bytes.set b 0 '\x01'; Bytes.set b 1 (char_of_int nsa); really_input ic b 2 n; decode ~endian b with End_of_file -> Error (Printf.sprintf "input malformed: unexpected end of file at offset %d \ parsing SID" (pos_in ic)) let to_channel ?(endian=Little) oc s = encode ~endian s |> output_bytes oc end (* [module PacketRep] *) module WellKnown = struct let cu = create_unsafe let sa = U32.of_int let ia = U64.of_int (* * see also * https://docs.microsoft.com/en-us/windows/desktop/secauthz/well-known-sids *) let null = cu U64.zero [| U32.zero |] let everyone = cu U64.one [| U32.zero |] let world = everyone (* 1-2-… *) let local = cu (U64.of_int 2) [| U32.zero |] let console_logon = cu (U64.of_int 2) [| U32.one |] (* 1-3-… *) let creator_owner_id = cu (U64.of_int 3) [| U32.zero |] let creator_group_id = cu (U64.of_int 3) [| U32.one |] let creator_owner_server = cu (U64.of_int 3) [| U32.of_int 2 |] let creator_group_server = cu (U64.of_int 3) [| U32.of_int 3 |] let owner_rights = cu (U64.of_int 3) [| U32.of_int 4 |] let elite = cu U64.one [| U32.of_int 3 ; U32.of_int 3; U32.of_int 7 |] (* 1-5-… *) let nt_authority = cu (U64.of_int 5) [| |] let dialup = cu (U64.of_int 5) [| U32.one |] let network = cu (U64.of_int 5) [| U32.of_int 2 |] let batch = cu (U64.of_int 5) [| U32.of_int 3 |] let interactive = cu (U64.of_int 5) [| U32.of_int 4 |] let logon_id = cu (U64.of_int 5) [| U32.of_int 5 |] let service = cu (U64.of_int 5) [| U32.of_int 6 |] let anonymous = cu (U64.of_int 5) [| U32.of_int 7 |] let proxy = cu (U64.of_int 5) [| U32.of_int 8 |] let enterprise_domain_controllers = cu (U64.of_int 5) [| U32.of_int 9 |] let principal_self = cu (U64.of_int 5) [| U32.of_int 10 |] let authenticated_users = cu (U64.of_int 5) [| U32.of_int 11 |] let restricted_code = cu (U64.of_int 5) [| U32.of_int 12 |] let terminal_server_user = cu (U64.of_int 5) [| U32.of_int 13 |] let remote_interactive_logon = cu (U64.of_int 5) [| U32.of_int 14 |] let this_organisation = cu (U64.of_int 5) [| U32.of_int 15 |] let iusr = cu (U64.of_int 5) [| U32.of_int 17 |] let local_system = cu (U64.of_int 5) [| U32.of_int 18 |] let local_service = cu (U64.of_int 5) [| U32.of_int 19 |] let network_service = cu (U64.of_int 5) [| U32.of_int 20 |] let compounded_authentication = cu (ia 5) [| sa 21; U32.zero; U32.zero; U32.zero; sa 496 |] let claims_valid = cu (ia 5) [| sa 21; U32.zero; U32.zero; U32.zero; sa 497 |] let administrator machine = cu (ia 5) [| sa 21; machine; sa 500 |] let guest machine = cu (ia 5) [| sa 21; machine; sa 501 |] let krbtgt domain = cu (ia 5) [| sa 21; domain; sa 502 |] let domain_admins domain = cu (ia 5) [| sa 21; domain; sa 512 |] let domain_users domain = cu (ia 5) [| sa 21; domain; sa 513 |] let domain_guests domain = cu (ia 5) [| sa 21; domain; sa 514 |] let domain_computers domain = cu (ia 5) [| sa 21; domain; sa 515 |] let domain_domain_controllers domain = cu (ia 5) [| sa 21; domain; sa 516 |] let cert_publishers domain = cu (ia 5) [| sa 21; domain; sa 517 |] let schema_administrators root_domain = cu (ia 5) [| sa 21; root_domain; sa 518 |] let enterprise_admins root_domain = cu (ia 5) [| sa 21; root_domain; sa 519 |] let group_policy_creator_owners domain = cu (ia 5) [| sa 21; domain; sa 520 |] let readonly_domain_controllers domain = cu (ia 5) [| sa 21; domain; sa 521 |] let cloneable_controllers domain = cu (ia 5) [| sa 21; domain; sa 522 |] let protected_users domain = cu (ia 5) [| sa 21; domain; sa 525 |] let key_admins domain = cu (ia 5) [| sa 21; domain; sa 526 |] let enterprise_key_admins domain = cu (ia 5) [| sa 21; domain; sa 527 |] let ras_servers domain = cu (ia 5) [| sa 21; domain; sa 553 |] let allowed_rodc_password_replication_group domain = cu (ia 5) [| sa 21; domain; sa 571 |] let denied_rodc_password_replication_group domain = cu (ia 5) [| sa 21; domain; sa 572 |] let builtin_administrators = cu (ia 5) [| sa 32; sa 544 |] let builtin_users = cu (ia 5) [| sa 32; sa 545 |] let builtin_guests = cu (ia 5) [| sa 32; sa 546 |] let power_users = cu (ia 5) [| sa 32; sa 547 |] let account_operators = cu (ia 5) [| sa 32; sa 548 |] let server_operators = cu (ia 5) [| sa 32; sa 549 |] let printer_operators = cu (ia 5) [| sa 32; sa 550 |] let backup_operators = cu (ia 5) [| sa 32; sa 551 |] let replicator = cu (ia 5) [| sa 32; sa 552 |] let alias_prew2kcompacc = cu (ia 5) [| sa 32; sa 554 |] let remote_desktop = cu (ia 5) [| sa 32; sa 555 |] let network_configuration_ops = cu (ia 5) [| sa 32; sa 556 |] let incoming_forest_trust_builders = cu (ia 5) [| sa 32; sa 557 |] let perfmon_users = cu (ia 5) [| sa 32; sa 558 |] let perflog_users = cu (ia 5) [| sa 32; sa 559 |] let windows_authorization_access_group = cu (ia 5) [| sa 32; sa 560 |] let terminal_server_license_servers = cu (ia 5) [| sa 32; sa 561 |] let distributed_com_users = cu (ia 5) [| sa 32; sa 562 |] let iis_iusrs = cu (ia 5) [| sa 32; sa 568 |] let cryptographic_operators = cu (ia 5) [| sa 32; sa 569 |] let event_log_readers = cu (ia 5) [| sa 32; sa 573 |] let certificate_service_dcom_access = cu (ia 5) [| sa 32; sa 574 |] let rds_remote_access_servers = cu (ia 5) [| sa 32; sa 575 |] let rds_endpoint_servers = cu (ia 5) [| sa 32; sa 576 |] let rds_management_servers = cu (ia 5) [| sa 32; sa 577 |] let hyper_v_admins = cu (ia 5) [| sa 32; sa 578 |] let access_control_assistance_ops = cu (ia 5) [| sa 32; sa 579 |] let remote_management_users = cu (ia 5) [| sa 32; sa 580 |] let write_restricted_code = cu (ia 5) [| sa 33 |] let ntlm_authentication = cu (ia 5) [| sa 64; sa 10 |] let schannel_authentication = cu (ia 5) [| sa 64; sa 14 |] let digest_authentication = cu (ia 5) [| sa 64; sa 21 |] let this_organization_certificate = cu (ia 5) [| sa 65; sa 1 |] let nt_service = cu (ia 5) [| sa 80 |] let user_mode_drivers = cu (ia 5) [| sa 84; U32.zero; U32.zero; U32.zero; U32.zero; U32.zero |] let local_account = cu (ia 5) [| sa 113 |] let local_account_and_member_of_administrators_group = cu (ia 5) [| sa 114 |] let other_organization = cu (ia 5) [| sa 1000 |] (* 1-15-… *) let all_app_packages = cu (ia 15) [| sa 2; U32.one |] (* 1-16-… *) let ml_untrusted = cu (ia 16) [| U32.zero |] let ml_low = cu (ia 16) [| sa 4096 |] let ml_medium = cu (ia 16) [| sa 8192 |] let ml_medium_plus = cu (ia 16) [| sa 8448 |] let ml_high = cu (ia 16) [| sa 12288 |] let ml_system = cu (ia 16) [| sa 16384 |] let ml_protected_process = cu (ia 16) [| sa 20480 |] let ml_secure_process = cu (ia 16) [| sa 28672 |] (* 1-18-… *) let authentication_authority_asserted_identity = cu (ia 18) [| U32.one |] let service_asserted_identity = cu (ia 18) [| sa 2 |] let fresh_public_key_identity = cu (ia 18) [| sa 3 |] let key_trust_identity = cu (ia 18) [| sa 4 |] let key_property_mfa = cu (ia 18) [| sa 5 |] let key_property_attestation = cu (ia 18) [| sa 6 |] module Prefix = struct let security_null_sid_authority sa = create_unsafe (U64.of_int 0x00) sa let security_world_sid_authority sa = create_unsafe (U64.of_int 0x01) sa let security_local_sid_authority sa = create_unsafe (U64.of_int 0x02) sa let security_creator_sid_authority sa = create_unsafe (U64.of_int 0x03) sa let security_nt_authority sa = create_unsafe (U64.of_int 0x05) sa let security_app_package_authority sa = create_unsafe (U64.of_int 0x0f) sa let security_mandatory_label_authority sa = create_unsafe (U64.of_int 0x10) sa let security_scoped_policy_id_authority sa = create_unsafe (U64.of_int 0x11) sa let security_authentication_authority sa = create_unsafe (U64.of_int 0x12) sa end end let of_string = StringFmt.decode let to_string = StringFmt.encode let of_bytes = PacketRep.decode let to_bytes = PacketRep.encode type t = sid