Coverage report: /home/ellis/.stash/lisp/cl-plus-ssl/src/x509.lisp
Kind | Covered | All | % |
expression | 83 | 532 | 15.6 |
branch | 3 | 80 | 3.8 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;;; -*- Mode: LISP; Syntax: COMMON-LISP; indent-tabs-mode: nil; coding: utf-8; show-trailing-whitespace: t -*-
3
;;; Copyright (C) contributors as per cl+ssl git history
5
;;; See LICENSE for details.
10
ASN1 string validation references:
11
- https://github.com/digitalbazaar/forge/blob/909e312878838f46ba6d70e90264650b05eb8bde/js/asn1.js
12
- http://www.obj-sys.com/asn1tutorial/node128.html
13
- https://github.com/deadtrickster/ssl_verify_hostname.erl/blob/master/src/ssl_verify_hostname.erl
14
- https://golang.org/src/encoding/asn1/asn1.go?m=text
16
(defgeneric decode-asn1-string (asn1-string type))
18
(defun copy-bytes-to-lisp-vector (src-ptr vector count)
19
(declare (type (simple-array (unsigned-byte 8)) vector)
21
(optimize (safety 0) (debug 0) (speed 3)))
22
(dotimes (i count vector)
23
(setf (aref vector i) (cffi:mem-aref src-ptr :unsigned-char i))))
25
(defun asn1-string-bytes-vector (asn1-string)
26
(let* ((data (asn1-string-data asn1-string))
27
(length (asn1-string-length asn1-string))
28
(vector (cffi:make-shareable-byte-vector length)))
29
(copy-bytes-to-lisp-vector data vector length)
32
(defun asn1-iastring-char-p (byte)
33
(declare (type (unsigned-byte 8) byte)
39
(defun asn1-iastring-p (bytes)
40
(declare (type (simple-array (unsigned-byte 8)) bytes)
44
(every #'asn1-iastring-char-p bytes))
46
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-iastring+)))
47
(let ((bytes (asn1-string-bytes-vector asn1-string)))
48
(if (asn1-iastring-p bytes)
49
(flex:octets-to-string bytes :external-format :ascii)
50
(error 'invalid-asn1-string :type '+v-asn1-iastring+))))
52
(defun asn1-printable-char-p (byte)
53
(declare (type (unsigned-byte 8) byte)
59
((and (>= byte #.(char-code #\a))
60
(<= byte #.(char-code #\z)))
63
((and (>= byte #.(char-code #\'))
64
(<= byte #.(char-code #\/)))
67
((and (>= byte #.(char-code #\0))
68
(<= byte #.(char-code #\9)))
71
((and (>= byte #.(char-code #\A))
72
(<= byte #.(char-code #\Z)))
75
((= byte #.(char-code #\ )) t)
76
((= byte #.(char-code #\:)) t)
77
((= byte #.(char-code #\=)) t)
78
((= byte #.(char-code #\?)) t)))
80
(defun asn1-printable-string-p (bytes)
81
(declare (type (simple-array (unsigned-byte 8)) bytes)
85
(every #'asn1-printable-char-p bytes))
87
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-printablestring+)))
88
(let* ((bytes (asn1-string-bytes-vector asn1-string)))
89
(if (asn1-printable-string-p bytes)
90
(flex:octets-to-string bytes :external-format :ascii)
91
(error 'invalid-asn1-string :type '+v-asn1-printablestring+))))
93
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-utf8string+)))
94
(let* ((data (asn1-string-data asn1-string))
95
(length (asn1-string-length asn1-string)))
96
(cffi:foreign-string-to-lisp data :count length :encoding :utf-8)))
98
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-universalstring+)))
99
(if (= 0 (mod (asn1-string-length asn1-string) 4))
100
;; cffi sometimes fails here on sbcl? idk why (maybe threading?)
101
;; fail: Illegal :UTF-32 character starting at position 48...
102
;; when (length bytes) is 48...
103
;; so I'm passing :count explicitly
104
(or (ignore-errors (cffi:foreign-string-to-lisp (asn1-string-data asn1-string) :count (asn1-string-length asn1-string) :encoding :utf-32))
105
(error 'invalid-asn1-string :type '+v-asn1-universalstring+))
106
(error 'invalid-asn1-string :type '+v-asn1-universalstring+)))
108
(defun asn1-teletex-char-p (byte)
109
(declare (type (unsigned-byte 8) byte)
116
(defun asn1-teletex-string-p (bytes)
117
(declare (type (simple-array (unsigned-byte 8)) bytes)
121
(every #'asn1-teletex-char-p bytes))
123
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-teletexstring+)))
124
(let ((bytes (asn1-string-bytes-vector asn1-string)))
125
(if (asn1-teletex-string-p bytes)
126
(flex:octets-to-string bytes :external-format :ascii)
127
(error 'invalid-asn1-string :type '+v-asn1-teletexstring+))))
129
(defmethod decode-asn1-string (asn1-string (type (eql +v-asn1-bmpstring+)))
130
(if (= 0 (mod (asn1-string-length asn1-string) 2))
131
(or (ignore-errors (cffi:foreign-string-to-lisp (asn1-string-data asn1-string) :count (asn1-string-length asn1-string) :encoding :utf-16/be))
132
(error 'invalid-asn1-string :type '+v-asn1-bmpstring+))
133
(error 'invalid-asn1-string :type '+v-asn1-bmpstring+)))
135
;; TODO: respect asn1-string type
136
(defun try-get-asn1-string-data (asn1-string allowed-types)
137
(let ((type (asn1-string-type asn1-string)))
138
(assert (member (asn1-string-type asn1-string) allowed-types) nil "Invalid asn1 string type")
139
(decode-asn1-string asn1-string type)))
141
;; ASN1 Times are represented with ASN1 Strings
142
(defun decode-asn1-time (asn1-time)
143
(when (zerop (asn1-time-check asn1-time))
144
(error "asn1-time is not a syntactically valid ASN1 UTCTime"))
145
(let ((time-string (flex:octets-to-string (asn1-string-bytes-vector asn1-time)
146
:external-format :ascii)))
147
(let* ((utctime-p (= 1 (asn1-utctime-check asn1-time)))
148
(year-len (if utctime-p 2 4))
149
(year-part (parse-integer (subseq time-string 0 year-len)))
151
(if (>= year-part 50)
155
(flet ((get-element-after-year (position)
158
(+ position year-len)
159
(+ position year-len 2)))))
160
(let ((month (get-element-after-year 0))
161
(day (get-element-after-year 2))
162
(hour (get-element-after-year 4))
163
(minute (get-element-after-year 6))
164
(second (get-element-after-year 8)))
165
(encode-universal-time second minute hour day month year 0))))))
167
(defun slurp-stream (stream)
168
"Returns a sequence containing the STREAM bytes; the
169
sequence is created by CFFI:MAKE-SHAREABLE-BYTE-VECTOR,
170
therefore it can safely be passed to
171
CFFI:WITH-POINTER-TO-VECTOR-DATA."
172
(let ((seq (cffi:make-shareable-byte-vector (file-length stream))))
173
(read-sequence seq stream)
176
(defgeneric decode-certificate (format bytes)
178
"The BYTES must be created by CFFI:MAKE-SHAREABLE-BYTE-VECTOR (because
179
we are going to pass them to CFFI:WITH-POINTER-TO-VECTOR-DATA)"))
181
(defmethod decode-certificate ((format (eql :der)) bytes)
182
(cffi:with-pointer-to-vector-data (buf* bytes)
183
(cffi:with-foreign-object (buf** :pointer)
184
(setf (cffi:mem-ref buf** :pointer) buf*)
185
(let ((cert (d2i-x509 (cffi:null-pointer) buf** (length bytes))))
186
(when (cffi:null-pointer-p cert)
187
(error 'ssl-error-call :message "d2i-X509 failed" :queue (read-ssl-error-queue)))
190
(defun cert-format-from-path (path)
191
;; or match "pem" type too and raise unknown format error?
192
(if (equal "der" (pathname-type path))
196
(defun decode-certificate-from-file (path &key format)
197
(let ((bytes (with-open-file (stream path :element-type '(unsigned-byte 8))
198
(slurp-stream stream)))
199
(format (or format (cert-format-from-path path))))
200
(decode-certificate format bytes)))
202
(defun certificate-alt-names (cert)
204
* The return value is the decoded extension or NULL on
205
* error. The actual error can have several different causes,
206
* the value of *crit reflects the cause:
207
* >= 0, extension found but not decoded (reflects critical value).
208
* -1 extension not found.
209
* -2 extension occurs more than once.
211
(cffi:with-foreign-object (crit* :int)
212
(let ((result (x509-get-ext-d2i cert +NID-subject-alt-name+ crit* (cffi:null-pointer))))
213
(if (cffi:null-pointer-p result)
214
(let ((crit (cffi:mem-ref crit* :int)))
217
(error "X509_get_ext_d2i: subject-alt-name extension decoding error"))
218
((= crit -1) ;; extension not found, return NULL
221
(error "X509_get_ext_d2i: subject-alt-name extension occurs more than once"))))
224
(defun certificate-dns-alt-names (cert)
225
(let ((altnames (certificate-alt-names cert)))
226
(unless (cffi:null-pointer-p altnames)
228
(flet ((alt-name-to-string (alt-name)
229
(cffi:with-foreign-slots ((type data) alt-name (:struct general-name))
232
(let ((address (asn1-string-bytes-vector data)))
233
(usocket:host-to-hostname address)))
235
(or (try-get-asn1-string-data data '(#. +v-asn1-iastring+))
236
(error "Malformed certificate: possibly NULL in dns-alt-name")))))))
237
(let ((altnames-count (sk-general-name-num altnames)))
238
(loop for i from 0 below altnames-count
239
as alt-name = (sk-general-name-value altnames i)
240
collect (alt-name-to-string alt-name))))
241
(general-names-free altnames)))))
243
(defun certificate-subject-common-names (cert)
245
(subject-name (x509-get-subject-name cert)))
246
(when (cffi:null-pointer-p subject-name)
247
(error "X509_get_subject_name returned NULL"))
248
(flet ((extract-cn ()
249
(setf i (x509-name-get-index-by-nid subject-name +NID-commonName+ i))
251
(let* ((entry (x509-name-get-entry subject-name i)))
252
(when (cffi:null-pointer-p entry)
253
(error "X509_NAME_get_entry returned NULL"))
254
(let ((entry-data (x509-name-entry-get-data entry)))
255
(when (cffi:null-pointer-p entry-data)
256
(error "X509_NAME_ENTRY_get_data returned NULL"))
257
(try-get-asn1-string-data entry-data '(#.+v-asn1-utf8string+
259
#.+v-asn1-printablestring+
260
#.+v-asn1-universalstring+
261
#.+v-asn1-teletexstring+)))))))
268
(defun certificate-not-after-time (certificate)
269
"Returns a universal-time representing the time after
270
which the CERTIFICATE is not valid. Signals an ERROR if the
271
CERTIFICATE does not have a properly formatted time. "
273
(when (or (openssl-is-not-even 1 1 0)
275
(error "certificate-not-after-time currently requires version OpenSSL 1.1.0 or newer"))
277
(let ((asn1-time (x509-get0-not-after certificate)))
278
(when (cffi:null-pointer-p asn1-time)
279
(error "X509_get0_notAfter returned NULL"))
280
(decode-asn1-time asn1-time)))
282
(defun certificate-not-before-time (certificate)
283
"Returns a universal-time representing the time before
284
which the CERTIFICATE is not valid. Signals an ERROR if
285
the CERTIFICATE does not have a properly formatted time."
287
(when (or (openssl-is-not-even 1 1 0)
289
(error "certificate-not-before-time currently requires version OpenSSL 1.1.0 or newer"))
291
(let ((asn1-time (x509-get0-not-before certificate)))
292
(when (cffi:null-pointer-p asn1-time)
293
(error "X509_get0_notBefore returned NULL"))
294
(decode-asn1-time asn1-time)))
296
(defun certificate-fingerprint (certificate &optional (algorithm :sha1))
297
"Return the fingerprint of CERTIFICATE as a byte-vector. ALGORITHM is a string
298
designator for the digest algorithm to use (it defaults to SHA-1)."
300
(let ((evp (evp-get-digest-by-name (string algorithm))))
301
(when (cffi:null-pointer-p evp)
302
(error 'ssl-error-call
303
:message (format nil "unknown digest algorithm ~A" algorithm)
304
:queue (read-ssl-error-queue)))
305
(let* ((size (funcall (if (openssl-is-at-least 3 0 0)
309
(fingerprint (cffi:make-shareable-byte-vector size)))
310
(cffi:with-pointer-to-vector-data (buf fingerprint)
311
(unless (= 1 (x509-digest certificate evp buf (cffi:null-pointer)))
312
(error 'ssl-error-call
313
:message "failed to compute fingerprint of certificate"
314
:queue (read-ssl-error-queue))))
317
(defun x509-cert-from-pem (pem)
318
(with-bio-input-from-string (bio pem)
319
(pem-read-x509 bio 0 0 0)))
321
(defun certificate-pem (x509)
322
(with-bio-output-to-string (bio)
323
;; man PEM_write_bio_X509:
324
;; The write routines return 1 for success or 0 for failure.
325
(unless (= 1 (pem-write-x509 bio x509))
326
(error "X509 cert cant be printed: ~s"
327
(cl+ssl::err-error-string (cl+ssl::err-get-error) (cffi:null-pointer))))))