;; disfluid, implementation of the Solid specification
;; Copyright (C) 2020, 2021 Vivien Kraus
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see .
(define-module (webid-oidc jws)
#:use-module (webid-oidc jwk)
#:use-module (webid-oidc errors)
#:use-module (webid-oidc web-i18n)
#:use-module ((webid-oidc stubs) #:prefix stubs:)
#:use-module (rnrs bytevectors)
#:use-module (ice-9 receive)
#:use-module (ice-9 exceptions)
#:use-module (ice-9 match)
#:use-module (oop goops)
#:declarative? #t
#:export
(
&invalid-jws
make-invalid-jws
invalid-jws?
the-jws
jws?
jws-alg
&cannot-query-identity-provider
make-cannot-query-identity-provider
cannot-query-identity-provider?
cannot-query-identity-provider-value
&signed-in-future
make-signed-in-future
signed-in-future?
error-signature-date
error-current-date
&expired
make-expired
expired?
error-expiration-date
;; error-current-date works for that one too
jws-decode
jws-encode
))
(define-exception-type
&invalid-jws
&external-error
make-invalid-jws
invalid-jws?)
(define (the-jws x)
(with-exception-handler
(lambda (error)
(let ((final-message
(if (exception-with-message? error)
(format #f (G_ "the JWS is invalid: ~a")
(exception-message error))
(format #f (G_ "the JWS is invalid")))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)
error))))
(lambda ()
(match x
((header . payload)
(let examine-header ((header header)
(alg #f)
(other-header-fields '()))
(match header
(()
(let examine-payload ((payload payload)
(other-payload-fields '()))
(match payload
(()
(unless alg
(fail (format #f (G_ "the JWS header does not have an \"alg\" field"))))
`(((alg . ,(symbol->string alg))
,@(reverse other-header-fields))
. ,(reverse other-payload-fields)))
((((? symbol? key) . value) payload ...)
(examine-payload payload
`((,key . ,value) ,@other-payload-fields)))
(else
(fail (format #f (G_ "invalid JSON object as payload")))))))
((('alg . (? string? given-alg)) header ...)
(case (string->symbol given-alg)
((HS256 HS384 HS512
RS256 RS384 RS512
ES256 ES384 ES512
PS256 PS384 PS512)
#t)
(else
(fail (format #f (G_ "invalid signature algorithm: ~s") given-alg))))
(examine-header header (or alg (string->symbol given-alg))
other-header-fields))
((('alg . invalid) header ...)
(fail (format #f (G_ "invalid \"alg\" value: ~s") invalid)))
((((? symbol? key) . value) header ...)
(examine-header header alg
`((,key . ,value) ,@other-header-fields)))
(else
(fail (format #f (G_ "invalid JSON object as header")))))))
(else
(fail (format #f (G_ "this is not a pair"))))))))
(define (jws? x)
(false-if-exception
(the-jws x)))
(define (jws-alg jws)
(match (the-jws jws)
((header . _)
(string->symbol (assq-ref header 'alg)))))
(define (split-in-3-parts string separator)
(match (string-split string separator)
((header payload signature)
(values header payload signature))
(else
(let ((final-message
(format #f (G_ "the encoded JWS is not in 3 parts"))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)))))))
(define (base64-decode-json str)
(with-exception-handler
(lambda (error)
(let ((final-message
(if (exception-with-message? error)
(format #f (G_ "the encoded JWS header or payload is not a JSON object encoded in base64: ~a")
(exception-message error))
(format #f (G_ "the encoded JWS header or payload is not a JSON object encoded in base64")))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)
error))))
(lambda ()
(stubs:json-string->scm
(utf8->string (stubs:base64-decode str))))))
(define-exception-type
&cannot-query-identity-provider
&external-error
make-cannot-query-identity-provider
cannot-query-identity-provider?
(identity-provider cannot-query-identity-provider-value))
(define-exception-type
&signed-in-future
&external-error
make-signed-in-future
signed-in-future?
(signature-date error-signature-date)
(current-date error-current-date*))
(define-exception-type
&expired
&external-error
make-expired
expired?
(expiration-date error-expiration-date)
(current-date error-current-date**))
(define error-current-date
(match-lambda
((or ($ &signed-in-future _ date)
($ &expired _ date)
($ &compound-exception (($ &signed-in-future _ date) _ ...))
($ &compound-exception (($ &expired _ date) _ ...)))
date)
(($ &compound-exception (_ sub-exceptions ...))
(error-current-date (apply make-exception sub-exceptions)))
(else #f)))
(define (parse str verify)
(receive (header payload signature)
(split-in-3-parts str #\.)
(let ((base (string-append header "." payload))
(header (base64-decode-json header))
(payload (base64-decode-json payload)))
(let ((ret `(,header . ,payload)))
(verify ret base signature)
ret))))
(define (verify-any alg keys payload signature)
(let try-with-key ((keys keys))
(match keys
(()
(let ((final-message
(format #f (G_ "the JWS is not signed by any of the expected set of public keys"))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)))))
((next-key keys ...)
(with-exception-handler
(lambda (error)
(unless (stubs:invalid-signature? error)
(let ((final-message
(if (exception-with-message? error)
(format #f (G_ "while verifying the JWS signature: ~a")
(exception-message error))
(format #f (G_ "an unexpected error happened while verifying a JWS")))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)
error))))
(try-with-key keys))
(lambda ()
(stubs:verify alg (key->jwk next-key) payload signature))
#:unwind? #t
#:unwind-for-type stubs:&invalid-signature)))))
;; For verification, we can supply a JWKS, or a public key, or a list
;; of public keys. The JWKS case is handled in (webid-oidc jwk).
(define-method (keys (key ))
(list key))
(define-method (keys (key ))
(list (public-key key)))
(define-method (keys (keys ))
(map public-key keys))
(define (jws-decode str lookup-keys)
(with-exception-handler
(lambda (error)
(let ((final-message
(if (exception-with-message? error)
(format #f (G_ "cannot decode a JWS: ~a")
(exception-message error))
(format #f (G_ "cannot decode a JWS")))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)
error))))
(lambda ()
(parse str
(lambda (jws payload signature)
(let ((k (keys (lookup-keys jws))))
(verify-any (jws-alg jws) k payload signature)))))))
(define (jws-encode jws key)
(with-exception-handler
(lambda (error)
(let ((final-message
(if (exception-with-message? error)
(format #f (G_ "cannot encode a JWS: ~a")
(exception-message error))
(format #f (G_ "cannot encode a JWS")))))
(raise-exception
(make-exception
(make-invalid-jws)
(make-exception-with-message final-message)
error))))
(lambda ()
(match jws
((header . payload)
(let ((header (stubs:scm->json-string header))
(payload (stubs:scm->json-string payload)))
(let ((header (stubs:base64-encode header))
(payload (stubs:base64-encode payload)))
(let ((payload (string-append header "." payload)))
(let ((signature (stubs:sign (jws-alg jws) (key->jwk key) payload)))
(string-append payload "." signature))))))))))