-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(go): add mTLS verify reference implementation
Signed-off-by: Artur Troian <[email protected]>
- Loading branch information
Showing
4 changed files
with
502 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package utils | ||
|
||
import "encoding/asn1" | ||
|
||
var AuthVersionOID = asn1.ObjectIdentifier{2, 23, 133, 2, 6} |
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,311 @@ | ||
package utils | ||
|
||
import ( | ||
"bytes" | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"math/big" | ||
"net" | ||
"os" | ||
"time" | ||
|
||
"go.step.sm/crypto/pemutil" | ||
Check failure on line 20 in go/node/cert/v1beta3/utils/key_pair_manager.go GitHub Actions / test
Check failure on line 20 in go/node/cert/v1beta3/utils/key_pair_manager.go GitHub Actions / coverage
|
||
|
||
types "github.com/akash-network/akash-api/go/node/cert/v1beta3" | ||
sdkclient "github.com/cosmos/cosmos-sdk/client" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
|
||
certerrors "github.com/akash-network/node/x/cert/errors" | ||
Check failure on line 26 in go/node/cert/v1beta3/utils/key_pair_manager.go GitHub Actions / test
Check failure on line 26 in go/node/cert/v1beta3/utils/key_pair_manager.go GitHub Actions / coverage
|
||
) | ||
|
||
var ( | ||
errCertificateNotFoundInPEM = fmt.Errorf("%w: certificate not found in PEM", certerrors.ErrCertificate) | ||
errPrivateKeyNotFoundInPEM = fmt.Errorf("%w: private key not found in PEM", certerrors.ErrCertificate) | ||
errPublicKeyNotFoundInPEM = fmt.Errorf("%w: public key not found in PEM", certerrors.ErrCertificate) | ||
errUnsupportedEncryptedPEM = errors.New("unsupported encrypted PEM") | ||
) | ||
|
||
type KeyPairManager interface { | ||
KeyExists() (bool, error) | ||
Generate(notBefore, notAfter time.Time, domains []string) error | ||
|
||
// Read the PEM blocks, containing the cert, private key, & public key | ||
Read(fin ...io.Reader) ([]byte, []byte, []byte, error) | ||
|
||
ReadX509KeyPair(fin ...io.Reader) (*x509.Certificate, tls.Certificate, error) | ||
} | ||
|
||
type keyPairManager struct { | ||
addr sdk.AccAddress | ||
passwordBytes []byte | ||
passwordLegacy []byte | ||
homeDir string | ||
} | ||
|
||
func NewKeyPairManager(cctx sdkclient.Context, fromAddress sdk.AccAddress) (KeyPairManager, error) { | ||
sig, _, err := cctx.Keyring.SignByAddress(fromAddress, []byte(fromAddress.String())) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// ignore error if ledger device is being used | ||
// due to its jsonparser not liking bech address sent as data in binary format | ||
// if test or file keyring used it will allow to decode old private keys for the mTLS cert | ||
sigLegacy, _, _ := cctx.Keyring.SignByAddress(fromAddress, fromAddress.Bytes()) | ||
|
||
return &keyPairManager{ | ||
addr: fromAddress, | ||
passwordBytes: sig, | ||
passwordLegacy: sigLegacy, | ||
homeDir: cctx.HomeDir, | ||
}, nil | ||
} | ||
|
||
func (kpm *keyPairManager) getKeyPath() string { | ||
return kpm.homeDir + "/" + kpm.addr.String() + ".pem" | ||
} | ||
|
||
func (kpm *keyPairManager) ReadX509KeyPair(fin ...io.Reader) (*x509.Certificate, tls.Certificate, error) { | ||
certData, privKeyData, _, err := kpm.Read(fin...) | ||
if err != nil { | ||
return nil, tls.Certificate{}, err | ||
} | ||
|
||
x509cert, err := x509.ParseCertificate(certData) | ||
if err != nil { | ||
return nil, tls.Certificate{}, fmt.Errorf("could not parse x509 cert: %w", err) | ||
} | ||
|
||
result := tls.Certificate{ | ||
Certificate: [][]byte{certData}, | ||
} | ||
|
||
result.PrivateKey, err = x509.ParsePKCS8PrivateKey(privKeyData) | ||
if err != nil { | ||
return nil, tls.Certificate{}, fmt.Errorf("%w: failed parsing private key data", err) | ||
} | ||
|
||
return x509cert, result, err | ||
} | ||
|
||
func (kpm *keyPairManager) KeyExists() (bool, error) { | ||
_, err := os.Stat(kpm.getKeyPath()) | ||
if err == nil { | ||
return true, nil | ||
} | ||
|
||
if os.IsNotExist(err) { | ||
return false, nil | ||
} | ||
|
||
return false, err | ||
} | ||
|
||
func (kpm *keyPairManager) Generate(notBefore, notAfter time.Time, domains []string) error { | ||
var err error | ||
var pemOut *os.File | ||
if pemOut, err = os.OpenFile(kpm.getKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { | ||
return err | ||
} | ||
|
||
err = kpm.generateImpl(notBefore, notAfter, domains, pemOut) | ||
|
||
closeErr := pemOut.Close() | ||
if closeErr != nil { | ||
return closeErr | ||
} | ||
|
||
return err | ||
} | ||
|
||
func (kpm *keyPairManager) generateImpl(notBefore, notAfter time.Time, domains []string, fout io.Writer) error { | ||
var err error | ||
// Generate the private key | ||
var priv *ecdsa.PrivateKey | ||
if priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil { | ||
return fmt.Errorf("could not generate key: %w", err) | ||
} | ||
|
||
serialNumber := new(big.Int).SetInt64(time.Now().UTC().UnixNano()) | ||
|
||
extKeyUsage := []x509.ExtKeyUsage{ | ||
x509.ExtKeyUsageClientAuth, | ||
} | ||
|
||
if len(domains) != 0 { | ||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth) | ||
} | ||
|
||
template := x509.Certificate{ | ||
SerialNumber: serialNumber, | ||
Subject: pkix.Name{ | ||
CommonName: kpm.addr.String(), | ||
ExtraNames: []pkix.AttributeTypeAndValue{ | ||
{ | ||
Type: AuthVersionOID, | ||
Value: "v0.0.1", | ||
}, | ||
}, | ||
}, | ||
Issuer: pkix.Name{ | ||
CommonName: kpm.addr.String(), | ||
}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment, | ||
ExtKeyUsage: extKeyUsage, | ||
BasicConstraintsValid: true, | ||
} | ||
|
||
var ips []net.IP | ||
|
||
for i := len(domains) - 1; i >= 0; i-- { | ||
if ip := net.ParseIP(domains[i]); ip != nil { | ||
ips = append(ips, ip) | ||
domains = append(domains[:i], domains[i+1:]...) | ||
} | ||
} | ||
|
||
if len(domains) != 0 || len(ips) != 0 { | ||
template.PermittedDNSDomainsCritical = true | ||
template.PermittedDNSDomains = domains | ||
template.DNSNames = domains | ||
template.IPAddresses = ips | ||
} | ||
|
||
var certDer []byte | ||
if certDer, err = x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv); err != nil { | ||
return fmt.Errorf("could not create certificate: %w", err) | ||
} | ||
|
||
var keyDer []byte | ||
if keyDer, err = x509.MarshalPKCS8PrivateKey(priv); err != nil { | ||
return fmt.Errorf("could not create private key: %w", err) | ||
} | ||
|
||
var blk *pem.Block | ||
blk, err = pemutil.EncryptPKCS8PrivateKey(rand.Reader, keyDer, kpm.passwordBytes, x509.PEMCipherAES256) | ||
if err != nil { | ||
return fmt.Errorf("could not encrypt private key as PEM: %w", err) | ||
} | ||
|
||
// Write the certificate | ||
if err = pem.Encode(fout, &pem.Block{Type: types.PemBlkTypeCertificate, Bytes: certDer}); err != nil { | ||
return fmt.Errorf("could not encode certificate as PEM: %w", err) | ||
} | ||
|
||
// Write the encrypted private key | ||
if err = pem.Encode(fout, blk); err != nil { | ||
return fmt.Errorf("could not encode private key as PEM: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (kpm *keyPairManager) Read(fin ...io.Reader) ([]byte, []byte, []byte, error) { | ||
var pemIn io.Reader | ||
var closeMe io.ReadCloser | ||
|
||
if len(fin) != 0 { | ||
if len(fin) != 1 { | ||
return nil, nil, nil, fmt.Errorf("%w: Read() takes exactly 1 or 0 arguments, not %d", certerrors.ErrCertificate, len(fin)) | ||
} | ||
pemIn = fin[0] | ||
} | ||
|
||
if pemIn == nil { | ||
fopen, err := os.OpenFile(kpm.getKeyPath(), os.O_RDONLY, 0x0) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("could not open certificate PEM file: %w", err) | ||
} | ||
closeMe = fopen | ||
pemIn = fopen | ||
} | ||
|
||
cert, privKey, pubKey, err := kpm.readImpl(pemIn) | ||
|
||
if closeMe != nil { | ||
closeErr := closeMe.Close() | ||
if closeErr != nil { | ||
return nil, nil, nil, fmt.Errorf("could not close PEM file: %w", closeErr) | ||
} | ||
} | ||
|
||
return cert, privKey, pubKey, err | ||
} | ||
|
||
func (kpm *keyPairManager) readImpl(fin io.Reader) ([]byte, []byte, []byte, error) { | ||
buf := &bytes.Buffer{} | ||
_, err := io.Copy(buf, fin) | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("failed reading certificate PEM file: %w", err) | ||
} | ||
data := buf.Bytes() | ||
|
||
// Read certificate | ||
block, remaining := pem.Decode(data) | ||
if block == nil { | ||
return nil, nil, nil, errCertificateNotFoundInPEM | ||
} | ||
cert := block.Bytes | ||
|
||
// Read private key | ||
block, _ = pem.Decode(remaining) | ||
if block == nil { | ||
return nil, nil, nil, errPrivateKeyNotFoundInPEM | ||
} | ||
|
||
var privKeyPlaintext []byte | ||
var privKeyI interface{} | ||
|
||
// PKCS#8 header defined in RFC7468 section 11 | ||
// nolint: gocritic | ||
if block.Type == "ENCRYPTED PRIVATE KEY" { | ||
privKeyPlaintext, err = pemutil.DecryptPKCS8PrivateKey(block.Bytes, kpm.passwordBytes) | ||
} else if block.Headers["Proc-Type"] == "4,ENCRYPTED" { | ||
// nolint: staticcheck | ||
privKeyPlaintext, _ = x509.DecryptPEMBlock(block, kpm.passwordBytes) | ||
|
||
// DecryptPEMBlock may not return IncorrectPasswordError. | ||
// Try parse private key instead and if it fails give another try with legacy password | ||
privKeyI, err = x509.ParsePKCS8PrivateKey(privKeyPlaintext) | ||
if err != nil { | ||
// nolint: staticcheck | ||
privKeyPlaintext, err = x509.DecryptPEMBlock(block, kpm.passwordLegacy) | ||
} | ||
} else { | ||
return nil, nil, nil, errUnsupportedEncryptedPEM | ||
} | ||
if err != nil { | ||
return nil, nil, nil, fmt.Errorf("%w: failed decrypting x509 block with private key", err) | ||
} | ||
|
||
if privKeyI == nil { | ||
if privKeyI, err = x509.ParsePKCS8PrivateKey(privKeyPlaintext); err != nil { | ||
return nil, nil, nil, fmt.Errorf("%w: failed parsing private key data", err) | ||
} | ||
} | ||
|
||
eckey, valid := privKeyI.(*ecdsa.PrivateKey) | ||
if !valid { | ||
return nil, nil, nil, fmt.Errorf("%w: unexpected private key type, expected %T but got %T", | ||
errPublicKeyNotFoundInPEM, | ||
&ecdsa.PrivateKey{}, | ||
privKeyI) | ||
} | ||
|
||
var pubKey []byte | ||
if pubKey, err = x509.MarshalPKIXPublicKey(eckey.Public()); err != nil { | ||
return nil, nil, nil, fmt.Errorf("%w: failed extracting public key", err) | ||
} | ||
|
||
return cert, privKeyPlaintext, pubKey, nil | ||
} |
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,60 @@ | ||
package utils | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
"io" | ||
"time" | ||
|
||
certerrors "github.com/akash-network/node/x/cert/errors" | ||
|
||
"github.com/cosmos/cosmos-sdk/client" | ||
|
||
ctypes "github.com/akash-network/akash-api/go/node/cert/v1beta3" | ||
) | ||
|
||
// LoadAndQueryCertificateForAccount wraps LoadAndQueryPEMForAccount and tls.X509KeyPair | ||
func LoadAndQueryCertificateForAccount(ctx context.Context, cctx client.Context, fin io.Reader) (tls.Certificate, error) { | ||
kpm, err := NewKeyPairManager(cctx, cctx.FromAddress) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
x509cert, tlsCert, err := kpm.ReadX509KeyPair(fin) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
// Check if valid according to time | ||
if x509cert.NotBefore.After(time.Now().UTC()) { | ||
return tls.Certificate{}, fmt.Errorf("%w: certificate is not yet active, start ts %s", certerrors.ErrCertificate, x509cert.NotBefore) | ||
} | ||
|
||
if time.Now().UTC().After(x509cert.NotAfter) { | ||
return tls.Certificate{}, fmt.Errorf("%w: certificate has been expired since %s", certerrors.ErrCertificate, x509cert.NotAfter) | ||
} | ||
|
||
params := &ctypes.QueryCertificatesRequest{ | ||
Filter: ctypes.CertificateFilter{ | ||
Owner: x509cert.Subject.CommonName, | ||
Serial: x509cert.SerialNumber.String(), | ||
}, | ||
} | ||
|
||
certs, err := ctypes.NewQueryClient(cctx).Certificates(ctx, params) | ||
if err != nil { | ||
return tls.Certificate{}, err | ||
} | ||
|
||
if len(certs.Certificates) == 0 { | ||
return tls.Certificate{}, fmt.Errorf("%w: certificate has not been committed to blockchain", certerrors.ErrCertificate) | ||
} | ||
|
||
foundCert := certs.Certificates[0] | ||
if foundCert.GetCertificate().State != ctypes.CertificateValid { | ||
return tls.Certificate{}, fmt.Errorf("%w: certificate is not valid", certerrors.ErrCertificate) | ||
} | ||
|
||
return tlsCert, nil | ||
} |
Oops, something went wrong.