상세 컨텐츠

본문 제목

[애플월렛 패스] 참가증, 쿠폰, 티켓같은 패스 서버 API 호출로 생성하기-기능구현

iOS 캐기/토이 프로젝트

by Atlas 2024. 8. 12. 18:36

본문

728x90
반응형

시나리오:  저번 포스팅에서 서버를 만들어봤습니다. 이번에는 API 호출을 통해서 패스를 만들어보도록 하겠습니다.

 

2023.11.17 - [iOS 캐기/토이 프로젝트] - [애플월렛 패스] 참가증, 쿠폰, 티켓 같은 패스 만들어 애플월렛에 넣어보기

2024.08.12 - [iOS 캐기/토이 프로젝트] - [애플월렛 패스] 참가증, 쿠폰, 티켓같은 패스 서버 API 호출로 생성하기- 개발환경 세팅

2024.08.12 - [iOS 캐기/토이 프로젝트] - [애플월렛 패스] 참가증, 쿠폰, 티켓같은 패스 서버 API 호출로 생성하기- 구조 작성

 


이 코드는 Apple Wallet에서 사용할 수 있는 패스(Pass)를 생성하는 FastAPI 엔드포인트를 정의합니다. 주로 Apple Wallet용 패스(Pass)를 만들 때 필요한 작업을 처리하며, 생성된 패스를 클라이언트에게 파일로 반환합니다. 코드를 단계별로 설명하겠습니다.

1.  패스 생성 엔드포인트 정의

@app.get("/generate_pass")
async def generate_pass():

 

이 데코레이터는 FastAPI에서 HTTP GET 요청을 처리하기 위한 엔드포인트를 정의합니다. 클라이언트가 `/generate_pass` 경로로 요청을 보내면, 이 함수가 호출됩니다.

2.  로그 시작

logging.info("Start generating pass")

 

패스 생성 프로세스가 시작되었음을 기록합니다.


3.  패스 정보 설정

# 패스 정보 설정
card_info = EventTicket()
card_info.add_primary_field('event', 'Atlas Book Talk', 'event')
card_info.add_secondary_field('timestamp', '11/23/2024', 'DATE')

 

4.  Apple Pass 생성기 초기화

 

# Apple Pass 설정
team_identifier = "{#team_identifier}"
pass_type_identifier = "{#pass_type_identifier}"
organization_name = "{#organization_name}"
applepassgenerator_client = ApplePassGeneratorClient(team_identifier, pass_type_identifier, organization_name)
apple_pass = applepassgenerator_client.get_pass(card_info)

   

  • applepassgenerator_client = ApplePassGeneratorClient(team_identifier, pass_type_identifier, organization_name): Apple Pass를 생성하는 클라이언트를 초기화합니다. 팀 식별자, 패스 타입 식별자, 조직 이름을 사용하여 클라이언트를 설정합니다.
  • apple_pass = applepassgenerator_client.get_pass(card_info): 위에서 정의한 card_info를 사용하여 Apple Pass 객체를 생성합니다.

5.  인증서 및 키 파일 추출 

def extract_certificate_and_key(p12_path, cert_out_path, key_out_path, password):
    try:
        with open(p12_path, "rb") as p12_file:
            p12_data = p12_file.read()
        private_key, certificate, _ = pkcs12.load_key_and_certificates(p12_data, password.encode(), default_backend())

        with open(cert_out_path, "wb") as pem_file:
            pem_file.write(certificate.public_bytes(serialization.Encoding.PEM))

        with open(key_out_path, "wb") as key_file:
            key_file.write(private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
            ))

        logging.info("Certificate and private key extracted successfully.")
    except Exception as e:
        logging.error(f"Failed to extract certificate and key: {e}")
        raise HTTPException(status_code=500, detail="Failed to extract certificate and key")

 

사용법:

# 인증서 및 키 파일 추출
extract_certificate_and_key(CERTIFICATE_P12, CERTIFICATE_PEM, PRIVATE_KEY_PEM, CERTIFICATE_PASSWORD)


   extract_certificate_and_key(CERTIFICATE_P12, CERTIFICATE_PEM, PRIVATE_KEY_PEM, CERTIFICATE_PASSWORD) : P12 형식의 인증서를 PEM 형식으로 변환하고, 개인 키를 추출합니다. 이 과정에서 비밀번호가 필요합니다.

 

6.  WWDR 인증서 변환 

 

def convert_cer_to_pem(cer_path, pem_path):
    try:
        # OpenSSL을 사용하여 CER 파일을 PEM 형식으로 변환
        subprocess.run(['openssl', 'x509', '-inform', 'DER', '-in', cer_path, '-out', pem_path], check=True)
        logging.info(f"{cer_path} successfully converted to PEM format.")
    except subprocess.CalledProcessError as e:
        logging.error(f"Failed to convert {cer_path} to PEM: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to convert {cer_path} to PEM: {e}")

 

 

