It works!

This commit is contained in:
Reynir Björnsson 2023-11-14 09:19:24 +01:00
parent 101822a9e7
commit 29e249d854
22 changed files with 2967 additions and 1 deletions

BIN
audio/Series_of_Tubes.ogg Normal file

Binary file not shown.

1
css/default.css Normal file
View file

@ -0,0 +1 @@
body{color:black;font-size:16px;margin-right:auto;margin-left:auto;max-width:700px}body *{margin-right:initial;margin-left:initial}body > header{border-bottom:2px solid black;margin-bottom:30px;padding:12px 6px 12px 6px}div#logo a{color:black;float:left;font-size:18px;font-weight:bold;text-decoration:none}body > header nav{text-align:right}body > header nav a{color:black;font-size:18px;font-weight:bold;margin-left:12px;text-decoration:none;text-transform:uppercase}footer{border-top:solid 2px black;color:#555;font-size:12px;margin-top:30px;padding:12px 0px 12px 0px;text-align:right;clear:both}article header{color:#555;font-size:14px;font-style:italic}body img{margin:10px}.caption{color:#555;font-size:14px;font-style:italic}

1
css/syntax.css Normal file
View file

@ -0,0 +1 @@
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre{margin:0;padding:0;border:0;vertical-align:baseline;border:none}td.lineNumbers{border-right:1px solid #AAAAAA;text-align:right;color:#AAAAAA;padding-right:5px;padding-left:5px}td.sourceCode{padding-left:5px}.sourceCode span.kw{color:#007020;font-weight:bold}.sourceCode span.dt{color:#902000}.sourceCode span.dv{color:#40a070}.sourceCode span.bn{color:#40a070}.sourceCode span.fl{color:#40a070}.sourceCode span.ch{color:#4070a0}.sourceCode span.st{color:#4070a0}.sourceCode span.co{color:#60a0b0;font-style:italic}.sourceCode span.ot{color:#007020}.sourceCode span.al{color:red;font-weight:bold}.sourceCode span.fu{color:#06287e}.sourceCode span.re{}.sourceCode span.er{color:red;font-weight:bold}

BIN
images/KEEPCAML.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/P1030359.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 MiB

BIN
images/RAK.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/mario.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

2461
js/index.js Normal file

File diff suppressed because one or more lines are too long

30
js/index.map Normal file

File diff suppressed because one or more lines are too long

7
pages/about.md Normal file
View file

@ -0,0 +1,7 @@
## About
I have an interest in programming languages, and in particular *functional* ones with a *good* type system.
I studied [datalogi (computer science)](http://cs.au.dk/) at [Aarhus University](https://au.dk/).
I have a [GitHub account](https://github.com/reynir) with various [projects](https://github.com/reynir?tab=repositories).

8
pages/archive.md Normal file
View file

@ -0,0 +1,8 @@
---
title: Blog
---
Here you can find all my previous posts:
{% for article in articles %}
- [{{article.title}}]({{article.location}}) - {{article.date.month_repr}} {{article.date.day}}, {{article.date.year}}
{% endfor %}

13
pages/contact.md Normal file
View file

@ -0,0 +1,13 @@
## Email
I have written my email in [BNF](http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form):
```
my_email ::= user "@" domain
user ::= "reynir"
domain ::= subdomain "." TLD
subdomain ::= "reynir"
TLD ::= "dk"
```
People say you shouldnt put your email in plaintext on websites, because crawlers will find it and send you *SPAM* - and no one likes that!

20
pages/index.md Normal file
View file

@ -0,0 +1,20 @@
---
title: Root of Reynir
head_extra: '<script async src="./js/index.js" type="text/javascript"></script>'
---
<img src="./images/mario.jpg" id="mario" alt="Mario" />
<!-- Lambda lists: cons -->
<p>λxλyλz.z x y</p>
<!-- fst: λxλy.x -->
<!-- snd: λxλy.y -->
## Hello, World!
I've reproduced a list of recent posts here for your reading pleasure:
## Recent posts
{% for article in articles %}
- [{{article.title}}]({{article.location}}) - {{article.date.month_repr}} {{article.date.day}}, {{article.date.year}}
{% endfor %}

View file

@ -0,0 +1,115 @@
---
author:
name: Reynir Björnsson
title: Miragevpn & tls-crypt-v2
date: 2023-11-14
---
hannes is undecided - should it be "MirageVPN"?
In 2019 [Robur][robur.coop] started working on a [OpenVPN™-compatible implementation in OCaml][miragevpn].
The project was funded for 6 months in 2019 by [prototypefund](https://prototypefund.de).
In late 2022 we applied again for funding this time to the [NGI Assure][ngi-assure] open call, and our application was eventually accepted.
In this blog post I will explain why reimplementing the OpenVPN™ protocol in OCaml is a worthwhile effort, and describe the Miragevpn implementation and in particular the `tls-crypt-v2` mechanism.
## What even is OpenVPN™?
[OpenVPN™][openvpn] is a protocol and software implementation to provide [virtual private networks][vpn-wiki]: computer networks that do not exist in hardware and are encrypted and tunnelled through existing networks.
Common use cases for this is to provide access to internal networks for remote workers, and for routing internet traffic through another machine for various reasons e.g. when using untrusted wifi, privacy from a snooping ISP, circumventing geoblock etc.
It is a protocol that has been worked on and evolved over the decades.
OpenVPN™ has a number of modes of operations as well as a number of options in the order of hundreds.
The modes can be categorized into two main categories: static mode and TLS mode.
The former mode uses static symmetric keys, and will be removed in the upcoming OpenVPN™ 2.7 (community edition).
I will not focus on static mode in this post.
The latter uses separate data & control channels where the control channel uses TLS - more on that later.
### Why reimplement it? And why in OCaml?
Before diving into TLS mode and eventually tls-crypt-v2 it's worth to briefly discuss why we spend time reimplementing the OpenVPN™ protocol.
You may ask yourself: why not just use the existing tried and tested implementation?
OpenVPN™ community edition is implemented in the C programming language.
It heavily uses the OpenSSL library[^mbedtls] which is as well written in C and has in the past had some notable security vulnerabilities.
Many vulnerabilities and bugs in C can be easily avoided in other languages due to bounds checking and stricter and more expressive type systems.
The state machine of the protocol can be more easily be expressed in OCaml, and some properties of the protocol can be encoded in the type system.
[^mbedtls]: It is possible to compile OpenVPN™ community edition with Mbed TLS instead of OpenSSL which is written in C as well.
Another reason is [Mirage OS][mirage], a library operating system implemented in OCaml.
We work on the Mirage project and write applications (unikernels) using Mirage.
In many cases it would be desirable to be able to connect to an existing VPN network[^vpn-network],
or be able to offer a VPN network to clients using OpenVPN™.
<!-- hannes: consider the current setup: as a VPN provider you offer lots of machines that run an operating system just for the user-space OpenVPN service. there are no users on the system, a lot of legacy layers are just around that are not needed.
With a MirageOS unikernel (and reproducible builds), which basically is a statically linked binary which is a complete operating system, such a setup and deployment (including updates) will be straightforward. With OCaml 5 and multicore in mind, this will even scale much better than OpenVPN (which is limited to a single core (hannes is not sure whether this is still true)).
-->
One very interesting example is a unikernel for [Qubes OS][qubes] that we have planned.
Qubes OS is an operating system with a high focus on security.
It offers an almost seamless experience of running applications in different virtual machines on the same machine.
The networking provided to a application (virtual machine) can be restricted to only go through the VPN.
It is possible to use OpenVPN™ for such a setup, but that requires running OpenVPN™ in a full Linux virtual machine.
With Mirage OS the resource footprint is typically much smaller than an equivalent application running in a Linux virtual machine; often the memory footprint is smaller by an order.
[^vpn-network]: I use the term "VPN network" to mean the virtual private network itself. It is a bit odd because the 'N' in 'VPN' is 'Network', but without disambiguation 'VPN' could refer to the network itself, the software or the service.
Finally, while it's not an explicit goal of ours, reimplementing a protocol without an explicit specification can help uncover bugs and things that need better documentation in the original implementation.
### TLS mode
There are different variants of TLS mode, but what they share is separate "control" channel and "data" channel.
The control channel is used to do a TLS handshake, and with the established TLS session data channel encryption keys, username/password authentication, etc. is negotiated.
Once this dance has been performed and data channel encryption keys have been negotiated the peers can exchange IP packets over the data channel.
Over the years a number of mechanisms has been implemented to protect the TLS stack from being exposed to third parties, protect against denial of service attacks and to hide information exchanged during a TLS handshake such as certificates (which was an isue before TLS 1.3).
These are known as `tls-auth`, `tls-crypt` and `tls-crypt-v2`.
The `tls-auth` mechanism adds a pre-shared key for hmac authentication on the control channel.
This makes it possible for an OpenVPN™ server to reject early clients that don't know the shared key before any TLS handshakes are performed.
In `tls-crypt` the control channel is encrypted as well as hmac authenticated using a pre-shared key.
Common to both is that the pre-shared key is shared between the server and all clients.
For large deployments this significantly reduces the usefulness - the key is more likely to be leaked the greate the number of clients who share this key.
### tls-crypt-v2
To improve on `tls-crypt`, `tls-crypt-v2` uses one pre-shared key per client.
This could be a lot of keys for the server to keep track of, so instead of storing all the client keys on the server the server has a special tls-crypt-v2 server key that is used to *[wrap][wiki-wrap]* the client keys.
That is, each client has their own client key as well as the client key wrapped using the server key.
The protocol is then extended so the client in the first message appends the wrapped key *unencrypted*.
The server can then decrypt and verify the client key and decrypt the rest of the packet.
Then the client and server use the client key just as in `tls-crypt`.
This is great!
Each client can have their own key, and the server doesn't need to keep a potentially large database of client keys.
What if the client's key is leaked?
A detail I didn't mention is that the wrapped key contains metadata.
By default this is the current timestamp, but it is possible on creation to put any (relative short) binary data in there as the metadata.
The server can then be configured to check the metadata by calling a script.
An issue exists that an initial packet takes up resources on the server because the server needs to
1) decrypt the wrapped key, and
2) keep the unwrapped key and other data in memory while waiting for the handshake to complete.
This can be abused in an attack very similar to a TCP [SYN flood][syn-flood].
Without `tls-crypt-v2` OpenVPN uses a specially crafted session ID (a 64 bit identifier) to avoid this issue similar to [SYN cookies][syn-cookie].
To address this in OpenVPN 2.6 the protocol for `tls-crypt-v2` was extended yet further with a 'HMAC cookie' mechanism.
The client sends the same packet as before, but uses a sequence number `0x0f000001` instead of `1` to signal support of this mechanism.
The server responds in a similar manner with a sequence number of `0x0f000001` and the packet is appended with a tag-length-value encoded list of flags.
At the moment only one tag and one value is defined which signifies the server supports HMAC cookies - this seems unnecessarily complex, but is done to allow future extensibility.
Finally, if the server supports HMAC cookies, the client sends a packet where the wrapped key is appended in cleartext.
The server is now able to decrypt the third packet without having to keep the key from the first packet around and can verify the session id.
<!-- hannes
something along the lines: if you're keen on setting this up yourself, go to <here> and download the latest binary, and execute it (well, just a brief howto get it up and running). Don't hesitate to reach out if you're stuck.
-->
[robur.coop]: https://robur.coop/
[miragevpn]: https://github.com/robur-coop/miragevpn/
[ngi-assure]: https://www.assure.ngi.eu/
[openvpn]: https://openvpn.net/
[vpn-wiki]: https://en.wikipedia.org/wiki/Virtual_private_network
[mirage]: https://mirage.io/
[qubes]: https://www.qubes-os.org/
[wiki-wrap]: https://en.wikipedia.org/wiki/Key_wrap
[syn-flood]: https://en.wikipedia.org/wiki/SYN_flood
[syn-cookie]: https://en.wikipedia.org/wiki/SYN_cookies

41
reynir-www.opam Normal file
View file

@ -0,0 +1,41 @@
opam-version: "2.0"
version: "dev"
synopsis: "The reynir.dk website"
maintainer: "reynir@reynir.dk"
authors: [ "Reynir Björnsson <reynir@reynir.dk>" ]
build: [
[ "dune" "subst" ]
[ "dune" "build" "-p" name "-j" jobs ]
[ "dune" "runtest" "-p" name ] {with-test}
[ "dune" "build" "@doc" "-p" name ] {with-doc}
]
license: "GPL-3.0-or-later"
homepage: "https://git.robur.io/robur/robur.coop"
dev-repo: "git://git.robur.io/robur/robur.coop.git"
bug-reports: "https://git.robur.io/robur/robur.coop/issues"
depends: [
"ocaml" { >= "4.11.1" }
"dune" { >= "2.8" }
"preface" { >= "0.1.0" }
"logs" {>= "0.7.0" }
"cmdliner" { >= "1.0.0"}
"http-lwt-client"
"yocaml"
"yocaml_unix"
"yocaml_yaml"
"yocaml_markdown"
"yocaml_git"
"yocaml_jingoo"
]
pin-depends: [
["yocaml.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
["yocaml_unix.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
["yocaml_yaml.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
["yocaml_markdown.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
["yocaml_jingoo.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
["yocaml_git.dev" "git+https://github.com/xhtmlboi/yocaml.git"]
]

146
src/model.ml Normal file
View file

@ -0,0 +1,146 @@
open Yocaml
let article_path file =
let filename = basename $ replace_extension file "html" in
filename |> into "posts"
module Author = struct
type t = {
name : string;
}
let equal a b = String.equal a.name b.name
let make name = { name }
let from (type a) (module V : Metadata.VALIDABLE with type t = a) obj =
V.object_and
(fun assoc ->
let open Validate.Applicative in
make
<$> V.(required_assoc string) "name" assoc)
obj
let default_author =
make "Reynir Björnsson"
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a) { name } =
D.[ "name", string name ]
end
module Article = struct
type t = {
title : string;
date : Date.t;
author : Author.t;
}
let make title date author =
{ title; date; author }
let from (type a) (module V : Metadata.VALIDABLE with type t = a) obj =
V.object_and
(fun assoc ->
let open Validate.Applicative in
make
<$> V.(required_assoc string) "title" assoc
<*> V.(required_assoc (Metadata.Date.from (module V))) "date" assoc
<*> V.(required_assoc (Author.from (module V))) "author" assoc)
obj
let from_string (module V : Metadata.VALIDABLE) = function
| None -> Validate.error $ Error.Required_metadata [ "Article" ]
| Some str ->
let open Validate.Monad in
V.from_string str >>=
from (module V)
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a)
{ title; date; author } =
D.[ "title", string title;
"date", object_ $ Metadata.Date.inject (module D) date;
"author", object_ $ Author.inject (module D) author ];
end
module Articles = struct
type t = (string * Article.t) list
let make path article = (path, article)
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a) articles =
D.[
"articles",
list
(List.map (fun (path, article) ->
object_ @@
[ "location", string path ] @
Article.inject (module D) article)
articles)
]
end
module With_path (V : Metadata.INJECTABLE) = struct
type t = {
path : string;
extension : V.t;
}
let merge path v =
{ path; extension=v; }
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a) { path; extension } =
D.[ "path", string path ] @ V.inject (module D) extension
end
module Page = struct
type t = {
title : string;
head_extra : string option;
}
let make title head_extra = { title; head_extra }
let from (type a) (module V : Metadata.VALIDABLE with type t = a) obj =
V.object_and
(fun assoc ->
let open Validate.Applicative in
make
<$> V.(required_assoc string) "title" assoc
<*> V.(optional_assoc string) "head_extra" assoc)
obj
let from_string (module V : Metadata.VALIDABLE) = function
| None -> Validate.valid { title = "TODO"; head_extra = None }
| Some str ->
Validate.Monad.bind
(from (module V))
(V.from_string str)
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a)
{ title; head_extra } =
let r = D.[ "title", string title ] in
match head_extra with
| None -> r
| Some head_extra -> ("head_extra", D.string head_extra) :: r
end
module With_layout (V : Metadata.INJECTABLE) = struct
type t = {
title : string;
head_extra : string option;
extension : V.t
}
let merge ~title ~head_extra v =
{ title; head_extra; extension = v }
let inject (type a) (module D : Key_value.DESCRIBABLE with type t = a)
{ title; head_extra; extension = v } =
let r =
D.[ "title", string title ] @
V.inject (module D) v
in
match head_extra with
| None -> r
| Some head_extra -> ("head_extra", D.string head_extra) :: r
end

View file

@ -6,7 +6,14 @@ let default_target = Fpath.v "_site"
let program ~target = let program ~target =
let open Yocaml in let open Yocaml in
let* () = Task.move_css target in let* () = Task.move_css target in
Task.move_images target let* () = Task.move_images target in
let* () = Task.move_js target in
let* () = Task.move_audio target in
let* () = Task.process_articles target in
let* () = Task.generate_about target in
let* () = Task.generate_contact target in
let* () = Task.generate_archive target in
Task.generate_index target
let local_build _quiet target = let local_build _quiet target =
Yocaml_unix.execute (program ~target:(Fpath.to_string target)) Yocaml_unix.execute (program ~target:(Fpath.to_string target))

View file

@ -5,9 +5,13 @@ module Template = Yocaml_jingoo
let css_target target = "css" |> into target let css_target target = "css" |> into target
let images_target target = "images" |> into target let images_target target = "images" |> into target
let js_target target = "js" |> into target
let audio_target target = "audio" |> into target
let index_html target = "index.html" |> into target let index_html target = "index.html" |> into target
let about_html target = "about.html" |> into target let about_html target = "about.html" |> into target
let contact_html target = "contact.html" |> into target let contact_html target = "contact.html" |> into target
let archive_html target = "archive.html" |> into target
let article_target file target = Model.article_path file |> into target
let move_css target = let move_css target =
process_files process_files
@ -21,3 +25,78 @@ let move_images target =
File.is_image File.is_image
(Build.copy_file ~into:(images_target target)) (Build.copy_file ~into:(images_target target))
let move_js target =
process_files
[ "js" ]
(fun f -> File.is_javascript f || with_extension "map" f)
(Build.copy_file ~into:(js_target target))
let move_audio target =
process_files
[ "audio" ]
(with_extension "ogg")
(Build.copy_file ~into:(audio_target target))
let with_layout (type a) (module M : Metadata.INJECTABLE with type t = a)
(read_model : (_, a) Build.t) page target =
let open Build in
let module M = Model.With_layout(M) in
let apply_template_from_string (v, content) =
let values = M.inject (module Template) v in
let content = Template.to_string ~strict:true values content in
(v, content)
in
create_file target
(watch Sys.argv.(0)
>>> Metaformat.read_file_with_metadata (module Model.Page) page
&&& read_model
>>^ (fun (({ Model.Page.title; head_extra }, content), v) ->
M.merge ~title ~head_extra v, content)
>>^ apply_template_from_string
>>> Markup.content_to_html ()
>>> Template.apply_as_template (module M) "templates/layout.html"
>>^ Stdlib.snd)
let process_articles target =
let open Build in
process_files [ "posts" ] File.is_markdown (fun article_file ->
create_file (article_target article_file target)
(Metaformat.read_file_with_metadata (module Model.Article) article_file
>>> Markup.content_to_html ()
>>> Template.apply_as_template (module Model.Article) "templates/article.html"
>>> Template.apply_as_template (module Model.Article) "templates/layout.html"
>>^ Stdlib.snd))
let articles =
let open Build in
collection
(read_child_files "posts" (with_extension "md"))
(fun path ->
Metaformat.read_file_with_metadata (module Model.Article) path
>>^ fun (meta, _data) ->
(Model.article_path path, meta))
(fun articles () -> articles)
let generate_archive target =
let* articles = articles in
with_layout (module Model.Articles) articles "pages/archive.md" (archive_html target)
let page page_file target =
let open Build in
create_file target
(watch Sys.argv.(0)
>>> Metaformat.read_file_with_metadata (module Model.Page) page_file
>>> Markup.content_to_html ()
>>> Template.apply_as_template (module Model.Page) "templates/layout.html"
>>^ Stdlib.snd)
let generate_about target =
page "pages/about.md" (about_html target)
let generate_contact target =
page "pages/contact.md" (contact_html target)
let generate_index target =
let* articles = articles in
with_layout (module Model.Articles) articles "pages/index.md" (index_html target)

4
templates/article.html Normal file
View file

@ -0,0 +1,4 @@
<h2>{{title}}</h2>
<address>Written by {{author.name}}, {{date.month_repr}} {{date.day}}, {{date.year}}</address>
{{body|safe}}

33
templates/layout.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Reynir 网 - {{ title }}</title>
<link rel="stylesheet" type="text/css" href="{{root}}/css/default.css" />
<link rel="stylesheet" type="text/css" href="{{root}}/css/syntax.css" />
<link rel="me" value="https://bsd.network/@reynir" />
{{ head_extra|safe }}
</head>
<body>
<header>
<div id="logo">
<a href="{{root}}/">Reynir</a>
</div>
<nav>
<a href="{{root}}/">Home</a>
<a href="{{root}}/about.html">About</a>
<a href="{{root}}/contact.html">Contact</a>
<a href="{{root}}/archive.html">Blog</a>
</nav>
</header>
<main>
{%- autoescape false -%}
{{ body }}
{% endautoescape %}
</main>
<footer>
This is a footer.
</footer>
</body>
</html>