Coder Social home page Coder Social logo

passkit's Introduction

PassKit

This is a library for generating Apple Wallet PKPasses.

How to use

This library was heavily inspired by drallgood's jpasskit library which was written in Java, so the objects and functions are very similar to the ones available on jpasskit.

Define a pass

To define a pass you use the Pass struct, which represents the pass.json file. This struct is modeled as closely as possible to the json file, so adding data is straightforward:

c := passkit.NewBoardingPass(passkit.TransitTypeAir)

// Utility functions for adding fields to a pass
c.AddHeaderField(passkit.Field{
    Key: "your_head_key",
    Label: "your_displayable_head_label",
    Value:"value",
})
c.AddPrimaryFields(passkit.Field{
    Key: "your_prim_key",
    Label: "your_displayable_prim_label",
    Value:"value",
})
c.AddSecondaryFields(passkit.Field{
    Key: "your_sec_key",
    Label: "your_displayable_sec_label",
    Value:"value",
})
c.AddAuxiliaryFields(passkit.Field{
    Key: "your_aux_key",
    Label: "your_displayable_aux_label",
    Value:"value",
})
c.AddBackFields(passkit.Field{
    Key: "your_back_key",
    Label: "your_displayable_back_label",
    Value:"value",
})

boarding := time.Date(2023, 1, 1, 23, 50, 00, 00, time.UTC)

pass := passkit.Pass{
    FormatVersion:       1,
    TeamIdentifier:      "TEAMID",
    PassTypeIdentifier:  "pass.type.id",
    AuthenticationToken: "123141lkjdasj12314",
    OrganizationName:    "Your Organization",
    SerialNumber:        "1234",
    Description:         "test",
    BoardingPass:        c,
    Semantics: []passkit.SemanticTag{
        {
            AirlineCode:            "AA1234",
            TransitProvider:        "American Airlines",
            DepartureAirportCode:   "LAX",
            DepartureAirportName:   "Los Angeles International Airport",
            DepartureGate:          "28",
            DepartureTerminal:      "2",
            DestinationAirportCode: "LGA",
            DestinationAirportName: "LaGuardia Airport",
            DestinationGate:        "12",
            DestinationTerminal:    "1",
            TransitStatus:          "On Time",
            OriginalBoardingDate:   &boarding,
        },
    },
    Barcodes: []passkit.Barcode{
        {
            Format:          passkit.BarcodeFormatPDF417,
            Message:         "1312312312312312312312312312",
            MessageEncoding: "utf-8",
        },
    },
}

Templates

Passes contain additional data that has to be included in the final, signed pass, like images (icons, logos, background images) and translations. Usually, passes of the same type share images and translations. This shared information is what I call a PassTemplate, so that every generated pass of a type have the same images and whatnot.

The template contents are defined as described by the apple wallet developer documentation.

To create the pass structure you need a PassTemplate instance, either with streams (using InMemoryPassTemplate) or with files (using FolderPassTemplate).

Using files

To create the pass bundle create an instance of FolderPassTemplate using the absolute file path of the folder that contain the pass images and translations:

template := passkit.NewFolderPassTemplate("/home/user/pass")

All the files in the folder will be added to the bundled pass exactly as provided, which means they must align to the naming and location conventions described in the apple wallet developer documentation.

Using streams (In Memory)

The second approach is more flexible, having the option of loading files from data streams, or downloaded from a public URL:

template := passkit.NewInMemoryPassTemplate()

template.AddFileBytes(passkit.BundleThumbnail, bytes)
template.AddFileBytesLocalized(passkit.BundleIcon, "en", bytes)
err := template.AddFileFromURL(passkit.BundleLogo, "https://example.com/file.png")
err := template.AddFileFromURLLocalized(passkit.BundleLogo, "en", "https://example.com/file.png")
err := template.AddAllFiles("/home/user/pass")

Note: There are no checks that the contents of a provided file are valid. If a PDF file is provided, but is named icon.png, when viewing the pass on a device it probably won't work. The InMemoryPassTemplate doesn't provide any authentication for the downloads, so the URLs used must be public for the download to work as expected. The downloads use a default http.Client without any SSL configuration, so if the download is from an HTTPS site the certificate must be valid in the system where the library is running, or the download will fail with a certificate error.

Signing and zipping a pass

As all passes need to be signed when bundled we need to use a Signer instance. There are two types of signer:

  • FileBasedSigner: creates a temp folder to store the signed pass file contents.
  • MemoryBasedSigner: keeps the signed pass file contents in memory.

