Skip to content

Commit

Permalink
[BACK-2637] Add palmtree partner certificate fetching api.
Browse files Browse the repository at this point in the history
  • Loading branch information
lostlevels committed Aug 31, 2023
1 parent dd63082 commit a812081
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 16 deletions.
5 changes: 5 additions & 0 deletions appvalidate/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type AssertionUpdate struct {
AssertionCounter uint32 `bson:"assertionCounter,omitempty"`
}

type AssertionResponse struct {
Data any `json:"data"`
}

type AssertionClientData struct {
Challenge string `json:"challenge"`
Partner string `json:"partner"` // Which partner are we requesting a secret from - currently only one supported
Expand All @@ -44,6 +48,7 @@ func NewAssertionVerify(userID string) *AssertionVerify {
func (av *AssertionVerify) Validate(v structure.Validator) {
v.String("assertion", &av.Assertion).NotEmpty().Matches(base64Chars)
v.String("clientData.challenge", &av.ClientData.Challenge).NotEmpty()
v.String("clientData.partner", &av.ClientData.Partner).OneOf(PartnerCoastal, PartnerPalmTree)

v.String("userId", &av.UserID).NotEmpty()
v.String("keyId", &av.KeyID).NotEmpty()
Expand Down
41 changes: 28 additions & 13 deletions appvalidate/coastal_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
Expand All @@ -19,12 +20,14 @@ const (
)

type CoastalSecretsConfig struct {
BaseURL string `env:"COASTAL_BASE_URL"`
APICertificatePath string `env:"COASTAL_API_CERTIFICATE_PATH"`
APIKey string `env:"COASTAL_API_KEY"`
ClientID string `env:"COASTAL_CLIENT_ID"`
ClientSecret string `env:"COSTAL_CLIENT_SECRET"`
APICertificatePath string `envconfig:"COASTAL_API_CERTIFICATE_PATH"`
APIKey string `envconfig:"COASTAL_API_KEY"`
BaseURL string `envconfig:"COASTAL_BASE_URL"`
ClientID string `envconfig:"COASTAL_CLIENT_ID"`
ClientSecret string `envconfig:"COSTAL_CLIENT_SECRET"`
RCTypeID string `envconfig:"COASTAL_RC_TYPE_ID"`
}

type CoastalSecrets struct {
Config CoastalSecretsConfig
}
Expand Down Expand Up @@ -57,13 +60,14 @@ type CoastalResponse struct {
}

func (c *CoastalSecrets) GetSecret(ctx context.Context, partnerDataRaw []byte) (*CoastalResponse, error) {
var payload CoastalPayload
if err := json.Unmarshal(partnerDataRaw, &payload); err != nil {
return nil, err
payload := newCoastalPayload(c.Config.RCTypeID)
if err := json.Unmarshal(partnerDataRaw, payload); err != nil {
return nil, fmt.Errorf("unable to unmarshal Coastal payload: %w", err)
}
// Todo: calculate rcbMac / rcbSignature when partner API is updated.

if err := structValidator.New().Validate(&payload); err != nil {
return nil, err
if err := structValidator.New().Validate(payload); err != nil {
return nil, fmt.Errorf("unable to validate Coastal payload: %w", err)
}

var buf bytes.Buffer
Expand All @@ -73,7 +77,7 @@ func (c *CoastalSecrets) GetSecret(ctx context.Context, partnerDataRaw []byte) (

u, err := url.Parse(c.Config.BaseURL)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to prase Coastal API baseURL: %w", err)
}
u.Path = path.Join(u.Path, c.Config.APICertificatePath)

Expand All @@ -89,12 +93,17 @@ func (c *CoastalSecrets) GetSecret(ctx context.Context, partnerDataRaw []byte) (

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to issue Coastal API request: %w", err)
}
defer res.Body.Close()

if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("unsuccessful Coastal API response: %v: %v", res.StatusCode, res.Status)
}

var response CoastalResponse
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, err
return nil, fmt.Errorf("unable to read Coastal API response: %w", err)
}
return &response, nil
}
Expand All @@ -109,3 +118,9 @@ func (p *CoastalPayload) Validate(v structure.Validator) {
v.String("csr", &p.CSR).NotEmpty()
v.String("rcbMac", &p.RCBMac).NotEmpty()
}

func newCoastalPayload(rcTypeID string) *CoastalPayload {
return &CoastalPayload{
RCTypeID: rcTypeID,
}
}
162 changes: 162 additions & 0 deletions appvalidate/palmtree_secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package appvalidate

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"

"github.com/kelseyhightower/envconfig"

"github.com/tidepool-org/platform/structure"
structValidator "github.com/tidepool-org/platform/structure/validator"
)

const (
PartnerPalmTree = "PalmTree"
)

type PalmTreeSecretsConfig struct {
BaseURL string `envconfig:"PALMTREE_BASE_URL"`
CalID string `envconfig:"PALMTREE_CAL_ID"`
ProfileID string `envconfig:"PALMTREE_PROFILE_ID"`
CertFile string `envconfig:"PALMTREE_TLS_CERT_FILE"`
KeyFile string `envconfig:"PALMTREE_TLS_KEY_FILE"`
}

type PalmTreeSecrets struct {
Config PalmTreeSecretsConfig
client *http.Client
}

func NewPalmTreeSecretsConfig() (*PalmTreeSecretsConfig, error) {
cfg := &PalmTreeSecretsConfig{}
if err := envconfig.Process("", cfg); err != nil {
return nil, err
}
return cfg, nil
}

func NewPalmTreeSecrets(c *PalmTreeSecretsConfig) (*PalmTreeSecrets, error) {
if c == nil {
return nil, errors.New("empty PalmTree config")
}
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("unable to load PalmTree X.509 key pair: %w", err)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}

return &PalmTreeSecrets{
Config: *c,
client: &http.Client{Transport: tr},
}, nil
}

type PalmTreePayload struct {
CSR string `json:"csr"`
ProfileID string `json:"profileId"`
RequiredFormat palmTreeRequiredFormat `json:"requiredFormat"`
CertificateRequestDetails palmTreeCertificateRequestDetails `json:"optionalCertificateRequestDetails"`
}

type palmTreeCertificateRequestDetails struct {
SubjectDN string `json:"subjectDn"`
}

type palmTreeRequiredFormat struct {
Format string `json:"format"`
}

type PalmTreeResponse struct {
Type string `json:"type"`

Message struct {
Message string `json:"message"`
Details []any `json:"details"`
} `json:"message"`

Enrollment struct {
ID string `json:"id"`
SerialNumber string `json:"serialNumber"`
SubjectName string `json:"subjectName"`
IssuerName string `json:"issuerName"`
ValidityPeroid string `json:"validityPeriod"`
Status string `json:"status"`
Body string `json:"body"`
} `json:"enrollment"`
}

func (pt *PalmTreeSecrets) GetSecret(ctx context.Context, partnerDataRaw []byte) (*PalmTreeResponse, error) {
payload := newPalmtreePayload(pt.Config.ProfileID)

if err := json.Unmarshal(partnerDataRaw, payload); err != nil {
return nil, fmt.Errorf("unable to unmarshal PalmTree payload: %w", err)
}

if err := structValidator.New().Validate(payload); err != nil {
return nil, fmt.Errorf("unable to validate PalmTree payload: %w", err)
}

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return nil, err
}

u, err := url.Parse(pt.Config.BaseURL)
if err != nil {
return nil, fmt.Errorf("unable to prase PalmTree API baseURL: %w", err)
}
u.Path = path.Join(u.Path, fmt.Sprintf("v1/certificate-authorities/%s/enrollments", url.PathEscape(pt.Config.CalID)))

req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), &buf)
if err != nil {
return nil, err
}
req.Header.Add("content-type", "application/json")
req.Header.Add("accept", "application/json")

