521 lines
20 KiB
OCaml
521 lines
20 KiB
OCaml
|
|
let ( let* ) = Result.bind
|
||
|
|
|
||
|
|
let sha2 = [ `SHA256 ; `SHA384 ; `SHA512 ]
|
||
|
|
let all_hashes = [ `MD5 ; `SHA1 ; `SHA224 ] @ sha2
|
||
|
|
|
||
|
|
let src = Logs.Src.create "x509.validation" ~doc:"X509 validation"
|
||
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
||
|
|
|
||
|
|
type signature_error = [
|
||
|
|
| `Bad_signature of Distinguished_name.t * string
|
||
|
|
| `Bad_encoding of Distinguished_name.t * string * string
|
||
|
|
| `Hash_not_allowed of Distinguished_name.t * [ `MD5 | `SHA1 | `SHA224 | `SHA256 | `SHA384 | `SHA512 ]
|
||
|
|
| `Unsupported_keytype of Distinguished_name.t * Public_key.t
|
||
|
|
| `Unsupported_algorithm of Distinguished_name.t * string
|
||
|
|
| `Msg of string
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_signature_error ppf = function
|
||
|
|
| `Bad_signature (subj, msg) ->
|
||
|
|
Fmt.pf ppf "failed to verify signature of %a: %s"
|
||
|
|
Distinguished_name.pp subj msg
|
||
|
|
| `Bad_encoding (subj, err, sig_) ->
|
||
|
|
Fmt.pf ppf "bad signature encoding of %a, ASN error %s:@.%a"
|
||
|
|
Distinguished_name.pp subj err Ohex.pp sig_
|
||
|
|
| `Hash_not_allowed (subj, hash) ->
|
||
|
|
Fmt.pf ppf "hash algorithm %a is not allowed, but %a is signed using it"
|
||
|
|
Certificate.pp_hash hash Distinguished_name.pp subj
|
||
|
|
| `Unsupported_keytype (subj, pk) ->
|
||
|
|
Fmt.pf ppf "unsupported key used to sign %a: %a" Distinguished_name.pp subj
|
||
|
|
Public_key.pp pk
|
||
|
|
| `Unsupported_algorithm (subj, alg) ->
|
||
|
|
Fmt.pf ppf "unsupported algorithm used to sign %a: %s"
|
||
|
|
Distinguished_name.pp subj alg
|
||
|
|
| `Msg msg -> Fmt.string ppf msg
|
||
|
|
|
||
|
|
let maybe_validate_hostname cert = function
|
||
|
|
| None -> true
|
||
|
|
| Some x -> Certificate.supports_hostname cert x
|
||
|
|
|
||
|
|
let maybe_validate_ip cert = function
|
||
|
|
| None -> true
|
||
|
|
| Some ip -> Certificate.supports_ip cert ip
|
||
|
|
|
||
|
|
let issuer_matches_subject
|
||
|
|
{ Certificate.asn = parent ; _ } { Certificate.asn = cert ; _ } =
|
||
|
|
Distinguished_name.equal parent.tbs_cert.subject cert.tbs_cert.issuer
|
||
|
|
|
||
|
|
let is_self_signed cert = issuer_matches_subject cert cert
|
||
|
|
|
||
|
|
let validate_raw_signature subject allowed_hashes msg sig_alg signature pk =
|
||
|
|
match Algorithm.to_signature_algorithm sig_alg with
|
||
|
|
| Some (scheme, siga) ->
|
||
|
|
(* we check that siga is a member of allowed_hashes, to ensure not
|
||
|
|
using a weak one. *)
|
||
|
|
if not (List.mem siga allowed_hashes) then
|
||
|
|
Error (`Hash_not_allowed (subject, siga))
|
||
|
|
else if not (Key_type.supports_signature_scheme (Public_key.key_type pk) scheme) then
|
||
|
|
Error (`Unsupported_keytype (subject, pk))
|
||
|
|
else
|
||
|
|
let* () =
|
||
|
|
Result.map_error (function `Msg m -> `Bad_signature (subject, m))
|
||
|
|
(Public_key.verify siga ~scheme ~signature pk (`Message msg))
|
||
|
|
in
|
||
|
|
if not (List.mem siga sha2) then
|
||
|
|
Log.warn (fun m -> m "%a signature uses %a, a weak hash algorithm"
|
||
|
|
Distinguished_name.pp subject Certificate.pp_hash siga);
|
||
|
|
Ok ()
|
||
|
|
| None ->
|
||
|
|
Error (`Unsupported_algorithm (subject, Algorithm.to_string sig_alg))
|
||
|
|
|
||
|
|
let shift str off =
|
||
|
|
String.sub str off (String.length str - off)
|
||
|
|
|
||
|
|
(* XXX should return the tbs_cert blob from the parser, this is insane *)
|
||
|
|
let raw_cert_hack raw =
|
||
|
|
(* we only support definite-length *)
|
||
|
|
let loff = 1 in
|
||
|
|
let snd = String.get_uint8 raw loff in
|
||
|
|
let lenl = 2 + if 0x80 land snd = 0 then 0 else 0x7F land snd in
|
||
|
|
(* cut away the SEQUENCE and LENGTH from outer sequence (tbs, sigalg, sig) *)
|
||
|
|
let cert_buf = shift raw lenl in
|
||
|
|
let rec l acc idx last =
|
||
|
|
if idx = last then
|
||
|
|
acc
|
||
|
|
else
|
||
|
|
l (acc lsl 8 + String.get_uint8 cert_buf idx) (succ idx) last
|
||
|
|
in
|
||
|
|
let cert_len_byte = String.get_uint8 cert_buf loff in
|
||
|
|
let cert_len =
|
||
|
|
(* two cases: *)
|
||
|
|
if 0x80 land cert_len_byte = 0 then
|
||
|
|
(* length < 127: highest bit is zero and lower 7 bits encode the length *)
|
||
|
|
2 + (0x7F land cert_len_byte)
|
||
|
|
else
|
||
|
|
(* length > 127: highest bit is 1 and lower 7 bits encode the bytes used
|
||
|
|
to encode the length *)
|
||
|
|
let len_len = 2 + 0x7F land cert_len_byte in
|
||
|
|
len_len + (l 0 2 len_len)
|
||
|
|
in
|
||
|
|
String.sub cert_buf 0 cert_len
|
||
|
|
|
||
|
|
let validate_signature allowed_hashes { Certificate.asn = trusted ; _ } { Certificate.asn ; raw } =
|
||
|
|
let tbs_raw = raw_cert_hack raw in
|
||
|
|
validate_raw_signature asn.tbs_cert.subject allowed_hashes tbs_raw
|
||
|
|
asn.signature_algo asn.signature_val trusted.tbs_cert.pk_info
|
||
|
|
|
||
|
|
let validate_time time { Certificate.asn = cert ; _ } =
|
||
|
|
match time with
|
||
|
|
| None -> true
|
||
|
|
| Some now ->
|
||
|
|
let (not_before, not_after) = cert.tbs_cert.validity in
|
||
|
|
Ptime.(is_later ~than:not_before now && is_earlier ~than:not_after now)
|
||
|
|
|
||
|
|
let version_matches_extensions { Certificate.asn = cert ; _ } =
|
||
|
|
let tbs = cert.tbs_cert in
|
||
|
|
match tbs.version, Extension.is_empty tbs.extensions with
|
||
|
|
| (`V1 | `V2), true -> true
|
||
|
|
| (`V1 | `V2), _ -> false
|
||
|
|
| `V3, _ -> true
|
||
|
|
|
||
|
|
let validate_path_len pathlen { Certificate.asn = cert ; _ } =
|
||
|
|
(* X509 V1/V2 certificates do not contain X509v3 extensions! *)
|
||
|
|
(* thus, we cannot check the path length. this will only ever happen for trust anchors: *)
|
||
|
|
(* intermediate CAs are checked by is_cert_valid, which checks that the CA extensions are there *)
|
||
|
|
(* whereas trust anchor are ok with getting V1/2 certificates *)
|
||
|
|
(* TODO: make it configurable whether to accept V1/2 certificates at all *)
|
||
|
|
let exts = cert.tbs_cert.extensions in
|
||
|
|
match cert.tbs_cert.version, Extension.(find Basic_constraints exts) with
|
||
|
|
| (`V1 | `V2), _ -> true
|
||
|
|
| `V3, Some (_ , (true, None)) -> true
|
||
|
|
| `V3, Some (_ , (true, Some n)) -> n >= pathlen
|
||
|
|
| _ -> false
|
||
|
|
|
||
|
|
let validate_ca_extensions { Certificate.asn = cert ; _ } =
|
||
|
|
let exts = cert.tbs_cert.extensions in
|
||
|
|
(* comments from RFC5280 *)
|
||
|
|
(* 4.2.1.9 Basic Constraints *)
|
||
|
|
(* Conforming CAs MUST include this extension in all CA certificates used *)
|
||
|
|
(* to validate digital signatures on certificates and MUST mark the *)
|
||
|
|
(* extension as critical in such certificates *)
|
||
|
|
(* unfortunately, there are 8 CA certs (including the one which
|
||
|
|
signed google.com) which are _NOT_ marked as critical *)
|
||
|
|
( match Extension.(find Basic_constraints exts) with
|
||
|
|
| Some (_ , (true, _)) -> true
|
||
|
|
| _ -> false ) &&
|
||
|
|
|
||
|
|
(* 4.2.1.3 Key Usage *)
|
||
|
|
(* Conforming CAs MUST include key usage extension *)
|
||
|
|
(* CA Cert (cacert.org) does not *)
|
||
|
|
( match Extension.(find Key_usage exts) with
|
||
|
|
(* When present, conforming CAs SHOULD mark this extension as critical *)
|
||
|
|
(* yeah, you wish... *)
|
||
|
|
| Some (_, usage) -> List.mem `Key_cert_sign usage
|
||
|
|
| _ -> false ) &&
|
||
|
|
|
||
|
|
(* if we require this, we cannot talk to github.com
|
||
|
|
(* 4.2.1.12. Extended Key Usage
|
||
|
|
If a certificate contains both a key usage extension and an extended
|
||
|
|
key usage extension, then both extensions MUST be processed
|
||
|
|
independently and the certificate MUST only be used for a purpose
|
||
|
|
consistent with both extensions. If there is no purpose consistent
|
||
|
|
with both extensions, then the certificate MUST NOT be used for any
|
||
|
|
purpose. *)
|
||
|
|
( match extn_ext_key_usage cert with
|
||
|
|
| Some (_, Ext_key_usage usages) -> List.mem Any usages
|
||
|
|
| _ -> true ) &&
|
||
|
|
*)
|
||
|
|
|
||
|
|
(* Name Constraints - name constraints should match servername *)
|
||
|
|
|
||
|
|
(* check criticality *)
|
||
|
|
Extension.for_all (fun (Extension.B (k, v)) ->
|
||
|
|
match k with
|
||
|
|
| Extension.Key_usage -> true
|
||
|
|
| Extension.Basic_constraints -> true
|
||
|
|
| _ -> not (Extension.critical k v) )
|
||
|
|
exts
|
||
|
|
|
||
|
|
let validate_server_extensions cert =
|
||
|
|
Extension.for_all (fun (Extension.B (k, v)) ->
|
||
|
|
match k, v with
|
||
|
|
| Extension.Basic_constraints, (_, (true, _)) ->
|
||
|
|
if is_self_signed cert then
|
||
|
|
(Log.warn (fun m -> m "allowing self-signed certificate with BasicConstraints CA true");
|
||
|
|
true)
|
||
|
|
else
|
||
|
|
false
|
||
|
|
| Extension.Basic_constraints, (_, (false, _)) -> true
|
||
|
|
| Extension.Key_usage, _ -> true
|
||
|
|
| Extension.Ext_key_usage, _ -> true
|
||
|
|
| Extension.Subject_alt_name, _ -> true
|
||
|
|
| Extension.Policies, (crit, ps) -> not crit || List.mem `Any ps
|
||
|
|
(* we've to deal with _all_ extensions marked critical! *)
|
||
|
|
| _, _ -> not (Extension.critical k v))
|
||
|
|
cert.Certificate.asn.tbs_cert.extensions
|
||
|
|
|
||
|
|
let valid_trust_anchor_extensions cert =
|
||
|
|
match cert.Certificate.asn.tbs_cert.version with
|
||
|
|
| `V1 | `V2 -> true
|
||
|
|
| `V3 -> validate_ca_extensions cert
|
||
|
|
|
||
|
|
let ext_authority_matches_subject trusted cert =
|
||
|
|
match Extension.(find Authority_key_id (Certificate.extensions cert),
|
||
|
|
find Subject_key_id (Certificate.extensions trusted))
|
||
|
|
with
|
||
|
|
| (_, None) | (None, _) -> true (* not mandatory *)
|
||
|
|
| Some (_, (Some auth, _, _)), Some (_, au) -> String.equal auth au
|
||
|
|
(* TODO: check exact rules in RFC5280 *)
|
||
|
|
| Some (_, (None, _, _)), _ -> true (* not mandatory *)
|
||
|
|
|
||
|
|
(* t -> t list (* set *) -> t list list *)
|
||
|
|
let rec build_paths fst rst =
|
||
|
|
match
|
||
|
|
List.filter
|
||
|
|
(fun x -> Distinguished_name.equal (Certificate.issuer fst) (Certificate.subject x))
|
||
|
|
rst
|
||
|
|
with
|
||
|
|
| [] -> [[fst]]
|
||
|
|
| xs ->
|
||
|
|
let tails =
|
||
|
|
List.fold_left
|
||
|
|
(fun acc x -> acc @ build_paths x (List.filter (fun y -> x <> y) rst))
|
||
|
|
[[]]
|
||
|
|
xs
|
||
|
|
in
|
||
|
|
List.map (fun x -> fst :: x) tails
|
||
|
|
|
||
|
|
type ca_error = [
|
||
|
|
| signature_error
|
||
|
|
| `CAIssuerSubjectMismatch of Certificate.t
|
||
|
|
| `CAInvalidVersion of Certificate.t
|
||
|
|
| `CACertificateExpired of Certificate.t * Ptime.t option
|
||
|
|
| `CAInvalidExtensions of Certificate.t
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_ca_error ppf = function
|
||
|
|
| #signature_error as e -> pp_signature_error ppf e
|
||
|
|
| `CAIssuerSubjectMismatch c ->
|
||
|
|
Fmt.pf ppf "CA certificate %a: issuer does not match subject" Certificate.pp c
|
||
|
|
| `CAInvalidVersion c ->
|
||
|
|
Fmt.pf ppf "CA certificate %a: version 3 is required for extensions" Certificate.pp c
|
||
|
|
| `CAInvalidExtensions c ->
|
||
|
|
Fmt.pf ppf "CA certificate %a: invalid CA extensions" Certificate.pp c
|
||
|
|
| `CACertificateExpired (c, now) ->
|
||
|
|
let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
|
||
|
|
Fmt.pf ppf "CA certificate %a: expired (now %a)" Certificate.pp c
|
||
|
|
Fmt.(option ~none:(any "no timestamp provided") pp_pt) now
|
||
|
|
|
||
|
|
type leaf_validation_error = [
|
||
|
|
| `LeafCertificateExpired of Certificate.t * Ptime.t option
|
||
|
|
| `LeafInvalidIP of Certificate.t * Ipaddr.t option
|
||
|
|
| `LeafInvalidName of Certificate.t * [`host] Domain_name.t option
|
||
|
|
| `LeafInvalidVersion of Certificate.t
|
||
|
|
| `LeafInvalidExtensions of Certificate.t
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_leaf_validation_error ppf = function
|
||
|
|
| `LeafCertificateExpired (c, now) ->
|
||
|
|
let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
|
||
|
|
Fmt.pf ppf "leaf certificate %a expired (now %a)" Certificate.pp c
|
||
|
|
Fmt.(option ~none:(any "no timestamp provided") pp_pt) now
|
||
|
|
| `LeafInvalidIP (c, ip) ->
|
||
|
|
Fmt.pf ppf "leaf certificate %a does not contain the IP %a (IPs present: %a)String"
|
||
|
|
Certificate.pp c Fmt.(option ~none:(any "none") Ipaddr.pp) ip
|
||
|
|
Fmt.(list ~sep:(any ", ") Ipaddr.pp) (Certificate.ips c |> Ipaddr.Set.elements)
|
||
|
|
| `LeafInvalidName (c, n) ->
|
||
|
|
Fmt.pf ppf "leaf certificate %a does not contain the name %a"
|
||
|
|
Certificate.pp c Fmt.(option ~none:(any "none") Domain_name.pp) n
|
||
|
|
| `LeafInvalidVersion c ->
|
||
|
|
Fmt.pf ppf "leaf certificate %a: version 3 is required for extensions" Certificate.pp c
|
||
|
|
| `LeafInvalidExtensions c ->
|
||
|
|
Fmt.pf ppf "leaf certificate %a: invalid server extensions" Certificate.pp c
|
||
|
|
|
||
|
|
type chain_validation_error = [
|
||
|
|
| `IntermediateInvalidExtensions of Certificate.t
|
||
|
|
| `IntermediateCertificateExpired of Certificate.t * Ptime.t option
|
||
|
|
| `IntermediateInvalidVersion of Certificate.t
|
||
|
|
| `ChainIssuerSubjectMismatch of Certificate.t * Certificate.t
|
||
|
|
| `ChainAuthorityKeyIdSubjectKeyIdMismatch of Certificate.t * Certificate.t
|
||
|
|
| `ChainInvalidPathlen of Certificate.t * int
|
||
|
|
| `EmptyCertificateChain
|
||
|
|
| `NoTrustAnchor of Certificate.t
|
||
|
|
| `Revoked of Certificate.t
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_chain_validation_error ppf = function
|
||
|
|
| `IntermediateInvalidExtensions c ->
|
||
|
|
Fmt.pf ppf "intermediate certificate %a: invalid extensions" Certificate.pp c
|
||
|
|
| `IntermediateCertificateExpired (c, now) ->
|
||
|
|
let pp_pt = Ptime.pp_human ~tz_offset_s:0 () in
|
||
|
|
Fmt.pf ppf "intermediate certificate %a expired (now %a)" Certificate.pp c
|
||
|
|
Fmt.(option ~none:(any "no timestamp provided") pp_pt) now
|
||
|
|
| `IntermediateInvalidVersion c ->
|
||
|
|
Fmt.pf ppf "intermediate certificate %a: version 3 is required for extensions"
|
||
|
|
Certificate.pp c
|
||
|
|
| `ChainIssuerSubjectMismatch (c, parent) ->
|
||
|
|
Fmt.pf ppf "invalid chain: issuer of %a does not match subject of %a"
|
||
|
|
Certificate.pp c Certificate.pp parent
|
||
|
|
| `ChainAuthorityKeyIdSubjectKeyIdMismatch (c, parent) ->
|
||
|
|
Fmt.pf ppf "invalid chain: authority key id extension of %a does not match subject key id extension of %a"
|
||
|
|
Certificate.pp c Certificate.pp parent
|
||
|
|
| `ChainInvalidPathlen (c, pathlen) ->
|
||
|
|
Fmt.pf ppf "invalid chain: the path length of %a is smaller than the required path length %d"
|
||
|
|
Certificate.pp c pathlen
|
||
|
|
| `EmptyCertificateChain -> Fmt.string ppf "certificate chain is empty"
|
||
|
|
| `NoTrustAnchor c ->
|
||
|
|
Fmt.pf ppf "no trust anchor found for %a" Certificate.pp c
|
||
|
|
| `Revoked c ->
|
||
|
|
Fmt.pf ppf "certificate %a is revoked" Certificate.pp c
|
||
|
|
|
||
|
|
type chain_error = [
|
||
|
|
| signature_error
|
||
|
|
| leaf_validation_error
|
||
|
|
| chain_validation_error
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_chain_error ppf = function
|
||
|
|
| #signature_error as e -> pp_signature_error ppf e
|
||
|
|
| #leaf_validation_error as l -> pp_leaf_validation_error ppf l
|
||
|
|
| #chain_validation_error as c -> pp_chain_validation_error ppf c
|
||
|
|
|
||
|
|
type fingerprint_validation_error = [
|
||
|
|
| `InvalidFingerprint of Certificate.t * string * string
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_fingerprint_validation_error ppf = function
|
||
|
|
| `InvalidFingerprint (c, c_fp, fp) ->
|
||
|
|
Fmt.pf ppf "fingerprint for %a (computed %a) does not match, expected %a"
|
||
|
|
Certificate.pp c Ohex.pp c_fp Ohex.pp fp
|
||
|
|
|
||
|
|
type validation_error = [
|
||
|
|
| signature_error
|
||
|
|
| leaf_validation_error
|
||
|
|
| fingerprint_validation_error
|
||
|
|
| `EmptyCertificateChain
|
||
|
|
| `InvalidChain
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_validation_error ppf = function
|
||
|
|
| #signature_error as e -> pp_signature_error ppf e
|
||
|
|
| #leaf_validation_error as l -> pp_leaf_validation_error ppf l
|
||
|
|
| #fingerprint_validation_error as f -> pp_fingerprint_validation_error ppf f
|
||
|
|
| `EmptyCertificateChain ->
|
||
|
|
Fmt.string ppf "provided certificate chain is empty"
|
||
|
|
| `InvalidChain -> Fmt.string ppf "invalid certificate chain"
|
||
|
|
|
||
|
|
type r = ((Certificate.t list * Certificate.t) option, validation_error) result
|
||
|
|
|
||
|
|
(* TODO RFC 5280: A certificate MUST NOT include more than one
|
||
|
|
instance of a particular extension. *)
|
||
|
|
|
||
|
|
let is_cert_valid now cert =
|
||
|
|
match
|
||
|
|
validate_time now cert,
|
||
|
|
version_matches_extensions cert,
|
||
|
|
validate_ca_extensions cert
|
||
|
|
with
|
||
|
|
| (true, true, true) -> Ok ()
|
||
|
|
| (false, _, _) -> Error (`IntermediateCertificateExpired (cert, now))
|
||
|
|
| (_, false, _) -> Error (`IntermediateInvalidVersion cert)
|
||
|
|
| (_, _, false) -> Error (`IntermediateInvalidExtensions cert)
|
||
|
|
|
||
|
|
let is_ca_cert_valid allowed_hashes now cert =
|
||
|
|
match
|
||
|
|
is_self_signed cert,
|
||
|
|
version_matches_extensions cert,
|
||
|
|
validate_signature allowed_hashes cert cert,
|
||
|
|
validate_time now cert,
|
||
|
|
valid_trust_anchor_extensions cert
|
||
|
|
with
|
||
|
|
| (true, true, Ok (), true, true) -> Ok ()
|
||
|
|
| (false, _, _, _, _) -> Error (`CAIssuerSubjectMismatch cert)
|
||
|
|
| (_, false, _, _, _) -> Error (`CAInvalidVersion cert)
|
||
|
|
| (_, _, Error e, _, _) -> Error e
|
||
|
|
| (_, _, _, false, _) -> Error (`CACertificateExpired (cert, now))
|
||
|
|
| (_, _, _, _, false) -> Error (`CAInvalidExtensions cert)
|
||
|
|
|
||
|
|
let valid_ca ?(allowed_hashes = all_hashes) ?time cacert =
|
||
|
|
is_ca_cert_valid allowed_hashes time cacert
|
||
|
|
|
||
|
|
let is_server_cert_valid ip host now cert =
|
||
|
|
match
|
||
|
|
validate_time now cert,
|
||
|
|
maybe_validate_ip cert ip,
|
||
|
|
maybe_validate_hostname cert host,
|
||
|
|
version_matches_extensions cert,
|
||
|
|
validate_server_extensions cert
|
||
|
|
with
|
||
|
|
| (true, true, true, true, true) -> Ok ()
|
||
|
|
| (false, _, _, _, _) -> Error (`LeafCertificateExpired (cert, now))
|
||
|
|
| (_, false, _, _, _) -> Error (`LeafInvalidIP (cert, ip))
|
||
|
|
| (_, _, false, _, _) -> Error (`LeafInvalidName (cert, host))
|
||
|
|
| (_, _, _, false, _) -> Error (`LeafInvalidVersion cert)
|
||
|
|
| (_, _, _, _, false) -> Error (`LeafInvalidExtensions cert)
|
||
|
|
|
||
|
|
let signs hash pathlen trusted cert =
|
||
|
|
match
|
||
|
|
issuer_matches_subject trusted cert,
|
||
|
|
ext_authority_matches_subject trusted cert,
|
||
|
|
validate_signature hash trusted cert,
|
||
|
|
validate_path_len pathlen trusted
|
||
|
|
with
|
||
|
|
| (true, true, Ok (), true) -> Ok ()
|
||
|
|
| (false, _, _, _) -> Error (`ChainIssuerSubjectMismatch (trusted, cert))
|
||
|
|
| (_, false, _, _) -> Error (`ChainAuthorityKeyIdSubjectKeyIdMismatch (trusted, cert))
|
||
|
|
| (_, _, Error e, _) -> Error e
|
||
|
|
| (_, _, _, false) -> Error (`ChainInvalidPathlen (trusted, pathlen))
|
||
|
|
|
||
|
|
let issuer trusted cert =
|
||
|
|
List.filter (fun p -> issuer_matches_subject p cert) trusted
|
||
|
|
|
||
|
|
let rec validate_anchors revoked hash pathlen cert = function
|
||
|
|
| [] -> Error (`NoTrustAnchor cert)
|
||
|
|
| x::xs -> match signs hash pathlen x cert with
|
||
|
|
| Ok _ -> if revoked ~issuer:x ~cert then Error (`Revoked cert) else Ok x
|
||
|
|
| Error _ -> validate_anchors revoked hash pathlen cert xs
|
||
|
|
|
||
|
|
let verify_single_chain now ?(revoked = fun ~issuer:_ ~cert:_ -> false) hash anchors chain =
|
||
|
|
let rec climb pathlen = function
|
||
|
|
| cert :: issuer :: certs ->
|
||
|
|
let* () = is_cert_valid now issuer in
|
||
|
|
let* () = if revoked ~issuer ~cert then Error (`Revoked cert) else Ok () in
|
||
|
|
let* () = signs hash pathlen issuer cert in
|
||
|
|
climb (succ pathlen) (issuer :: certs)
|
||
|
|
| [c] ->
|
||
|
|
let anchors = issuer anchors c in
|
||
|
|
validate_anchors revoked hash pathlen c anchors
|
||
|
|
| [] -> Error `EmptyCertificateChain
|
||
|
|
in
|
||
|
|
climb 0 chain
|
||
|
|
|
||
|
|
let verify_chain ?ip ~host ~time ?revoked ?(allowed_hashes = sha2) ~anchors = function
|
||
|
|
| [] -> Error `EmptyCertificateChain
|
||
|
|
| server :: certs ->
|
||
|
|
let now = time () in
|
||
|
|
let anchors = List.filter (validate_time now) anchors in
|
||
|
|
let* () = is_server_cert_valid ip host now server in
|
||
|
|
verify_single_chain now ?revoked allowed_hashes anchors (server :: certs)
|
||
|
|
|
||
|
|
let rec any_m e f = function
|
||
|
|
| [] -> Error e
|
||
|
|
| c::cs -> match f c with
|
||
|
|
| Ok ta -> Ok (Some (c, ta))
|
||
|
|
| Error _ -> any_m e f cs
|
||
|
|
|
||
|
|
let verify_chain_of_trust ?ip ~host ~time ?revoked ?(allowed_hashes = sha2) ~anchors = function
|
||
|
|
| [] -> Error `EmptyCertificateChain
|
||
|
|
| server :: certs ->
|
||
|
|
let now = time () in
|
||
|
|
(* verify server! *)
|
||
|
|
let* () = is_server_cert_valid ip host now server in
|
||
|
|
(* build all paths *)
|
||
|
|
let paths = build_paths server certs
|
||
|
|
and anchors = List.filter (validate_time now) anchors
|
||
|
|
in
|
||
|
|
(* exists there one which is good? *)
|
||
|
|
any_m `InvalidChain (verify_single_chain now ?revoked allowed_hashes anchors) paths
|
||
|
|
|
||
|
|
let valid_cas ?(allowed_hashes = all_hashes) ?time cas =
|
||
|
|
List.filter (fun cert ->
|
||
|
|
Result.is_ok (is_ca_cert_valid allowed_hashes time cert))
|
||
|
|
cas
|
||
|
|
|
||
|
|
let fingerprint_verification ?ip host now fingerprint fp = function
|
||
|
|
| [] -> Error `EmptyCertificateChain
|
||
|
|
| server::_ ->
|
||
|
|
let computed_fingerprint = fp server in
|
||
|
|
if String.equal computed_fingerprint fingerprint then
|
||
|
|
match
|
||
|
|
validate_time now server,
|
||
|
|
maybe_validate_hostname server host,
|
||
|
|
maybe_validate_ip server ip
|
||
|
|
with
|
||
|
|
| true , true , true -> Ok None
|
||
|
|
| false, _ , _ -> Error (`LeafCertificateExpired (server, now))
|
||
|
|
| _ , false, _ -> Error (`LeafInvalidName (server, host))
|
||
|
|
| _ , _ , false -> Error (`LeafInvalidIP (server, ip))
|
||
|
|
else
|
||
|
|
Error (`InvalidFingerprint (server, computed_fingerprint, fingerprint))
|
||
|
|
|
||
|
|
let trust_key_fingerprint ?ip ~host ~time ~hash ~fingerprint =
|
||
|
|
let now = time () in
|
||
|
|
let fp cert = Public_key.fingerprint ~hash (Certificate.public_key cert) in
|
||
|
|
fingerprint_verification ?ip host now fingerprint fp
|
||
|
|
|
||
|
|
let trust_cert_fingerprint ?ip ~host ~time ~hash ~fingerprint =
|
||
|
|
let now = time () in
|
||
|
|
let fp = Certificate.fingerprint hash in
|
||
|
|
fingerprint_verification ?ip host now fingerprint fp
|
||
|
|
|
||
|
|
(* RFC5246 says 'root certificate authority MAY be omitted' *)
|
||
|
|
|
||
|
|
(* TODO: how to deal with
|
||
|
|
2.16.840.1.113730.1.1 - Netscape certificate type
|
||
|
|
2.16.840.1.113730.1.12 - SSL server name
|
||
|
|
2.16.840.1.113730.1.13 - Netscape certificate comment *)
|
||
|
|
|
||
|
|
(* stuff from 4366 (TLS extensions):
|
||
|
|
- root CAs
|
||
|
|
- client cert url *)
|
||
|
|
|
||
|
|
(* Future TODO Certificate Revocation Lists and OCSP (RFC6520)
|
||
|
|
2.16.840.1.113730.1.2 - Base URL
|
||
|
|
2.16.840.1.113730.1.3 - Revocation URL
|
||
|
|
2.16.840.1.113730.1.4 - CA Revocation URL
|
||
|
|
2.16.840.1.113730.1.7 - Renewal URL
|
||
|
|
2.16.840.1.113730.1.8 - Netscape CA policy URL
|
||
|
|
|
||
|
|
2.5.4.38 - id-at-authorityRevocationList
|
||
|
|
2.5.4.39 - id-at-certificateRevocationList
|
||
|
|
|
||
|
|
do not forget about 'authority information access' (private internet extension -- 4.2.2 of 5280) *)
|
||
|
|
|
||
|
|
(* Future TODO: Policies
|
||
|
|
2.5.29.32 - Certificate Policies
|
||
|
|
2.5.29.33 - Policy Mappings
|
||
|
|
2.5.29.36 - Policy Constraints
|
||
|
|
*)
|
||
|
|
|
||
|
|
(* Future TODO: anything with subject_id and issuer_id ? seems to be not used by anybody *)
|