[minor][feature]: Add caller-provided keys for AT PoP - #3072
[minor][feature]: Add caller-provided keys for AT PoP#3072Antonio Alwan (antonioalwan) wants to merge 12 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5568cdb9-6670-4ed1-bab0-8ef8ffe00049
There was a problem hiding this comment.
Pull request overview
Adds a new public API surface to support caller-provided RSA key pairs for Access Token Proof-of-Possession (AT PoP) flows, enabling MSAL to derive req_cnf and sign the Signed HTTP Request (SHR) using caller-owned keys while marking broker requests for controlled rollout.
Changes:
- Introduces public
MSALExternalKeyPair(with RFC 7638keyId) and wires it intoMSALAuthenticationSchemePopvia an additive initializer/property. - Updates PoP scheme parameter generation and SHR signing to use an injected external PoP manager when an external key pair is provided (including
external_key_pop=1flagging). - Adds unit coverage for external key validation and SHR signature verification, plus updates project/umbrella headers and changelog.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| MSAL/test/unit/MSALExternalKeyPairTests.m | New unit tests validating external key-pair failure reasons and success path. |
| MSAL/test/unit/MSALAuthSchemeTests.m | Adds regression coverage ensuring SHR is signed with the external key and broker flagging is set. |
| MSAL/src/public/MSALExternalKeyPair.h | New public type representing caller-owned RSA key pair + keyId. |
| MSAL/src/public/MSALError.h | Adds MSALErrorInvalidExternalKeyPair to model validation failures. |
| MSAL/src/public/MSALAuthenticationSchemePop.h | Adds external-key initializer and exposes externalKeyPair property. |
| MSAL/src/public/MSAL.h | Exposes MSALExternalKeyPair via the public umbrella header. |
| MSAL/src/MSALExternalKeyPair+Internal.h | Internal bridge to IdentityCore key-pair representation. |
| MSAL/src/MSALExternalKeyPair.m | Implements validation + keyId derivation and error mapping. |
| MSAL/src/MSALAuthenticationSchemePop.m | Uses external PoP manager when provided and adds broker classification parameter. |
| MSAL/MSAL.xcodeproj/project.pbxproj | Wires new sources/headers/tests into build targets. |
| CHANGELOG.md | Records the new API and dependency/update notes under [TBD]. |
Suppressed comments (3)
MSAL/test/unit/MSALAuthSchemeTests.m:279
- Issue:
createRSA2048PrivateKeydoesn’t release theCFErrorRefreturned bySecKeyCreateRandomKeywhen key creation fails.
Impact: Unit tests can leak CoreFoundation objects, masking real leak regressions.
Recommendation: Releaseerrorwhen non-NULL after the assertions.
CFErrorRef error = NULL;
SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
XCTAssertNotEqual(privateKey, NULL);
XCTAssertEqual(error, NULL);
return privateKey;
MSAL/src/MSALAuthenticationSchemePop.m:56
- Issue: The initializer declaration in the .m file uses nonnull
nonce/additionalParameters, but the public header declares them nullable and the implementation already handlesnilby generating defaults.
Impact: This internal/public nullability mismatch can trigger compiler warnings and makes the implementation signature inconsistent with the public API.
Recommendation: Update the initializer declaration in the implementation to accept nullable values, matching the header.
- (instancetype)initWithHttpMethod:(MSALHttpMethod)httpMethod
requestUrl:(NSURL *)requestUrl
nonce:(NSString *)nonce
additionalParameters:(NSDictionary *)additionalParameters
MSAL/src/MSALAuthenticationSchemePop.m:69
- Issue: The external-key initializer implementation also declares
nonce/additionalParametersas nonnull, while the public header declares them nullable and the implementation supportsnil.
Impact: This can lead to nullability-mismatch warnings and makes the implementation inconsistent with the public API contract.
Recommendation: MarknonceandadditionalParametersas nullable in this initializer declaration as well.
- (instancetype)initWithHttpMethod:(MSALHttpMethod)httpMethod
requestUrl:(NSURL *)requestUrl
nonce:(NSString *)nonce
additionalParameters:(NSDictionary *)additionalParameters
externalKeyPair:(nullable MSALExternalKeyPair *)externalKeyPair
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Update MSALExternalKeyPair to pass the new IdentityCore validation context parameter and advance the IdentityCore submodule to the corresponding fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9de0e32b-fd81-4c9e-985a-e88da735198b
Co-authored-by: Forge <forge-bot@entra.github.io> Forge-Agent: Coder
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MSAL/src/MSALExternalKeyPair.m:40
- Issue: When external-key validation fails, the returned NSError drops the underlying
validationError.
Impact: Callers lose diagnostic detail (and any structured info in the underlying error), making it harder to troubleshoot why the key pair is rejected.
Recommendation: PreservevalidationErrorunderNSUnderlyingErrorKeywhen constructingMSALErrorInvalidExternalKeyPair.
static MSALExternalKeyPairFailureReason MSALFailureReasonFromMSIDReason(MSIDExternalKeyPairValidationFailureReason reason)
{
switch (reason)
{
MSAL/src/public/MSALExternalKeyPair.h:39
- Issue: The initializer parameters are implicitly nonnull (inside
NS_ASSUME_NONNULL_BEGIN), but unit tests intentionally passNULLkey handles to validate error behavior.
Impact: This will trigger-Wnonnullwarnings (and can break builds if warnings are treated as errors), and the header doesn’t reflect the implementation’s ability to gracefully rejectNULL.
Recommendation: Mark theSecKeyRefparameters as nullable (and keep returningnil+MSALErrorInvalidExternalKeyPairfor invalid handles).
MSALExternalKeyPairFailureReasonKeySizeTooSmall,
MSALExternalKeyPairFailureReasonInvalidKeyClass,
MSALExternalKeyPairFailureReasonNotSigningCapable,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MSAL/src/MSALExternalKeyPair.m:68
- Issue: The implementation signature for
initWithPrivateKey:publicKey:error:uses nonnullSecKeyReftypes, but the initializer’s behavior supportsNULLinputs (it returnsMSALErrorInvalidExternalKeyPair).
Impact: If the public header is updated to accept nullable keys (to reflect actual behavior), the implementation should match to avoid nullability mismatches and warnings.
Recommendation: Update the method signature here to accept_NullableSecKeyRefinputs (no behavioral change).
- (nullable instancetype)initWithPrivateKey:(SecKeyRef)privateKey
publicKey:(SecKeyRef)publicKey
error:(NSError * _Nullable __autoreleasing * _Nullable)error
MSAL/src/public/MSALExternalKeyPair.h:57
- Issue:
initWithPrivateKey:publicKey:error:is declared underNS_ASSUME_NONNULL_BEGINwith nonnullSecKeyRefparameters, but the API explicitly validates forNULLhandles (and unit tests passNULL).
Impact: Callers (and tests) can get compile-time warnings for passingNULLto nonnull parameters, which is inconsistent with the intended “failable initializer” contract.
Recommendation: Mark theSecKeyRefparameters as_Nullableto match the validation behavior and avoid false-positive nonnull diagnostics.
- (nullable instancetype)initWithPrivateKey:(SecKeyRef)privateKey
publicKey:(SecKeyRef)publicKey
error:(NSError * _Nullable __autoreleasing * _Nullable)error NS_DESIGNATED_INITIALIZER;
|
Copilot resolve the merge conflicts in this pull request |
…atpop_external_key # Conflicts: # CHANGELOG.md # MSAL/IdentityCore # MSAL/MSAL.xcodeproj/project.pbxproj Co-authored-by: antonioalwan <11672290+antonioalwan@users.noreply.github.com>
Merge conflicts resolved in 841b74d (merge of Resolutions:
The PR no longer reports conflicts; remaining |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MSAL/src/MSALAuthenticationSchemePop.m:83
- Severity: Medium – This implementation depends on new IdentityCore APIs (
MSIDDevicePopManager initWithExternalKeyPair:) that aren’t part of MSAL itself.
Issue: MSALAuthenticationSchemePop now calls [[MSIDDevicePopManager alloc] initWithExternalKeyPair:externalKeyPair.msidKeyPair], so the build requires an IdentityCore update that provides this initializer.
Impact: If the IdentityCore submodule pointer (and any packaging references) aren’t updated in the same PR/release, MSAL will fail to compile/link.
Recommendation: Ensure the IdentityCore submodule commit included in this PR contains initWithExternalKeyPair: (and any related constants like MSID_OAUTH2_EXTERNAL_KEY_POP) and that CI validates with git submodule update --init --recursive.
_externalKeyPair = externalKeyPair;
if (externalKeyPair)
{
_externalPopManager = [[MSIDDevicePopManager alloc] initWithExternalKeyPair:externalKeyPair.msidKeyPair];
}
MSAL/src/MSALExternalKeyPair.m:76
- Severity: Medium – This new public API wrapper relies on IdentityCore symbols that must exist in the pinned submodule version.
Issue: MSALExternalKeyPair uses MSIDExternalKeyPairValidationFailureReason and -[MSIDKeyOperationUtil validateExternalRSAKeyPair:publicKey:failureReason:context:error:].
Impact: If the IdentityCore submodule pointer/dependency isn’t updated to a version that defines these, MSAL won’t compile.
Recommendation: Make sure the PR includes (or is paired with) the IdentityCore submodule update referenced in the PR description, and that CI builds with that updated submodule commit.
MSIDExternalKeyPairValidationFailureReason validationReason = MSIDExternalKeyPairValidationFailureReasonNone;
NSError *validationError = nil;
BOOL valid = [[MSIDKeyOperationUtil sharedInstance] validateExternalRSAKeyPair:privateKey
publicKey:publicKey
failureReason:&validationReason
context:nil
error:&validationError];
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9de0e32b-fd81-4c9e-985a-e88da735198b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MSAL/src/MSALExternalKeyPair+Internal.h:36
- Issue:
msidKeyPairis exposed as a read/write property in the internal class extension.
Impact: This makes it easy for other internal code (or tests) to mutate the key pair after validation, undermining the immutability assumptions ofMSALExternalKeyPairand complicating debugging if the key identifier changes unexpectedly.
Recommendation: Make the internalmsidKeyPairproperty read-only and set it only during initialization.
@property (nonatomic) MSIDAssymetricKeyPair *msidKeyPair;
MSAL/src/MSALExternalKeyPair.m:123
- Issue: The failure reason is always reported as
MSALExternalKeyPairFailureReasonPublicKeySerializationFailedwhen_msidKeyPairis nil or when_msidKeyPair.kidis nil.
Impact: This can misclassify the validation category exposed viaMSALExternalKeyPairFailureReasonKey, making it harder for callers to distinguish “failed to construct/derive key info” vs “constructed but couldn’t serialize/compute kid”.
Recommendation: ReportPublicKeyDerivationFailedwhen_msidKeyPairis nil, and reservePublicKeySerializationFailedfor cases where the key pair object exists butkidis missing.
if (!_msidKeyPair || !_msidKeyPair.kid)
{
if (error)
{
*error = [NSError errorWithDomain:MSALErrorDomain
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (9)
MSAL/test/unit/MSALExternalKeyPairTests.m:87
- Issue:
SecKeyCopyPublicKeycan return NULL, but this test doesn't assert it before later using/releasing the reference.
Impact: A NULLpublicKeywould causeCFRelease(NULL)(crash) and mask the real failure.
Recommendation: Add anXCTAssertNotEqual(publicKey, NULL)immediately after copying the public key.
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@2048];
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
NSError *error = nil;
MSAL/test/unit/MSALExternalKeyPairTests.m:139
- Issue:
SecKeyCopyPublicKeycan return NULL, but this test doesn't assert it before later using/releasing the reference.
Impact: A NULLpublicKeycan crash the test (CFRelease(NULL)) and obscure the true failure.
Recommendation: AssertpublicKey != NULLright afterSecKeyCopyPublicKey.
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeECSECPrimeRandom size:@256];
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
NSError *error = nil;
MSAL/test/unit/MSALExternalKeyPairTests.m:156
- Issue:
SecKeyCopyPublicKeycan return NULL, but this test doesn't assert it before later using/releasing the reference.
Impact: If NULL, this can crash the test suite (CFRelease(NULL)) instead of producing a clear assertion failure.
Recommendation: Add anXCTAssertNotEqual(publicKey, NULL)afterSecKeyCopyPublicKey.
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@1024];
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
NSError *error = nil;
MSAL/test/unit/MSALExternalKeyPairTests.m:174
- Issue:
SecKeyCopyPublicKeycan return NULL, but this test doesn't assert it before later using/releasing the reference.
Impact: A NULLotherPublicKeywould lead toCFRelease(NULL)(crash), hiding the root cause.
Recommendation: AssertotherPublicKey != NULLimmediately afterSecKeyCopyPublicKey.
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@2048];
SecKeyRef otherPrivateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@2048];
SecKeyRef otherPublicKey = SecKeyCopyPublicKey(otherPrivateKey);
NSError *error = nil;
MSAL/test/unit/MSALExternalKeyPairTests.m:192
- Issue:
SecKeyCopyPublicKeycan return NULL, but this test doesn't assert it before later using/releasing the reference.
Impact: A NULLpublicKeycan crash the test (CFRelease(NULL)) and make the failure non-actionable.
Recommendation: Add an assertion thatpublicKeyis non-NULL immediately after copying it.
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@2048];
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
NSError *error = nil;
MSAL/src/MSALExternalKeyPair.m:119
- Issue: This
NSErroruserInfo setsMSALErrorDescriptionKeybut notNSLocalizedDescriptionKey.
Impact: Callers that readerror.localizedDescriptionwon’t see the provided message, which reduces diagnosability for a new public API.
Recommendation: AddNSLocalizedDescriptionKeywith the same string value.
*error = [NSError errorWithDomain:MSALErrorDomain
code:MSALErrorInvalidExternalKeyPair
userInfo:@{
MSALErrorDescriptionKey : @"Unable to derive a public key thumbprint.",
MSALExternalKeyPairFailureReasonKey : @(MSALExternalKeyPairFailureReasonPublicKeySerializationFailed)
}];
MSAL/test/unit/MSALExternalKeyPairTests.m:52
- Issue:
SecKeyCopyPublicKeycan return NULL, but these tests don't assert it before using/releasing the returned reference.
Impact: A NULLpublicKeyhere would lead toCFRelease(NULL)(crash) and make failures harder to diagnose.
Recommendation: AssertpublicKey != NULLimmediately afterSecKeyCopyPublicKeyin each test that uses it.
This issue also appears in the following locations of the same file:
- line 85
- line 137
- line 154
- line 171
- line 190
SecKeyRef privateKey = [self createPrivateKeyWithType:kSecAttrKeyTypeRSA size:@2048];
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
NSError *error = nil;
MSAL/src/MSALExternalKeyPair.m:89
- Issue: The constructed
NSErroruserInfo setsMSALErrorDescriptionKeybut notNSLocalizedDescriptionKey.
Impact: Callers that surfaceerror.localizedDescription(common) will likely get a generic "(MSALErrorDomain error -50009)" message instead of the actionable validation description.
Recommendation: PopulateNSLocalizedDescriptionKeyalongsideMSALErrorDescriptionKeywith the same string.
This issue also appears on line 114 of the same file.
NSString *description = validationError.localizedDescription ?: @"Invalid external AT PoP key pair.";
NSMutableDictionary *userInfo = [@{
MSALErrorDescriptionKey : description,
MSALExternalKeyPairFailureReasonKey : @(MSALFailureReasonFromMSIDReason(validationReason))
} mutableCopy];
MSAL/src/public/MSALAuthenticationSchemePop.h:55
- Issue: The new designated initializer can return
nilbut provides noNSError **out parameter (it currently fails by returningniland logging internally).
Impact: Callers have no actionable way to learn why initialization failed for a new public API (they typically rely onNSError+localizedDescriptionrather than internal logs).
Recommendation: Since this initializer is newly introduced, consider adding an optionalerror:(NSError **)errorparameter and populate it when returningnil(mirroring other MSAL fallible initializers).
- (nullable instancetype)initWithHttpMethod:(MSALHttpMethod)httpMethod
requestUrl:(NSURL *)requestUrl
nonce:(nullable NSString *)nonce
additionalParameters:(nullable NSDictionary *)additionalParameters
externalKeyPair:(nullable MSALExternalKeyPair *)externalKeyPair NS_DESIGNATED_INITIALIZER;
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9de0e32b-fd81-4c9e-985a-e88da735198b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9de0e32b-fd81-4c9e-985a-e88da735198b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
MSAL/src/MSALAuthenticationSchemePop.m:147
- Issue: When
createSignedAccessToken:... error:errorfails, this method unconditionally overwrites any error it produced with a genericMSIDCreateError(...).
Impact: Callers lose the underlying failure details (e.g., signing/crypto/key-specific errors), making external-key PoP failures harder to diagnose.
Recommendation: Preserve the error fromcreateSignedAccessTokenand only create the generic fallback error when no error was provided.
MSIDDevicePopManager *manager = self.externalPopManager ?: popManager;
NSString *signedAccessToken = [manager createSignedAccessToken:accessToken.accessToken
httpMethod:MSALParameterStringForHttpMethod(self.httpMethod)
requestUrl:self.requestUrl.absoluteString
nonce:self.nonce
error:error];
MSAL/src/MSALExternalKeyPair.m:119
- Issue: This error path says it was "Unable to derive a public key thumbprint", but reports
MSALExternalKeyPairFailureReasonPublicKeySerializationFailed.
Impact: The failure reason exposed to SDK consumers can be misleading (thumbprint derivation vs serialization), making it harder to take the right remediation steps.
Recommendation: Align the failure reason with the message (or adjust the message). If the failure is specifically about deriving the RFC 7638 thumbprint (kid), useMSALExternalKeyPairFailureReasonPublicKeyDerivationFailed.
*error = [NSError errorWithDomain:MSALErrorDomain
code:MSALErrorInvalidExternalKeyPair
userInfo:@{
MSALErrorDescriptionKey : @"Unable to derive a public key thumbprint.",
MSALExternalKeyPairFailureReasonKey : @(MSALExternalKeyPairFailureReasonPublicKeySerializationFailed)
}];
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (3)
MSAL/src/public/MSALAuthenticationSchemePop.h:55
- Issue: The new designated initializer can return nil (it’s declared nullable) but provides no NSError out-parameter and the header comment doesn’t state when/why initialization can fail.
Impact: Callers have no deterministic way to diagnose failures (especially if MSAL logging isn’t enabled), which makes this public API harder to use and support.
Recommendation: Either (a) add anNSError **out-parameter to this initializer and document the failure conditions, or (b) make the initializer nonnull and treat external key manager initialization failures as programmer errors (assert) sinceMSALExternalKeyPairis expected to already be validated.
- (nullable instancetype)initWithHttpMethod:(MSALHttpMethod)httpMethod
requestUrl:(NSURL *)requestUrl
nonce:(nullable NSString *)nonce
additionalParameters:(nullable NSDictionary *)additionalParameters
externalKeyPair:(nullable MSALExternalKeyPair *)externalKeyPair NS_DESIGNATED_INITIALIZER;
CHANGELOG.md:4
- Issue: The changelog entry states “Update IdentityCore with external RSA key validation and injected PoP manager support.”, but this PR’s diff doesn’t include a submodule pointer bump for
MSAL/IdentityCore.
Impact: Without the submodule update, the new MSAL code that depends on these IdentityCore APIs may not build for consumers/CI, and the changelog becomes misleading.
Recommendation: Either bump theMSAL/IdentityCoresubmodule to the commit that contains the referenced CommonCore change (and keep this bullet), or adjust the changelog entry to reflect the actual dependency/update included in this PR.
#TBD
* Add SSPR V2 #3039
* Add `MSALExternalKeyPair` and an additive AT PoP authentication-scheme initializer for caller-owned RSA keys.
* Update IdentityCore with external RSA key validation and injected PoP manager support.
MSAL/src/MSALExternalKeyPair.m:123
- Issue: The failure reason reported when
_msidKeyPairinitialization fails vs when_msidKeyPair.kidis missing is alwaysMSALExternalKeyPairFailureReasonPublicKeySerializationFailed.
Impact: Callers relying onMSALExternalKeyPairFailureReasonKeycan’t distinguish serialization failure from thumbprint derivation failure, even though the public enum exposes both categories.
Recommendation: Set the failure reason based on whether_msidKeyPairis nil (serialization) orkidis nil (derivation).
if (!_msidKeyPair || !_msidKeyPair.kid)
{
if (error)
| requestUrl:(NSURL *)requestUrl | ||
| nonce:(nullable NSString *)nonce | ||
| additionalParameters:(nullable NSDictionary *)additionalParameters | ||
| externalKeyPair:(nullable MSALExternalKeyPair *)externalKeyPair NS_DESIGNATED_INITIALIZER; |
There was a problem hiding this comment.
This initializer can return nil when the external pop manager fails to init, but we only MSID_LOG the reason in the .m, so the caller just gets nil with nothing to go on. MSALExternalKeyPair's init already surfaces MSALErrorInvalidExternalKeyPair + MSALExternalKeyPairFailureReasonKey, will it be better to add an error: param here too so it is consistent? testExternalKeyPair_whenInternalPairIsMissing_shouldFailInitialization shows the nil path is reachable, and caller-owned keys can go bad after MSALExternalKeyPair was created (deleted / invalidated), so this is likely to be hit in the field :)
Summary
Adds the public MSAL API for caller-provided RSA keys in Access Token Proof-of-Possession flows. MSAL validates and retains the caller-owned key pair, derives
req_cnffrom its public key, signs the SHR with its private key, and marks Broker requests for selective rollout control.Key changes
MSALExternalKeyPairwith a read-only RFC 7638keyId.MSALAuthenticationSchemePop.external_key_pop=1to scheme parameters for Broker classification.Dependency
How to validate
MSALExternalKeyPairTests.MSALAuthSchemeTests.Notes
The caller owns key creation, storage, rotation, and deletion. MSAL does not expose or persist private key material.