Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R4R: Implement HTTPS for the LCD REST server #2364

Merged
merged 5 commits into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ BREAKING CHANGES

* Gaia REST API (`gaiacli advanced rest-server`)
* [x/stake] Validator.Owner renamed to Validator.Operator
* [\#595](https://github.com/cosmos/cosmos-sdk/issues/595) Connections to the REST server are now secured using Transport Layer Security by default. The --insecure flag is provided to switch back to insecure HTTP.

* Gaia CLI (`gaiacli`)
* [x/stake] Validator.Owner renamed to Validator.Operator
Expand Down
174 changes: 174 additions & 0 deletions client/lcd/certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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: 30 days
const defaultValidFor = 30 * 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{"Gaia Lite"},
},
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)
cwgoes marked this conversation as resolved.
Show resolved Hide resolved
if block == nil {
return "", fmt.Errorf("couldn't find PEM data in %s", certFile)
}
return fingerprintForCertificate(block.Bytes)
}
93 changes: 93 additions & 0 deletions client/lcd/certificates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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)

// test failure
emptyFile, err := ioutil.TempFile("", "test_cert_")
require.Nil(t, err)
err = emptyFile.Close()
require.Nil(t, err)
defer os.Remove(emptyFile.Name())
_, err = fingerprintFromFile(emptyFile.Name())
require.NotNil(t, err)
}
Loading