Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase certificate flexibility #14

Merged
merged 1 commit into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 42 additions & 74 deletions cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// openssl x509 -inform der -in AppleRootCA-G3.cer -out apple_root.pem
const rootPEM = `
const defaultRootPEM = `
-----BEGIN CERTIFICATE-----
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
Expand All @@ -30,106 +30,74 @@ at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
`

type Cert struct {
rootCertPool *x509.CertPool
}

func (c *Cert) extractCertByIndex(tokenStr string, index int) ([]byte, error) {
if index > 2 {
return nil, errors.New("invalid index")
func newCert(rootCertPool *x509.CertPool) *Cert {
if rootCertPool == nil {
rootCertPool = x509.NewCertPool()
rootCertPool.AppendCertsFromPEM([]byte(defaultRootPEM))
}
return &Cert{rootCertPool: rootCertPool}
}

tokenArr := strings.Split(tokenStr, ".")
headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
func (c *Cert) parseCert(certStr string) (*x509.Certificate, error) {
certByte, err := base64.StdEncoding.DecodeString(certStr)
if err != nil {
return nil, err
}
return x509.ParseCertificate(certByte)
}

type Header struct {
func (c *Cert) extractPublicKeyFromToken(token string) (*ecdsa.PublicKey, error) {
headerStr, _, _ := strings.Cut(token, ".")
headerByte, err := base64.RawStdEncoding.DecodeString(headerStr)
if err != nil {
return nil, err
}

var header struct {
Alg string `json:"alg"`
X5c []string `json:"x5c"`
}
var header Header
err = json.Unmarshal(headerByte, &header)
if err != nil {
return nil, err
}

certByte, err := base64.StdEncoding.DecodeString(header.X5c[index])
if err != nil {
return nil, err
if len(header.X5c) == 0 {
return nil, errors.New("appstore found no certificates in x5c header field")
}

return certByte, nil
}
opts := x509.VerifyOptions{Roots: c.rootCertPool}

func (c *Cert) verifyCert(rootCert, intermediaCert, leafCert *x509.Certificate) error {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(rootPEM))
if !ok {
return errors.New("failed to parse root certificate")
}

intermedia := x509.NewCertPool()
intermedia.AddCert(intermediaCert)

opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermedia,
}
_, err := rootCert.Verify(opts)
leafCert, err := c.parseCert(header.X5c[0])
if err != nil {
return err
return nil, fmt.Errorf("appstore failed to parse leaf certificate: %w", err)
}
header.X5c = header.X5c[1:]

_, err = leafCert.Verify(opts)
if err != nil {
return err
pk, ok := leafCert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
}

// TODO: maybe we need the chains info later
//for _, ch := range chains {
// for _, c := range ch {
// fmt.Printf("%+v, %s, %+v \n", c.AuthorityKeyId, c.Subject.Organization, c.ExtKeyUsage)
// }
//}
// Build intermediate cert pool if there is more than 1 certificate in the header
if len(header.X5c) > 0 {
opts.Intermediates = x509.NewCertPool()

return nil
}

func (c *Cert) extractPublicKeyFromToken(token string) (*ecdsa.PublicKey, error) {
rootCertBytes, err := c.extractCertByIndex(token, 2)
if err != nil {
return nil, err
}
rootCert, err := x509.ParseCertificate(rootCertBytes)
if err != nil {
return nil, fmt.Errorf("appstore failed to parse root certificate")
}

intermediaCertBytes, err := c.extractCertByIndex(token, 1)
if err != nil {
return nil, err
}
intermediaCert, err := x509.ParseCertificate(intermediaCertBytes)
if err != nil {
return nil, fmt.Errorf("appstore failed to parse intermediate certificate")
for i, certStr := range header.X5c {
cert, err := c.parseCert(certStr)
if err != nil {
return nil, fmt.Errorf("appstore failed to parse intermediate certificate %d: %w", i, err)
}
opts.Intermediates.AddCert(cert)
}
}

leafCertBytes, err := c.extractCertByIndex(token, 0)
if err != nil {
return nil, err
}
leafCert, err := x509.ParseCertificate(leafCertBytes)
_, err = leafCert.Verify(opts)
if err != nil {
return nil, fmt.Errorf("appstore failed to parse leaf certificate")
}
if err = c.verifyCert(rootCert, intermediaCert, leafCert); err != nil {
return nil, err
return nil, fmt.Errorf("appstore failed to verify leaf certificate: %w", err)
}

switch pk := leafCert.PublicKey.(type) {
case *ecdsa.PublicKey:
return pk, nil
default:
return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
}
return pk, nil
}
59 changes: 12 additions & 47 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package appstore
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -36,13 +35,14 @@ const (
)

type StoreConfig struct {
KeyContent []byte // Loads a .p8 certificate
KeyID string // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
BundleID string // Your app’s bundle ID
Issuer string // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")
Sandbox bool // default is Production
TokenIssuedAtFunc func() int64 // The token’s creation time func. Default is current timestamp.
TokenExpiredAtFunc func() int64 // The token’s expiration time func. Default is one hour later.
KeyContent []byte // Loads a .p8 certificate
KeyID string // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
BundleID string // Your app’s bundle ID
Issuer string // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")
Sandbox bool // default is Production
TokenIssuedAtFunc func() int64 // The token’s creation time func. Default is current timestamp.
TokenExpiredAtFunc func() int64 // The token’s expiration time func. Default is one hour later.
TrustedCertPool *x509.CertPool // The pool of trusted root certificates. Default is a pool containing only Apple Root CA - G3.
}

type StoreClient struct {
Expand All @@ -63,7 +63,7 @@ func NewStoreClient(config *StoreConfig) *StoreClient {

client := &StoreClient{
Token: token,
cert: &Cert{},
cert: newCert(config.TrustedCertPool),
httpCli: &http.Client{
Timeout: 30 * time.Second,
},
Expand All @@ -83,7 +83,7 @@ func NewStoreClientWithHTTPClient(config *StoreConfig, httpClient HTTPClient) *S

client := &StoreClient{
Token: token,
cert: &Cert{},
cert: newCert(config.TrustedCertPool),
httpCli: httpClient,
hostUrl: hostUrl,
}
Expand Down Expand Up @@ -454,43 +454,8 @@ func (c *StoreClient) ParseJWSEncodeString(jwsEncode string) (interface{}, error
}

func (c *StoreClient) parseJWS(jwsEncode string, claims jwt.Claims) error {
rootCertBytes, err := c.cert.extractCertByIndex(jwsEncode, 2)
if err != nil {
return err
}
rootCert, err := x509.ParseCertificate(rootCertBytes)
if err != nil {
return fmt.Errorf("appstore failed to parse root certificate")
}

intermediaCertBytes, err := c.cert.extractCertByIndex(jwsEncode, 1)
if err != nil {
return err
}
intermediaCert, err := x509.ParseCertificate(intermediaCertBytes)
if err != nil {
return fmt.Errorf("appstore failed to parse intermediate certificate")
}

leafCertBytes, err := c.cert.extractCertByIndex(jwsEncode, 0)
if err != nil {
return err
}
leafCert, err := x509.ParseCertificate(leafCertBytes)
if err != nil {
return fmt.Errorf("appstore failed to parse leaf certificate")
}
if err = c.cert.verifyCert(rootCert, intermediaCert, leafCert); err != nil {
return err
}

pk, ok := leafCert.PublicKey.(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("appstore public key must be of type ecdsa.PublicKey")
}

_, err = jwt.ParseWithClaims(jwsEncode, claims, func(token *jwt.Token) (interface{}, error) {
return pk, nil
_, err := jwt.ParseWithClaims(jwsEncode, claims, func(token *jwt.Token) (interface{}, error) {
return c.cert.extractPublicKeyFromToken(jwsEncode)
})
return err
}
Expand Down
Loading