app_store_connect/
certs_api.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use crate::{AppStoreConnectClient, Result};
8use base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine};
9use rand::rngs::OsRng;
10use rsa::pkcs8::{EncodePrivateKey, LineEnding};
11use rsa::RsaPrivateKey;
12use serde::{Deserialize, Serialize};
13use std::fs::File;
14use std::io::Write;
15use std::path::Path;
16use x509_certificate::{InMemorySigningKeyPair, X509CertificateBuilder};
17
18pub fn generate_signing_certificate(api_key: &Path, ty: CertificateType, pem: &Path) -> Result<()> {
19    let secret = RsaPrivateKey::new(&mut OsRng, 2048)?;
20    let key = InMemorySigningKeyPair::from_pkcs8_der(secret.to_pkcs8_der()?.as_bytes())?;
21    let mut builder = X509CertificateBuilder::default();
22    builder
23        .subject()
24        .append_common_name_utf8_string("Apple Code Signing CSR")
25        .expect("only valid chars");
26    let csr = builder
27        .create_certificate_signing_request(&key)?
28        .encode_pem()?;
29    let cer = AppStoreConnectClient::from_json_path(api_key)?
30        .create_certificate(csr, ty)?
31        .data
32        .attributes
33        .certificate_content;
34    let cer = pem::encode(&pem::Pem::new("CERTIFICATE", STANDARD_ENGINE.decode(cer)?));
35    let mut f = File::create(pem)?;
36    f.write_all(secret.to_pkcs8_pem(LineEnding::CRLF)?.as_bytes())?;
37    f.write_all(cer.as_bytes())?;
38    Ok(())
39}
40
41const APPLE_CERTIFICATE_URL: &str = "https://api.appstoreconnect.apple.com/v1/certificates";
42
43impl AppStoreConnectClient {
44    pub fn create_certificate(
45        &self,
46        csr: String,
47        ty: CertificateType,
48    ) -> Result<CertificateResponse> {
49        let token = self.get_token()?;
50        let body = CertificateCreateRequest {
51            data: CertificateCreateRequestData {
52                attributes: CertificateCreateRequestAttributes {
53                    certificate_type: ty.to_string(),
54                    csr_content: csr,
55                },
56                r#type: "certificates".into(),
57            },
58        };
59        let req = self
60            .client
61            .post(APPLE_CERTIFICATE_URL)
62            .bearer_auth(token)
63            .header("Accept", "application/json")
64            .header("Content-Type", "application/json")
65            .json(&body);
66        Ok(self.send_request(req)?.json()?)
67    }
68
69    pub fn list_certificates(&self) -> Result<CertificatesResponse> {
70        let token = self.get_token()?;
71        let req = self
72            .client
73            .get(APPLE_CERTIFICATE_URL)
74            .bearer_auth(token)
75            .header("Accept", "application/json");
76        Ok(self.send_request(req)?.json()?)
77    }
78
79    pub fn get_certificate(&self, id: &str) -> Result<CertificateResponse> {
80        let token = self.get_token()?;
81        let req = self
82            .client
83            .get(format!("{APPLE_CERTIFICATE_URL}/{id}"))
84            .bearer_auth(token)
85            .header("Accept", "application/json");
86        Ok(self.send_request(req)?.json()?)
87    }
88
89    pub fn revoke_certificate(&self, id: &str) -> Result<()> {
90        let token = self.get_token()?;
91        let req = self
92            .client
93            .delete(format!("{APPLE_CERTIFICATE_URL}/{id}"))
94            .bearer_auth(token);
95        self.send_request(req)?;
96        Ok(())
97    }
98}
99
100#[derive(Debug, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct CertificateCreateRequest {
103    pub data: CertificateCreateRequestData,
104}
105
106#[derive(Debug, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct CertificateCreateRequestData {
109    pub attributes: CertificateCreateRequestAttributes,
110    pub r#type: String,
111}
112
113#[derive(Debug, Serialize)]
114#[serde(rename_all = "camelCase")]
115pub struct CertificateCreateRequestAttributes {
116    pub certificate_type: String,
117    pub csr_content: String,
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
121pub enum CertificateType {
122    Development,
123    Distribution,
124    DeveloperIdApplication,
125    IosDistribution,
126}
127
128impl std::fmt::Display for CertificateType {
129    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
130        let s = match self {
131            Self::Development => "DEVELOPMENT",
132            Self::Distribution => "DISTRIBUTION",
133            Self::DeveloperIdApplication => "DEVELOPER_ID_APPLICATION",
134            Self::IosDistribution => "IOS_DISTRIBUTION",
135        };
136        write!(f, "{s}")
137    }
138}
139
140#[derive(Debug, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct CertificateResponse {
143    pub data: Certificate,
144}
145
146#[derive(Debug, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct CertificatesResponse {
149    pub data: Vec<Certificate>,
150}
151
152#[derive(Debug, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Certificate {
155    pub attributes: CertificateAttributes,
156    pub id: String,
157}
158
159#[derive(Debug, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct CertificateAttributes {
162    pub certificate_content: String,
163    pub display_name: String,
164    pub expiration_date: String,
165    pub name: String,
166    pub platform: Option<String>,
167    pub serial_number: String,
168    pub certificate_type: String,
169}