github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/oauth/ios.go (about)

     1  package oauth
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/ecdsa"
     6  	"crypto/elliptic"
     7  	"crypto/sha256"
     8  	"crypto/x509"
     9  	"encoding/asn1"
    10  	"encoding/base64"
    11  	"encoding/binary"
    12  	"errors"
    13  	"fmt"
    14  
    15  	"github.com/cozy/cozy-stack/model/instance"
    16  	build "github.com/cozy/cozy-stack/pkg/config"
    17  	"github.com/cozy/cozy-stack/pkg/config/config"
    18  	"github.com/ugorji/go/codec"
    19  )
    20  
    21  // AppleAppAttestRootCert is the certificate coming from
    22  // https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem
    23  const AppleAppAttestRootCert = `-----BEGIN CERTIFICATE-----
    24  MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYw
    25  JAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwK
    26  QXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNa
    27  Fw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlv
    28  biBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9y
    29  bmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdh
    30  NbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9au
    31  Yen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/
    32  MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYw
    33  CgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn
    34  53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijV
    35  oyFraWVIyd/dganmrduC1bmTBGwD
    36  -----END CERTIFICATE-----`
    37  
    38  type appleAttestationObject struct {
    39  	Format       string                 `codec:"fmt"`
    40  	AttStatement map[string]interface{} `codec:"attStmt,omitempty"`
    41  	RawAuthData  []byte                 `codec:"authData"`
    42  	AuthData     authenticatorData
    43  }
    44  
    45  // authenticatorData is described by
    46  // https://www.w3.org/TR/webauthn/#sctn-authenticator-data
    47  type authenticatorData struct {
    48  	RPIDHash       []byte
    49  	Flags          authenticatorFlags
    50  	Counter        uint32
    51  	AttestedData   attestedCredentialData
    52  	ExtensionsData []byte
    53  }
    54  
    55  type attestedCredentialData struct {
    56  	AAGUID       []byte
    57  	CredentialID []byte
    58  }
    59  
    60  type authenticatorFlags byte
    61  
    62  func (f authenticatorFlags) Has(flag authenticatorFlags) bool {
    63  	return (f & flag) == flag
    64  }
    65  
    66  const (
    67  	flagAttestedCredentialData authenticatorFlags = 64 // Bit 6: Attested credential data included (AT)
    68  )
    69  
    70  // checkAppleAttestation will check an attestation made by the DeviceCheck API.
    71  // Cf https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server#3576643
    72  func (c *Client) checkAppleAttestation(inst *instance.Instance, req AttestationRequest) error {
    73  	store := GetStore()
    74  	if ok := store.CheckAndClearChallenge(inst, c.ID(), req.Challenge); !ok {
    75  		return errors.New("invalid challenge")
    76  	}
    77  
    78  	obj, err := parseAppleAttestation(req.Attestation)
    79  	if err != nil {
    80  		return fmt.Errorf("cannot parse attestation: %s", err)
    81  	}
    82  	inst.Logger().Debugf("checkAppleAttestation claims = %#v", obj)
    83  
    84  	if err := obj.checkCertificate(req.Challenge, req.KeyID); err != nil {
    85  		return err
    86  	}
    87  	if err := obj.checkAttestationData(req.KeyID); err != nil {
    88  		return err
    89  	}
    90  	return nil
    91  }
    92  
    93  func parseAppleAttestation(attestation string) (*appleAttestationObject, error) {
    94  	raw, err := base64.StdEncoding.DecodeString(attestation)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("error decoding base64: %s", err)
    97  	}
    98  	obj := appleAttestationObject{}
    99  	cborHandler := codec.CborHandle{}
   100  	err = codec.NewDecoderBytes(raw, &cborHandler).Decode(&obj)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("error decoding cbor: %s", err)
   103  	}
   104  	if obj.Format != "apple-appattest" {
   105  		return nil, errors.New("invalid webauthn format")
   106  	}
   107  
   108  	obj.AuthData, err = parseAuthData(obj.RawAuthData)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("error decoding auth data: %v", err)
   111  	}
   112  	if !obj.AuthData.Flags.Has(flagAttestedCredentialData) {
   113  		return nil, errors.New("missing attested credential data flag")
   114  	}
   115  	return &obj, nil
   116  }
   117  
   118  // parseAuthData parse webauthn Attestation object.
   119  // Cf https://www.w3.org/TR/webauthn/#sctn-attestation
   120  func parseAuthData(raw []byte) (authenticatorData, error) {
   121  	var data authenticatorData
   122  	if len(raw) < 37 {
   123  		return data, errors.New("raw AuthData is too short")
   124  	}
   125  	data.RPIDHash = raw[:32]
   126  	data.Flags = authenticatorFlags(raw[32])
   127  	data.Counter = binary.BigEndian.Uint32(raw[33:37])
   128  	if len(raw) == 37 {
   129  		return data, nil
   130  	}
   131  
   132  	if len(raw) < 55 {
   133  		return data, errors.New("raw AuthData is too short")
   134  	}
   135  	data.AttestedData.AAGUID = raw[37:53]
   136  	idLength := binary.BigEndian.Uint16(raw[53:55])
   137  	if len(raw) < int(55+idLength) {
   138  		return data, errors.New("raw AuthData is too short")
   139  	}
   140  	data.AttestedData.CredentialID = raw[55 : 55+idLength]
   141  	return data, nil
   142  }
   143  
   144  func (obj *appleAttestationObject) checkCertificate(challenge string, keyID []byte) error {
   145  	// 1. Verify that the x5c array contains the intermediate and leaf
   146  	// certificates for App Attest, starting from the credential certificate
   147  	// stored in the first data buffer in the array (credcert). Verify the
   148  	// validity of the certificates using Apple’s root certificate.
   149  	credCert, opts, err := obj.setupAppleCertificates()
   150  	if err != nil {
   151  		return err
   152  	}
   153  	if _, err := credCert.Verify(*opts); err != nil {
   154  		return err
   155  	}
   156  
   157  	// 2. Create clientDataHash as the SHA256 hash of the one-time challenge
   158  	// sent to your app before performing the attestation, and append that hash
   159  	// to the end of the authenticator data (authData from the decoded object).
   160  	clientDataHash := sha256.Sum256([]byte(challenge))
   161  	composite := append(obj.RawAuthData, clientDataHash[:]...)
   162  
   163  	// 3. Generate a new SHA256 hash of the composite item to create nonce.
   164  	nonce := sha256.Sum256(composite)
   165  
   166  	// 4. Obtain the value of the credCert extension with OID
   167  	// 1.2.840.113635.100.8.2, which is a DER-encoded ASN.1 sequence. Decode
   168  	// the sequence and extract the single octet string that it contains.
   169  	// Verify that the string equals nonce.
   170  	extracted, err := extractNonceFromCertificate(credCert)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	if !bytes.Equal(nonce[:], extracted) {
   175  		return errors.New("invalid nonce")
   176  	}
   177  
   178  	// 5. Create the SHA256 hash of the public key in credCert, and verify that
   179  	// it matches the key identifier from your app.
   180  	pub, ok := credCert.PublicKey.(*ecdsa.PublicKey)
   181  	if !ok {
   182  		return errors.New("invalid algorithm for credCert")
   183  	}
   184  	pubKey := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
   185  	pubKeyHash := sha256.Sum256(pubKey)
   186  	if !bytes.Equal(pubKeyHash[:], keyID) {
   187  		return errors.New("invalid keyId")
   188  	}
   189  	return nil
   190  }
   191  
   192  func (obj *appleAttestationObject) setupAppleCertificates() (*x509.Certificate, *x509.VerifyOptions, error) {
   193  	roots := x509.NewCertPool()
   194  	ok := roots.AppendCertsFromPEM([]byte(AppleAppAttestRootCert))
   195  	if !ok {
   196  		return nil, nil, errors.New("error adding root certificate to pool")
   197  	}
   198  
   199  	x5c, ok := obj.AttStatement["x5c"].([]interface{})
   200  	if !ok || len(x5c) == 0 {
   201  		return nil, nil, errors.New("missing certification")
   202  	}
   203  
   204  	certs := make([]*x509.Certificate, 0, len(x5c))
   205  	for _, raw := range x5c {
   206  		rawBytes, ok := raw.([]byte)
   207  		if !ok {
   208  			return nil, nil, errors.New("missing certification")
   209  		}
   210  		cert, err := x509.ParseCertificate(rawBytes)
   211  		if err != nil {
   212  			return nil, nil, fmt.Errorf("error parsing cert: %s", err)
   213  		}
   214  		certs = append(certs, cert)
   215  	}
   216  	intermediates := x509.NewCertPool()
   217  	for _, cert := range certs {
   218  		intermediates.AddCert(cert)
   219  	}
   220  
   221  	opts := x509.VerifyOptions{
   222  		Roots:         roots,
   223  		Intermediates: intermediates,
   224  	}
   225  	credCert := certs[0]
   226  	return credCert, &opts, nil
   227  }
   228  
   229  func extractNonceFromCertificate(credCert *x509.Certificate) ([]byte, error) {
   230  	credCertOID := asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 2}
   231  	var credCertID []byte
   232  	for _, extension := range credCert.Extensions {
   233  		if extension.Id.Equal(credCertOID) {
   234  			credCertID = extension.Value
   235  		}
   236  	}
   237  	if len(credCertID) == 0 {
   238  		return nil, errors.New("missing credCert extension")
   239  	}
   240  	var values []asn1.RawValue
   241  	_, err := asn1.Unmarshal(credCertID, &values)
   242  	if err != nil || len(values) == 0 {
   243  		return nil, errors.New("missing credCert value")
   244  	}
   245  	var value asn1.RawValue
   246  	if _, err = asn1.Unmarshal(values[0].Bytes, &value); err != nil {
   247  		return nil, errors.New("missing credCert value")
   248  	}
   249  	return value.Bytes, nil
   250  }
   251  
   252  func (obj *appleAttestationObject) checkAttestationData(keyID []byte) error {
   253  	// 6. Compute the SHA256 hash of your app’s App ID, and verify that this is
   254  	// the same as the authenticator data’s RP ID hash.
   255  	if err := checkAppID(obj.AuthData.RPIDHash); err != nil {
   256  		return err
   257  	}
   258  
   259  	// 7. Verify that the authenticator data’s counter field equals 0.
   260  	if obj.AuthData.Counter != 0 {
   261  		return errors.New("invalid counter")
   262  	}
   263  
   264  	// 8. Verify that the authenticator data’s aaguid field is either
   265  	// appattestdevelop if operating in the development environment, or
   266  	// appattest followed by seven 0x00 bytes if operating in the production
   267  	// environment.
   268  	aaguid := [16]byte{'a', 'p', 'p', 'a', 't', 't', 'e', 's', 't', 0, 0, 0, 0, 0, 0, 0}
   269  	if build.IsDevRelease() {
   270  		copy(aaguid[:], "appattestdevelop")
   271  	}
   272  	if !bytes.Equal(obj.AuthData.AttestedData.AAGUID, aaguid[:]) {
   273  		return errors.New("invalid aaguid")
   274  	}
   275  
   276  	// 9. Verify that the authenticator data’s credentialId field is the same
   277  	// as the key identifier.
   278  	if !bytes.Equal(obj.AuthData.AttestedData.CredentialID, keyID) {
   279  		return errors.New("invalid credentialId")
   280  	}
   281  	return nil
   282  }
   283  
   284  func checkAppID(hash []byte) error {
   285  	appIDs := config.GetConfig().Flagship.AppleAppIDs
   286  	for _, appID := range appIDs {
   287  		appIDHash := sha256.Sum256([]byte(appID))
   288  		if bytes.Equal(hash, appIDHash[:]) {
   289  			return nil
   290  		}
   291  	}
   292  	return errors.New("invalid RP ID hash")
   293  }