2P by GN⁺ | ★ favorite | 댓글 1개
  • Seattle Public Library와 King County Library System의 플라스틱 카드를 줄이려는 목표에서, iPhone Wallet 패스의 JSON·이미지·서명 구조를 직접 다루게 됨
  • Wallet은 QR code, PDF417, Aztec Code, Code 128만 지원해 도서관 카드의 Codabar를 기본 바코드로 넣을 수 없었고, 실제 스캔용 바코드는 이미지로 우회함
  • 패스는 Apple이 신뢰하는 키로 서명해야 했지만, 연 99달러 Apple Developer 멤버십 대신 오프라인 생성이 가능한 iOS 패스 앱에서 인증서와 키를 찾아 사용함
  • storeCard 레이아웃, 해상도별 로고, Codabar strip.png, 빈 PKBarcodeFormatCode128 항목을 조합해 화면에는 이미지 바코드를 보여주면서도 iOS의 화면 밝기 증가 동작을 살림
  • 완성된 .pkpass는 manifest 생성, openssl smime 서명, ZIP 패키징으로 만들 수 있었고, 실제 도서관 테스트 전이지만 별도 스캐너는 휴대폰 화면의 바코드를 읽었음

목표: 플라스틱 카드 없이 도서관 바코드 쓰기

  • Seattle Public Library와 King County Library System은 대출 계정에 연결된 바코드 인쇄 플라스틱 카드를 발급함
  • 2023년 기준 Seattle 도서관의 대부분 기능은 셀프서비스이며, 보통 도서관 바코드를 수동 입력할 수 있음
  • 그래도 바코드를 바로 보여주는 편이 훨씬 편리하므로, 거의 쓰지 않는 플라스틱 카드를 지갑에 넣지 않고 iPhone Wallet 앱에 넣는 것이 목표였음

Wallet 패스는 JSON·이미지·서명으로 구성됨

  • Wallet 앱은 현재 결제 카드, ID, 키 등을 다루지만, 2012년 Passbook 출시 당시에는 “패스”만 관리했음
  • Apple의 패스 문서에 따르면 패스는 이메일이나 웹으로 배포할 수 있는 자체 포함 ZIP 파일이며, 내부에는 JSON과 PNG가 들어감
  • .pkpass 파일의 핵심 구성은 단순함
    • pass.json: 이미지가 아닌 패스 내용을 설명함
    • manifest.json: 다른 파일들의 SHA-1 체크섬 목록
    • signature: manifest.json 내용에 대한 S/MIME 서명
    • 지원되는 이미지 파일들

Codabar 미지원이 첫 번째 장벽

  • Wallet 패스가 지원하는 바코드는 네 가지뿐임
    • QR code
    • PDF417
    • Aztec Code
    • Code 128
  • 도서관 카드는 도서관에서 널리 쓰이는 Codabar 형식이었음
  • 도서관 바코드 스캐너가 다른 선형 바코드를 지원할 가능성은 있지만, 모든 지점에서 확실히 동작한다고 볼 수 있는 형식은 Codabar뿐임
  • 결국 Wallet의 기본 바코드 기능 대신, 스캔 가능한 Codabar 이미지를 만들어 패스 안에 넣어야 했음

패스 서명 인증서 문제

  • Wallet 패스는 암호화 서명이 필요하며, Apple 인증기관 중 하나가 신뢰하는 키로 서명되어야 함
  • 항공권의 게이트 변경이나 좌석 변경처럼 벤더가 패스를 자동 업데이트하는 용도라면 서명이 어느 정도 타당함
  • Apple 개발자라면 패스 서명 키를 비교적 쉽게 받을 수 있지만, 이 작업을 위해 연 99달러를 지불할 생각은 없었음

기존 패스 생성 앱이 맞지 않았던 이유

  • 이미 Apple 개발자인 사람들이 만든 여러 패스 디자인 앱이 있었지만, 필요한 수준의 제어를 제공하지 않았음
  • 로고 크기 제어

    • Apple은 패스 왼쪽 위 로고에 최대 50 device-independent pixels 높이를 허용함
    • 텍스트 오른쪽에 붙는 정사각형 로고는 약 40px 높이가 가장 적당했음
    • 개발자는 logo.png, logo@2x.png, logo@3x.png를 각각 제공해야 하지만, 앱들은 보통 단일 로고 선택만 허용하고 스케일 제어를 주지 않음
  • 화면 밝기 동작

    • 일반 지원 바코드가 있는 패스를 열면 스캐너 대비를 위해 화면이 밝아짐
    • 실제로는 동작하지 않는 바코드를 표시하지 않으면서도 iOS가 화면을 밝게 만들려면 pass.json을 직접 다뤄야 했음
    • 무료로 내려받을 수 있는 앱들도 Wallet 저장 가능 패스 수를 제한하고 결제를 요구하는 경우가 있었음

