Skip to content

Commit

Permalink
Increase certificate flexibility
Browse files Browse the repository at this point in the history
  • Loading branch information
adventureisyou committed Jun 11, 2024
1 parent ca4efc5 commit 7660da2
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 121 deletions.
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

0 comments on commit 7660da2

Please sign in to comment.