How to verify JWS transaction of app store server api in Go

5k Views Asked by At

Recently, one new API Look Up Order ID was added into app store server API. And the JWSTransaction of this API response signed by the App Store, in JSON Web Signature format. We want to verify it with go.

What we have tried

  1. The jwt-go is used and we try to extract public key from pem file per this question. Also per this link, the response should be decoded by extracting a public key from private key
type JWSTransaction struct {
    BundleID             string `json:"bundleId"`
    InAppOwnershipType   string `json:"inAppOwnershipType"`
    TransactionID        string `json:"transactionId"`
    ProductID            string `json:"productId"`
    PurchaseDate         int64  `json:"purchaseDate"`
    Type                 string `json:"type"`
    OriginalPurchaseDate int64  `json:"originalPurchaseDate"`
}

func (ac *JWSTransaction) Valid() error {

    return nil
}

func (a *AppStore) readPrivateKeyFromFile(keyFile string) (*ecdsa.PrivateKey, error) {
    bytes, err := ioutil.ReadFile(keyFile)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(bytes)
    if block == nil {
        return nil, errors.New("appstore private key must be a valid .p8 PEM file")
    }

    key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    switch pk := key.(type) {
    case *ecdsa.PrivateKey:
        return pk, nil
    default:
        return nil, errors.New("appstore private key must be of type ecdsa.PrivateKey")
    }
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    privateKey, err := a.readPrivateKeyFromFile()
    if err != nil {
        return nil, err
    }
    
    publicKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
    if err != nil {
        return nil, err
    }
    fmt.Println(publicKey)

    tran := JWSTransaction{}

    token, err := jwt.ParseWithClaims(tokenStr, &tran, func(token *jwt.Token) (interface{}, error) {
        fmt.Println(token.Claims)
        fmt.Println(token.Method.Alg())

        return publicKey, nil
    })
    if err != nil {
        fmt.Println(err)
    }

However, the error key is of invalid type comes up from jwt.ParseWithClaims.

  1. Another way to verify it through the jwt-go and jwk packages per this link
    token, err := jwt.ParseWithClaims(tokenStr, &tran, func(token *jwt.Token) (interface{}, error) {
        fmt.Println(token.Claims)
        fmt.Println(token.Method.Alg())

        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, errors.New("failed to find kid from headers")
        }
        key, found := keySet.LookupKeyID(kid)
        if !found {
            return nil, errors.New("failed to find kid from key set")
        }
        
        return publicKey, nil
    })

However, we failed to find the public key URL in app store server API doc. Also, there is no kid from the headers of JWSTransaction.

We want to know how to verify JWS transaction of app store server api in Go? Is there anything am I missing?

3

There are 3 best solutions below

15
zangw On

Thanks Paulw11 , Per doc

The "x5c" (X.509 certificate chain) Header Parameter contains the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS.

func (a *AppStore) extractPublicKeyFromToken(tokenStr string) (*ecdsa.PublicKey, error) {
    tokenArr := strings.Split(tokenStr, ".")
    headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
    if err != nil {
        return nil, err
    }

    type 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[0])
    if err != nil {
        return nil, err
    }

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return nil, err
    }

    switch pk := cert.PublicKey.(type) {
    case *ecdsa.PublicKey:
        return pk, nil
    default:
        return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
    }
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}
    _, err := jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}

Update 01/26/2022

In order to verify the root cert of x5c headers with apple root key from site

Refer to this loop. Here are sample codes

// Per doc: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6
func (a *AppStore) extractPublicKeyFromToken(tokenStr string) (*ecdsa.PublicKey, error) {
    certStr, err := a.extractHeaderByIndex(tokenStr, 0)
    if err != nil {
        return nil, err
    }

    cert, err := x509.ParseCertificate(certStr)
    if err != nil {
        return nil, err
    }

    switch pk := cert.PublicKey.(type) {
    case *ecdsa.PublicKey:
        return pk, nil
    default:
        return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
    }
}

