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
64 changes: 21 additions & 43 deletions ee/server/service/apple_psso.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/psso/pssocrypto"
"github.com/fleetdm/fleet/v4/server/mdm/apple/psso/regtoken"
jose "github.com/go-jose/go-jose/v3"
)
Expand All @@ -42,11 +43,6 @@ type pssoServiceState struct {
}

const (
pssoSigningAlg = "ES256"
// pssoEncryptionAlg is the JWK `alg` published for the password-encryption
// key and the `alg` Apple uses in the embedded login-assertion JWE.
pssoEncryptionAlg = "ECDH-ES"

// The host app bundle ID is included alongside the extension's just in case;
// PSSO validates against the extension, but listing both is harmless and matches
// what the IdPs analyzed do.
Expand Down Expand Up @@ -398,10 +394,10 @@ func (svc *Service) PSSORegisterDevice(ctx context.Context, req fleet.PSSODevice
// Reject unparseable key material up front: a bad PEM stored here would
// otherwise only surface as opaque verification failures at every
// subsequent login.
if _, err := parseECPublicKeyPEM([]byte(req.DeviceSigningKey)); err != nil {
if _, err := pssocrypto.ParseECPublicKeyPEM([]byte(req.DeviceSigningKey)); err != nil {
return &fleet.BadRequestError{Message: "psso registration: signing key is not a valid P-256 public key"}
}
if _, err := parseECPublicKeyPEM([]byte(req.DeviceEncryptionKey)); err != nil {
if _, err := pssocrypto.ParseECPublicKeyPEM([]byte(req.DeviceEncryptionKey)); err != nil {
return &fleet.BadRequestError{Message: "psso registration: encryption key is not a valid P-256 public key"}
}

Expand All @@ -420,12 +416,12 @@ func (svc *Service) PSSORegisterDevice(ctx context.Context, req fleet.PSSODevice
// alphabet differences between the extension and Apple's framework.
keys := []fleet.PSSOKey{
{
KID: canonicalizeKID(req.SigningKeyID),
KID: pssocrypto.CanonicalizeKID(req.SigningKeyID),
KeyType: fleet.PSSOKeyTypeSigning,
PEM: req.DeviceSigningKey,
},
{
KID: canonicalizeKID(req.EncryptionKeyID),
KID: pssocrypto.CanonicalizeKID(req.EncryptionKeyID),
KeyType: fleet.PSSOKeyTypeEncryption,
PEM: req.DeviceEncryptionKey,
},
Expand Down Expand Up @@ -471,9 +467,9 @@ func (svc *Service) PSSOToken(ctx context.Context, jwtBytes []byte) ([]byte, err

// Key requests/exchanges carry a request_type and are dispatched first.
switch claims.RequestType {
case pssoRequestKey:
case pssocrypto.RequestKey:
return svc.handlePSSOKeyRequest(ctx, signKey.HostUUID, claims)
case pssoRequestExchange:
case pssocrypto.RequestExchange:
return svc.handlePSSOKeyExchange(ctx, signKey.HostUUID, claims)
}

Expand All @@ -482,30 +478,12 @@ func (svc *Service) PSSOToken(ctx context.Context, jwtBytes []byte) ([]byte, err
// grant and ships the password inside an encrypted embedded assertion. Both
// land here and differ only in where handlePSSOPasswordLogin reads the
// password from.
if claims.GrantType == pssoGrantTypePassword || claims.GrantType == pssoGrantTypeJWTBearer {
if claims.GrantType == pssocrypto.GrantTypePassword || claims.GrantType == pssocrypto.GrantTypeJWTBearer {
return svc.handlePSSOPasswordLogin(ctx, settings, signKey.HostUUID, claims)
}
return nil, &fleet.BadRequestError{Message: "psso token: unsupported grant_type/request_type"}
}

// PSSO grant types in the login request JWT. With plaintext passwords Apple
// sends grant_type=password; when the password is encrypted into the embedded
// assertion it switches to the JWT-bearer grant and the password moves out of
// the top-level claim into the (encrypted) assertion.
const (
pssoGrantTypePassword = "password" //nolint:gosec // G101 not a credential, a grant type
pssoGrantTypeJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolint:gosec // G101 not a credential, a grant type
)

// JWE header `typ` media types. The first two are responses Fleet returns; the
// last is the embedded login assertion the device sends when password
// encryption is enabled.
const (
pssoTypLoginResponse = "platformsso-login-response+jwt"
pssoTypKeyResponse = "platformsso-key-response+jwt"
pssoTypEncryptedLoginAssertion = "platformsso-encrypted-login-assertion+jwt"
)

// pssoDefaultTokenTTL is the id_token / refresh_token lifetime used when the
// upstream IdP doesn't return an expires_in.
const pssoDefaultTokenTTL = time.Hour
Expand Down Expand Up @@ -579,7 +557,7 @@ func buildPSSOIDTokenClaims(idpClaims *fleet.PSSOClaims, issuer, audience, nonce
// Fleet validates the password against the upstream IdP, then returns the
// resulting OIDC claims as a server-signed JWT wrapped in a JWE encrypted per
// that recipe.
func (svc *Service) handlePSSOPasswordLogin(ctx context.Context, settings *fleet.PSSOSettings, hostUUID string, claims *pssoTokenClaims) ([]byte, error) {
func (svc *Service) handlePSSOPasswordLogin(ctx context.Context, settings *fleet.PSSOSettings, hostUUID string, claims *pssocrypto.TokenClaims) ([]byte, error) {
if claims.JWECrypto == nil || claims.JWECrypto.APV == "" {
return nil, &fleet.BadRequestError{Message: "psso password login: missing jwe_crypto recipe"}
}
Expand Down Expand Up @@ -654,7 +632,7 @@ func (svc *Service) handlePSSOPasswordLogin(ctx context.Context, settings *fleet
return nil, ctxerr.Wrap(ctx, err, "marshal psso login response")
}

jwe, err := buildPSSOResponseJWE(payload, recipientPub, claims.JWECrypto.APV, pssoTypLoginResponse)
jwe, err := pssocrypto.BuildPartyInfoJWE(payload, recipientPub, claims.JWECrypto.APV, pssocrypto.TypLoginResponse)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "build psso login response jwe")
}
Expand All @@ -666,7 +644,7 @@ func (svc *Service) handlePSSOPasswordLogin(ctx context.Context, settings *fleet
// enabled the Password claim is empty and the password lives in the encrypted
// embedded assertion, which Fleet decrypts with its PSSO encryption key. The
// username is always taken from the (signed) outer JWT, not the assertion.
func (svc *Service) resolvePSSOLoginPassword(ctx context.Context, claims *pssoTokenClaims) (string, error) {
func (svc *Service) resolvePSSOLoginPassword(ctx context.Context, claims *pssocrypto.TokenClaims) (string, error) {
if claims.Password != "" {
return claims.Password, nil
}
Expand All @@ -677,11 +655,11 @@ func (svc *Service) resolvePSSOLoginPassword(ctx context.Context, claims *pssoTo
if err != nil {
return "", ctxerr.Wrap(ctx, err, "load psso encryption key")
}
plaintext, err := decryptPSSOInboundJWE([]byte(claims.Assertion), encKey)
plaintext, err := pssocrypto.DecryptPartyInfoJWE([]byte(claims.Assertion), encKey, pssocrypto.TypEncryptedLoginAssertion)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "decrypt psso login assertion")
}
password, err := parseEmbeddedAssertionPassword(plaintext)
password, err := pssocrypto.ParseEmbeddedAssertionPassword(plaintext)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "parse psso login assertion")
}
Expand All @@ -695,7 +673,7 @@ func (svc *Service) resolvePSSOLoginPassword(ctx context.Context, claims *pssoTo
// key_context} in a JWE (typ=platformsso-key-response+jwt) encrypted to the
// device. key_context carries the provisioned PRIVATE key, sealed under a
// server key, so the later key exchange can recover it statelessly.
func (svc *Service) handlePSSOKeyRequest(ctx context.Context, hostUUID string, claims *pssoTokenClaims) ([]byte, error) {
func (svc *Service) handlePSSOKeyRequest(ctx context.Context, hostUUID string, claims *pssocrypto.TokenClaims) ([]byte, error) {
if claims.JWECrypto == nil || claims.JWECrypto.APV == "" {
return nil, &fleet.BadRequestError{Message: "psso key request: missing jwe_crypto recipe"}
}
Expand Down Expand Up @@ -737,7 +715,7 @@ func (svc *Service) handlePSSOKeyRequest(ctx context.Context, hostUUID string, c
return nil, ctxerr.Wrap(ctx, err, "marshal key_request payload")
}

jwe, err := buildPSSOResponseJWE(payload, encPub, claims.JWECrypto.APV, pssoTypKeyResponse)
jwe, err := pssocrypto.BuildPartyInfoJWE(payload, encPub, claims.JWECrypto.APV, pssocrypto.TypKeyResponse)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "build key_request JWE")
}
Expand Down Expand Up @@ -779,7 +757,7 @@ func (svc *Service) issuePSSOProvisionedCertificate(ctx context.Context, provisi
// provisioned private key from key_context, computes the raw ECDH shared
// secret against other_publickey (this is the unlock key), and returns
// {iat, exp, key, key_context} in the same JWE envelope.
func (svc *Service) handlePSSOKeyExchange(ctx context.Context, hostUUID string, claims *pssoTokenClaims) ([]byte, error) {
func (svc *Service) handlePSSOKeyExchange(ctx context.Context, hostUUID string, claims *pssocrypto.TokenClaims) ([]byte, error) {
if claims.JWECrypto == nil || claims.JWECrypto.APV == "" {
return nil, &fleet.BadRequestError{Message: "psso key exchange: missing jwe_crypto recipe"}
}
Expand Down Expand Up @@ -808,11 +786,11 @@ func (svc *Service) handlePSSOKeyExchange(ctx context.Context, hostUUID string,
return nil, &fleet.BadRequestError{Message: "psso key exchange: unsupported key_context purpose"}
}

otherRaw, err := decodeBase64Flexible(claims.OtherPublicKey)
otherRaw, err := pssocrypto.DecodeBase64Flexible(claims.OtherPublicKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decode other_publickey")
}
shared, err := computeECDHShared(provisioned, otherRaw)
shared, err := pssocrypto.ComputeECDHShared(provisioned, otherRaw)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "compute key exchange shared secret")
}
Expand All @@ -833,7 +811,7 @@ func (svc *Service) handlePSSOKeyExchange(ctx context.Context, hostUUID string,
return nil, ctxerr.Wrap(ctx, err, "marshal key_exchange payload")
}

jwe, err := buildPSSOResponseJWE(payload, encPub, claims.JWECrypto.APV, pssoTypKeyResponse)
jwe, err := pssocrypto.BuildPartyInfoJWE(payload, encPub, claims.JWECrypto.APV, pssocrypto.TypKeyResponse)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "build key_exchange JWE")
}
Expand Down Expand Up @@ -869,7 +847,7 @@ func (svc *Service) PSSOJWKS(ctx context.Context) ([]byte, error) {
{
Key: &key.PublicKey,
KeyID: kid,
Algorithm: pssoSigningAlg,
Algorithm: pssocrypto.SigningAlg,
Use: "sig",
},
// The extension sets this key as loginRequestEncryptionPublicKey and
Expand All @@ -878,7 +856,7 @@ func (svc *Service) PSSOJWKS(ctx context.Context) ([]byte, error) {
{
Key: &encKey.PublicKey,
KeyID: encKID,
Algorithm: pssoEncryptionAlg,
Algorithm: pssocrypto.EncryptionAlg,
Use: "enc",
},
}}
Expand Down
Loading
Loading