res, err := pt.client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to issue PalmTree API request: %w", err)
}
defer res.Body.Close()

if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("unsuccessful PalmTree API response: %v: %v", res.StatusCode, res.Status)
}

var response PalmTreeResponse
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("unable to read PalmTree API response: %w", err)
}
return &response, nil
}

func (p *PalmTreePayload) Validate(v structure.Validator) {
v.String("csr", &p.CSR).NotEmpty()
v.String("profileId", &p.ProfileID).NotEmpty()
v.String("requiredFormat.format", &p.RequiredFormat.Format).NotEmpty()
v.String("optionalCertificateRequestDetails.subjectDn", &p.CertificateRequestDetails.SubjectDN).NotEmpty()
}

func newPalmtreePayload(profileID string) *PalmTreePayload {
return &PalmTreePayload{
ProfileID: profileID,
RequiredFormat: palmTreeRequiredFormat{
Format: "PEM",
},
CertificateRequestDetails: palmTreeCertificateRequestDetails{
SubjectDN: "C=US",
},
}
}
17 changes: 14 additions & 3 deletions auth/service/api/v1/appvalidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ func (r *Router) AppValidateRoutes() []*rest.Route {
rest.Post("/v1/attestations/challenges", api.RequireUser(r.CreateAttestationChallenge)),
rest.Post("/v1/attestations/verifications", api.RequireUser(r.VerifyAttestation)),
rest.Post("/v1/assertions/challenges", api.RequireUser(r.CreateAssertionChallenge)),
// Rename this route to show actual intent of retrieving secret when secret retrieval is implemented.
rest.Post("/v1/assertions/verifications", api.RequireUser(r.VerifyAssertion)),
}
}
Expand Down Expand Up @@ -143,11 +142,23 @@ func (r *Router) VerifyAssertion(res rest.ResponseWriter, req *rest.Request) {
case appvalidate.PartnerCoastal:
secret, err := r.CoastalSecrets().GetSecret(ctx, []byte(assertVerify.ClientData.PartnerData))
if err != nil {
log.LoggerFromContext(ctx).WithFields(logFields).WithError(err).Error("unable to create fetch coastal secrets")
log.LoggerFromContext(ctx).WithFields(logFields).WithError(err).Error("unable to create fetch Coastal secrets")
responder.InternalServerError(err)
return
}
responder.Data(http.StatusOK, secret)
responder.Data(http.StatusOK, appvalidate.AssertionResponse{
Data: secret,
})
case appvalidate.PartnerPalmTree:
secret, err := r.PalmTreeSecrets().GetSecret(ctx, []byte(assertVerify.ClientData.PartnerData))
if err != nil {
log.LoggerFromContext(ctx).WithFields(logFields).WithError(err).Error("unable to create fetch PalmTree secrets")
responder.InternalServerError(err)
return
}
responder.Data(http.StatusOK, appvalidate.AssertionResponse{
Data: secret,
})
default:
responder.Error(http.StatusBadRequest, fmt.Errorf("unknown partner, %s", assertVerify.ClientData.Partner))
}
Expand Down
4 changes: 4 additions & 0 deletions auth/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ type Service interface {
Status(context.Context) *Status

AppValidator() *appvalidate.Validator

// As there are only 2 secrets for now, I will keep them separated as
// opposed to having a more "factory" of secrets.
CoastalSecrets() *appvalidate.CoastalSecrets
PalmTreeSecrets() *appvalidate.PalmTreeSecrets
}

