Skip to content
Draft
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
rsc.io/script v0.0.2
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/yaml v1.6.0
software.sslmate.com/src/go-pkcs12 v0.7.1
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 44 additions & 5 deletions internal/provider/cisco/nxos/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"

"github.com/openconfig/gnoi/cert"
"google.golang.org/grpc"
pkcs12 "software.sslmate.com/src/go-pkcs12"

"github.com/ironcore-dev/network-operator/internal/transport/gnmiext"
)

// Certificate represents a X.509 certificate and its associated private key.
// It can be used to load the certificate into a NX-OS device truspoint via gNOI.
type Certificate struct {
Key *rsa.PrivateKey
Cert *x509.Certificate
Key *rsa.PrivateKey
Cert *x509.Certificate
CACerts []*x509.Certificate
}

// Load loads the certificate into the specified trustpoint via the gNOI [cert service].
Expand All @@ -39,12 +42,22 @@ func (c *Certificate) Load(ctx context.Context, conn *grpc.ClientConn, trustpoin
return err
}

var chain []*cert.Certificate
for _, ca := range c.CACerts {
var buf bytes.Buffer
if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}); err != nil {
return fmt.Errorf("failed to encode CA certificate: %w", err)
}
chain = append(chain, &cert.Certificate{Type: cert.CertificateType_CT_X509, Certificate: buf.Bytes()})
}

// Only the `LoadCertificate` method is currently supported on the Nexus 9000 series, despite the fact that the gNOI certificate service is deprecated in favor of the gNSI certz service.
// See: https://www.cisco.com/c/en/us/td/docs/dcn/nx-os/nexus9000/104x/programmability/cisco-nexus-9000-series-nx-os-programmability-guide-104x/gnoi---operation-interface.html
_, err = cert.NewCertificateManagementClient(conn).LoadCertificate(ctx, &cert.LoadCertificateRequest{ //nolint:staticcheck
Certificate: &cert.Certificate{Type: cert.CertificateType_CT_X509, Certificate: b},
KeyPair: &cert.KeyPair{PrivateKey: priv, PublicKey: pub},
CertificateId: trustpoint,
Certificate: &cert.Certificate{Type: cert.CertificateType_CT_X509, Certificate: b},
KeyPair: &cert.KeyPair{PrivateKey: priv, PublicKey: pub},
CertificateId: trustpoint,
CaCertificates: chain,
}, grpc.WaitForReady(true))
return err
}
Expand Down Expand Up @@ -115,3 +128,29 @@ func (*KeyPair) IsListItem() {}
func (k *KeyPair) XPath() string {
return "System/userext-items/pkiext-items/keyring-items/KeyRing-list[name=" + k.Name + "]"
}

// EncodeCertificatePKCS12 encodes a tls.Certificate (leaf + chain + private key) into
// PKCS#12 format protected by the given passphrase. LegacyRC2 encoding is used
// for compatibility with NX-OS.
func EncodeCertificatePKCS12(cert *tls.Certificate, passphrase string) ([]byte, error) {
key, ok := cert.PrivateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("unsupported private key type: expected *rsa.PrivateKey, got %T", cert.PrivateKey)
}

var chain []*x509.Certificate
for _, der := range cert.Certificate[1:] {
ca, err := x509.ParseCertificate(der)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificate: %w", err)
}
chain = append(chain, ca)
}

pfx, err := pkcs12.LegacyRC2.Encode(key, cert.Leaf, chain, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to encode PKCS#12: %w", err)
}

return pfx, nil
}
61 changes: 47 additions & 14 deletions internal/provider/cisco/nxos/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"maps"
Expand Down Expand Up @@ -599,27 +602,57 @@ func (p *Provider) GetPeerStatus(ctx context.Context, req *provider.BGPPeerStatu
}

func (p *Provider) EnsureCertificate(ctx context.Context, req *provider.EnsureCertificateRequest) error {
tp := new(Trustpoint)
tp.Name = req.ID
if NXVersion(p.client.Capabilities()) >= VersionNX10_7_1 {
tp := new(Trustpoint)
tp.Name = req.ID

if err := p.Patch(ctx, tp); err != nil {
return err
}
if err := p.Patch(ctx, tp); err != nil {
return err
}

key, ok := req.Certificate.PrivateKey.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("unsupported private key type: expected *rsa.PrivateKey, got %T", req.Certificate.PrivateKey)
}

kp := new(KeyPair)
kp.Name = req.ID
if err := p.client.GetConfig(ctx, kp); !errors.Is(err, gnmiext.ErrNil) {
// If the key pair already exists, we cannot update it, so we skip the rest of the process.
return err
}

key, ok := req.Certificate.PrivateKey.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("unsupported private key type: expected *rsa.PrivateKey, got %T", req.Certificate.PrivateKey)
cert := &Certificate{Key: key, Cert: req.Certificate.Leaf}
for _, der := range req.Certificate.Certificate[1:] {
ca, err := x509.ParseCertificate(der)
if err != nil {
return fmt.Errorf("failed to parse CA certificate: %w", err)
}
cert.CACerts = append(cert.CACerts, ca)
}
return cert.Load(ctx, p.conn, req.ID)
}

kp := new(KeyPair)
kp.Name = req.ID
if err := p.client.GetConfig(ctx, kp); !errors.Is(err, gnmiext.ErrNil) {
// If the key pair already exists, we cannot update it, so we skip the rest of the process.
var pass [16]byte
_, _ = rand.Read(pass[:])
passphrase := hex.EncodeToString(pass[:])

pfx, err := EncodeCertificatePKCS12(req.Certificate, passphrase)
if err != nil {
return err
}

cert := &Certificate{Key: key, Cert: req.Certificate.Leaf}
return cert.Load(ctx, p.conn, req.ID)
b64 := base64.StdEncoding.EncodeToString(pfx)
file := "/bootflash/" + req.ID + ".pfx"

_, err = p.nxapi.Do(ctx, nxapi.NewRequest(
"feature bash-shell",
"crypto ca trustpoint "+req.ID,
"run bash echo '"+b64+"' | base64 -d > "+file,
"crypto ca import "+req.ID+" pkcs12 "+file+" "+passphrase,
"run bash rm "+file,
).WithRollback(nxapi.Stop))
return err
}

func (p *Provider) DeleteCertificate(ctx context.Context, req *provider.DeleteCertificateRequest) error {
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/cisco/nxos/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const (
VersionNX10_6_1 Version = "10.6(1)"
VersionNX10_6_2 Version = "10.6(2)"
VersionNX10_6_3 Version = "10.6(3)"

// VersionNX10_7_1 is the minimum version that supports CA chains via gNOI LoadCertificate.
// TODO: Update with the correct YANG model revision date once NX-OS 10.7(1) is released.
VersionNX10_7_1 Version = "10.7(1)"
)

// nxosVersions maps the revision date of the Cisco-NX-OS-device yang model to the corresponding [Version].
Expand Down
Loading