사용법: 

# WWDR.cer 파일을 PEM 파일로 변환
convert_cer_to_pem(WWDR_CER, WWDR_PEM)

 

   - convert_cer_to_pem(WWDR_CER, WWDR_PEM): WWDR(Apple WorldWide Developer Relations) 인증서를 PEM 형식으로 변환합니다. 이 파일은 Apple 패스 생성 시 필요합니다.

 

7.  필요한 파일 존재 여부 확인

# 파일이 존재하는지 확인
    for file_path in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE, CERTIFICATE_PEM, WWDR_PEM, PRIVATE_KEY_PEM]:
        if not os.path.isfile(file_path):
            logging.error(f"File not found: {file_path}")
            raise HTTPException(status_code=500, detail=f"File not found: {file_path}")

 

  • for file_path in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE, CERTIFICATE_PEM, WWDR_PEM, PRIVATE_KEY_PEM]  패스 생성에 필요한 모든 파일들이 존재하는지 확인합니다.
  •  if not os.path.isfile(file_path): 파일이 없으면 에러 로그를 기록하고, HTTP 500 에러를 반환합니다.

8.  Apple Pass에 파일 추가

# Apple Pass에 파일 추가
    for file_key in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE]:
        with open(file_key, "rb") as file:
            apple_pass.add_file(os.path.basename(file_key), file)


for file_key in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE]n : 패스에 필요한 이미지 파일들을 추가합니다. add_file 메서드를 통해 파일을 추가합니다.

9.  패스 파일 생성

  # 패스 파일 생성
    try:
        apple_pass.create(CERTIFICATE_PEM, PRIVATE_KEY_PEM, WWDR_PEM, CERTIFICATE_PASSWORD, OUTPUT_PASS_NAME)
    except Exception as e:
        logging.error(f"Failed to create pass: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to create pass: {e}")

    return FileResponse(OUTPUT_PASS_NAME, media_type='application/vnd.apple.pkpass', filename=OUTPUT_PASS_NAME)

 

  •  apple_pass.create(CERTIFICATE_PEM, PRIVATE_KEY_PEM, WWDR_PEM, CERTIFICATE_PASSWORD, OUTPUT_PASS_NAME): 패스 파일을 생성합니다. 인증서, 키, WWDR 인증서를 사용하여 패스 파일을 OUTPUT_PASS_NAME이라는 이름으로 저장합니다.
  •  except Exception as e:  패스 생성 중 에러가 발생하면, 에러를 로그에 기록하고 HTTP 500 에러를 반환합니다.
  •  return FileResponse(OUTPUT_PASS_NAME, media_type='application/vnd.apple.pkpass', filename=OUTPUT_PASS_NAME): 생성된 패스 파일을 클라이언트에게 반환합니다. 이 파일은 `.pkpass` 확장자를 가지며, Apple Wallet에서 사용할 수 있습니다.


4. OpenSSL을 사용한 CER -> PEM 변환 함수

def convert_cer_to_pem(cer_path, pem_path):
    ...


- CER 형식의 인증서를 PEM 형식으로 변환하는 함수입니다. OpenSSL을 사용하여 변환합니다.

 

5. P12에서 인증서 및 키 추출 함수

def extract_certificate_and_key(p12_path, cert_out_path, key_out_path, password):
    ...


- P12 파일에서 인증서와 개인 키를 추출하여 PEM 형식으로 저장합니다.

 

 

전체코드 

from applepassgenerator.client import ApplePassGeneratorClient
from fastapi import FastAPI,HTTPException
from fastapi.responses import FileResponse
from applepassgenerator.models import EventTicket
import subprocess
import os
import logging

from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
app = FastAPI()

# 설정 파일
CERTIFICATE_P12 = "certificate.p12"
CERTIFICATE_PEM = "certificate.pem"
PRIVATE_KEY_PEM = "privatekey_encrypted.pem"
WWDR_PEM = "WWDR.pem"
WWDR_CER = "WWDR.cer"
CERTIFICATE_PASSWORD = "{#CERTIFICATE_PASSWORD}"
OUTPUT_PASS_NAME = "atlas_demo.pkpass"

# 로고 및 아이콘 파일 경로 설정
LOGO_FILE = "logo.png"
ICON_FILE = "icon.png"
BACKGROUND_FILE = "background.png"
THUMBNAIL_FILE = "thumbnail.png"

# 로그 설정
logging.basicConfig(level=logging.DEBUG)

