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
111 changes: 92 additions & 19 deletions api/service/certificateissuer/certificate_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package certificateissuer

import (
"context"
"fmt"
"strings"
"time"

"github.com/openkcm/plugin-sdk/api"
)
Expand All @@ -11,36 +14,106 @@ type CertificateIssuer interface {

IssueCertificate(ctx context.Context, req *IssueCertificateRequest) (*IssueCertificateResponse, error)
}
type CertificateFormat int

type ValidityType int32
const (
CertificateFormatUnspecified CertificateFormat = iota
CertificateFormatPEM
CertificateFormatDER
CertificateFormatPKCS7
)

type KeyFormat int

const (
KeyFormatUnspecified KeyFormat = iota
KeyFormatPKCS1
KeyFormatPKCS8
KeyFormatSEC1
)

func (k KeyFormat) String() string {
switch k {
case KeyFormatPKCS1:
return "PKCS1"
case KeyFormatPKCS8:
return "PKCS8"
case KeyFormatSEC1:
return "SEC1"
default:
return "UNSPECIFIED"
}
}

type ValidityUnit int

const (
Unspecified ValidityType = iota
Days
Months
Years
ValidityUnitUnspecified ValidityUnit = iota
ValidityUnitDays
ValidityUnitMonths
ValidityUnitYears
)

// Domain Models

type RelativeValidity struct {
Value int32
Unit ValidityUnit
}

// CertificateLifetime represents the oneof field.
// In pure Go, using pointers allows us to check which field is active (not nil).
type CertificateLifetime struct {
Duration *time.Duration
NotAfter *time.Time
Relative *RelativeValidity
}

type Subject struct {
CommonName string
SerialNumber *string
Country []string
Organization []string
OrganizationalUnit []string
Locality []string
Province []string
StreetAddress []string
PostalCode []string
}

type PrivateKey struct {
Data []byte
Format KeyFormat
}

type IssueCertificateRequest struct {
// V1 Fields
CommonName string
Localities []string
Validity *CertificateValidity
PrivateKey *CertificatePrivateKey
Lifetime CertificateLifetime
Subject Subject
PrivateKey *PrivateKey
PreferredFormat CertificateFormat
}

type IssueCertificateResponse struct {
// V1 Fields
ChainPem string
CertificateData []byte
Format CertificateFormat
CAChain [][]byte
}

type CertificateValidity struct {
// V1 Fields
Value int64
Type ValidityType
type SupportedKeyFormatsError struct {
RejectedFormat string
SupportedFormats []KeyFormat
Reason string
}

type CertificatePrivateKey struct {
// V1 Fields
Data []byte
func (e *SupportedKeyFormatsError) Error() string {
var formats []string
for _, f := range e.SupportedFormats {
formats = append(formats, f.String())
}

return fmt.Sprintf("unsupported private key format '%s': %s (supported formats: %s)",
e.RejectedFormat,
e.Reason,
strings.Join(formats, ", "),
)
}
142 changes: 142 additions & 0 deletions api/service/certificateissuer/parse_certificate_chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package certificateissuer

import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"

"github.com/cloudflare/cfssl/crypto/pkcs7"
)

// ParseCertificateChain extracts the leaf certificate and its accompanying CA chain.
// Cognitive Complexity: drastically reduced by delegating parsing to parseBytes.
func (resp *IssueCertificateResponse) ParseCertificateChain() ([]*x509.Certificate, error) {
if len(resp.CertificateData) == 0 {
return nil, errors.New("certificate data is empty")
}

var rawCerts []*x509.Certificate

// Parse main payload
certs, err := parseBytes(resp.CertificateData, resp.Format)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate data: %w", err)
}
rawCerts = append(rawCerts, certs...)

// Parse CA chain elements
for i, caBytes := range resp.CAChain {
certs, err := parseBytes(caBytes, resp.Format)
if err != nil {
return nil, fmt.Errorf("failed to parse CA chain at index %d: %w", i, err)
}
rawCerts = append(rawCerts, certs...)
}

return orderCertificateChain(rawCerts), nil
}

