open X509 let time () = None let with_loaded_file file ~f = let fullpath = "./testcertificates/" ^ file ^ ".pem" in let fd = open_in fullpath in let ln = in_channel_length fd in let buf = Bytes.create ln in really_input fd buf 0 ln; let buf = Bytes.unsafe_to_string buf in try let r = f buf in close_in fd; match r with | Ok data -> data | Error (`Msg m) -> Alcotest.failf "decoding error in %s: %s" fullpath m with e -> close_in fd; Alcotest.failf "exception in %s: %s" fullpath (Printexc.to_string e) let priv = match with_loaded_file "private/cakey" ~f:Private_key.decode_pem with | `RSA x -> x | _ -> assert false let cert name = with_loaded_file name ~f:Certificate.decode_pem let host name = Domain_name.host_exn (Domain_name.of_string_exn name) let invalid_cas = [ "cacert-basicconstraint-ca-false"; "cacert-unknown-critical-extension" ; "cacert-keyusage-crlsign" ; "cacert-ext-usage-timestamping" ] let cert_public_is_pub cert = let pub = Mirage_crypto_pk.Rsa.pub_of_priv priv in ( match Certificate.public_key cert with | `RSA pub' when pub = pub' -> () | _ -> Alcotest.fail "public / private key doesn't match" ) let test_invalid_ca name () = let c = cert name in cert_public_is_pub c ; Alcotest.(check int "CA list is empty" 0 (List.length (Validation.valid_cas [c]))) let invalid_ca_tests = List.mapi (fun i args -> "invalid CA " ^ string_of_int i, `Quick, test_invalid_ca args) invalid_cas let cacert = cert "cacert" let cacert_pathlen0 = cert "cacert-pathlen-0" let cacert_ext = cert "cacert-unknown-extension" let cacert_ext_ku = cert "cacert-ext-usage" let cacert_v1 = cert "cacert-v1" let test_valid_ca c () = cert_public_is_pub c ; Alcotest.(check int "CA is valid" 1 (List.length (Validation.valid_cas [c]))) let valid_ca_tests = [ "valid CA cacert", `Quick, test_valid_ca cacert ; "valid CA cacert_pathlen0", `Quick, test_valid_ca cacert_pathlen0 ; "valid CA cacert_ext", `Quick, test_valid_ca cacert_ext ; "valid CA cacert_v1", `Quick, test_valid_ca cacert_v1 ] let first_cert name = with_loaded_file ("first/" ^ name) ~f:Certificate.decode_pem (* ok, now some real certificates *) let first_certs = [ ( "first", true, [ "foo.foobar.com" ; "foobar.com" ], (* commonName: "bar.foobar.com" *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ( "first-basicconstraint-true" , false, [ "ca.foobar.com" ], (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ( "first-keyusage-and-timestamping", true, [ "ext.foobar.com" ], (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], Some [`Time_stamping] ) ; ( "first-keyusage-any", true, [ "any.foobar.com" ], (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], Some [`Time_stamping; `Any] ) ; ( "first-keyusage-nonrep", true, [ "key.foobar.com" ], (* no subjAltName *) [ `Content_commitment ], None ) ; ( "first-unknown-critical-extension", false, (* commonName: "blafasel.com" *) [ "foo.foobar.com" ; "foobar.com" ], [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ( "first-unknown-extension", true, [ "foobar.com" ], (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ] let allowed_hashes = [ `MD5 ; `SHA1 ; `SHA224 ; `SHA256 ; `SHA384 ; `SHA512 ] let test_valid_ca_cert ?(allowed_hashes = allowed_hashes) server chain valid name ca () = let anchors = ca and host = Some (host name) and full_chain = server :: chain in match valid, Validation.verify_chain_of_trust ~time ~allowed_hashes ~host ~anchors full_chain with | false, Ok _ -> Alcotest.fail "expected to fail, but didn't" | false, Error _ -> () | true , Ok _ -> () | true , Error c -> Alcotest.failf "valid certificate %a" Validation.pp_validation_error c let test_cert c usages extusage () = let ku, eku = let exts = Certificate.extensions c in let ku = match Extension.(find Key_usage exts) with | None -> [] | Some (_crit, ku) -> ku and eku = match Extension.(find Ext_key_usage exts) with | None -> [] | Some (_crit, eku) -> eku in ku, eku in ( if List.for_all (fun u -> List.mem u ku) usages then () else Alcotest.fail "key usage is different" ) ; ( match extusage with | None -> () | Some x when List.for_all (fun u -> List.mem u eku) x -> () | _ -> Alcotest.fail "extended key usage is broken" ) let first_cert_tests = List.mapi (fun i (name, _, _, us, eus) -> "certificate property testing " ^ string_of_int i, `Quick, test_cert (first_cert name) us eus) first_certs let first_cert_ca_test (ca, x) = List.flatten (List.map (fun (name, valid, cns, _, _) -> let c = first_cert name in ("verification CA " ^ x ^ " cn blablbalbala", `Quick, test_valid_ca_cert c [] false "blablabalbal" [ca]) :: List.mapi (fun i cn -> "certificate verification testing using CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c [] valid cn [ca]) cns) first_certs) let ca_tests f = List.flatten (List.map f [ (cacert, "cacert") ; (cacert_pathlen0, "cacert_pathlen0") ; (cacert_ext, "cacert_ext") ; (cacert_ext_ku, "cacert_ext_ku") ; (cacert_v1, "cacert_v1") ]) let first_wildcard_certs = [ ( "first-wildcard-subjaltname", [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ( "first-wildcard", [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ] let first_wildcard_cert_tests = List.mapi (fun i (name, us, eus) -> "wildcard certificate property testing " ^ string_of_int i, `Quick, test_cert (first_cert name) us eus) first_wildcard_certs let first_wildcard_cert_ca_test (ca, x) = List.flatten (List.map (fun (name, _, _) -> let c = first_cert name in ("verification CA " ^ x ^ " cn blablbalbala", `Quick, test_valid_ca_cert c [] false "blablabalbal" [ca]) :: List.mapi (fun i cn -> "wildcard certificate CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c [] true cn [ca]) [ "foo.foobar.com" ; "bar.foobar.com" ; "www.foobar.com" ] @ List.mapi (fun i cn -> "wildcard certificate CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c [] false cn [ca]) [ "foo.foo.foobar.com" ; "bar.fbar.com" ; "foobar.com" ; "com" ; "foobar.com.bla" ] ) first_wildcard_certs) let intermediate_cas = [ (true, "cacert") ; (true, "cacert-any-ext") ; (false, "cacert-ba-false") ; (false, "cacert-no-bc") ; (false, "cacert-no-keyusage") ; (true, "cacert-ku-critical") ; (true, "cacert-timestamp") ; (* if we require CAs to have ext_key_usage any, github.com doesn't talk to us *) (false, "cacert-unknown") ; (false, "cacert-v1") ] let im_cert name = with_loaded_file ("intermediate/" ^ name) ~f:Certificate.decode_pem let second_certs = [ ("second", [ "second.foobar.com" ], true, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-any", [ "second.foobar.com" ], true, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], Some [ `Any ] ) ; ("second-subj", [ "foobar.com" ; "foo.foobar.com" ], true, (* commonName: "second.foobar.com" *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-unknown-noncrit", [ "second.foobar.com" ], true, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-nonrepud", [ "second.foobar.com" ], true, (* no subjAltName *) [ `Content_commitment ], None ) ; ("second-time", [ "second.foobar.com" ], true, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], Some [ `Time_stamping ]) ; ("second-subj-wild", [ "foo.foobar.com" ], true, (* commonName: "second.foobar.com" *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-bc-true", [ "second.foobar.com" ], false, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-unknown", [ "second.foobar.com" ], false, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-no-cn", [ ], false, (* no subjAltName *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ("second-subjaltemail", [ ], false, (* email in subjAltName, do not use CN *) [ `Digital_signature ; `Content_commitment ; `Key_encipherment ], None ) ; ] let second_cert name = with_loaded_file ("intermediate/second/" ^ name) ~f:Certificate.decode_pem let second_cert_tests = List.mapi (fun i (name, _, _, us, eus) -> "second certificate property testing " ^ string_of_int i, `Quick, test_cert (second_cert name) us eus) second_certs let second_cert_ca_test (cavalid, ca, x) = List.flatten (List.flatten (List.map (fun (imvalid, im) -> let chain = [im_cert im] in List.map (fun (name, cns, valid, _, _) -> let c = second_cert name in ("verification CA " ^ x ^ " cn blablbalbala", `Quick, test_valid_ca_cert c chain false "blablabalbal" [ca]) :: List.mapi (fun i cn -> "strict certificate verification testing using CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c chain (cavalid && imvalid && valid) cn [ca]) cns) second_certs) intermediate_cas)) let im_ca_tests f = List.flatten (List.map f [ (true, cacert, "cacert") ; (true, cacert_ext, "cacert_ext") ; (true, cacert_ext_ku, "cacert_ext_ku") ; (true, cacert_v1, "cacert_v1") ; (false, cacert_pathlen0, "cacert_pathlen0") ]) let second_wildcard_cert_ca_test (cavalid, ca, x) = List.flatten (List.map (fun (imvalid, im) -> let chain = [im_cert im] in let c = second_cert "second-subj-wild" in ("verification CA " ^ x ^ " cn blablbalbala", `Quick, test_valid_ca_cert c chain false "blablabalbal" [ca]) :: List.mapi (fun i cn -> "wildcard certificate verification CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c chain (cavalid && imvalid) cn [ca]) [ "a.foobar.com" ; "foo.foobar.com" ; "foobar.foobar.com" ; "www.foobar.com" ] @ List.mapi (fun i cn -> "wildcard certificate verification CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c chain false cn [ca]) [ "a.b.foobar.com" ; "f.foobar.com.com" ; "f.f.f." ; "foobar.com.uk" ; "foooo.bar.com" ; "foobar.com" ]) intermediate_cas) let second_no_cn_cert_ca_test (_, ca, x) = List.flatten (List.map (fun (_, im) -> let chain = [im_cert im] in let c = second_cert "second-no-cn" in ("verification CA " ^ x ^ " cn blablbalbala", `Quick, test_valid_ca_cert c chain false "blablabalbal" [ca]) :: List.mapi (fun i cn -> "certificate verification CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c chain false cn [ca]) [ "a.foobar.com" ; "foo.foobar.com" ; "foobar.foobar.com" ; "foobar.com" ; "www.foobar.com" ] @ List.mapi (fun i cn -> "certificate verification CA " ^ x ^ " and CN " ^ cn ^ " " ^ string_of_int i, `Quick, test_valid_ca_cert c chain false cn [ca]) [ "a.b.foobar.com" ; "f.foobar.com.com" ; "f.f.f." ; "foobar.com.uk" ; "foooo.bar.com" ]) intermediate_cas) let invalid_tests = let c = second_cert "second" in let h = "second.foobar.com" in let allowed_hashes = [ `SHA256 ; `SHA384 ; `SHA512 ] in [ "invalid chain", `Quick, test_valid_ca_cert c [] false h [cacert] ; "broken chain", `Quick, test_valid_ca_cert c [cacert] false h [cacert] ; "no trust anchor", `Quick, test_valid_ca_cert c [im_cert "cacert"] false h [] ; "2chain invalid", `Quick, test_valid_ca_cert ~allowed_hashes c [im_cert "cacert" ; cacert] false h [cacert] ; "2chain valid", `Quick, test_valid_ca_cert c [im_cert "cacert" ; cacert] true h [cacert] ; "3chain invalid", `Quick, test_valid_ca_cert ~allowed_hashes c [im_cert "cacert" ; cacert ; cacert] false h [cacert] ; "3chain valid", `Quick, test_valid_ca_cert c [im_cert "cacert" ; cacert ; cacert] true h [cacert] ; "chain-order invalid", `Quick, test_valid_ca_cert ~allowed_hashes c [im_cert "cacert" ; im_cert "cacert" ; cacert] false h [cacert] ; "chain-order valid", `Quick, test_valid_ca_cert c [im_cert "cacert" ; im_cert "cacert" ; cacert] true h [cacert] ; "not a CA", `Quick, (fun _ -> Alcotest.(check int "is not a CA" 0 (List.length (Validation.valid_cas [im_cert "cacert"])))) ; "not a CA", `Quick, (fun _ -> Alcotest.(check int "is also not a CA" 0 (List.length (Validation.valid_cas [c])))) ; ] let x509_tests = [ "Invalid CA", invalid_ca_tests ; "Valid CA", valid_ca_tests ; "Certificate", first_cert_tests ; "CA tests with certificate", ca_tests first_cert_ca_test ; "Wildcard certificate", first_wildcard_cert_tests ; "CA tests with wildcard certificate", ca_tests first_wildcard_cert_ca_test ; "Second certificate test", second_cert_tests ; "Intermediate CA with second certificate", im_ca_tests second_cert_ca_test ; "Intermediate CA with CA and second", im_ca_tests second_wildcard_cert_ca_test ; "Intermediate CA with second no common name", im_ca_tests second_no_cn_cert_ca_test ; "Tests with invalid data", invalid_tests ]