From 8bb307c11b51cb0fda3df374bdc9c0958b7d522e Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Mon, 22 Nov 2021 18:39:22 -0800 Subject: Session keys can be JSON serialized --- app/models/session_key.go | 207 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 38 deletions(-) diff --git a/app/models/session_key.go b/app/models/session_key.go index b1fdc90..c8b327e 100644 --- a/app/models/session_key.go +++ b/app/models/session_key.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "time" @@ -18,8 +19,16 @@ import ( // that will be stored on the user's record in the user store. These keys are // used for signing authentication JWTs. // -// This object is designed to be serialized to BSON. Other serializations can -// be added in the future as needed. +// This object is designed to be serialized to and from BSON and JSON. Other +// serializations can be added in the future as needed. +// +// The ExposePrivateKeysInJSON controls how JSON serialization of this struct +// works. When the field is set to false (the default) then serialization into +// JSON will never encode a private key, but may encode a public key. If this +// is set to true then the private key will be encoded into the JSON value and +// not the public key. SETTING THIS TO TRUE AND EXPOSING THE RESULTS TO THE +// USER IS A SECURITY ERROR so this should normally not be changed. This value +// of this field will never be persisted in any form. // // There are two flavors of this record. A record with a private key (which // implies a public key) is a key that the service generated and is used by the @@ -36,13 +45,14 @@ import ( // private key and the service will validate the signature with the public key. // These keys (as of Nov 2021) do not expire, though they can be revoked. type SessionKey struct { - KeyId string - Description string - Revoked *time.Time - NotAfter *time.Time - NotBefore *time.Time - PublicKey crypto.PublicKey - PrivateKey *ecdsa.PrivateKey + KeyId string + Description string + Revoked *time.Time + NotAfter *time.Time + NotBefore *time.Time + PublicKey crypto.PublicKey + PrivateKey *ecdsa.PrivateKey + ExposePrivateKeysInJSON bool `"-" json:"-" bson:"-"` } func GenerateSessionKey(ttl time.Duration) (*SessionKey, error) { @@ -108,12 +118,156 @@ func (s *SessionKey) IsValid() bool { return true } +// MarshalPrivateKey marshals the private key to a X509 encoded base64 string +func (s *SessionKey) MarshalPrivateKey() (string, error) { + priv, err := x509.MarshalECPrivateKey(s.PrivateKey) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(priv), nil +} + +// MarshalPublicKey marshals the public key to an X509 encoded base64 string +func (s *SessionKey) MarshalPublicKey() (string, error) { + pub, err := x509.MarshalPKIXPublicKey(s.PublicKey) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(pub), nil +} + +// UnmarshalPrivateKey unmarshals the private key from a base64 encoded X509 +// string into the public and private key fields. +func (s *SessionKey) UnmarshalPrivateKey(k string) error { + privb, err := base64.StdEncoding.DecodeString(k) + if err != nil { + return err + } + + priv, err := x509.ParseECPrivateKey(privb) + if err != nil { + return err + } + + s.PrivateKey = priv + s.PublicKey = priv.Public() + + return nil +} + +// UnmarshalPublicKey unmarshals the public key from a base64 encoded X509 +// string into the public key field. +func (s *SessionKey) UnmarshalPublicKey(k string) error { + pubb, err := base64.StdEncoding.DecodeString(k) + if err != nil { + return err + } + + pubp, err := x509.ParsePKIXPublicKey(pubb) + if err != nil { + return err + } + + pub, ok := pubp.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("Failed to convert public key to *ecdsa.PublicKey") + } + + s.PublicKey = pub + return nil +} + +// MarshalJSON marshals a struct to JSON +// +// This method will have different behavior if the ExposePrivateKeysInJSON +// field is set in the struct (the default is false). If this field is set to +// true the private keys will be exposed in the JSON results. If it is false +// then private keys will not be exposed. The ExposePrivateKeysInJSON itself +// will never be serialized. +func (s *SessionKey) MarshalJSON() ([]byte, error) { + var err error + var privKey *string + var pub, priv string + + if s.PrivateKey != nil && s.ExposePrivateKeysInJSON { + priv, err = s.MarshalPrivateKey() + if err != nil { + return nil, err + } + if priv != "" { + privKey = &priv + } + } + + // If there's a private key and a public key set, and exposing the private + // key is allowed, then just save the private key. The private key already + // contains a copy of the public key. + if s.PublicKey != nil && (s.PrivateKey == nil || !s.ExposePrivateKeysInJSON) { + pub, err = s.MarshalPublicKey() + if err != nil { + return nil, err + } + } + + return json.Marshal(struct { + KeyId string `json:"key_id"` + Revoked *time.Time `json:"revoked,omitempty"` + NotAfter *time.Time `json:"not_after"` + NotBefore *time.Time `json:"not_before"` + PublicKey string `json:"public_key"` + PrivateKey *string `json:"private_key,omitempty"` + }{ + s.KeyId, + s.Revoked, s.NotAfter, s.NotBefore, + pub, privKey, + }) +} + +// UnmarshalJSON unmarshals a struct from JSON. +// +// This method does attempt to unmarshal private keys. +func (s *SessionKey) UnmarshalJSON(d []byte) error { + v := struct { + KeyId string `json:"key_id"` + Revoked *time.Time `json:"revoked,omitempty"` + NotAfter *time.Time `json:"not_after"` + NotBefore *time.Time `json:"not_before"` + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` + }{} + if err := json.Unmarshal(d, &v); err != nil { + return err + } + + s.KeyId = v.KeyId + s.Revoked = v.Revoked + s.NotAfter = v.NotAfter + s.NotBefore = v.NotBefore + + if v.PrivateKey != "" { + if err := s.UnmarshalPrivateKey(v.PrivateKey); err != nil { + return err + } + } + + // If there was a private key then the public key was already set by + // decoding that private key. No need to do this a second time (also it's + // rather unlikely that both would be set). + if v.PublicKey != "" && s.PublicKey == nil { + if err := s.UnmarshalPublicKey(v.PublicKey); err != nil { + return err + } + } + + return nil +} + func (s *SessionKey) MarshalBSON() ([]byte, error) { var err error - var pub, priv []byte + var pub, priv string if s.PrivateKey != nil { - priv, err = x509.MarshalECPrivateKey(s.PrivateKey) + priv, err = s.MarshalPrivateKey() if err != nil { return nil, err } @@ -122,7 +276,7 @@ func (s *SessionKey) MarshalBSON() ([]byte, error) { // If there's a private key and a public key set then just save the private // key. The private key already contains a copy of the public key. if s.PublicKey != nil && s.PrivateKey == nil { - pub, err = x509.MarshalPKIXPublicKey(s.PublicKey) + pub, err = s.MarshalPublicKey() if err != nil { return nil, err } @@ -138,8 +292,7 @@ func (s *SessionKey) MarshalBSON() ([]byte, error) { }{ s.KeyId, s.Revoked, s.NotAfter, s.NotBefore, - base64.StdEncoding.EncodeToString(pub), - base64.StdEncoding.EncodeToString(priv), + pub, priv, }) } @@ -162,40 +315,18 @@ func (s *SessionKey) UnmarshalBSON(d []byte) error { s.NotBefore = v.NotBefore if v.PrivateKey != "" { - privb, err := base64.StdEncoding.DecodeString(v.PrivateKey) - if err != nil { + if err := s.UnmarshalPrivateKey(v.PrivateKey); err != nil { return err } - - priv, err := x509.ParseECPrivateKey(privb) - if err != nil { - return err - } - - s.PrivateKey = priv - s.PublicKey = priv.Public() } // If there was a private key then the public key was already set by // decoding that private key. No need to do this a second time (also it's // rather unlikely that both would be set). if v.PublicKey != "" && s.PublicKey == nil { - pubb, err := base64.StdEncoding.DecodeString(v.PublicKey) - if err != nil { + if err := s.UnmarshalPublicKey(v.PublicKey); err != nil { return err } - - pubp, err := x509.ParsePKIXPublicKey(pubb) - if err != nil { - return err - } - - pub, ok := pubp.(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("Failed to convert public key to *ecdsa.PublicKey") - } - - s.PublicKey = pub } return nil -- cgit v1.2.3