오프라인 생성 앱에서 서명 키 찾기

  • 여러 무료 시작형 패스 생성 앱을 내려받아, 오프라인에서도 패스 생성이 되는지 확인함
  • 비행기 모드와 Wi-Fi 비활성화 상태에서 테스트한 결과, 최소 한 앱은 오프라인으로 패스를 만들 수 있었음
  • 해당 앱의 구체적 이름은 키가 폐기되는 것을 피하기 위해 밝히지 않음
  • 과정은 예상보다 단순했음
    • Apple silicon Mac에서 iOS 앱을 실행할 수 있으므로 Mac에 앱을 다운로드함
    • 래핑된 iOS 앱 번들 안에서 눈에 띄는 .p12 파일을 찾음
    • PKCS#12 파일에는 가져오기 비밀번호가 필요하므로, 메인 바이너리에 strings를 실행해 비밀번호로 보이는 문자열을 찾음
  • openssl pkcs12로 인증서 정보를 확인하자 issuer가 Apple Worldwide Developer Relations Certification Authority로 나타남
  • 인증서 체인도 필요하며, 중간 인증서는 앱 번들 안에 있을 가능성이 있고 X.509 확장 필드 안의 URL로도 다운로드할 수 있음

패스 레이아웃 구성

  • 패스 스타일boarding pass, coupon, event ticket, generic, store card 중 하나를 선택해야 함
  • 큰 가로 이미지를 넣으려면 strip 이미지를 지원하는 레이아웃이 필요함
    • 후보는 coupon, event ticket, store card
    • 물리적 도서관 카드와 가장 비슷한 형태로 store card를 선택함
  • pass.json에는 다음 값들이 들어감
    • passTypeIdentifier
    • teamIdentifier
    • formatVersion
    • serialNumber
    • organizationName
    • logoText
    • description
    • storeCard
    • backgroundColor
    • foregroundColor
    • sharingProhibited
  • passTypeIdentifierteamIdentifier는 Apple 인증서 subject의 UID, OU 필드와 각각 일치해야 함
  • 같은 passTypeIdentifier로 만든 각 패스에는 고유한 serialNumber가 필요함
  • 이미지는 용도별로 준비함
    • icon.png는 필수지만 패스 자체에는 표시되지 않음
    • logo.png는 왼쪽 위에 표시되는 로고임
    • 40×40 logo.png, 80×80 logo@2x.png, 120×120 logo@3x.png를 생성함
    • logo@3x.pngicon.png로 복사함
    • 사전 생성한 바코드는 strip.png에 넣음

Codabar 바코드 이미지 만들기

  • iOS는 strip.png를 기기 화면의 박스에 맞게 스케일하고 크롭하므로, strip.png는 해상도별로 3종을 만들 필요가 없었음
  • 두 도서관 카드는 시작·종료 심볼로 A와 D를 사용함
  • 시작·종료 심볼은 바코드 스캐너가 있으면 가장 쉽게 확인할 수 있고, 없으면 Wikipedia의 Codabar 인코딩 표와 눈으로 비교할 수 있음
  • 온라인에서 바로 쓸 수 있는 Codabar 생성기는 많지 않았지만, 형식 자체는 직접 구현하기 쉬웠음
  • 프로토타입에서는 Rust용 Barcoders 라이브러리로 SVG를 만들고, SVG를 조정한 뒤 PNG로 내보냄
  • 최종 레이아웃은 스캔 가능성과 iOS 축소 표시를 고려해 잡음
    • 바코드 높이는 시작·종료 심볼을 포함한 전체 심볼 수의 2배 단위로 설정함
    • 예를 들어 13자리 바코드 번호는 15개 심볼이므로 높이를 30단위로 만듦
    • 시작 전과 종료 후에 각각 15단위 quiet space를 둠
    • 바코드 위아래에는 각각 50단위 패딩을 둠
    • 각 단위는 8픽셀로 확대해 iOS가 항상 이미지를 축소하도록 함
    • 예시 15심볼 바코드에서는 최종 이미지 높이가 1040픽셀이고 바코드 자체 높이는 240픽셀임
  • 위 레이아웃의 Codabar BMP를 만들고 sips로 PNG로 변환하는 69줄 셸 스크립트를 작성함
  • 스크립트 출력물을 strip.png로 저장하면 패스에 들어갈 바코드 이미지가 준비됨