func (a *AppStore) extractHeaderByIndex(tokenStr string, index int) ([]byte, error) {
    if index > 2 {
        return nil, errors.New("invalid index")
    }

    tokenArr := strings.Split(tokenStr, ".")
    headerByte, err := base64.RawStdEncoding.DecodeString(tokenArr[0])
    if err != nil {
        return nil, err
    }

    type 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
    }

    return certByte, nil
}

// rootPEM is from `openssl x509 -inform der -in AppleRootCA-G3.cer -out apple_root.pem`
const rootPEM = `
-----BEGIN CERTIFICATE-----
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
....
-----END CERTIFICATE-----
`

func (a *AppStore) verifyCert(certByte []byte) error {
    roots := x509.NewCertPool()
    ok := roots.AppendCertsFromPEM([]byte(rootPEM))
    if !ok {
        return errors.New("failed to parse root certificate")
    }

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return err
    }

    opts := x509.VerifyOptions{
        Roots: roots,
    }

    if _, err := cert.Verify(opts); err != nil {
        return err
    }

    return nil
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}

    rootCertStr, err := a.extractHeaderByIndex(tokenStr, 2)
    if err != nil {
        return nil, err
    }
    if err = a.verifyCert(rootCertStr); err != nil {
        return nil, err
    }

    _, err = jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}

Update 01/30/2022

Add verify intermediate certificate logic as below

func (a *AppStore) verifyCert(certByte, intermediaCertStr []byte) error {
    roots := x509.NewCertPool()
    ok := roots.AppendCertsFromPEM([]byte(rootPEM))
    if !ok {
        return errors.New("failed to parse root certificate")
    }

    interCert, err := x509.ParseCertificate(intermediaCertStr)
    if err != nil {
        return errors.New("failed to parse intermedia certificate")
    }
    intermedia := x509.NewCertPool()
    intermedia.AddCert(interCert)

    cert, err := x509.ParseCertificate(certByte)
    if err != nil {
        return err
    }

    opts := x509.VerifyOptions{
        Roots:         roots,
        Intermediates: intermedia,
    }

    chains, err := cert.Verify(opts)
    if err != nil {
        return err
    }

    for _, ch := range chains {
        for _, c := range ch {
            fmt.Printf("%+v, %s, %+v \n", c.AuthorityKeyId, c.Subject.Organization, c.ExtKeyUsage)
        }
    }

    return nil
}

func (a *AppStore) ExtractClaims(tokenStr string) (*JWSTransaction, error) {
    tran := &JWSTransaction{}

    rootCertStr, err := a.extractHeaderByIndex(tokenStr, 2)
    if err != nil {
        return nil, err
    }
    intermediaCertStr, err := a.extractHeaderByIndex(tokenStr, 1)
    if err != nil {
        return nil, err
    }
    if err = a.verifyCert(rootCertStr, intermediaCertStr); err != nil {
        return nil, err
    }

    _, err = jwt.ParseWithClaims(tokenStr, tran, func(token *jwt.Token) (interface{}, error) {
        return a.extractPublicKeyFromToken(tokenStr)
    })
    if err != nil {
        return nil, err
    }

    return tran, nil
}

The details of implementation could be found here https://github.com/richzw/appstore

1
TjerkW On

We really need a golang library that can do this, i'm currently implementing a server callback, could combine it in an open source library so its easier to implement in golang.

0
Guillem Bonet On

Correct me if I'm wrong but the highest score answer doesn't seem correct to me. The certificate chain is not validated and the public key used to validate the message comes from the leaf certificate which is not validated at all. It looks like I could forge a fake message by putting a certificate chain in the x5c field of the JWSDecodedHeader as such:

  1. My own issued certificate
  2. Apple's intermediate certificate
  3. Apple's root certificate Since this code is only validating the authenticity of the last 2 and not checking if the first one is issued by the second one, I can put whatever I want there.

I think that in order to verify that the first certificate in the chain is valid, it's missing this:

certStr, err := a.extractHeaderByIndex(tokenStr, 0)
if err != nil {
    return nil, err
}

cert, err := x509.ParseCertificate(certStr)
if err != nil {
    return nil, err
}

opts := x509.VerifyOptions{
    Roots:         roots,
    Intermediates: intermediates,
}

chains, err := cert.Verify(opts)
if err != nil {
    return err
}