├── .github └── logo.png ├── .gitignore ├── README.md ├── csr.go └── main.go /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjojo/yubiTLS/b65e6dd1944a397eff529ba533567559ad91e850/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yubiTLS 2 | *.csr 3 | *.crt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | YubiTLS 2 | === 3 | 4 | ![logo](/.github/logo.png) 5 | 6 | This is a Golang HTTPS server demo that can be driven from a YubiKey as the key backend 7 | source. 8 | 9 | This was made for a post on my blog: 10 | 11 | https://blog.benjojo.co.uk/post/tls-https-server-from-a-yubikey 12 | 13 | You will need a functioning setup for Yubikey + GPG. 14 | 15 | Program options: 16 | 17 | ``` 18 | Usage of ./yubiTLS: 19 | -cacrtpath string 20 | the ssl CA certificate path 21 | -crtpath string 22 | the ssl certificate path 23 | -csr.cn string 24 | the Common Name of the CSR you want to generate (default "yubitls.benjojo.co.uk") 25 | -keyid string 26 | the Key ID in the agent to use 27 | -signcsr 28 | set to try to output a CSR 29 | ``` 30 | -------------------------------------------------------------------------------- /csr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | "crypto/x509/pkix" 7 | "encoding/pem" 8 | "flag" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | 13 | "github.com/prep/gpg/agent" 14 | ) 15 | 16 | var csrCN = flag.String("csr.cn", "yubitls.benjojo.co.uk", "the Common Name of the CSR you want to generate") 17 | 18 | func GenerateCSR(inkey agent.Key) { 19 | 20 | pub := inkey.Public() 21 | 22 | var csrTemplate = x509.CertificateRequest{ 23 | Subject: pkix.Name{CommonName: *csrCN}, 24 | DNSNames: []string{*csrCN}, 25 | PublicKeyAlgorithm: x509.RSA, 26 | SignatureAlgorithm: x509.SHA256WithRSA, 27 | PublicKey: pub, 28 | } 29 | 30 | csrDER, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, inkey) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | pemEncode := func(b []byte, t string) []byte { 36 | return pem.EncodeToMemory(&pem.Block{Bytes: b, Type: t}) 37 | } 38 | 39 | csrPEM := pemEncode(csrDER, "CERTIFICATE REQUEST") 40 | 41 | if err := ioutil.WriteFile(fmt.Sprintf("%s.csr", *csrCN), csrPEM, 0644); err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/pem" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | 14 | "github.com/prep/gpg/agent" 15 | ) 16 | 17 | func main() { 18 | csrgen := flag.Bool("signcsr", false, "set to try to output a CSR") 19 | selectedkeyid := flag.String("keyid", "", "the Key ID in the agent to use") 20 | certpath := flag.String("crtpath", "", "the ssl certificate path") 21 | cacertpath := flag.String("cacrtpath", "", "the ssl CA certificate path") 22 | flag.Parse() 23 | 24 | options := []string{ 25 | "allow-pinentry-notify", 26 | "agent-awareness=2.1.0", 27 | } 28 | 29 | conn, err := agent.Dial("/run/user/1000/gnupg/S.gpg-agent", options) 30 | 31 | if err != nil { 32 | log.Fatalf("Unable to connect to GPG agent! %s", err.Error()) 33 | } 34 | 35 | key, err := conn.Key(*selectedkeyid) 36 | 37 | if *selectedkeyid == "" || err != nil { 38 | keys, err := conn.Keys() 39 | if err != nil { 40 | log.Fatalf("Unable to read keys from GPG agent! %s", err.Error()) 41 | } 42 | 43 | printKeysAndFail(keys) 44 | } 45 | 46 | if *csrgen { 47 | GenerateCSR(key) 48 | os.Exit(0) 49 | } 50 | 51 | // We are now going to a TLS server I guess 52 | 53 | if *certpath == "" { 54 | log.Fatalf("please provide a cert with -crtpath") 55 | } 56 | 57 | certbytes, err := ioutil.ReadFile(*certpath) 58 | 59 | if err != nil { 60 | log.Fatalf("Unable to read cert %s", err.Error()) 61 | } 62 | 63 | cacertbytes, err := ioutil.ReadFile(*cacertpath) 64 | 65 | if err != nil { 66 | log.Fatalf("Unable to read ca cert %s", err.Error()) 67 | } 68 | 69 | tlsConfig := &tls.Config{ 70 | // Causes servers to use Go's default ciphersuite preferences, 71 | // which are tuned to avoid attacks. Does nothing on clients. 72 | PreferServerCipherSuites: true, 73 | MinVersion: tls.VersionTLS12, 74 | CipherSuites: []uint16{ 75 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 76 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 77 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only 78 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // Go 1.8 only 79 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 80 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 81 | 82 | // Best disabled, as they don't provide Forward Secrecy, 83 | // but might be necessary for some clients 84 | tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 85 | tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 86 | }, 87 | } 88 | 89 | tlsConfig.Certificates = make([]tls.Certificate, 1) 90 | var cert tls.Certificate 91 | var skippedBlockTypes []string 92 | 93 | var certDERBlock *pem.Block 94 | certDERBlock, _ = pem.Decode(certbytes) 95 | 96 | if certDERBlock.Type == "CERTIFICATE" { 97 | cert.Certificate = append(cert.Certificate, certDERBlock.Bytes) 98 | } else { 99 | skippedBlockTypes = append(skippedBlockTypes, certDERBlock.Type) 100 | } 101 | 102 | certDERBlock, _ = pem.Decode(cacertbytes) 103 | 104 | if certDERBlock.Type == "CERTIFICATE" { 105 | cert.Certificate = append(cert.Certificate, certDERBlock.Bytes) 106 | } else { 107 | skippedBlockTypes = append(skippedBlockTypes, certDERBlock.Type) 108 | } 109 | 110 | cert.PrivateKey = key 111 | 112 | tlsConfig.Certificates[0] = cert 113 | 114 | // Apparently this works?? 115 | 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | srv := &http.Server{ 121 | ReadTimeout: 5 * time.Second, 122 | WriteTimeout: 10 * time.Second, 123 | IdleTimeout: 120 * time.Second, 124 | TLSConfig: tlsConfig, 125 | Handler: http.DefaultServeMux, 126 | Addr: "[::]:8443", 127 | } 128 | 129 | http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { 130 | rw.Write( 131 | []byte("Secure hello from a YubiKey or GPG Smartcard!")) 132 | }) 133 | 134 | log.Printf("Listening") 135 | log.Println(srv.ListenAndServeTLS("", "")) 136 | 137 | conn.Close() 138 | } 139 | 140 | func printKeysAndFail(keys []agent.Key) { 141 | fmt.Printf("You appear to have not selected a key to use, or the key you selected\n") 142 | fmt.Printf("Does not exist in the agent at this time, Do you see your key in this list?\n") 143 | 144 | for _, v := range keys { 145 | log.Printf("Key: %s - %+v\n", v.Keygrip, v) 146 | } 147 | 148 | os.Exit(1) 149 | } 150 | --------------------------------------------------------------------------------