바코드 번호 표시와 화면 밝기 우회

  • 바코드 번호는 secondaryFields로 바코드 아래에 표시함
    • key: number
    • label: CARD NUMBER
    • value: 카드 번호
  • iOS는 바코드가 있는 패스를 선택하면 스캐너를 돕기 위해 화면을 밝게 만듦
  • 이미지로 넣은 Codabar만으로는 iOS가 바코드가 있다고 판단하지 않음
  • pass.json 최상위에 빈 바코드 항목을 지정해 이 동작을 우회함
    • message는 빈 문자열
    • formatPKBarcodeFormatCode128
    • messageEncodingiso-8859-1
  • 이 방식은 패스 하단에 바코드를 표시하지 않으면서도, iPhone이 바코드가 있는 패스처럼 화면을 밝게 만들었음

서명과 패키징

  • 모든 파일을 준비한 뒤 manifest.json을 생성해야 함
  • manifest.json은 파일명을 키로, SHA-1 체크섬을 값으로 갖는 객체임
  • sha1sum, jq를 조합해 PNG 파일들과 pass.json의 manifest를 생성함
  • manifest 서명은 openssl smime 명령으로 수행함
    • signer 인증서
    • private key
    • Apple WWDR 중간 인증서
    • 입력 manifest.json
    • 출력 signature
  • openssl smime-attime 옵션으로 원하는 서명 시각을 지정할 수 있음
    • 옵션 값은 UNIX epoch임
    • Apple에서 받았거나 찾아낸 인증서가 만료됐더라도 해당 시각으로 서명할 수 있음
  • 마지막으로 다음 파일들을 ZIP으로 묶어 .pkpass를 만듦
    • PNG 이미지들
    • pass.json
    • manifest.json
    • signature

테스트 결과와 아쉬운 점

  • macOS에는 패스 미리보기 도구가 있어 패스가 유효하고 대체로 제대로 보이는지 확인할 수 있음
  • 유효하지 않으면 Console.app에서 오류를 찾을 수 있음
  • 미리보기 도구는 100% 정확하지 않지만, iCloud를 통해 iPhone으로 보내는 버튼이 있음
  • 완성된 패스는 실제 도서관에서는 아직 테스트하지 않았음
  • 별도 바코드 스캐너는 휴대폰 화면의 바코드를 실제 플라스틱 카드와 마찬가지로 읽을 수 있었음
    • 다만 화면 밝기를 최대로 올렸을 때 잘 읽힘
    • 빈 바코드 우회로 올라가는 밝기보다 더 밝게 설정했음
  • 패스 명세가 10년 동안 거의 변하지 않은 점은 좋지만, PNG와 JSON으로 된 무해한 패스를 서명하려고 연 99달러 Apple Developer 멤버십이 필요한 점은 아쉬움
  • 일부 패스 기능에는 서명이 필요하다고 볼 수 있지만, 이 작업에서 한 일에는 서명이 필요하지 않아야 한다고 봄
  • Apple이 Wallet에 Codabar 지원을 추가하면, 도서관 시스템 전체의 스캐너가 Code 128을 지원하는지 감사하지 않고도 디지털 도서관 카드를 지원할 수 있음

댓글과 토론

