diff --git a/cert.go b/cert.go index dd052c5..8ead34b 100644 --- a/cert.go +++ b/cert.go @@ -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 @@ -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 } diff --git a/store.go b/store.go index b4430fd..6a892ed 100644 --- a/store.go +++ b/store.go @@ -3,7 +3,6 @@ package appstore import ( "bytes" "context" - "crypto/ecdsa" "crypto/x509" "encoding/base64" "encoding/json" @@ -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 { @@ -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, }, @@ -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, } @@ -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 }