├── _config.yml ├── index.md ├── about.md ├── vger.md ├── pf.md ├── iihk.md └── isv.md /_config.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - jekyll-relative-links 3 | relative_links: 4 | enabled: true 5 | collections: true 6 | include: 7 | - CONTRIBUTING.md 8 | - README.md 9 | - LICENSE.md 10 | - COPYING.md 11 | - CODE_OF_CONDUCT.md 12 | - CONTRIBUTING.md 13 | - ISSUE_TEMPLATE.md 14 | - PULL_REQUEST_TEMPLATE.md 15 | 16 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # Developer Journal 2 | 3 | ## 05/2022 SSH InsecureIgnoreHostKey 4 | [=> HostKeyCallback & InsecureIgnoreHostKey in ssh/knownhosts](iihk.html) 5 | 6 | ## 05/2022 TLS InsecureSkipVerify 7 | [=> VerifyConnection & InsecureSkipVerify in tls.Config](isv.html) 8 | 9 | ## 02/2022 OpenBSD Gemini Vger 10 | [=> OpenBSD Gemini Vger Configuration](vger.html) 11 | 12 | ## 02/2022 OpenBSD PF Router 13 | [=> OpenBSD PF Router Build](pf.html) 14 | 15 | 16 | --- 17 | 18 | [=> About](about.html) 19 | [=> WWW archive](https://www.tfx.tw/2020/) 20 | 21 | ###### 2022 興怡 | Always wrong, sometimes lucky 22 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | # About 2 | These journal entries are to help my future self remember odds and ends. 3 | 4 | ## § DIY 5 | Gemini by design encourages the client implementation to be done in ~100 LOC. It is asking folks to build it. We are in a time of starting over, "going back to school", and that's alright. 6 | 7 | ===== 8 | 9 | ## * Notes, Lessons, Monologue 10 | * Source the guides, "Here is the short list of articles that help configure X:" 11 | * Mainly as a journal entry, try to avoid repeating information, but explain notes from the guides where there was something specific required. 12 | 13 | _________________________________________ 14 | 2022 興怡 | Always wrong, sometimes lucky 15 | 16 | -------------------------------------------------------------------------------- /vger.md: -------------------------------------------------------------------------------- 1 | # Gemini, the Middle Child Between Mercury and Apollo 2 | Here is the short list of articles that helped us configure: 3 | 4 | [=> Gemini server configuration on OpenBSD](https://0x19.org/posts/2022-01-02.php) 5 | [=> Vger Gemini server](https://tildegit.org/solene/vger/) 6 | [=> OpenBSD and Let's Encrypt](https://www.openbsdhandbook.com/services/webserver/ssl/) 7 | 8 | 9 | ## § TLS 10 | Just follow the steps for the acme-client. Don't install Certbot. It has dependencies and adds endless packages required as a Python application. (Probably a good candidate for being Snap'd or Dockerized) 11 | 12 | We use the FQDN (gemini.tfx.tw) when the examples show the IP address or the root domain name. There are trade offs; since we do not have a static IP we will have to deal with updating DNS records as necessary. 13 | 14 | For Cloudflare DNS, we want to stop the proxy option on the A and CNAME records because it expects HTTPS. 15 | 16 | We may need to add the cron job to renew the certificate in 90 days. Skipped it for now. 17 | 18 | ===== 19 | 20 | ## * Notes, Lessons, Monologue 21 | * Is Gemini good? The protocol has many critics and the FAQ addresses the most pressing concerns. There it's stated that the purpose is not to replace web pages, but rather to fill the space between Mercury/Gopher and Apollo/Web. Reading these limitations, it reminded me of the early days of Twitter when messages were only 128 characters, and adopters tried to explain its immediacy. 22 | * Why abandon CSS styling? The FAQ answer promotes the responsiblity of presentation to the client. It states matter of factly that each user has their own preference, and basically how ridiculous the demand can be on the implementation to achieve a consistent user experience. To some, this may seem like a relinquishing of the problem. However in my mind, this is great in its admission of real world stresses. One of the claims that surface weekly now that makes me chuckle is that the web browser promise was to develop once and have your app run across platforms. How naive we were. 23 | 24 | _________________________________________ 25 | 2022 興怡 | Always wrong, sometimes lucky 26 | 27 | -------------------------------------------------------------------------------- /pf.md: -------------------------------------------------------------------------------- 1 | # OpenBSD PF Router 2 | 3 | [=> OpenBSD PF Router Build](https://www.openbsd.org/faq/pf/example1.html) 4 | [=> Home Router Build](https://hackaday.com/2018/04/12/building-the-perfect-home-router/) 5 | [=> Ars Linux Router Build](https://arstechnica.com/gadgets/2016/04/the-ars-guide-to-building-a-linux-router-from-scratch/) 6 | [=> Router7 Build](https://michael.stapelberg.ch/posts/2021-07-10-linux-25gbit-internet-router-pc-build/) 7 | 8 | ## §1 /Networking 9 | Used the guide exactly to set hostname.em0 for autoconf. Partly to make sure the uplink would work. Ultimately, like the Router7 article, the lease would change rarely allowing it to be safe to use a pseudo-static address config. 10 | 11 | This is also related to the DNS record that we expose for the public gemini hostname. Ideally, we want to do DynDNS setup, but at this time it's skipped. 12 | 13 | ## §2 /Firewall 14 | Followed exactly as shown in the guide. The line to pass traffic to the web server is adjusted for the Let's Encrypt configuration. Afterwards, we omit it and have a similar rule for port 1965 to allow gemini requests. 15 | 16 | To check the configuration before applying: 17 | ``` 18 | # pfctl -n -f /etc/pf.conf 19 | ``` 20 | 21 | ## §3 /DNS 22 | Follow the guide. The one addition we make is inclusion of Oznu's NXDOMAIN list to sinkhole ads. 23 | 24 | ``` 25 | #### file: /var/unbound/etc/unbound.conf 26 | include: /var/unbound/etc/unbound.bl 27 | forward-zone: 28 | name: "." 29 | forward-addr: 1.1.1.1 30 | ``` 31 | 32 | ## §4 /Equipment 33 | We had a regular D-Link ethernet hub that was good to go from the OpenBSD box and expand out to 8 more ports. This saved us from needing an expansion card or purchasing a hub brand new. 34 | 35 | Not as power efficient as the SOC options. So when summer rolls around and temps rise, it may be important to investigate the NanoPi again. 36 | 37 | ===== 38 | 39 | ## * Notes, Lessons, Monologue 40 | * Why? Our EdgeRouter Lite stopped working. Similar to one of the articles, we thought about buying a replacement and for some period we swapped in the wifi router in dual role of uplink and wifi access. After reading and searching, very close to ordering the NanoPi R4S. Then saw our under utilized old NAS server which ran SmartOS from Joyent. Also a build from Ars guides. 41 | * OpenBSD? This was our favorite OS in the distant past (95/96?) and we revisited before until it wasn't convenient to run your own SMTP/DNS/HTTPD server. It still seems as good as ever, if not better (has ARM support in the field). 42 | * Unbound? We have a todo to revisit and try its native versus the DNSCrypt Proxy's upstream TLS connections. 43 | 44 | _________________________________________ 45 | 2022 興怡 | Always wrong, sometimes lucky 46 | 47 | -------------------------------------------------------------------------------- /iihk.md: -------------------------------------------------------------------------------- 1 | # SSH InsecureIgnoreHostKey 2 | 3 | [=> SSH Host Key](https://skarlso.github.io/2019/02/17/go-ssh-with-host-key-verification/) 4 | [=> TLS Steps](https://gemini.circumlunar.space/docs/tls-tutorial.gmi) 5 | [=> SSH](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process) 6 | 7 | ## §1 /TOFU 8 | Among our tasks was how to mimic the way SSH negotiates the first connection to a remote. The user continue prompt was already a familiar interaction. Important because our dialer must keep the `known_hosts` file to track "trusted" capsules. The other subtle point is the reason behind this act of trust -- the self-signed certificate. Meaning, capsules on self-signed certificates are assumed to be a fast path to launching Gemini content, and common place. To sum up: 9 | 10 | * keep `known_hosts` file 11 | * prompt user to confirm first visit 12 | * identify self-signed certificates 13 | 14 | ## §2 /Known_capsules 15 | To begin the recovery flow, we use the prompt bit for self-signed certificates. The other recovery bit flags are `gmi.AcceptUAE` and `gmi.UAEReject` which mean continue and halt, respectively: 16 | 17 | ``` 18 | func knownCapsules(ctx context.Context, capsule string, cert *x509.Certificate, isv Mask) bool { 19 | if !isv.Has(PromptUAE) { 20 | return false 21 | } 22 | ``` 23 | 24 | We made this bitmask configurable (via the `-json` commandline argument), and assigned in the safe defaults as prompt (`gmi.PromptUAE`). 25 | 26 | Then the "trick" is to invoke `HostKeyCallback`: 27 | 28 | ``` 29 | import kh "golang.org/x/crypto/ssh/knownhosts" 30 | ... 31 | func searchKnown(cert *x509.Certificate, capsule string, kcp string) error { 32 | sshpk, err := ssh.NewPublicKey(cert.PublicKey) 33 | if err != nil { 34 | log.Printf("DEBUG crt to ssh key failed, %v", err) 35 | return err 36 | } 37 | abs, err := filepath.Abs(kcp) 38 | if err != nil { 39 | log.Printf("DEBUG known_capsules path, %v", err) 40 | return err 41 | } 42 | hostKeyCallback, err := kh.New(abs) 43 | if err != nil { 44 | log.Printf("DEBUG callback not created, %v", err) 45 | return err 46 | } 47 | addr, err := net.ResolveTCPAddr("tcp", capsule) 48 | if err != nil { 49 | log.Printf("DEBUG resolve, %v", err) 50 | return err 51 | } 52 | err = hostKeyCallback(capsule, addr, sshpk) 53 | if err != nil { 54 | log.Printf("DEBUG known error, %v", err) 55 | } 56 | ``` 57 | 58 | The `ssh/knownhosts` package has the `New()` function which creates a callback for us. We just need to supply the absolute path to our `known_capsules` file. 59 | 60 | Also, we use the filename `known_capsules` to emphasize that the remotes are capsules that are tracked, instead of SSH hosts. 61 | 62 | The callback takes capsule name, IP address, and public key as parameters. To find the public key, we obtain the public key of the certificate, then use `ssh.NewPublicKey`. 63 | 64 | ## §3 /User Prompt 65 | For the time being, we have a placeholder. The log output shows the step/event where it is appropriate to make the UI to prompt the user. Currently, it acts as if the choice is `Y` and continues. It's worth pointing out because we have a todo, and it maps out the continue flow. For continue flow, the new remote needs a) its public key produced, and b) to be appended to the `known_hosts` file: 66 | 67 | ``` 68 | sshpk, err := ssh.NewPublicKey(cert.PublicKey) 69 | if err != nil { 70 | return fmt.Errorf("Capsule prompt failed new key, %w", err) 71 | } 72 | line := kh.Line([]string{capsule}, sshpk) 73 | file, err := os.OpenFile(abs, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 644) 74 | if err != nil { 75 | return fmt.Errorf("Capsule prompt failed file, %w", err) 76 | } 77 | defer file.Close() 78 | _, err = file.WriteString(line) 79 | if err != nil { 80 | return fmt.Errorf("Capsule prompt failed append, %w", err) 81 | } 82 | file.WriteString("\n") 83 | ``` 84 | 85 | 86 | ## §4 /Self-signed Cert 87 | Sounds weird, but the TLS dial has to fail THEN we have an error (more in notes below). This error can be cast into different types. `x509.UnknownAuthorityError` occurs for self-signed certificates because the CA is not recognized. 88 | 89 | Snippet of code that identifies self-signed certificates: 90 | 91 | ``` 92 | func certFrom(err error) *x509.Certificate { 93 | switch et := err.(type) { 94 | case x509.UnknownAuthorityError: 95 | uae, _ := err.(x509.UnknownAuthorityError) 96 | return uae.Cert 97 | ``` 98 | 99 | 100 | --- 101 | 102 | ## * Notes, Lessons, Monologue 103 | * When? During prototype/poc, when iterating inside controlled networks; when you want to concentrate on the functional requirements. The part I really like is the naming clearly identifies `InsecureIgnoreHostKey` as dangerous. So any code review should easily flag it when found in commits. 104 | 105 | * Self-signed certs? It can be argued whether self-signed certs are too much risk. May be historical, and as time progresses it will change. *Let's Encrypt* definitely changed things for the better. I was able to obtain a cert with their CA for __free__. You need your own domain name, and be able to configure httpd to respond to the ACME requests. So in theory, all web servers can say goodbye to self-signed certs. 106 | 107 | * Why waste TLS dial for the error type? I know it seems wasteful. Consider this. The best flow is the standard TLS dial with all the checks in verify. So making the initial TLS dial is actually our ideal, and the recovery flow is to accomodate (Gemini) self-signed certs. 108 | 109 | ###### 2022 興怡 | Always wrong, sometimes lucky 110 | 111 | -------------------------------------------------------------------------------- /isv.md: -------------------------------------------------------------------------------- 1 | # TLS InsecureSkipVerify 2 | 3 | [=> VerifyConnection](https://pkg.go.dev/crypto/tls@master#example-Config-VerifyConnection) 4 | [=> TLS Steps](https://gemini.circumlunar.space/docs/tls-tutorial.gmi) 5 | [=> Solderpunk Demo3](https://tildegit.org/solderpunk/gemini-demo-3) 6 | [=> SSL/TLS](https://security.stackexchange.com/q/20803/5568) 7 | 8 | ## §1 /Certificates 9 | *Let's Encrypt* is a tremendous life saver. It eliminated the mystery and wall around certificates. A TLS connection to our capsule becomes: 10 | 11 | ``` 12 | tls.Dial("tcp", url.Host, nil) 13 | ``` 14 | 15 | That's it. Seems __too__ easy. If every server used *Let's Encrypt*, we would be done. In reality, we only found two other capsules that run on CA certificates. Enter the `InsecureSkipVerify` toggle. To define alternate verify logic, we enable the toggle with `tls.Config`, and supply our version in the `VerifyConnection` hook. 16 | 17 | ## §2 /VerifyConnection 18 | The Go doc example is the critical part: 19 | 20 | ``` 21 | // VerifyConnection can be used to replace and customize connection 22 | // verification. This example shows a VerifyConnection implementation that 23 | // will be approximately equivalent to what crypto/tls does normally to 24 | // verify the peer's certificate. 25 | 26 | // Client side configuration. 27 | _ = &tls.Config{ 28 | // Set InsecureSkipVerify to skip the default validation we are 29 | // replacing. This will not disable VerifyConnection. 30 | InsecureSkipVerify: true, 31 | VerifyConnection: func(cs tls.ConnectionState) error { 32 | opts := x509.VerifyOptions{ 33 | DNSName: cs.ServerName, 34 | Intermediates: x509.NewCertPool(), 35 | } 36 | for _, cert := range cs.PeerCertificates[1:] { 37 | opts.Intermediates.AddCert(cert) 38 | } 39 | _, err := cs.PeerCertificates[0].Verify(opts) 40 | return err 41 | }, 42 | } 43 | ``` 44 | 45 | Running it against CA certs continued to produce TLS connections successfully. At the same time, it correctly rejected the non-CA capsules. Which meant that `InsecureSkipVerify` can be enabled (`true`), without automatically opening the floodgates to every connect request. Now we can start, and check for properties on the remote cert. 46 | 47 | ## §3 /UnknownAuthorityError 48 | Self-signed certs will cause the standard `tls.Dial` to error with `x509.UnknownAuthorityError`. We test for the error type and cast it to utilize the cert: 49 | 50 | ``` 51 | func certFrom(err error) *x509.Certificate { 52 | switch et := err.(type) { 53 | case x509.UnknownAuthorityError: 54 | uae, _ := err.(x509.UnknownAuthorityError) 55 | return uae.Cert 56 | ``` 57 | 58 | Then within our alternate verify, we take the cert and promote it to a CA root cert in the `x509.VerifyOptions`. Notice that we use a guard to condition this logic. It is permitted when we configure with the `gmi.AcceptUAE` bit. It is also permitted when we configure with the `gmi.PromptUAE` bit, but the cert must belong to a capsule from the `known_hosts` file (more in next write-up). 59 | 60 | ``` 61 | if isv.Has(AcceptUAE) || known && isv.Has(PromptUAE) { 62 | // treat self-signed cert as if root 63 | cert.IsCA = true 64 | opts.Roots = x509.NewCertPool() 65 | opts.Roots.AddCert(cert) 66 | } 67 | ``` 68 | 69 | ## §4 /HostnameError 70 | From Go 1.15 release, the Subject Alternate Name (SAN) is expected and Common Name (CN) becomes legacy. So there can be capsules on CN until their expiration/renewal generates replacement certs. The legacy CN is exposed as `x509.HostnameError`: 71 | 72 | ``` 73 | case x509.HostnameError: 74 | hne, _ := err.(x509.HostnameError) 75 | if hne.Certificate.Subject.CommonName == hne.Host { 76 | return hne.Certificate 77 | } 78 | ``` 79 | 80 | Since it is possible for different name related issues to fall in this category, we use the `Host` value that caused the error, and compare it to the `CommonName` in the cert. This will tell us that the CN was the source of the error. By falling through for other errors, the TLS dial will stop the connection (and can be used for further analysis). 81 | 82 | Then back in our alternate verify, we take the cert to obtain its CN and include it with the `DNSNames`; the list for SAN. 83 | 84 | ``` 85 | if isv.Has(AcceptLCN) { 86 | // inject SAN 87 | leaf.DNSNames = append(leaf.DNSNames, cert.Subject.CommonName) 88 | } 89 | ``` 90 | 91 | Notice the guard statement prevents the legacy CN recovery unless the `gmi.AcceptLCN` bit is enabled by configuration. So the condition for `gmi.PromptLCN` bit is conspicuously absent. We just haven't built that yet; in terms of severity, the CN is checked, not bypassed technically so we bought more time to work on it. 92 | 93 | ## §5 /CertificateInvalidError 94 | The expired cert is the remaining case that were encountered among the capsules toured. Since an expired cert is not listed as an exception to rules (Gemini/ otherwise), we left this case for last. For now only reserved configuration to consider for recovery, but feel there is no rush. To detect the case, the error type is `x509.CertificateInvalidError`: 95 | 96 | ``` 97 | case x509.CertificateInvalidError: 98 | cie, _ := err.(x509.CertificateInvalidError) 99 | if cie.Reason == x509.Expired { 100 | log.Printf("DEBUG Expired cert, %s", cie.Detail) 101 | return cie.Cert 102 | } 103 | ``` 104 | 105 | Since we don't provide any recovery currently, it will always cause the TLS connection to stop. 106 | 107 | --- 108 | 109 | ## * Notes, Lessons, Monologue 110 | * Bitmasks? With masks, we take the approach to combine recovery options and apply them on the recovery `tls.Dial`. It wasn't our initial attempt which was to loop the `tls.Dial`, and only toggle one recovery option each pass; the idea being to only enable the least number of options. In concept it sounds good, but the code was garbage. Maybe we can think of a design that is elegant, but for now we're doing masks. 111 | * `VerifyPeerCertificate`? We initially experimented with implementing this hook and failed repeatedly. We just don't have enough understanding yet. 112 | * Opinion? I'm a big fan of the toggle `InsecureSkipVerify`. Let me explain! NOT the use, but more about its design. It recognizes that there is real need to run that flow, whether a team is doing testing or there is a local/controlled network. Making this toggle built-in, accelerates that necessary work, while at the same time makes it explicit that it must be temporary or isolated, and reviewed under a microscope before shipping. I think this kind of language decision is what was meant when Go was charged with reducing stagnation and fear of touching legacy code. 113 | 114 | ###### 2022 興怡 | Always wrong, sometimes lucky 115 | 116 | --------------------------------------------------------------------------------