To use any of the Signer instances you need to provide an instance of SigningInformation. SigningInformation defines the certificates used to generate the signature file bundled with every pass. There are two ways to obtain an instance. Either by
reading the certificates from the filesystem, or from already loaded bytes in memory:

// Using the certificate files from your filesystem
signInfo, err := passkit.LoadSigningInformationFromFiles("/home/user/pass_cert.p12", "pass_cert_password", "/home/user/AppleWWDRCA.cer")
// Using the bytes from the certificates, loaded from a database or vault, for example.
signInfo, err := passkit.LoadSigningInformationFromBytes(passCertBytes, "pass_cert_password", wwdrcaBytes)

Important: The provided certificates must be encoded in DER form. If the files are encoded as PEM the signature generation will fail. Errors will also be returned if the certificates are invalid (expired, not x509 certs, etc).

Bundling the pass

Finally, to create the signed pass bundle you use the Pass, Signer, SigningInformation, and PassTemplate instances created previously, like so:

signer := passkit.NewMemoryBasedSigner()
signInfo, err := passkit.LoadSigningInformationFromFiles("/home/user/pass_cert.p12", "pass_cert_password", "/home/user/AppleWWDRCA.cer")
if err != nil {
    panic(err)
}

z, err := signer.CreateSignedAndZippedPassArchive(&pass, template, signInfo)
if err != nil {
    panic(err)
}

err = os.WriteFile("/home/user/pass.pkpass", z, 0644)
if err != nil {
    panic(err)
}

After this step the pass bundle is ready to be distributed as you see fit.

Contributing

Right now I'm not really working on a project where this library is being actively used, so any bugs are hard for me to detect and fix. That's why this project is open to contributions. Just make a Pull Request with fixes or any other feature request, and I will probably accept them and merge them (I will at least look at your code before approving anything).

passkit's People

Contributors

alvinbaena avatar edouard-claude avatar myypo avatar nathan-tw avatar nimajalali avatar rwngallego avatar ryan-lang avatar sercand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

passkit's Issues

x509: malformed certificate

With an exported WWDR certificate (https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer):

        signInfo, err := passkit.LoadSigningInformationFromFiles(
		"apple.pass/Certificates.p12",
		"xxxxxxxx",
		"apple.pass/wwdr.pem",
	)
	if err != nil {
		panic(err)
	}
2023/11/01 23:56:09 http: panic serving 127.0.0.1:59909: x509: malformed certificate
goroutine 65 [running]:
net/http.(*conn).serve.func1()
        /usr/local/go/src/net/http/server.go:1868 +0xb9
panic({0x1664420?, 0xc000486310?})
        /usr/local/go/src/runtime/panic.go:920 +0x270
main.SignAndStore({0x1, {0x17655d1, 0x4}, {0x1774ca0, 0x19}, {0x0, 0x0}, {0x176f8f4, 0x12}, {0x176f906, ...}, ...}, ...)
        /Users/theobouwman/dev/projects/momo/momo-wallet-pass-api/pass.go:68 +0x129
main.(*App).Handler(0x18617e0?, {0x18603f0?, 0xc0003d22a0}, 0xc000163100)
        /Users/theobouwman/dev/projects/momo/momo-wallet-pass-api/handler.go:40 +0x1e5
net/http.HandlerFunc.ServeHTTP(0xc000163000?, {0x18603f0?, 0xc0003d22a0?}, 0x2f8ea50?)
        /usr/local/go/src/net/http/server.go:2136 +0x29
github.com/gorilla/mux.(*Router).ServeHTTP(0xc0000ec000, {0x18603f0, 0xc0003d22a0}, 0xc000162f00)
        /Users/theobouwman/go/pkg/mod/github.com/gorilla/[email protected]/mux.go:210 +0x1c5
net/http.serverHandler.ServeHTTP({0xc0003fa930?}, {0x18603f0?, 0xc0003d22a0?}, 0x6?)
        /usr/local/go/src/net/http/server.go:2938 +0x8e
net/http.(*conn).serve(0xc0001c9320, {0x18617e0, 0xc0003fa3c0})
        /usr/local/go/src/net/http/server.go:2009 +0x5f4
created by net/http.(*Server).Serve in goroutine 45
        /usr/local/go/src/net/http/server.go:3086 +0x5cb

Failed on loading certificates "asn1: structure error: tags don't match"

Got an error

asn1: structure error: tags don't match (16 vs {class:0 tag:13 length:45 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} certificate @2

on calling passkit.LoadSigningInformationFromFiles(). Certificate files are valid, I use them for building passes in PHP.

What could that be?

License

Thanks for the great package! Noticed there was no License attached to it.

