mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
9080563b7d
* Fix go lint warnings * Add test for SMTPConfigured update
592 lines
18 KiB
Go
592 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
|
|
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/kolide/kit/version"
|
|
)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get AppConfig
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type appConfigResponse struct {
|
|
fleet.AppConfig
|
|
|
|
UpdateInterval *fleet.UpdateIntervalConfig `json:"update_interval"`
|
|
Vulnerabilities *fleet.VulnerabilitiesConfig `json:"vulnerabilities"`
|
|
|
|
// License is loaded from the service
|
|
License *fleet.LicenseInfo `json:"license,omitempty"`
|
|
// Logging is loaded on the fly rather than from the database.
|
|
Logging *fleet.Logging `json:"logging,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r appConfigResponse) error() error { return r.Err }
|
|
|
|
func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return nil, errors.New("could not fetch user")
|
|
}
|
|
config, err := svc.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
license, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loggingConfig, err := svc.LoggingConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateIntervalConfig, err := svc.UpdateIntervalConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vulnConfig, err := svc.VulnerabilitiesConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var smtpSettings fleet.SMTPSettings
|
|
var ssoSettings fleet.SSOSettings
|
|
var hostExpirySettings fleet.HostExpirySettings
|
|
var agentOptions *json.RawMessage
|
|
// only admin can see smtp, sso, and host expiry settings
|
|
if vc.User.GlobalRole != nil && *vc.User.GlobalRole == fleet.RoleAdmin {
|
|
smtpSettings = config.SMTPSettings
|
|
ssoSettings = config.SSOSettings
|
|
hostExpirySettings = config.HostExpirySettings
|
|
agentOptions = config.AgentOptions
|
|
}
|
|
hostSettings := config.HostSettings
|
|
response := appConfigResponse{
|
|
AppConfig: fleet.AppConfig{
|
|
OrgInfo: config.OrgInfo,
|
|
ServerSettings: config.ServerSettings,
|
|
HostSettings: hostSettings,
|
|
VulnerabilitySettings: config.VulnerabilitySettings,
|
|
|
|
SMTPSettings: smtpSettings,
|
|
SSOSettings: ssoSettings,
|
|
HostExpirySettings: hostExpirySettings,
|
|
AgentOptions: agentOptions,
|
|
|
|
WebhookSettings: config.WebhookSettings,
|
|
Integrations: config.Integrations,
|
|
},
|
|
UpdateInterval: updateIntervalConfig,
|
|
Vulnerabilities: vulnConfig,
|
|
License: license,
|
|
Logging: loggingConfig,
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (svc *Service) AppConfig(ctx context.Context) (*fleet.AppConfig, error) {
|
|
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
ac, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ac.SMTPSettings.SMTPPassword != "" {
|
|
ac.SMTPSettings.SMTPPassword = "********"
|
|
}
|
|
|
|
for _, jiraIntegration := range ac.Integrations.Jira {
|
|
jiraIntegration.APIToken = "********"
|
|
}
|
|
|
|
for _, zdIntegration := range ac.Integrations.Zendesk {
|
|
zdIntegration.APIToken = "********"
|
|
}
|
|
|
|
return ac, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Modify AppConfig
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type modifyAppConfigRequest struct {
|
|
json.RawMessage
|
|
}
|
|
|
|
func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
req := request.(*modifyAppConfigRequest)
|
|
config, err := svc.ModifyAppConfig(ctx, req.RawMessage)
|
|
if err != nil {
|
|
return appConfigResponse{Err: err}, nil
|
|
}
|
|
license, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loggingConfig, err := svc.LoggingConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response := appConfigResponse{
|
|
AppConfig: *config,
|
|
License: license,
|
|
Logging: loggingConfig,
|
|
}
|
|
|
|
if response.SMTPSettings.SMTPPassword != "" {
|
|
response.SMTPSettings.SMTPPassword = "********"
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte) (*fleet.AppConfig, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// we need the config from the datastore because API tokens are obfuscated at the service layer
|
|
// we will retrieve the obfuscated config before we return
|
|
appConfig, err := svc.ds.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldSmtpSettings := appConfig.SMTPSettings
|
|
|
|
storedJira := appConfig.Integrations.Jira
|
|
storedJiraByProjectKey := make(map[string]fleet.JiraIntegration)
|
|
if len(storedJira) > 0 {
|
|
for _, settings := range storedJira {
|
|
if _, ok := storedJiraByProjectKey[settings.ProjectKey]; ok {
|
|
// each jira integration must have unique project key
|
|
return nil, ctxerr.Wrap(ctx, fmt.Errorf("duplicate Jira integration for project key %s", settings.ProjectKey))
|
|
}
|
|
storedJiraByProjectKey[settings.ProjectKey] = *settings
|
|
}
|
|
}
|
|
|
|
storedZendesk := appConfig.Integrations.Zendesk
|
|
storedZendeskByGroupID := make(map[int64]fleet.ZendeskIntegration)
|
|
if len(storedZendesk) > 0 {
|
|
for _, settings := range storedZendesk {
|
|
if _, ok := storedZendeskByGroupID[settings.GroupID]; ok {
|
|
// each zendesk integration must have unique group id
|
|
return nil, ctxerr.Wrap(ctx, fmt.Errorf("duplicate Zendesk integration for group id %v", settings.GroupID))
|
|
}
|
|
storedZendeskByGroupID[settings.GroupID] = *settings
|
|
}
|
|
}
|
|
|
|
// TODO(mna): this ports the validations from the old validationMiddleware
|
|
// correctly, but this could be optimized so that we don't unmarshal the
|
|
// incoming bytes twice.
|
|
invalid := &fleet.InvalidArgumentError{}
|
|
var newAppConfig fleet.AppConfig
|
|
if err := json.Unmarshal(p, &newAppConfig); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
|
|
validateSSOSettings(newAppConfig, appConfig, invalid)
|
|
if invalid.HasErrors() {
|
|
return nil, ctxerr.Wrap(ctx, invalid)
|
|
}
|
|
|
|
// We apply the config that is incoming to the old one
|
|
decoder := json.NewDecoder(bytes.NewReader(p))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&appConfig); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, &badRequestError{message: err.Error()})
|
|
}
|
|
|
|
validateVulnerabilitiesAutomation(appConfig, invalid)
|
|
if invalid.HasErrors() {
|
|
return nil, ctxerr.Wrap(ctx, invalid)
|
|
}
|
|
|
|
// ignore the values for SMTPEnabled and SMTPConfigured
|
|
oldSmtpSettings.SMTPEnabled = appConfig.SMTPSettings.SMTPEnabled
|
|
oldSmtpSettings.SMTPConfigured = appConfig.SMTPSettings.SMTPConfigured
|
|
|
|
// if we enable SMTP and the settings have changed, then we send a test email
|
|
if appConfig.SMTPSettings.SMTPEnabled {
|
|
if oldSmtpSettings != appConfig.SMTPSettings || !appConfig.SMTPSettings.SMTPConfigured {
|
|
if err = svc.sendTestEmail(ctx, appConfig); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err)
|
|
}
|
|
}
|
|
appConfig.SMTPSettings.SMTPConfigured = true
|
|
} else {
|
|
appConfig.SMTPSettings.SMTPConfigured = false
|
|
}
|
|
|
|
// if Jira integration settings are modified or added, test it.
|
|
var newJiraConfig []*fleet.JiraIntegration
|
|
newJiraByProjectKey := make(map[string]*fleet.JiraIntegration)
|
|
for i, new := range newAppConfig.Integrations.Jira {
|
|
// first check for project key uniqueness
|
|
if _, ok := newJiraByProjectKey[new.ProjectKey]; ok {
|
|
return nil, ctxerr.Wrap(ctx, &badRequestError{message: fmt.Sprintf("duplicate Jira integration for project key %s", new.ProjectKey)})
|
|
}
|
|
newJiraByProjectKey[new.ProjectKey] = new
|
|
|
|
// check if existing integration is being edited
|
|
if old, ok := storedJiraByProjectKey[new.ProjectKey]; ok {
|
|
if old == *new {
|
|
newJiraConfig = append(newJiraConfig, &old)
|
|
// no further validation for unchanged integration
|
|
continue
|
|
}
|
|
// use stored API token if request does not contain new token
|
|
if new.APIToken == "" || new.APIToken == "********" {
|
|
new.APIToken = old.APIToken
|
|
}
|
|
}
|
|
|
|
// new or updated, test it
|
|
if err := svc.makeTestJiraRequest(ctx, new); err != nil {
|
|
return nil, ctxerr.Wrapf(ctx, err, "Jira integration at index %d", i)
|
|
}
|
|
newJiraConfig = append(newJiraConfig, new)
|
|
}
|
|
appConfig.Integrations.Jira = newJiraConfig
|
|
|
|
// if Zendesk integration settings are modified or added, test it.
|
|
var newZendeskConfig []*fleet.ZendeskIntegration
|
|
newZendeskByGroupID := make(map[int64]*fleet.ZendeskIntegration)
|
|
for i, new := range newAppConfig.Integrations.Zendesk {
|
|
// first check for group id uniqueness
|
|
if _, ok := newZendeskByGroupID[new.GroupID]; ok {
|
|
return nil, ctxerr.Wrap(ctx, &badRequestError{message: fmt.Sprintf("duplicate Zendesk integration for group id %v", new.GroupID)})
|
|
}
|
|
newZendeskByGroupID[new.GroupID] = new
|
|
|
|
// check if existing integration is being edited
|
|
if old, ok := storedZendeskByGroupID[new.GroupID]; ok {
|
|
if old == *new {
|
|
newZendeskConfig = append(newZendeskConfig, &old)
|
|
// no further validation for unchanged integration
|
|
continue
|
|
}
|
|
// use stored API token if request does not contain new token
|
|
// intended only as a short-term accommodation for the frontend
|
|
// will be redesigned in dedicated endpoint for integration config
|
|
if new.APIToken == "" || new.APIToken == "********" {
|
|
new.APIToken = old.APIToken
|
|
}
|
|
}
|
|
|
|
// new or updated, test it
|
|
if err := svc.makeTestZendeskRequest(ctx, new); err != nil {
|
|
return nil, ctxerr.Wrapf(ctx, err, "Zendesk integration at index %d", i)
|
|
}
|
|
newZendeskConfig = append(newZendeskConfig, new)
|
|
}
|
|
appConfig.Integrations.Zendesk = newZendeskConfig
|
|
|
|
if err := svc.ds.SaveAppConfig(ctx, appConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// retrieve new app config with obfuscated secrets
|
|
obfuscatedConfig, err := svc.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return obfuscatedConfig, nil
|
|
}
|
|
|
|
func validateSSOSettings(p fleet.AppConfig, existing *fleet.AppConfig, invalid *fleet.InvalidArgumentError) {
|
|
if p.SSOSettings.EnableSSO {
|
|
if p.SSOSettings.Metadata == "" && p.SSOSettings.MetadataURL == "" {
|
|
if existing.SSOSettings.Metadata == "" && existing.SSOSettings.MetadataURL == "" {
|
|
invalid.Append("metadata", "either metadata or metadata_url must be defined")
|
|
}
|
|
}
|
|
if p.SSOSettings.Metadata != "" && p.SSOSettings.MetadataURL != "" {
|
|
invalid.Append("metadata", "both metadata and metadata_url are defined, only one is allowed")
|
|
}
|
|
if p.SSOSettings.EntityID == "" {
|
|
if existing.SSOSettings.EntityID == "" {
|
|
invalid.Append("entity_id", "required")
|
|
}
|
|
} else {
|
|
if len(p.SSOSettings.EntityID) < 5 {
|
|
invalid.Append("entity_id", "must be 5 or more characters")
|
|
}
|
|
}
|
|
if p.SSOSettings.IDPName == "" {
|
|
if existing.SSOSettings.IDPName == "" {
|
|
invalid.Append("idp_name", "required")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func validateVulnerabilitiesAutomation(merged *fleet.AppConfig, invalid *fleet.InvalidArgumentError) {
|
|
webhookEnabled := merged.WebhookSettings.VulnerabilitiesWebhook.Enable
|
|
var jiraEnabledCount int
|
|
for _, jira := range merged.Integrations.Jira {
|
|
if jira.EnableSoftwareVulnerabilities {
|
|
jiraEnabledCount++
|
|
}
|
|
}
|
|
var zendeskEnabledCount int
|
|
for _, zendesk := range merged.Integrations.Zendesk {
|
|
if zendesk.EnableSoftwareVulnerabilities {
|
|
zendeskEnabledCount++
|
|
}
|
|
}
|
|
|
|
if webhookEnabled && (jiraEnabledCount > 0 || zendeskEnabledCount > 0) {
|
|
invalid.Append("vulnerabilities", "cannot enable both webhook vulnerabilities and integration automations")
|
|
}
|
|
if jiraEnabledCount > 0 && zendeskEnabledCount > 0 {
|
|
invalid.Append("vulnerabilities", "cannot enable both jira integration and zendesk automations")
|
|
}
|
|
if jiraEnabledCount > 1 {
|
|
invalid.Append("vulnerabilities", "cannot enable more than one jira integration")
|
|
}
|
|
if zendeskEnabledCount > 1 {
|
|
invalid.Append("vulnerabilities", "cannot enable more than one zendesk integration")
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Apply enroll secret spec
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type applyEnrollSecretSpecRequest struct {
|
|
Spec *fleet.EnrollSecretSpec `json:"spec"`
|
|
}
|
|
|
|
type applyEnrollSecretSpecResponse struct {
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r applyEnrollSecretSpecResponse) error() error { return r.Err }
|
|
|
|
func applyEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
req := request.(*applyEnrollSecretSpecRequest)
|
|
err := svc.ApplyEnrollSecretSpec(ctx, req.Spec)
|
|
if err != nil {
|
|
return applyEnrollSecretSpecResponse{Err: err}, nil
|
|
}
|
|
return applyEnrollSecretSpecResponse{}, nil
|
|
}
|
|
|
|
func (svc *Service) ApplyEnrollSecretSpec(ctx context.Context, spec *fleet.EnrollSecretSpec) error {
|
|
if err := svc.authz.Authorize(ctx, &fleet.EnrollSecret{}, fleet.ActionWrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, s := range spec.Secrets {
|
|
if s.Secret == "" {
|
|
return ctxerr.New(ctx, "enroll secret must not be empty")
|
|
}
|
|
}
|
|
|
|
return svc.ds.ApplyEnrollSecrets(ctx, nil, spec.Secrets)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get enroll secret spec
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getEnrollSecretSpecResponse struct {
|
|
Spec *fleet.EnrollSecretSpec `json:"spec"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getEnrollSecretSpecResponse) error() error { return r.Err }
|
|
|
|
func getEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
specs, err := svc.GetEnrollSecretSpec(ctx)
|
|
if err != nil {
|
|
return getEnrollSecretSpecResponse{Err: err}, nil
|
|
}
|
|
return getEnrollSecretSpecResponse{Spec: specs}, nil
|
|
}
|
|
|
|
func (svc *Service) GetEnrollSecretSpec(ctx context.Context) (*fleet.EnrollSecretSpec, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.EnrollSecret{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secrets, err := svc.ds.GetEnrollSecrets(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &fleet.EnrollSecretSpec{Secrets: secrets}, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Version
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type versionResponse struct {
|
|
*version.Info
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r versionResponse) error() error { return r.Err }
|
|
|
|
func versionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
info, err := svc.Version(ctx)
|
|
if err != nil {
|
|
return versionResponse{Err: err}, nil
|
|
}
|
|
return versionResponse{Info: info}, nil
|
|
}
|
|
|
|
func (svc *Service) Version(ctx context.Context) (*version.Info, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := version.Version()
|
|
return &info, nil
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Get Certificate Chain
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getCertificateResponse struct {
|
|
CertificateChain []byte `json:"certificate_chain"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getCertificateResponse) error() error { return r.Err }
|
|
|
|
func getCertificateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) {
|
|
chain, err := svc.CertificateChain(ctx)
|
|
if err != nil {
|
|
return getCertificateResponse{Err: err}, nil
|
|
}
|
|
return getCertificateResponse{CertificateChain: chain}, nil
|
|
}
|
|
|
|
// Certificate returns the PEM encoded certificate chain for osqueryd TLS termination.
|
|
func (svc *Service) CertificateChain(ctx context.Context) ([]byte, error) {
|
|
config, err := svc.AppConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u, err := url.Parse(config.ServerSettings.ServerURL)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "parsing serverURL")
|
|
}
|
|
|
|
conn, err := connectTLS(ctx, u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return chain(ctx, conn.ConnectionState(), u.Hostname())
|
|
}
|
|
|
|
func connectTLS(ctx context.Context, serverURL *url.URL) (*tls.Conn, error) {
|
|
var hostport string
|
|
if serverURL.Port() == "" {
|
|
hostport = net.JoinHostPort(serverURL.Host, "443")
|
|
} else {
|
|
hostport = serverURL.Host
|
|
}
|
|
|
|
// attempt dialing twice, first with a secure conn, and then
|
|
// if that fails, use insecure
|
|
dial := func(insecure bool) (*tls.Conn, error) {
|
|
conn, err := tls.Dial("tcp", hostport, &tls.Config{
|
|
InsecureSkipVerify: insecure,
|
|
})
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "dial tls")
|
|
}
|
|
defer conn.Close()
|
|
return conn, nil
|
|
}
|
|
|
|
var (
|
|
conn *tls.Conn
|
|
err error
|
|
)
|
|
|
|
conn, err = dial(false)
|
|
if err == nil {
|
|
return conn, nil
|
|
}
|
|
conn, err = dial(true)
|
|
return conn, err
|
|
}
|
|
|
|
// chain builds a PEM encoded certificate chain using the PeerCertificates
|
|
// in tls.ConnectionState. chain uses the hostname to omit the Leaf certificate
|
|
// from the chain.
|
|
func chain(ctx context.Context, cs tls.ConnectionState, hostname string) ([]byte, error) {
|
|
buf := bytes.NewBuffer([]byte(""))
|
|
|
|
verifyEncode := func(chain []*x509.Certificate) error {
|
|
for _, cert := range chain {
|
|
if len(chain) > 1 {
|
|
// drop the leaf certificate from the chain. osqueryd does not
|
|
// need it to establish a secure connection
|
|
if err := cert.VerifyHostname(hostname); err == nil {
|
|
continue
|
|
}
|
|
}
|
|
if err := encodePEMCertificate(buf, cert); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// use verified chains if available(which adds the root CA), otherwise
|
|
// use the certificate chain offered by the server (if terminated with
|
|
// self-signed certs)
|
|
if len(cs.VerifiedChains) != 0 {
|
|
for _, chain := range cs.VerifiedChains {
|
|
if err := verifyEncode(chain); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "encode verified chains pem")
|
|
}
|
|
}
|
|
} else {
|
|
if err := verifyEncode(cs.PeerCertificates); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "encode peer certificates pem")
|
|
}
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func encodePEMCertificate(buf io.Writer, cert *x509.Certificate) error {
|
|
block := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
}
|
|
return pem.Encode(buf, block)
|
|
}
|