type Status struct {
Expand Down
17 changes: 17 additions & 0 deletions auth/service/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type Service struct {
deviceCheck apple.DeviceCheck
appValidator *appvalidate.Validator
coastalSecrets *appvalidate.CoastalSecrets
palmTreeSecrets *appvalidate.PalmTreeSecrets
}

func New() *Service {
Expand Down Expand Up @@ -114,6 +115,9 @@ func (s *Service) Initialize(provider application.Provider) error {
if err := s.initializeCoastalSecrets(); err != nil {
return err
}
if err := s.initializePalmTreeSecrets(); err != nil {
return err
}
return s.initializeUserEventsHandler()
}

Expand Down Expand Up @@ -169,6 +173,10 @@ func (s *Service) CoastalSecrets() *appvalidate.CoastalSecrets {
return s.coastalSecrets
}

func (s *Service) PalmTreeSecrets() *appvalidate.PalmTreeSecrets {
return s.palmTreeSecrets
}

func (s *Service) Status(ctx context.Context) *service.Status {
return &service.Status{
Version: s.VersionReporter().Long(),
Expand Down Expand Up @@ -472,6 +480,15 @@ func (s *Service) initializeCoastalSecrets() error {
return nil
}

func (s *Service) initializePalmTreeSecrets() error {
cfg, err := appvalidate.NewPalmTreeSecretsConfig()
if err != nil {
return err
}
s.palmTreeSecrets, err = appvalidate.NewPalmTreeSecrets(cfg)
return err
}

func (s *Service) terminateUserEventsHandler() {
if s.userEventsHandler != nil {
s.Logger().Info("Terminating the userEventsHandler")
Expand Down

0 comments on commit a812081

Please sign in to comment.