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") ;
  (false, "cacert-timestamp") ;
  (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
]