Hacker News 의견들
  • 다음엔 ORCA 카드도 다뤄주면 좋겠음. Seattle은 미국의 기술 중심지 중 하나인데, 지하철/대중교통 자체도 별로일 뿐 아니라 내가 써본 거의 모든 주요 도시의 대중교통보다 기술적으로도 뒤처져 있음
    유럽 아무 도시나, CDMX, Denver와 비교해도 Seattle보다 훨씬 앞서 있고, 개인적으로는 Denver가 제일 좋았던 것 같음
    ORCA를 운영하는 기관 중 하나가 Android 앱에 NFC 지원을 추가하겠다는 블로그 글을 올린 적도 있었는데, 그 글은 이제 사라졌고 몇 년이 지난 지금도 그 기능은 없음

    • Google Wallet이 곧 이를 지원할 예정이라고 며칠 전 올라옴: https://blog.google/products/google-pay/commute-around-the-w...
    • 우리 지역 교통 시스템은 최근 탭 결제를 추가해서 어떤 신용카드든, Apple Pay/Google Pay든 사용할 수 있게 됨
      같은 카드만 쓰면 환승, 며칠짜리 패스 같은 것도 매끄럽게 처리하고, 특정 기간 안에 일정 횟수 이상 타면 더 청구하지 않는 식으로 올바른 요금이 자동 적용됨
    • 대부분의 대중교통 시스템이 무료 승차로 바꾸면 잃는 재원은 일부에 불과한데, 그에 비해 요금 징수 시스템을 유지하는 비용이 얼마나 드는지 궁금함
    • 관련해서, 루팅한 Android로 NFC 카드 복제를 할 방법이 있지 않나?
      사무실 문이 NFC 카드로 열려서 iOS에서 가능한지 찾아봤는데, 기억상 Apple은 일반 PassKit보다 NFC 하드웨어를 더 엄격히 통제해서 일반 앱으로는 어렵다고 봄
    • https://info.myorca.com/news/can-i-use-my-phone-to-pay-for-a...
      ORCA에 2023년 안에 탭 결제가 온다고 했으니, 사실이 되려면 이제 두 달쯤 남았음
      Google이 Google Wallet에서 ORCA를 곧 지원한다고 글을 올렸으니 낙관적으로 보고 있음
  • Android 사용자 중 Google Wallet이 Apple Passbook처럼 아무 바코드나 넣을 수 없어서 싫다면, F-Droid에 Loyalty Card Keychain이라는 훌륭한 앱이 있음: https://f-droid.org/en/packages/protect.card_locker/
    숫자를 직접 입력하거나 기존 바코드를 스캔하고, Codabar를 포함한 여러 바코드 형식 중 하나를 골라 저장할 수 있음. 앱 메인 화면에서 항목을 누르면 생성된 바코드를 보여주고 화면 밝기도 올려줌
    바코드를 표시하는 것 말고는 별일을 안 해서인지 앱도 아주 빨리 열림. 기능은 하나뿐이지만 내가 가장 좋아하는 앱일지도 모름
    요즘은 Catima를 쓰라고 하는 듯한데, 잠깐 써보니 비슷하게 단순하고 같은 코드베이스 기반으로 보임

  • 패스가 암호학적으로 서명되어야 하고 Apple 인증기관이 아는 키로 서명되어야 한다는 게 왜 말이 되는지 모르겠음. 이런 업데이트 용도라면 이미 잘 지원되는 HTTPS가 있음
    Apple은 패스가 업데이트 시점뿐 아니라 휴대폰에서 오프라인으로도 검증되길 원하는 걸지도 모르지만, 그래도 이상함. 악의적인 행위자는 패스를 업데이트하는 대신 아예 바꿔치기할 수 있음

    • 최초 패스가 업데이트를 허용할 공개키를 지정하게 하는 것만으로 충분함
      Apple 개발자 계정과 연결한다고 업데이트 문제에 도움이 되는지는 전혀 모르겠음
      Apple이 승인한 주체여야 한다는 요구사항의 이유로 떠올릴 수 있는 건 가짜 티켓 판매 정도임. “콘서트 X 티켓”이라는 pkpass 파일이 진짜인지 아닌지는 그 요구사항이 있든 없든 알 방법이 없음
      사기 신고가 들어오면 해당 개발자 계정을 취소해서 대응하려는 시도일 수는 있지만, 이것도 해결책 같지는 않음. 개발자 계정 비용은 적발되기 전 사기로 벌 수 있는 돈보다 훨씬 쌀 가능성이 큼
  • 그냥 도서관 카드 번호가 들어 있는 바코드 PNG를 내게 이메일로 보내두고, 키오스크 앞에서 Photos나 Gmail 앱으로 열어 씀

    • Wallet 패스는 해당 패스가 “관련 있음”으로 뜰 위치와 시간 범위를 정의할 수 있음
      MakePass 앱으로 StarBucks 멤버십 코드를 넣은 패스를 만들었고, 자주 가는 매장 근처에 가면 휴대폰이 잠금 화면에서 StarBucks 패스를 자동 제안함
      이벤트 티켓도 마찬가지로 위치와 시간 범위를 지정하면 현장에 도착했을 때 패스를 찾아 헤맬 필요 없이 자동으로 제안됨. 이벤트가 끝나면 더 이상 제안하지 않고 “Expired Passes” 섹션으로 옮겨 메인 화면을 어지럽히지 않음
      MakePass: https://pvieito.com
    • 글을 훑어보면서 왜 그냥 사진을 찍지 않았는지 찾고 있었음
  • 좋은 블로그 글이었지만 마지막이 “아직 실제 도서관에서 이 패스를 테스트해보지는 않았다”로 끝남
    재미로 한 프로젝트라는 건 알겠지만, 결과물을 공유하기 전에 최종 해법을 테스트하는 데 필요한 10분을 왜 쓰지 않았는지 모르겠음

    • 그 부분 때문에 글 전체의 힘이 빠졌음. “달리기 중 산소 손실에 관한 훌륭한 과학 이론이 있습니다. 인정하자면 아직 조깅을 시작해보진 않았지만…” 같은 느낌임
    • 결국 스캔될 바코드를 나타내는 픽셀 묶음일 뿐임. 제대로 표시되기만 하면 뭐가 잘못될 수 있겠나?
    • 도서관이 토요일에는 안 열 수도 있음
  • 바코드를 만들 때는 개인적으로 PostScript로 된 바코드 작성기를 선호함
    https://bwipp.terryburton.co.uk/

  • 이 문제는 도서관 카드 사진을 찍어서 해결했음. 책을 대출할 때 사진을 열고 스캐너에 대면 됨

    • 바코드 이미지를 위치 기반 알림에 넣어뒀더니, 동네 도서관에 가면 알림으로 뜸
    • 정적인 바코드라면 사진만 있으면 충분함. Wallet에 넣는 이점은 크지 않음
      휴대폰의 사진 앨범에 모든 신분증을 저장해둠
  • Android에서 비슷한 걸 하고 싶다면 Google Play와 F-Droid에 있는 Catima가 있음. 여러 종류의 바코드를 지원함
    https://catima.app/

    • Google Pay나 Samsung Pay를 써도 됨
  • 저렴한 레이저식 1차원 바코드 스캐너는 화면에서 바코드를 읽지 못함. eInk는 될지도 모름
    자주 가는 슈퍼마켓의 멤버십 카드가 바코드 기반이라 불편한데, 다행히 바코드 리더는 키보드를 흉내 내므로 그냥 키보드로 코드를 입력할 수 있음

    • 내가 이해하기로는 구형 시스템이 디지털 이미지 센서로 스캔하는 게 아니라, 회전하는 레이저 빔과 단순한 포토다이오드로 흰색/검은색 바코드 영역에서 반사되는 밝기 변화를 읽기 때문임
      카메라 기반 스캐너는 바코드 조명이 주변광이든 자체 LED든 백라이트 화면이든 신경 쓰지 않지만, 레이저 기반 시스템은 자기 빛의 반사에 의존하므로 능동 백라이트 화면에서는 전혀 작동하지 않음
      e-ink나 수동 LCD 디스플레이에서는 작동하는지 궁금함
    • Bluetooth MCU와 E-ink 디스플레이로 저렴한 바코드 표시기를 만들었고, 1차원 전용 바코드 스캐너에서는 모두 잘 동작함
    • Costco의 휴대용 스캐너가 회원카드에 아주 좁고 작게 인쇄된 회원 바코드를 얼마나 빠르고 쉽게 읽는지 볼 때마다 감탄함
      겉보기엔 단순한 빨간 선 2D 타입 같아 보이는데, 역시 하드웨어가 중요한 듯함
  • KCLS 계정 번호는 그냥 외웠음. 외우는 데 30초 정도면 충분할 텐데, 사람마다 다르겠지만 이 방법이 더 빠를 거라고 봄
    이후에는 바코드를 스캔하는 대신 계정 번호를 입력하면 되고, 휴대폰을 꺼내 준비하는 시간보다 덜 걸릴 가능성이 큼
    SPL도 같은 방식인지는 모르겠음. Seattle에 살지는 않아서 확인은 못 함

    • 외우기 싫다면 원하는 비밀번호 관리자에 저장해두면 됨