Can one be added? MIT?

Pass could not be opened

Screenshot 2023-11-03 at 12 23 51
c := passkit.NewBoardingPass(passkit.TransitTypeAir)
	field := passkit.Field{
		Key:   "name",
		Label: "Name",
		Value: passInfo.name,
	}

	c.AddHeaderField(field)
	c.AddPrimaryFields(field)
	c.AddSecondaryFields(field)
	c.AddAuxiliaryFields(field)
	c.AddBackFields(field)

	pass := passkit.Pass{
		FormatVersion:      1,
		TeamIdentifier:     "xxxx",
		PassTypeIdentifier: "xxxx",
		OrganizationName:   "xxxx",
		SerialNumber:       "1234",
		Description:        "Card",
		BoardingPass:       c,
		Barcodes: []passkit.Barcode{
			{
				Format:          passkit.BarcodeFormatQR,
				Message:         "xxx",
				MessageEncoding: "utf-8",
			},
		},
	}

template := passkit.NewInMemoryPassTemplate()

iconUrl, _ := url.ParseRequestURI("xxxx")
template.AddFileFromURL(passkit.BundleIcon, *iconUrl)
template.AddFileFromURL(passkit.BundleThumbnail, *iconUrl)

signer := passkit.NewMemoryBasedSigner()
	signInfo, err := passkit.LoadSigningInformationFromFiles(
		"apple.pass/Certificates.p12",
		"xxxx",
		"apple.pass/wwdr.cer",
	)
	if err != nil {
		panic(err)
	}

	z, err := signer.CreateSignedAndZippedPassArchive(&pass, template, signInfo)
	if err != nil {
		panic(err)
	}

	err = os.WriteFile("pass.pkpass", z, 0644)
	if err != nil {
		panic(err)
	}

The pass.pkpass file is created.

unzip pass.pkpass 
Archive:  pass.pkpass
  inflating: signature               
  inflating: icon.png                
  inflating: thumbnail.png           
  inflating: pass.json               
  inflating: manifest.json           

And all files are included.

Failed to add pass error

Upon generating the .pkpass from the file template, the iOS Simulator throws the following error instead of loading the pass:

Failed to add pass: 'file:///Users/dev/Documents/projects/pass/pass.pkpass' Error Domain=PKPassKitErrorDomain Code=1 "No data supplied" UserInfo={NSLocalizedDescription=No data supplied}.

I've used the code from the README to generate the .pkpass:

package main

import (
	"archive/zip"
	"bytes"
	"fmt"
	"io/ioutil"
	"os"

	"github.com/alvinbaena/passkit"
	"github.com/google/uuid"
)

func main() {

	c := passkit.NewGenericPass()

	field := passkit.Field{
		Key:   "key",
		Label: "label",
		Value: "value",
	}

	serial := uuid.NewString()

	fmt.Printf("pass id: %s\n", serial)

	c.AddHeaderField(field)
	c.AddPrimaryFields(field)
	c.AddSecondaryFields(field)
	c.AddAuxiliaryFields(field)
	c.AddBackFields(field)

	pass := passkit.Pass{
		FormatVersion:      1,
		TeamIdentifier:     "==TEAM-ID==",
		PassTypeIdentifier: "==Pass-Identifier==",
		OrganizationName:   "orgName",
		SerialNumber:       serial,
		Description:        "test",
		Generic:            c,
		Barcodes: []passkit.Barcode{
			{
				Format:          passkit.BarcodeFormatQR,
				Message:         "1312312312312312312312312312",
				MessageEncoding: "utf-8",
			},
		},
	}

	template := passkit.NewFolderPassTemplate("passes/mypass.pass")

	signInfo, err := passkit.LoadSigningInformationFromFiles("certs/passmaker.p12", "test", "certs/wwdr.cer")

	signer := passkit.NewMemoryBasedSigner()

	if err != nil {
		panic(err)
	}

	fmt.Print("Pass signed\n")

	z, err := signer.CreateSignedAndZippedPassArchive(&pass, template, signInfo)

	if err != nil {
		fmt.Println("Error creating archive:", err)
		return
	}

	fmt.Print("Archive Created\n")

	err = os.WriteFile("pass.pkpass", z, 0644)

	if err != nil {
		panic(err)
	}

	fmt.Print("Finished creating pass\n")
}

No certificate errors are being thrown, so I think it's safe to say it's not related to certificates/signing.

I'm using the the Generic.pass from Apple as a template, and replaced the values of the JSON with the above fields. I also printed the contents of the pass.json after it was loaded into memory and all the fields were filled out as they were supposed to be, but I still get this error.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.