def convert_cer_to_pem(cer_path, pem_path):
    try:
        # OpenSSL을 사용하여 CER 파일을 PEM 형식으로 변환
        subprocess.run(['openssl', 'x509', '-inform', 'DER', '-in', cer_path, '-out', pem_path], check=True)
        logging.info(f"{cer_path} successfully converted to PEM format.")
    except subprocess.CalledProcessError as e:
        logging.error(f"Failed to convert {cer_path} to PEM: {e}")
        raise HTTPException(status_code=500, detail=f"Failed to convert {cer_path} to PEM: {e}")
def extract_certificate_and_key(p12_path, cert_out_path, key_out_path, password):
    try:
        with open(p12_path, "rb") as p12_file:
            p12_data = p12_file.read()
        private_key, certificate, _ = pkcs12.load_key_and_certificates(p12_data, password.encode(), default_backend())

        with open(cert_out_path, "wb") as pem_file:
            pem_file.write(certificate.public_bytes(serialization.Encoding.PEM))

        with open(key_out_path, "wb") as key_file:
            key_file.write(private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
            ))

        logging.info("Certificate and private key extracted successfully.")
    except Exception as e:
        logging.error(f"Failed to extract certificate and key: {e}")
        raise HTTPException(status_code=500, detail="Failed to extract certificate and key")

@app.get("/generate_pass")
async def generate_pass():
    logging.info("Start generating pass")
    try:
        # 패스 정보 설정
        card_info = EventTicket()
        card_info.add_primary_field('event', 'Atlas Book Talk', 'event')
        card_info.add_secondary_field('timestamp', '11/23/2024', 'DATE')

        # Apple Pass 설정
        team_identifier = "{#team_identifier}"
        pass_type_identifier = "{#pass_type_identifier}"
        organization_name = "{#organization_name}"
        applepassgenerator_client = ApplePassGeneratorClient(team_identifier, pass_type_identifier, organization_name)
        apple_pass = applepassgenerator_client.get_pass(card_info)

        # 인증서 및 키 파일 추출
        extract_certificate_and_key(CERTIFICATE_P12, CERTIFICATE_PEM, PRIVATE_KEY_PEM, CERTIFICATE_PASSWORD)

        # WWDR.cer 파일을 PEM 파일로 변환
        convert_cer_to_pem(WWDR_CER, WWDR_PEM)

        # 파일이 존재하는지 확인
        for file_path in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE, CERTIFICATE_PEM, WWDR_PEM, PRIVATE_KEY_PEM]:
            if not os.path.isfile(file_path):
                logging.error(f"File not found: {file_path}")
                raise HTTPException(status_code=500, detail=f"File not found: {file_path}")

        # Apple Pass에 파일 추가
        for file_key in [LOGO_FILE, ICON_FILE, BACKGROUND_FILE, THUMBNAIL_FILE]:
            with open(file_key, "rb") as file:
                apple_pass.add_file(os.path.basename(file_key), file)

        # 패스 파일 생성
        try:
            apple_pass.create(CERTIFICATE_PEM, PRIVATE_KEY_PEM, WWDR_PEM, CERTIFICATE_PASSWORD, OUTPUT_PASS_NAME)
        except Exception as e:
            logging.error(f"Failed to create pass: {e}")
            raise HTTPException(status_code=500, detail=f"Failed to create pass: {e}")

        return FileResponse(OUTPUT_PASS_NAME, media_type='application/vnd.apple.pkpass', filename=OUTPUT_PASS_NAME)
    except Exception as e:
        logging.error(f"An error occurred: {e}")
        raise HTTPException(status_code=500, detail=str(e))

 

 

사용법

- .p12 인증서를 폴더에 넣어주고 서버를 실행시킵니다.

- API 를 호출하면 필요한 PEM 파일을 생성하여 패스를 생성시킵니다. 

 

미리 WWDR(AppleWWDRCAG4.cer) 파일을 넣어두었습니다

.p12 인증서 파일만 폴더에 넣고 사용이 가능하다는 장점이 있습니다. 

인증서 생성관련해서 궁금하시면 이전 포스팅을 참고부탁드립니다. 

 

2023.11.17 - [iOS 캐기/토이 프로젝트] - [애플월렛 패스] 참가증, 쿠폰, 티켓같은 패스 만들어 애플월렛에 넣어보기

 

테스트

작업한 폴더에서 서버를 실행합니다.

 uvicorn main:app --reload

 

정상적으로 실행된 걸 확인합니다. 

 

 

다운로드 된 pkpass를 실행시키면 패스를 확인할 수 있습니다. 

 

 

 

마무리

- 코드는 비교적 간단했지만 인증서 관련해서 삽집을 많이했다 😉

 

 

 

ref.

https://github.com/PotatoArtie/Potato-iOS/tree/master/Labs/Playground/pass-generator

 

Potato-iOS/Labs/Playground/pass-generator at master · PotatoArtie/Potato-iOS

Contribute to PotatoArtie/Potato-iOS development by creating an account on GitHub.

github.com

 

반응형

관련글 더보기

댓글 영역