Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dangling comments #51

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Puumerkki
![NVD status](https://github.com/solita/puumerkki/actions/workflows/nvd.yml/badge.svg)
[![Clojars Project](https://img.shields.io/clojars/v/io.github.solita-antti-mottonen/puumerkki.svg)](https://clojars.org/io.github.solita-antti-mottonen/puumerkki)

Puumerkki is a library used for signing PDF documents. It can read and modify pdfs to prepare them for signing, calculate the hash for signing, create the signature container and embed it to the pdf. Signing is done by an external service, usually with an ID card from DVV together with a card reader and its software.

Expand Down
44 changes: 26 additions & 18 deletions src/clj/puumerkki/crypt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@
; (println "ERROR: certificate chain validation failed for certificate: " sub)
(cons :invalid-certificate-chain errs))))))

;; exception -> boolean
(defn cert-validity [errs cert]
(defn cert-validity
"exception -> boolean"
[errs cert]
(try
(.checkValidity cert)
errs
Expand All @@ -133,14 +134,16 @@
errs
)))

(defn cert-revocation-status [errs cert]
;; will likely need a both cert-provided checks and a custom one not relying
;; on direct connections to CRL endpoints at time of checks.
(defn cert-revocation-status
"Will likely need a both cert-provided checks and a custom one not relying
on direct connections to CRL endpoints at time of checks."
[errs cert]
(println "WARNING: No CRL/OCSP handling yet.")
errs)

;; -> nil = ok, list of error symbols otherwise
(defn validation-errors [roots sig-b64s msg-bytes chain]
(defn validation-errors
"-> nil = ok, list of error symbols otherwise"
[roots sig-b64s msg-bytes chain]
(try
(let [cert (chain->signing-cert chain)
pub (cert->pubkey cert)]
Expand All @@ -160,8 +163,9 @@
(catch Exception e
(list :validationerror))))

;; You probably want to call validation-errors instead to be able to log/report the reasons
(defn valid? [roots sig-b64s msg-string chain]
(defn valid?
"You probably want to call validation-errors instead to be able to log/report the reasons"
[roots sig-b64s msg-string chain]
(let [errs (validation-errors roots sig-b64s (string->bytes msg-string) chain)]
(empty? errs)))

Expand All @@ -171,8 +175,9 @@
n
(recur (+ n 1) (*' 2 h)))))

;; get bit count if applicable
(defn rsa-key-size [pub]
(defn rsa-key-size
"Get the bit count if applicable"
[pub]
(try
(n-bits (.getModulus pub))
(catch Exception e
Expand Down Expand Up @@ -212,8 +217,9 @@
:crl-points (crl-distribution-points cert)
})

;; -> nil if signature is invalid | map of signing certificate information
(defn signer-info [roots sig-b64s msg-string chain]
(defn signer-info
"-> nil if signature is invalid | map of signing certificate information"
[roots sig-b64s msg-string chain]
(let [errs (validation-errors roots sig-b64s (string->bytes msg-string) chain)]
(if (empty? errs)
(cert->signer-info
Expand All @@ -222,9 +228,10 @@
(println "signer-info: got errs " errs)
false))))

;; a variable data is usually a hash of the data/document/event.
;; host prefix is added later.
(defn authentication-challenge [secret variable-data]
(defn authentication-challenge
"A variable data is usually a hash of the data/document/event.
host prefix is added later."
[secret variable-data]
(let [now (System/currentTimeMillis)
timestamped-data (str now "\n" variable-data)
signature (hmac-sign secret timestamped-data)]
Expand All @@ -235,8 +242,9 @@
(codec/base64-encode
(str "https://" host "\n" data))))

;; -> json in a string | false if unsupported version/algorithm combination
(defn digisign-authentication-request [secret host version variable-data]
(defn digisign-authentication-request
"-> json in a string | false if unsupported version/algorithm combination"
[secret host version variable-data]
(let [challenge (digisign-authentication-challenge secret host version variable-data)]
;; future version-specific handling here later
(str
Expand Down
77 changes: 43 additions & 34 deletions src/clj/puumerkki/pdf.clj
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@
array))
nil))

;; data-bvec offset pattern-vec → false | offset+length(pattern-vec)
(defn vector-match-at [data offset pattern]
(defn vector-match-at
"data-bvec offset pattern-vec → false | offset+length(pattern-vec)"
[data offset pattern]
(loop [at 0]
(let [want (get pattern at)]
(if want
Expand Down Expand Up @@ -121,9 +122,9 @@
:else
(finish-num firstp n bs))))

;; parse four space delimited decimal numbers
;; these are the before and after byte ranges of signature
(defn grab-byte-ranges [offset bs]
(defn grab-byte-ranges
"Parse four space delimited decimal numbers. These are the before and after byte ranges of signature"
[offset bs]
(let
[[sa bs] (grab-num (skip-space bs))
[la bs] (grab-num (skip-space bs))
Expand All @@ -133,17 +134,17 @@
(vector offset sa la sb lb)
(vector offset false false false false))))

;; iterate over a data at a specific position
(defn walk-buffer [buff pos]
(defn walk-buffer
"Iterate over a data at a specific position"
[buff pos]
(let [val (aget buff pos)]
(if val
(lazy-seq (cons val (walk-buffer buff (+ pos 1))))
nil)))

;; Find position of signature byte ranges from pdf data and get the numbers.
;; Now that this is handled in backend, we could also get this while adding
;; the space or by parsing the whole pdf.
(defn find-byte-ranges [data]
(defn find-byte-ranges
"Find position of signature byte ranges from pdf data and get the numbers. Now that this is handled in backend, we could also get this while adding the space or by parsing the whole pdf."
[data]
(loop [at (- (count data) 1)]
(if (= at -1)
(vector false false false false false)
Expand All @@ -152,9 +153,9 @@
(grab-byte-ranges posp (walk-buffer data posp))
(recur (- at 1)))))))

;; read the byte ranges (for hashing)
;; bvec pos1 len1 pos2 len2 → bvec' | nil, if positions or lengths are missing
(defn maybe-get-byte-ranges [data sa al sb bl]
(defn maybe-get-byte-ranges
"Read the byte ranges (for hashing) bvec pos1 len1 pos2 len2 → bvec | nil, if positions or lengths are missing"
[data sa al sb bl]
(if (and sa al sb bl)
(let [temp (byte-array (+ al bl))] ;; room for data to be hashed
(copy-bytes! temp (subarray data sa al) 0) ;; copy before signature part
Expand All @@ -174,8 +175,9 @@
[:set [:octet-string (map (partial bit-and 255) sha256sum)]]]])))


;; This is the expression to be encoded and saved to PDF
(defn make-pkcs7-asn [chain certinfo-asn sha256sum sha256withrsa]
(defn make-pkcs7-asn
"This is the expression to be encoded and saved to PDF"
[chain certinfo-asn sha256sum sha256withrsa]
[:sequence
[:identifier 1 2 840 113549 1 7 2] ;; signedData
[:explicit 0
Expand Down Expand Up @@ -205,16 +207,18 @@
[:sequence [:identifier 1 2 840 113549 1 1 11] :null] ; sha256w/rsa
[:octet-string sha256withrsa]]]]]])

;; get certificate info for pkcs7
(defn maybe-get-certificate-info [cert]
(defn maybe-get-certificate-info
"Get certificate info for pkcs7"
[cert]
(let [issuerinfo (-> cert (nth 1) (nth 4)) ;; could now use codec -> asn selectors
keyid (nth (nth cert 1) 2)]
(if (and keyid issuerinfo)
[:sequence issuerinfo keyid]
nil)))

;; count number of ascii zeroes at position (which are used for signature area filling)
(defn zeroes-at [^bytes data pos]
(defn zeroes-at
"Count the number of ascii zeroes at position (which are used for signature area filling)"
[^bytes data pos]
(loop [pos pos n 0]
(let [val (aget data pos)]
(if (= val 48)
Expand Down Expand Up @@ -253,9 +257,9 @@
(sign [content]
(byte-array (byte-array 100)))))

;; "Signer Name", (nil | image) -> PDSignature

(defn signature [name]
(defn signature
"Signer Name -> PDSignature"
[name]
(doto (PDSignature.)
(.setFilter PDSignature/FILTER_ADOBE_PPKLITE)
(.setSubFilter PDSignature/SUBFILTER_ADBE_PKCS7_DETACHED)
Expand All @@ -274,8 +278,9 @@

;; Warning! Do not parse user supplied PDF:s without proper safety equipment.

;; "foo.pdf" "foo-signed.pdf" "Signer Name" -> "foo-signed.pdf" | nil on error
(defn add-signature-space [pdf-path output-pdf-path signer-name]
(defn add-signature-space
"foo.pdf foo-signed.pdf Signer Name -> foo-signed.pdf | nil on error"
[pdf-path output-pdf-path signer-name]
(try
(let [input-document (io/file pdf-path)
doc (PDDocument/load input-document)
Expand All @@ -290,7 +295,7 @@
output-pdf-path)
(catch Exception e
;; log reason
; add optoinal error logger
; add optional error logger
nil)))

(defn add-watermarked-signature-space [pdf-path output-pdf-path signer-name image-path x y]
Expand Down Expand Up @@ -327,8 +332,9 @@
))


;; write-signature pdf-data-byte-array pkcs7-asn1-der-byte-sequence → pdf-data-byte-array (modified) | nil
(defn write-signature! [data pkcs7]
(defn write-signature!
"Write-signature pdf-data-byte-array pkcs7-asn1-der-byte-sequence → pdf-data-byte-array (modified) | nil"
[data pkcs7]
(let [signature (seq->byte-array (codec/hex-encode pkcs7))
pos (find-signature-space data)]
(if pos
Expand All @@ -351,8 +357,9 @@
(if-let [node (codec/asn1-find asn [:sequence [:identifier 1 2 840 113549 1 9 4] [:set :octet-string]])]
(-> node (nth 2) (nth 1) (nth 1))))

;; pdf-data -> nil | validish-signature-ast (only structure and digest is verified, not the actual signature)
(defn cursory-verify-signature [data]
(defn cursory-verify-signature
"pdf-data -> nil | validish-signature-ast (only structure and digest is verified, not the actual signature)"
[data]
(let [[at sa la sb lb] (find-byte-ranges data)]
(if (and
at sa sb lb ;; byte ranges there
Expand All @@ -373,8 +380,9 @@
asn-ast
nil)))))))))

;; first part of verification
(defn partial-verify-signatures [pdf-path]
(defn partial-verify-signatures
"First part of verification"
[pdf-path]
(try
(let [pdf (read-pdf pdf-path)]
(reduce
Expand Down Expand Up @@ -406,8 +414,9 @@
(catch Exception e
nil)))

;; switch to new validation via .crypt
(defn verify-signatures [path]
(defn verify-signatures
"Switch to new validation via .crypt"
[path]
(and
(partial-verify-signatures path)
; check revocation lists
Expand Down
40 changes: 24 additions & 16 deletions src/cljc/puumerkki/codec.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@
(length-bs (count bs))
bs)))

;; as encode-set, but order is lexicographic
(defn encode-set-of [& encoded]
(defn encode-set-of
"As encode-set, but order is lexicographic"
[& encoded]
(let [bs (apply concat (sort lex< encoded))]
(concat
(identifier class-universal is-constructed tag-set)
Expand Down Expand Up @@ -350,8 +351,9 @@
:else
(vector true (bit-or (bit-shift-left out 7) this) lst))))

;; parsers are lst → ok/bool value/reason rest-of-input
(defn parse-identifier [tag tail]
(defn parse-identifier
"Parsers are lst → ok/bool value/reason rest-of-input"
[tag tail]
(let
[class (bit-shift-right tag 6)
consp (bit-and (bit-shift-right tag 5) 1)
Expand Down Expand Up @@ -610,8 +612,9 @@
(println "ERROR: ASN.1 decoding failed: " value)
nil))))

;; a function for ast → binary → back to ast conversion, to check for differences introduced by encoding or decoding
(defn asn1-rencode [ast]
(defn asn1-rencode
"A function for ast → binary → back to ast conversion, to check for differences introduced by encoding or decoding"
[ast]
(let
[bs (asn1-encode ast)
astp (asn1-decode bs)]
Expand Down Expand Up @@ -662,17 +665,19 @@
(rest lst)
(cons (concat lst left) opts)))))

;; fuzzing note: degenerate inputs make this go exponential
(defn match-set [asts pats rec]
(defn match-set
"Fuzzing note: degenerate inputs make this go exponential"
[asts pats rec]
(or (empty? pats)
(some
(fn [order] (match-set (rest order) (rest pats) rec))
(filter
(fn [x] (rec (first x) (first pats)))
(each-first asts)))))

;; AST pattern → bool
(defn asn1-match? [asn pat]
(defn asn1-match?
"AST pattern → bool"
[asn pat]
(cond
(= pat :?)
true
Expand Down Expand Up @@ -713,8 +718,9 @@
rout))
rout))

;; AST pattern → AST' ∊ AST ∨ nil
(defn asn1-find-left-dfs [asn pat]
(defn asn1-find-left-dfs
"AST pattern → AST' ∊ AST ∨ nil"
[asn pat]
(cond
(asn1-match? asn pat)
asn
Expand All @@ -723,8 +729,9 @@
:else
nil))

;; AST pattern → [matching-subast ...], depth first, left to right
(defn asn1-matches [asn pat]
(defn asn1-matches
"AST pattern → [matching-subast ...], depth first, left to right"
[asn pat]
(reverse (asn1-find-matches asn pat ())))

(def asn1-find
Expand Down Expand Up @@ -862,8 +869,9 @@
;;; Base64 encoder
;;;

;; digit in range → char in encoding
(defn base64-digit [b]
(defn base64-digit
"Digit in range → char in encoding"
[b]
(cond
(= b \=) b
(== b (bit-and b 63))
Expand Down