open X509

let time () = None

(* some revocation scenarios to convince myself *)
let ca_exts ?pathlen () =
  let ku =
    [ `Key_cert_sign ; `CRL_sign ; `Digital_signature ; `Content_commitment ]
  in
  Extension.(add Basic_constraints (true, (true, pathlen))
               (singleton Key_usage (true, ku)))

let key_ids exts subject_pubkey issuer_pubkey =
  let subject_key_id =
    false, Public_key.id subject_pubkey
  and authority_key_id =
    let cs = Public_key.id issuer_pubkey in
    false, (Some cs, General_name.empty, None)
  in
  Extension.(add Subject_key_id subject_key_id
               (add Authority_key_id authority_key_id exts))

let leaf_exts =
  Extension.(add Key_usage (true, [ `Digital_signature ; `Key_encipherment ])
               (add Ext_key_usage (true, [ `Server_auth ])
                  (singleton Basic_constraints (true, (false, None)))))

let validity now =
  match Ptime.add_span now (Ptime.Span.of_int_s 3600) with
  | Some fut -> (now, fut)
  | None -> invalid_arg "couldn't add 3600 seconds to now"

let key () =
  let key = Mirage_crypto_pk.Rsa.generate ~bits:1024 () in
  (`RSA (Mirage_crypto_pk.Rsa.pub_of_priv key), `RSA key)

let selfsigned ?(name = "test") now =
  let pub, priv = key () in
  let name = [ Distinguished_name.(Relative_distinguished_name.singleton (CN name)) ] in
  match Signing_request.create name priv with
  | Error _ -> assert false
  | Ok req ->
    let valid_from, valid_until = validity now in
    match X509.Signing_request.sign req ~valid_from ~valid_until ~extensions:(ca_exts ()) priv name with
    | Ok cacert -> (cacert, pub, priv)
    | Error _ -> assert false

let cert ?serial ?(name = "sub") now ca pubca privca issuer =
  let pub, priv = key () in
  let name = [ Distinguished_name.(Relative_distinguished_name.singleton (CN name)) ] in
  match Signing_request.create name priv with
  | Error _ -> assert false
  | Ok req ->
    let valid_from, valid_until = validity now in
    let extensions = key_ids (if ca then ca_exts () else leaf_exts)  pub pubca in
    match X509.Signing_request.sign req ~valid_from ~valid_until ?serial ~extensions privca issuer with
    | Ok cert -> (cert, pub, priv)
    | Error _ -> assert false

let verify () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let cert, _, _ = cert now false capub capriv (Certificate.subject ca) in
  match Validation.verify_chain ~host:None ~time ~anchors:[ca] [cert] with
  | Ok _ -> ()
  | Error _ -> Alcotest.fail "expected verification to succeed"

let crl () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let cert, _, _ = cert ~serial now false capub capriv issuer in
  let revoked = { CRL.serial ; date = now ; extensions = Extension.empty } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] capriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert] with
    | Ok _ -> Alcotest.fail "expected revocation"
    | Error (`Revoked _) -> ()
    | Error _ -> Alcotest.fail "expected revoked failure!"

let verify' () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let ica, ipub, ipriv = cert ~name:"subCA" ~serial now true capub capriv issuer in
  let cert, _pub, _priv = cert now false ipub ipriv (Certificate.subject ica) in
  match Validation.verify_chain ~host:None ~time ~anchors:[ca] [cert ; ica] with
  | Ok _ -> ()
  | Error _ -> Alcotest.fail "expected verification!"

let crl' () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let ica, ipub, ipriv = cert ~name:"subCA" ~serial now true capub capriv issuer in
  let cert, _pub, _priv = cert now false ipub ipriv (Certificate.subject ica) in
  let revoked = { CRL.serial ; date = now ; extensions = Extension.empty } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] capriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert ; ica] with
    | Ok _ -> Alcotest.fail "expected revocation"
    | Error (`Revoked _) -> ()
    | Error _ -> Alcotest.fail "expected revoked failure!"

let crl'leaf () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let ica, ipub, ipriv = cert ~name:"subCA" now true capub capriv (Certificate.subject ca) in
  let issuer = Certificate.subject ica in
  let cert, _pub, _priv = cert ~serial now false ipub ipriv issuer in
  let revoked = { CRL.serial ; date = now ; extensions = Extension.empty } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] ipriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert ; ica] with
    | Ok _ -> Alcotest.fail "expected revocation"
    | Error (`Revoked _) -> ()
    | Error _ -> Alcotest.fail "expected revoked failure!"

let crl'leaf'wrong () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let ica, ipub, ipriv = cert ~name:"subCA" now true capub capriv issuer in
  let cert, _pub, _priv = cert ~serial now false ipub ipriv (Certificate.subject ica) in
  let revoked = { CRL.serial ; date = now ; extensions = Extension.empty } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] ipriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert ; ica] with
    | Ok _ -> ()
    | Error _ -> Alcotest.fail "expected success!"

let verify'' () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let ica, ipub, ipriv = cert ~name:"subCA" now true capub capriv issuer in
  let cert, _pub, _priv = cert now false ipub ipriv (Certificate.subject ica) in
  let revoked = { CRL.serial ; date = now ; extensions = Extension.empty } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] capriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert ; ica] with
    | Ok _ -> ()
    | Error _ -> Alcotest.fail "expected verify to succeed!"

let crl'' () =
  let now = Ptime_clock.now () in
  let ca, capub, capriv = selfsigned now in
  let serial = "\x42" in
  let issuer = Certificate.subject ca in
  let ica, ipub, ipriv = cert ~name:"subCA" ~serial now true capub capriv issuer in
  let cert, _pub, _priv = cert now false ipub ipriv (Certificate.subject ica) in
  let extensions = Extension.(singleton Reason (false, `Remove_from_CRL)) in
  let revoked = { CRL.serial ; date = now ; extensions } in
  let extensions = Extension.(singleton CRL_number (false, 1)) in
  match CRL.revoke ~issuer ~this_update:now ~extensions [revoked] capriv with
  | Error _ -> Alcotest.fail "couldn't revoke"
  | Ok crl ->
    let revoked = CRL.is_revoked [crl] ?allowed_hashes:None in
    match Validation.verify_chain ~host:None ~time ~revoked ~anchors:[ca] [cert ; ica] with
    | Ok _ -> ()
    | Error _ -> Alcotest.fail "expected proper verification!"

let revoke_tests = [
  "Verify with a chain works", `Quick, verify ;
  "Verify with a revoked leaf fails", `Quick, crl ;
  "Verify with a longer chain works", `Quick, verify' ;
  "Verify with a revoked intermediate fails", `Quick, crl' ;
  "Verify with a longer chain works, even if some random serial is revoked", `Quick, verify'' ;
  "Verify with a revoked `Remove_from_CRL works", `Quick, crl'' ;
  "Verify with revoked leaf fails", `Quick, crl'leaf ;
  "Verify with wrongly revoked leaf works", `Quick, crl'leaf'wrong ;
]
