-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement HTTPS for the LCD REST server
In order to guarantee a secure connection between apps and the LCD the communication must be encrypted - even if clients and server run on the same local machine, credentials must never be transmitted in clear text. Upon start up, the server generates a self-signed certificate and a key. Both are stored as temporary files; removal is guaranteed on exit. This new behaviour is now enabled by default, though users are provided with a --insecure flag to switch it off. See #595
- Loading branch information
Alessio Treglia
committed
Sep 21, 2018
1 parent
8721dd6
commit f5ea1e6
Showing
5 changed files
with
338 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package lcd | ||
|
||
import ( | ||
"bytes" | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/sha256" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"math/big" | ||
"net" | ||
"os" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// default: 10 years | ||
const defaultValidFor = 365 * 24 * time.Hour | ||
|
||
func generateSelfSignedCert(host string) (certBytes []byte, priv *ecdsa.PrivateKey, err error) { | ||
priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
notBefore := time.Now() | ||
notAfter := notBefore.Add(defaultValidFor) | ||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||
if err != nil { | ||
err = fmt.Errorf("failed to generate serial number: %s", err) | ||
return | ||
} | ||
|
||
template := x509.Certificate{ | ||
SerialNumber: serialNumber, | ||
Subject: pkix.Name{ | ||
Organization: []string{"Acme Co"}, | ||
}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
BasicConstraintsValid: true, | ||
IsCA: true, | ||
} | ||
hosts := strings.Split(host, ",") | ||
for _, h := range hosts { | ||
if ip := net.ParseIP(h); ip != nil { | ||
template.IPAddresses = append(template.IPAddresses, ip) | ||
} else { | ||
template.DNSNames = append(template.DNSNames, h) | ||
} | ||
} | ||
|
||
certBytes, err = x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||
if err != nil { | ||
err = fmt.Errorf("couldn't create certificate: %s", err) | ||
return | ||
} | ||
return | ||
} | ||
|
||
func writeCertAndPrivKey(certBytes []byte, priv *ecdsa.PrivateKey) (certFile string, keyFile string, err error) { | ||
if priv == nil { | ||
err = errors.New("private key is nil") | ||
return | ||
} | ||
certFile, err = writeCertificateFile(certBytes) | ||
if err != nil { | ||
return | ||
} | ||
keyFile, err = writeKeyFile(priv) | ||
return | ||
} | ||
|
||
func writeCertificateFile(certBytes []byte) (filename string, err error) { | ||
f, err := ioutil.TempFile("", "cert_") | ||
if err != nil { | ||
return | ||
} | ||
defer f.Close() | ||
filename = f.Name() | ||
if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}); err != nil { | ||
return filename, fmt.Errorf("failed to write data to %s: %s", filename, err) | ||
} | ||
return | ||
} | ||
|
||
func writeKeyFile(priv *ecdsa.PrivateKey) (filename string, err error) { | ||
f, err := ioutil.TempFile("", "key_") | ||
if err != nil { | ||
return | ||
} | ||
defer f.Close() | ||
filename = f.Name() | ||
block, err := pemBlockForKey(priv) | ||
if err != nil { | ||
return | ||
} | ||
if err := pem.Encode(f, block); err != nil { | ||
return filename, fmt.Errorf("failed to write data to %s: %s", filename, err) | ||
} | ||
return | ||
} | ||
|
||
func pemBlockForKey(priv *ecdsa.PrivateKey) (*pem.Block, error) { | ||
b, err := x509.MarshalECPrivateKey(priv) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to marshal ECDSA private key: %v", err) | ||
} | ||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil | ||
|
||
} | ||
|
||
func genCertKeyFilesAndReturnFingerprint(sslHosts string) (certFile, keyFile string, fingerprint string, err error) { | ||
certBytes, priv, err := generateSelfSignedCert(sslHosts) | ||
if err != nil { | ||
return | ||
} | ||
certFile, keyFile, err = writeCertAndPrivKey(certBytes, priv) | ||
cleanupFunc := func() { | ||
os.Remove(certFile) | ||
os.Remove(keyFile) | ||
} | ||
// Either of the files could have been written already, | ||
// thus clean up regardless of the error. | ||
if err != nil { | ||
defer cleanupFunc() | ||
return | ||
} | ||
fingerprint, err = fingerprintForCertificate(certBytes) | ||
if err != nil { | ||
defer cleanupFunc() | ||
return | ||
} | ||
return | ||
} | ||
|
||
func fingerprintForCertificate(certBytes []byte) (string, error) { | ||
cert, err := x509.ParseCertificate(certBytes) | ||
if err != nil { | ||
return "", err | ||
} | ||
h := sha256.New() | ||
h.Write(cert.Raw) | ||
fingerprintBytes := h.Sum(nil) | ||
var buf bytes.Buffer | ||
for i, b := range fingerprintBytes { | ||
if i > 0 { | ||
fmt.Fprintf(&buf, ":") | ||
} | ||
fmt.Fprintf(&buf, "%02X", b) | ||
} | ||
return fmt.Sprintf("SHA256 Fingerprint=%s", buf.String()), nil | ||
} | ||
|
||
func fingerprintFromFile(certFile string) (string, error) { | ||
f, err := os.Open(certFile) | ||
if err != nil { | ||
return "", err | ||
} | ||
defer f.Close() | ||
data, err := ioutil.ReadAll(f) | ||
if err != nil { | ||
return "", err | ||
} | ||
block, _ := pem.Decode(data) | ||
return fingerprintForCertificate(block.Bytes) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package lcd | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/x509" | ||
"io/ioutil" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestGenerateSelfSignedCert(t *testing.T) { | ||
host := "127.0.0.1,localhost,::1" | ||
certBytes, _, err := generateSelfSignedCert(host) | ||
require.Nil(t, err) | ||
cert, err := x509.ParseCertificate(certBytes) | ||
require.Nil(t, err) | ||
require.Equal(t, 2, len(cert.IPAddresses)) | ||
require.Equal(t, 1, len(cert.DNSNames)) | ||
require.True(t, cert.IsCA) | ||
} | ||
|
||
func TestWriteCertAndPrivKey(t *testing.T) { | ||
expectedPerm := "-rw-------" | ||
derBytes, priv, err := generateSelfSignedCert("localhost") | ||
require.Nil(t, err) | ||
type args struct { | ||
certBytes []byte | ||
priv *ecdsa.PrivateKey | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
wantErr bool | ||
}{ | ||
{"valid certificate", args{derBytes, priv}, false}, | ||
{"garbage", args{[]byte("some garbage"), nil}, true}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
gotCertFile, gotKeyFile, err := writeCertAndPrivKey(tt.args.certBytes, tt.args.priv) | ||
defer os.Remove(gotCertFile) | ||
defer os.Remove(gotKeyFile) | ||
if tt.wantErr { | ||
require.NotNil(t, err) | ||
return | ||
} | ||
require.Nil(t, err) | ||
info, err := os.Stat(gotCertFile) | ||
require.Nil(t, err) | ||
require.True(t, info.Mode().IsRegular()) | ||
require.Equal(t, expectedPerm, info.Mode().String()) | ||
info, err = os.Stat(gotKeyFile) | ||
require.Nil(t, err) | ||
require.True(t, info.Mode().IsRegular()) | ||
require.Equal(t, expectedPerm, info.Mode().String()) | ||
}) | ||
} | ||
} | ||
|
||
func TestFingerprintFromFile(t *testing.T) { | ||
cert := `-----BEGIN CERTIFICATE----- | ||
MIIBbDCCARGgAwIBAgIQSuFKYv/22v+cxtVgMUrQADAKBggqhkjOPQQDAjASMRAw | ||
DgYDVQQKEwdBY21lIENvMB4XDTE4MDkyMDIzNDQyNloXDTE5MDkyMDIzNDQyNlow | ||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDIo | ||
ujAesRczcPVAWiLhpeV1B7hS/RI2LJaGj3QjyJ8hiUthJTPIamr8m7LuS/U5fS0o | ||
hY297YeTIGo9YkxClICjSTBHMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr | ||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA8GA1UdEQQIMAaHBH8AAAEwCgYIKoZI | ||
zj0EAwIDSQAwRgIhAKnwbhX9FrGG1otCVLwhClQ3RaLxnNpCgIGTqSimb34cAiEA | ||
stMN+IqMCKWlZyGqxGIiyksMLMEU3lRqKNQn2EoAZJY= | ||
-----END CERTIFICATE-----` | ||
wantFingerprint := `SHA256 Fingerprint=0B:ED:9A:AA:A2:D1:7E:B2:53:56:F6:FC:C0:E6:1A:69:70:21:A2:B0:90:FC:AF:BB:EF:AE:2C:78:52:AB:68:40` | ||
certFile, err := ioutil.TempFile("", "test_cert_") | ||
require.Nil(t, err) | ||
_, err = certFile.Write([]byte(cert)) | ||
require.Nil(t, err) | ||
err = certFile.Close() | ||
require.Nil(t, err) | ||
defer os.Remove(certFile.Name()) | ||
fingerprint, err := fingerprintFromFile(certFile.Name()) | ||
require.Nil(t, err) | ||
require.Equal(t, wantFingerprint, fingerprint) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.