// parseBytes is a dedicated helper that handles the format switching.
// This isolates the branching logic from the iteration logic.
func parseBytes(data []byte, format CertificateFormat) ([]*x509.Certificate, error) {
switch format {
case CertificateFormatDER:
cert, err := x509.ParseCertificate(data)
if err != nil {
return nil, err
}
return []*x509.Certificate{cert}, nil

case CertificateFormatPEM, CertificateFormatUnspecified:
block, _ := pem.Decode(data)
if block == nil || block.Type != "CERTIFICATE" {
return nil, errors.New("failed to decode valid PEM CERTIFICATE block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return []*x509.Certificate{cert}, nil

case CertificateFormatPKCS7:
p7, err := pkcs7.ParsePKCS7(data)
if err != nil {
return nil, err
}
if len(p7.Content.SignedData.Certificates) == 0 {
return nil, errors.New("no certificates found in the PKCS7 container")
}
return p7.Content.SignedData.Certificates, nil

default:
return nil, errors.New("unsupported certificate format")
}
}

// orderCertificateChain logically orders an unsorted slice of certificates.
// Cognitive Complexity: drastically reduced by using maps instead of nested loops.
func orderCertificateChain(certs []*x509.Certificate) []*x509.Certificate {
if len(certs) <= 1 {
return certs
}

subjects := make(map[string]*x509.Certificate)
issuers := make(map[string]bool)

// Build O(1) lookup maps
for _, cert := range certs {
subjects[string(cert.RawSubject)] = cert
issuers[string(cert.RawIssuer)] = true
}

leaf := findLeaf(certs, issuers)
if leaf == nil {
return certs // Fallback if no clean leaf is found
}

return buildChainFromLeaf(leaf, subjects, len(certs))
}

// findLeaf identifies the certificate that hasn't issued any other certificate in the pool.
func findLeaf(certs []*x509.Certificate, issuers map[string]bool) *x509.Certificate {
for _, cert := range certs {
if !issuers[string(cert.RawSubject)] {
return cert
}
}
return nil
}

// buildChainFromLeaf walks up the cryptographic chain using the subjects map.
func buildChainFromLeaf(leaf *x509.Certificate, subjects map[string]*x509.Certificate, total int) []*x509.Certificate {
ordered := make([]*x509.Certificate, 0, total)
ordered = append(ordered, leaf)

// Track added certs to prevent infinite loops (e.g., self-signed roots)
added := make(map[string]bool)
added[string(leaf.RawSubject)] = true

current := leaf
for len(ordered) < total {
parent, exists := subjects[string(current.RawIssuer)]

// Break if the chain breaks or we hit a circular loop/self-signed root
if !exists || added[string(parent.RawSubject)] {
break
}

ordered = append(ordered, parent)
added[string(parent.RawSubject)] = true
current = parent
}

// Append any remaining disconnected certificates
for _, cert := range subjects {
if !added[string(cert.RawSubject)] {
ordered = append(ordered, cert)
}
}

return ordered
}
74 changes: 61 additions & 13 deletions api/service/notification/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,73 @@ type Notification interface {
Send(ctx context.Context, req *SendNotificationRequest) (*SendNotificationResponse, error)
}

type Type int32
// Enums translated to pure Go types

type DeliveryChannel int

const (
Unspecified Type = iota
Email
Text
Web
DeliveryChannelUnspecified DeliveryChannel = iota
DeliveryChannelEmail
DeliveryChannelSMS
DeliveryChannelPush
DeliveryChannelInApp
)

func (d DeliveryChannel) String() string {
switch d {
case DeliveryChannelEmail:
return "EMAIL"
case DeliveryChannelSMS:
return "SMS"
case DeliveryChannelPush:
return "PUSH"
case DeliveryChannelInApp:
return "IN_APP"
default:
return "UNSPECIFIED"
}
}

// Domain Models

// Recipient uses pointers to represent the oneof field.
// Only one of these should be non-nil.
type Recipient struct {
EmailAddress *string
PhoneNumber *string
DeviceToken *string
UserID *string
}

type RawMessage struct {
Title string
Body string
Metadata map[string]string
}

type TemplateMessage struct {
TemplateID string
Parameters map[string]string
}

// NotificationContent uses pointers for the oneof field.
type NotificationContent struct {
Raw *RawMessage
Template *TemplateMessage
}

type SendNotificationRequest struct {
// V1 Fields
Type Type
Recipients []string
Subject string
Body string
Recipients []Recipient
Content NotificationContent
PreferredChannel DeliveryChannel
}

type DeliveryFailure struct {
Recipient Recipient
ErrorReason string
}

type SendNotificationResponse struct {
// V1 Fields
Success bool
Message string
TrackingID string
PartialFailures []DeliveryFailure
}
2 changes: 1 addition & 1 deletion cmd/protoc-gen-go-extension/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func generateServiceBridges(g *protogen.GeneratedFile, serviceName, serviceFullN
g.P()
g.P("const (")
if isPlugin {
g.P(" Type = ", strconv.Quote(serviceName))
g.P(" Type = ", strconv.Quote(strings.TrimSuffix(serviceName, "Service")))
}
g.P(" GRPCServiceFullName = ", strconv.Quote(serviceFullName))
g.P(")")
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/openkcm/plugin-sdk

go 1.25.4
go 1.26.0

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/go/protovalidate v1.1.2
github.com/cloudflare/cfssl v1.6.5
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.7.0
github.com/stretchr/testify v1.11.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
Loading
Loading