Skip to content

ES9+ — profile download

Three calls, then a notification. The LPA drives all four; the eUICC signs the parts only it can sign; the SM-DP+ never talks to the card directly.

Sequence: the LPA calls initiateAuthentication, authenticateClient and getBoundProfilePackage on the SM-DP+ through CloudFront, exchanging ES10b commands with the eUICC between each, then posts the signed installation result to handleNotification and receives 204 No Content.

Source: diagrams/smdp-plus/es9plus-download.puml. Paths and status codes from Es9PlusController.java; field names from Es9Requests.java and Es9Responses.java.

The four calls

All four are POST, all take and return application/json, and none of them carries an Authorization header — see who may call what.

1 · initiateAuthentication

The LPA has just asked the eUICC for a challenge. It presents that challenge along with the address it thinks it is talking to; the SM-DP+ refuses if that address is not its own.

Request
{
"euiccChallenge": "REDACTED (16 bytes, base64)",
"euiccInfo1": "REDACTED (DER EUICCInfo1, base64)",
"smdpAddress": "smdpplus.rsplab.click"
}
Response — 200
{
"header": { "functionExecutionStatus": { "status": "Executed-Success" } },
"transactionId": "REDACTED (16 hex characters)",
"serverSigned1": "REDACTED (DER, base64)",
"serverSignature1": "REDACTED (ECDSA signature, base64)",
"euiccCiPKIdToBeUsed": "REDACTED (SubjectKeyIdentifier of the chosen CI)",
"serverCertificate": "CERT.DPauth.ECDSA (DER, base64)"
}

euiccCiPKIdToBeUsed is the SM-DP+ choosing which certificate issuer the eUICC should verify against, out of the set the eUICC advertised in euiccInfo1. Get this wrong and the card rejects the server it was about to trust.

2 · authenticateClient

The eUICC has verified the server and signed its own half. This is the call where the profile stops being anonymous: the SM-DP+ matches the matchingId carried inside the eUICC’s response to a released order, and binds that specific profile to that specific EID.

Request
{
"transactionId": "REDACTED (16 hex characters)",
"authenticateServerResponse": "REDACTED (DER, base64)"
}
Response — 200
{
"header": { "functionExecutionStatus": { "status": "Executed-Success" } },
"transactionId": "REDACTED (16 hex characters)",
"profileMetadata": "REDACTED (DER StoreMetadataRequest, base64)",
"smdpSigned2": "REDACTED (DER, base64)",
"smdpSignature2": "REDACTED (ECDSA signature, base64)",
"smdpCertificate": "CERT.DPpb.ECDSA (DER, base64)"
}

Note the certificate changes between call 1 and call 2 — CERT.DPauth proves who the server is, CERT.DPpb proves it is entitled to bind a profile. Two roles, two keys, deliberately not interchangeable.

3 · getBoundProfilePackage

Request
{
"transactionId": "REDACTED (16 hex characters)",
"prepareDownloadResponse": "REDACTED (DER, base64)"
}
Response — 200
{
"header": { "functionExecutionStatus": { "status": "Executed-Success" } },
"transactionId": "REDACTED (16 hex characters)",
"boundProfilePackage": "REDACTED (DER BoundProfilePackage, base64)"
}

The Bound Profile Package is protected under SCP03t with session keys derived from the exchange above. It is bound to one eUICC: copied to another card it is inert.

4 · handleNotification

Request
{
"pendingNotification": "REDACTED (DER PendingNotification, base64)"
}
Response
HTTP/1.1 204 No Content

204 with an empty body, not 200. SGP.22 section 6.3 requires it, and Es9PlusController returns ResponseEntity.noContent().build() accordingly. An LPA that insists on a JSON body here will treat a correct server as broken.

Two more, not shown above

cancelSession (transactionId, cancelSessionResponse) returns 200 with a plain success header. handleDeviceChange (transactionId, deviceChangeRequest, SGP.22 5.6.6) lives on a separate controller and moves a profile to a new device.