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 }