es9p.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package device
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/tls"
  6. "encoding/base64"
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "strings"
  12. "time"
  13. )
  14. // es9pClient speaks SGP.22 ES9+ — JSON over HTTPS — to one SM-DP+. It is the
  15. // network half of the LPA download flow: the host authenticates nothing itself
  16. // (the eUICC does all certificate verification on-card); it only shuttles the
  17. // base64 DER blobs between the SM-DP+ and the eUICC.
  18. //
  19. // The wire contract mirrors lpac's euicc/es9p.c: every request is a POST to
  20. // https://<smdp>/gsma/rsp2/es9plus/<function> with a fixed header set, binary
  21. // fields base64-encoded, and the reply envelope carries the outcome in
  22. // header.functionExecutionStatus (with statusCodeData.message holding the
  23. // human-readable failure, e.g. "The matchingID is not found").
  24. type es9pClient struct {
  25. smdp string
  26. http *http.Client
  27. }
  28. func newES9PClient(smdp string) *es9pClient {
  29. // The eUICC — not the host — is the root of trust for RSP: during
  30. // AuthenticateServer the card verifies the SM-DP+'s CERT.DPauth.SIG against
  31. // its embedded CI root, so a rogue/TLS-MitM server cannot forge a signature
  32. // the card will accept. The host TLS layer is transport only, and a minimal
  33. // embedded box may ship no CA bundle (this is exactly what broke on the test
  34. // machine), so we don't anchor host TLS to system roots. InsecureSkipVerify
  35. // is safe here specifically because the card does the authoritative check.
  36. transport := &http.Transport{
  37. TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // eUICC is the RSP trust anchor
  38. }
  39. return &es9pClient{
  40. smdp: strings.TrimSpace(smdp),
  41. http: &http.Client{Timeout: 90 * time.Second, Transport: transport},
  42. }
  43. }
  44. // es9pError is a failed ES9+ functionExecutionStatus. Message is the SM-DP+'s
  45. // own explanation (surfaced verbatim, as the reference implementation does).
  46. type es9pError struct {
  47. Function string
  48. Status string
  49. Message string
  50. SubjectCode string
  51. ReasonCode string
  52. }
  53. func (e *es9pError) Error() string {
  54. if e.Message != "" {
  55. return e.Message
  56. }
  57. if mapped := es9pErrorMessage(e.SubjectCode, e.ReasonCode); mapped != "" {
  58. return mapped
  59. }
  60. if e.Status != "" {
  61. return fmt.Sprintf("SM-DP+ %s failed (%s)", e.Function, e.Status)
  62. }
  63. return fmt.Sprintf("SM-DP+ %s failed", e.Function)
  64. }
  65. // es9pStatusCodeData mirrors header.functionExecutionStatus.statusCodeData.
  66. type es9pStatusCodeData struct {
  67. ReasonCode string `json:"reasonCode"`
  68. SubjectCode string `json:"subjectCode"`
  69. SubjectIdentifier string `json:"subjectIdentifier"`
  70. Message string `json:"message"`
  71. }
  72. // call POSTs one ES9+ function and returns the parsed top-level fields. Failure
  73. // is decided the way lpac decides it: a non-success execution status, or a
  74. // missing required output field, yields an es9pError carrying the SM-DP+ message.
  75. func (c *es9pClient) call(ctx context.Context, function string, request map[string]string, requiredOut ...string) (map[string]json.RawMessage, error) {
  76. url := "https://" + c.smdp + "/gsma/rsp2/es9plus/" + function
  77. body, err := json.Marshal(request)
  78. if err != nil {
  79. return nil, err
  80. }
  81. httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
  82. if err != nil {
  83. return nil, err
  84. }
  85. httpReq.Header.Set("Content-Type", "application/json")
  86. httpReq.Header.Set("User-Agent", "gsma-rsp-lpad")
  87. httpReq.Header.Set("X-Admin-Protocol", "gsma/rsp/v2.2.2")
  88. resp, err := c.http.Do(httpReq)
  89. if err != nil {
  90. return nil, fmt.Errorf("es9p %s: %w", function, err)
  91. }
  92. defer resp.Body.Close()
  93. data, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
  94. if err != nil {
  95. return nil, fmt.Errorf("es9p %s: read response: %w", function, err)
  96. }
  97. var root map[string]json.RawMessage
  98. if err := json.Unmarshal(data, &root); err != nil {
  99. return nil, fmt.Errorf("es9p %s: invalid JSON (HTTP %d): %w", function, resp.StatusCode, err)
  100. }
  101. var header struct {
  102. FunctionExecutionStatus struct {
  103. Status string `json:"status"`
  104. StatusCodeData *es9pStatusCodeData `json:"statusCodeData"`
  105. } `json:"functionExecutionStatus"`
  106. }
  107. if raw, ok := root["header"]; ok {
  108. _ = json.Unmarshal(raw, &header)
  109. }
  110. fes := header.FunctionExecutionStatus
  111. // A non-success execution status is an outright failure.
  112. switch fes.Status {
  113. case "", "Executed-Success", "Executed-WithWarning":
  114. // proceed
  115. default:
  116. return nil, es9pErrFromStatus(function, fes.Status, fes.StatusCodeData)
  117. }
  118. // Success means the expected output fields are present at the top level.
  119. for _, key := range requiredOut {
  120. if _, ok := root[key]; !ok {
  121. return nil, es9pErrFromStatus(function, fes.Status, fes.StatusCodeData)
  122. }
  123. }
  124. return root, nil
  125. }
  126. func es9pErrFromStatus(function, status string, scd *es9pStatusCodeData) error {
  127. err := &es9pError{Function: function, Status: status}
  128. if scd != nil {
  129. err.Message = scd.Message
  130. err.SubjectCode = scd.SubjectCode
  131. err.ReasonCode = scd.ReasonCode
  132. }
  133. return err
  134. }
  135. // es9pErrorMessage maps an SGP.22 (subjectCode, reasonCode) pair to a
  136. // human-readable failure when the SM-DP+ omits statusCodeData.message. Table
  137. // mirrors lpac's euicc/es9p_errors.c.
  138. var es9pErrorTable = map[[2]string]string{
  139. {"8.1", "4.8"}: "eUICC does not have sufficient space for this Profile",
  140. {"8.1", "6.1"}: "eUICC signature is invalid or serverChallenge is invalid",
  141. {"8.1.1", "2.2"}: "EID is missing in the context of this order",
  142. {"8.1.1", "3.1"}: "a different EID is already associated with this ICCID",
  143. {"8.1.1", "3.8"}: "EID doesn't match the expected value",
  144. {"8.1.2", "6.1"}: "EUM Certificate is invalid",
  145. {"8.1.2", "6.3"}: "EUM Certificate has expired",
  146. {"8.1.3", "6.1"}: "eUICC Certificate is invalid",
  147. {"8.1.3", "6.3"}: "eUICC Certificate has expired",
  148. {"8.2", "1.2"}: "Profile has not yet been released",
  149. {"8.2", "3.7"}: "BPP is not available for a new binding",
  150. {"8.2.5", "3.7"}: "No more Profile available for the requested Profile Type",
  151. {"8.2.5", "4.3"}: "No eligible Profile for this eUICC/Device",
  152. {"8.2.6", "3.1"}: "a different MatchingID is associated with this ICCID",
  153. {"8.2.6", "3.3"}: "Conflicting MatchingID value",
  154. {"8.2.6", "3.8"}: "MatchingID (AC_Token or EventID) is refused",
  155. {"8.2.7", "2.2"}: "Confirmation Code is missing",
  156. {"8.2.7", "3.8"}: "Confirmation Code is refused",
  157. {"8.2.7", "6.4"}: "maximum number of retries for the Confirmation Code exceeded",
  158. {"8.8.1", "3.8"}: "Invalid SM-DP+ Address",
  159. {"8.8.4", "3.7"}: "The SM-DP+ has no CERT.DPauth.ECDSA signed by one of the CI Public Key supported by the eUICC",
  160. {"8.8.5", "4.1"}: "The Download order has expired",
  161. {"8.8.5", "6.4"}: "maximum number of retries for the Profile download order exceeded",
  162. {"8.10.1", "3.9"}: "The RSP session identified by the TransactionID is unknown",
  163. {"8.11.1", "3.9"}: "Unknown CI Public Key. The CI used by the EUM Certificate is not a trusted root.",
  164. }
  165. func es9pErrorMessage(subjectCode, reasonCode string) string {
  166. return es9pErrorTable[[2]string{subjectCode, reasonCode}]
  167. }
  168. // es9pString extracts a plain string field.
  169. func es9pString(root map[string]json.RawMessage, key string) (string, error) {
  170. raw, ok := root[key]
  171. if !ok {
  172. return "", fmt.Errorf("es9p: response missing %s", key)
  173. }
  174. var value string
  175. if err := json.Unmarshal(raw, &value); err != nil {
  176. return "", fmt.Errorf("es9p: decode %s: %w", key, err)
  177. }
  178. return value, nil
  179. }
  180. // es9pB64 extracts and base64-decodes a binary field.
  181. func es9pB64(root map[string]json.RawMessage, key string) ([]byte, error) {
  182. value, err := es9pString(root, key)
  183. if err != nil {
  184. return nil, err
  185. }
  186. return es9pBase64Decode(value)
  187. }
  188. func es9pBase64Decode(value string) ([]byte, error) {
  189. value = strings.TrimSpace(value)
  190. if decoded, err := base64.StdEncoding.DecodeString(value); err == nil {
  191. return decoded, nil
  192. }
  193. return base64.RawStdEncoding.DecodeString(value)
  194. }
  195. func es9pBase64Encode(value []byte) string {
  196. return base64.StdEncoding.EncodeToString(value)
  197. }
  198. // es9pInitiateResult carries the server's half of mutual authentication.
  199. type es9pInitiateResult struct {
  200. TransactionID string
  201. ServerSigned1 []byte
  202. ServerSignature1 []byte
  203. EuiccCiPKIDToBeUsed []byte
  204. ServerCertificate []byte
  205. }
  206. func (c *es9pClient) initiateAuthentication(ctx context.Context, euiccChallenge, euiccInfo1 []byte) (*es9pInitiateResult, error) {
  207. root, err := c.call(ctx, "initiateAuthentication", map[string]string{
  208. "smdpAddress": c.smdp,
  209. "euiccChallenge": es9pBase64Encode(euiccChallenge),
  210. "euiccInfo1": es9pBase64Encode(euiccInfo1),
  211. }, "transactionId", "serverSigned1", "serverSignature1", "euiccCiPKIdToBeUsed", "serverCertificate")
  212. if err != nil {
  213. return nil, err
  214. }
  215. result := &es9pInitiateResult{}
  216. if result.TransactionID, err = es9pString(root, "transactionId"); err != nil {
  217. return nil, err
  218. }
  219. if result.ServerSigned1, err = es9pB64(root, "serverSigned1"); err != nil {
  220. return nil, err
  221. }
  222. if result.ServerSignature1, err = es9pB64(root, "serverSignature1"); err != nil {
  223. return nil, err
  224. }
  225. if result.EuiccCiPKIDToBeUsed, err = es9pB64(root, "euiccCiPKIdToBeUsed"); err != nil {
  226. return nil, err
  227. }
  228. if result.ServerCertificate, err = es9pB64(root, "serverCertificate"); err != nil {
  229. return nil, err
  230. }
  231. return result, nil
  232. }
  233. // es9pAuthenticateResult carries the profile metadata and the SM-DP+ download
  234. // authorization needed for PrepareDownload.
  235. type es9pAuthenticateResult struct {
  236. TransactionID string
  237. ProfileMetadata []byte
  238. SmdpSigned2 []byte
  239. SmdpSignature2 []byte
  240. SmdpCertificate []byte
  241. }
  242. func (c *es9pClient) authenticateClient(ctx context.Context, transactionID string, authenticateServerResponse []byte) (*es9pAuthenticateResult, error) {
  243. root, err := c.call(ctx, "authenticateClient", map[string]string{
  244. "transactionId": transactionID,
  245. "authenticateServerResponse": es9pBase64Encode(authenticateServerResponse),
  246. }, "profileMetadata", "smdpSigned2", "smdpSignature2", "smdpCertificate")
  247. if err != nil {
  248. return nil, err
  249. }
  250. result := &es9pAuthenticateResult{TransactionID: transactionID}
  251. if result.ProfileMetadata, err = es9pB64(root, "profileMetadata"); err != nil {
  252. return nil, err
  253. }
  254. if result.SmdpSigned2, err = es9pB64(root, "smdpSigned2"); err != nil {
  255. return nil, err
  256. }
  257. if result.SmdpSignature2, err = es9pB64(root, "smdpSignature2"); err != nil {
  258. return nil, err
  259. }
  260. if result.SmdpCertificate, err = es9pB64(root, "smdpCertificate"); err != nil {
  261. return nil, err
  262. }
  263. return result, nil
  264. }
  265. func (c *es9pClient) getBoundProfilePackage(ctx context.Context, transactionID string, prepareDownloadResponse []byte) ([]byte, error) {
  266. root, err := c.call(ctx, "getBoundProfilePackage", map[string]string{
  267. "transactionId": transactionID,
  268. "prepareDownloadResponse": es9pBase64Encode(prepareDownloadResponse),
  269. }, "boundProfilePackage")
  270. if err != nil {
  271. return nil, err
  272. }
  273. return es9pB64(root, "boundProfilePackage")
  274. }
  275. // handleNotification delivers a pending notification (a ProfileInstallationResult
  276. // for the download case). It is best-effort: the profile is already installed, so
  277. // a notification failure is reported by the caller as a warning, not a failure.
  278. func (c *es9pClient) handleNotification(ctx context.Context, pendingNotification []byte) error {
  279. _, err := c.call(ctx, "handleNotification", map[string]string{
  280. "pendingNotification": es9pBase64Encode(pendingNotification),
  281. })
  282. return err
  283. }
  284. // cancelSession aborts an in-flight download so the SM-DP+ releases the
  285. // transaction. Best-effort cleanup on error/abort paths.
  286. func (c *es9pClient) cancelSession(ctx context.Context, transactionID string, cancelSessionResponse []byte) error {
  287. _, err := c.call(ctx, "cancelSession", map[string]string{
  288. "transactionId": transactionID,
  289. "cancelSessionResponse": es9pBase64Encode(cancelSessionResponse),
  290. })
  291. return err
  292. }