COCOS-397 - Agent certificate generation via CA service (#410)
CI / checkproto (push) Has been cancelled
CI / ci (push) Has been cancelled

* Initial commit, will be tested before creating a PR

* Initial commit, will be tested before creating a PR

* Fixed all issues

* Initial commit, will be tested before creating a PR

* Updated agent docs

* Fixed based on comments

* Fixed based on comments

* Initial commit, will be tested before creating a PR

* Updated agent docs

* Fixed based on comments

* Fixed based on comments

* added certificate verification

* Initial commit, will be tested before creating a PR

* Fixed all issues

* Initial commit, will be tested before creating a PR

* Initial commit, will be tested before creating a PR

* Updated agent docs

* Fixed based on comments

* Fixed based on comments

* added certificate verification

* Fixed rebase errors

* Fixed proto issues

* fixed proto issues

* Fixed format error

* Fixed based on comments

* NOISSUE - Simplify local agent running in non sev-snp environment (#411)

* Add vtpm attestation support to agent service and server

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Update mockery version to v2.53.2 and refactor VM factory to include logger

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Send event notification when computation is stopped in agentService

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove redundant assignment of Stderr in qemuVM Start method

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Rename SVM references to CVM in tracing, logging, metrics, and service layers

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Bump github.com/docker/docker (#416)

Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.0.1+incompatible to 28.0.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.0.1...v28.0.4)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump google.golang.org/protobuf from 1.36.5 to 1.36.6 (#412)

Bumps google.golang.org/protobuf from 1.36.5 to 1.36.6.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* COCOS-393 - Disable SSH service and update user shell in cloud config (#396)

* Disable SSH service and update user shell in cloud config

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove SSH server and clean up dependencies in cloud config

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add firewall configuration and ensure iptables rules persist after reboot

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add algo_user configuration and setup script for container execution

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Initial commit, will be tested before creating a PR

* Fixed all issues

* Initial commit, will be tested before creating a PR

* Initial commit, will be tested before creating a PR

* Fixed based on comments

* Fixed based on comments

* added certificate verification

* Initial commit, will be tested before creating a PR

* Fixed all issues

* Initial commit, will be tested before creating a PR

* Initial commit, will be tested before creating a PR

* Fixed based on comments

* Fixed rebase errors

* Fixed format error

* Fixed based on comments

* Fixed rebase errors

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Jovan Djukic
2025-04-02 16:52:57 +02:00
committed by GitHub
parent 12a20c74f7
commit ec306c72b9
14 changed files with 412 additions and 131 deletions
+137 -16
View File
@@ -4,6 +4,7 @@
package grpc
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
@@ -11,15 +12,23 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"math/big"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
certs "github.com/absmach/certs"
certscli "github.com/absmach/certs/cli"
"github.com/absmach/certs/errors"
certssdk "github.com/absmach/certs/sdk"
"github.com/google/go-sev-guest/client"
agentgrpc "github.com/ultravioletrs/cocos/agent/api/grpc"
"github.com/ultravioletrs/cocos/agent/auth"
@@ -55,13 +64,19 @@ type Server struct {
quoteProvider client.LeveledQuoteProvider
authSvc auth.Authenticator
health *health.Server
caUrl string
cvmId string
}
type csrReq struct {
CSR string `json:"csr,omitempty"`
}
type serviceRegister func(srv *grpc.Server)
var _ server.Server = (*Server)(nil)
func New(ctx context.Context, cancel context.CancelFunc, name string, config server.ServerConfiguration, registerService serviceRegister, logger *slog.Logger, qp client.LeveledQuoteProvider, authSvc auth.Authenticator) server.Server {
func New(ctx context.Context, cancel context.CancelFunc, name string, config server.ServerConfiguration, registerService serviceRegister, logger *slog.Logger, qp client.LeveledQuoteProvider, authSvc auth.Authenticator, caUrl string, cvmId string) server.Server {
base := config.GetBaseConfig()
listenFullAddress := fmt.Sprintf("%s:%s", base.Host, base.Port)
return &Server{
@@ -76,6 +91,8 @@ func New(ctx context.Context, cancel context.CancelFunc, name string, config ser
registerService: registerService,
quoteProvider: qp,
authSvc: authSvc,
caUrl: caUrl,
cvmId: cvmId,
}
}
@@ -95,7 +112,7 @@ func (s *Server) Start() error {
var listener net.Listener
if agCfg, ok := s.Config.(server.AgentConfig); ok && agCfg.AttestedTLS {
certificateBytes, privateKeyBytes, err := generateCertificatesForATLS()
certificateBytes, privateKeyBytes, err := generateCertificatesForATLS(s.caUrl, s.cvmId)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
}
@@ -258,33 +275,99 @@ func loadX509KeyPair(certfile, keyfile string) (tls.Certificate, error) {
return tls.X509KeyPair(cert, key)
}
func generateCertificatesForATLS() ([]byte, []byte, error) {
func generateCertificatesForATLS(caUrl string, cvmId string) ([]byte, []byte, error) {
curve := elliptic.P256()
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private/public key: %w", err)
}
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(202403311),
Subject: pkix.Name{
var certDERBytes []byte
if caUrl == "" || cvmId == "" {
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(202403311),
Subject: pkix.Name{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
DERBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
}
certDERBytes = DERBytes
} else {
csrmd := certs.CSRMetadata{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
}
certDERBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
csr, err := certscli.CreateCSR(csrmd, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create CSR: %w", err)
}
csrData := string(csr.CSR)
r := csrReq{
CSR: csrData,
}
data, error := json.Marshal(r)
if error != nil {
return nil, nil, errors.NewSDKError(error)
}
notBefore := time.Now()
notAfter := time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay)
ttlString := notAfter.Sub(notBefore).String()
query := url.Values{}
query.Add("ttl", ttlString)
query_string := query.Encode()
certsEndpoint := "certs"
csrEndpoint := "csrs"
endpoint := fmt.Sprintf("%s/%s/%s", certsEndpoint, csrEndpoint, cvmId)
url := fmt.Sprintf("%s/%s?%s", caUrl, endpoint, query_string)
_, body, sdkerr := processRequest(http.MethodPost, url, data, nil, http.StatusOK)
if sdkerr != nil {
return nil, nil, errors.NewSDKError(sdkerr)
}
var cert certssdk.Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return nil, nil, errors.NewSDKError(err)
}
cleanCertificateString := strings.ReplaceAll(cert.Certificate, "\\n", "\n")
block, rest := pem.Decode([]byte(cleanCertificateString))
if len(rest) != 0 {
return nil, nil, fmt.Errorf("failed to convert generated certificate to DER format: %s", cleanCertificateString)
}
certDERBytes = block.Bytes
}
certBytes := pem.EncodeToMemory(&pem.Block{
@@ -318,3 +401,41 @@ func generateCertificatesForATLS() ([]byte, []byte, error) {
return certBytes, keyBytes, nil
}
func processRequest(method, reqUrl string, data []byte, headers map[string]string, expectedRespCodes ...int) (http.Header, []byte, errors.SDKError) {
req, err := http.NewRequest(method, reqUrl, bytes.NewReader(data))
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
// Sets a default value for the Content-Type.
// Overridden if Content-Type is passed in the headers arguments.
req.Header.Add("Content-Type", "application/json")
for key, value := range headers {
req.Header.Add(key, value)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Do(req)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
defer resp.Body.Close()
sdkerr := errors.CheckError(resp, expectedRespCodes...)
if sdkerr != nil {
return make(http.Header), []byte{}, sdkerr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
return resp.Header, body, nil
}
+5 -5
View File
@@ -73,7 +73,7 @@ func TestNew(t *testing.T) {
qp := new(mocks.LeveledQuoteProvider)
authSvc := new(authmocks.Authenticator)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc, "", "")
assert.NotNil(t, srv)
assert.IsType(t, &Server{}, srv)
@@ -123,7 +123,7 @@ func TestServerStartWithTLSFile(t *testing.T) {
qp := new(mocks.LeveledQuoteProvider)
authSvc := new(authmocks.Authenticator)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc, "", "")
var wg sync.WaitGroup
wg.Add(1)
@@ -170,7 +170,7 @@ func TestServerStartWithmTLSFile(t *testing.T) {
qp := new(mocks.LeveledQuoteProvider)
authSvc := new(authmocks.Authenticator)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc, "", "")
var wg sync.WaitGroup
wg.Add(1)
@@ -210,7 +210,7 @@ func TestServerStop(t *testing.T) {
qp := new(mocks.LeveledQuoteProvider)
authSvc := new(authmocks.Authenticator)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc)
srv := New(ctx, cancel, "TestServer", config, func(srv *grpc.Server) {}, logger, qp, authSvc, "", "")
go func() {
err := srv.Start()
@@ -402,7 +402,7 @@ func TestServerInitializationAndStartup(t *testing.T) {
qp := new(mocks.LeveledQuoteProvider)
authSvc := new(authmocks.Authenticator)
srv := New(ctx, cancel, "TestServer", tc.config, func(srv *grpc.Server) {}, logger, qp, authSvc)
srv := New(ctx, cancel, "TestServer", tc.config, func(srv *grpc.Server) {}, logger, qp, authSvc, "", "")
var wg sync.WaitGroup
wg.Add(1)