Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • renovate/bootstrap-5.x
  • renovate/github.com-crewjam-saml-0.x
  • renovate/github.com-duo-labs-webauthn-digest
  • renovate/github.com-go-webauthn-webauthn-0.x
  • renovate/github.com-gorilla-csrf-1.x
  • renovate/github.com-prometheus-client_golang-1.x
  • renovate/glob-10.x
  • renovate/glob-11.x
  • renovate/go-1.x
  • renovate/golang.org-x-crypto-0.x
  • renovate/opentelemetry-go-monorepo
  • renovate/purgecss-webpack-plugin-7.x
13 results

Target

Select target project
  • id/sso-server
1 result
Select Git revision
  • master
  • renovate/bootstrap-5.x
  • renovate/github.com-crewjam-saml-0.x
  • renovate/github.com-duo-labs-webauthn-digest
  • renovate/github.com-go-webauthn-webauthn-0.x
  • renovate/github.com-gorilla-csrf-1.x
  • renovate/github.com-prometheus-client_golang-1.x
  • renovate/glob-10.x
  • renovate/glob-11.x
  • renovate/go-1.x
  • renovate/golang.org-x-crypto-0.x
  • renovate/opentelemetry-go-monorepo
  • renovate/purgecss-webpack-plugin-7.x
13 results
Show changes
Commits on Source (10)
Showing
with 1800 additions and 780 deletions
module git.autistici.org/id/sso-server
go 1.21.0
go 1.23.0
toolchain go1.22.1
toolchain go1.24.1
require (
git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4
git.autistici.org/id/auth v0.0.0-20241017204112-73812019f8b2
git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d
git.autistici.org/id/auth v0.0.0-20250317113727-af5086998135
git.autistici.org/id/go-sso v0.0.0-20241017184626-0e26b5e055dc
git.autistici.org/id/keystore v0.0.0-20230901162242-63f23c4799e9
git.autistici.org/id/usermetadb v0.0.0-20241017171915-b5c24a0ff9b7
github.com/crewjam/saml v0.4.14
github.com/elazarl/go-bindata-assetfs v1.0.1
github.com/go-webauthn/webauthn v0.10.2
github.com/go-webauthn/webauthn v0.12.2
github.com/gorilla/csrf v1.7.2
github.com/gorilla/mux v1.8.1
github.com/gorilla/securecookie v1.1.2
......@@ -23,7 +23,7 @@ require (
github.com/yl2chen/cidranger v1.0.2
go.opentelemetry.io/otel v1.10.0
go.opentelemetry.io/otel/trace v1.10.0
golang.org/x/crypto v0.28.0
golang.org/x/crypto v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
......@@ -38,10 +38,10 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-webauthn/x v0.1.9 // indirect
github.com/go-webauthn/x v0.1.19 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
......@@ -60,6 +60,6 @@ require (
go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.10.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.31.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
......@@ -56,12 +56,16 @@ git.autistici.org/ai3/go-common v0.0.0-20240906100150-439608162088 h1:VD6KF7j6y0
git.autistici.org/ai3/go-common v0.0.0-20240906100150-439608162088/go.mod h1:JpPfOTGgbvAElF0wdmv81y6l3CRsdS4z3IkNOETUDVo=
git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4 h1:xB5K4GL4VlguEOknhgz+AN3k8nmx19y91RRUvByaLnQ=
git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4/go.mod h1:JpPfOTGgbvAElF0wdmv81y6l3CRsdS4z3IkNOETUDVo=
git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d h1:20u44cuDBH0xxPojMsDt0j9x+8FGZhifOuNe9zwoqos=
git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d/go.mod h1:JpPfOTGgbvAElF0wdmv81y6l3CRsdS4z3IkNOETUDVo=
git.autistici.org/id/auth v0.0.0-20240906112625-ac605e4de165 h1:Tc5yH61Tm8Xy1is9QTC190wzKV5rBgZcb1uT8+aCnzw=
git.autistici.org/id/auth v0.0.0-20240906112625-ac605e4de165/go.mod h1:edbMYeIHztZqdEFPh0zMBf/dLPVg2C/uOd8klPD3pBA=
git.autistici.org/id/auth v0.0.0-20240906122543-bdae984d05cd h1:m4CRIt/3Zv40/cEEOxC0nnVnQv6qPp6pkIn8fGtq9b0=
git.autistici.org/id/auth v0.0.0-20240906122543-bdae984d05cd/go.mod h1:wBtDkNIYOwSU+i4hOiKOM0C40HSZylGZbFXhyrKk0hA=
git.autistici.org/id/auth v0.0.0-20241017204112-73812019f8b2 h1:K6HeELCAypbrF9SlsxdWOYduw6bERy83mPuXDfYDLy8=
git.autistici.org/id/auth v0.0.0-20241017204112-73812019f8b2/go.mod h1:exRuu1+EazJrZBwC27XoPRXymwbXMDjsu/Q+GMMlS4s=
git.autistici.org/id/auth v0.0.0-20250317113727-af5086998135 h1:v6D6egIdYvSPNNZwjeLlMhw5yzkXnNZrA/vIUmjJiak=
git.autistici.org/id/auth v0.0.0-20250317113727-af5086998135/go.mod h1:YzbKrzXdEGyHwxBEmladn81ypJVBG4q61WD++Co+zUo=
git.autistici.org/id/go-sso v0.0.0-20230822064459-ed921a53bb33 h1:Z3kE2hayP75WNItTfODiC6zUiGiVtYEp00OVpx4YndE=
git.autistici.org/id/go-sso v0.0.0-20230822064459-ed921a53bb33/go.mod h1:n3YNIlKKfYWYqPGPLh4KDT1QpOVqoSp8w3l4DBR/oXk=
git.autistici.org/id/go-sso v0.0.0-20241017184626-0e26b5e055dc h1:Zdu47ZFAgvEiNijZ/UVsCjaM8/EbsYbLTLI7S/JFbzI=
......@@ -297,8 +301,12 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4=
github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs=
github.com/go-webauthn/webauthn v0.12.2 h1:yLaNPgBUEXDQtWnOjhsGhMMCEWbXwjg/aNkC8riJQI8=
github.com/go-webauthn/webauthn v0.12.2/go.mod h1:Q8SZPPj4sZ469fNTcQXxRpzJOdb30jQrn/36FX8jilA=
github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE=
github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA=
github.com/go-webauthn/x v0.1.19 h1:IUfdHiBRoTdujpBA/14qbrMXQ3LGzYe/PRGWdZcmudg=
github.com/go-webauthn/x v0.1.19/go.mod h1:C5arLuTQ3pVHKPw89v7CDGnqAZSZJj+4Jnr40dsn7tk=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
......@@ -379,6 +387,8 @@ github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVgg
github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
......@@ -948,6 +958,8 @@ golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
......@@ -1146,6 +1158,8 @@ golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
......
......@@ -133,14 +133,14 @@ func (l *loginState) authRequestFromRequest(req *http.Request, deviceInfo *userm
l.Password = req.FormValue("password")
return &auth.Request{
Username: l.Username,
Password: []byte(l.Password),
Password: l.Password,
DeviceInfo: deviceInfo,
}, nil
case State2FA_OTP:
return &auth.Request{
Username: l.Username,
Password: []byte(l.Password),
Password: l.Password,
DeviceInfo: deviceInfo,
OTP: req.FormValue("otp"),
}, nil
......@@ -153,7 +153,7 @@ func (l *loginState) authRequestFromRequest(req *http.Request, deviceInfo *userm
}
return &auth.Request{
Username: l.Username,
Password: []byte(l.Password),
Password: l.Password,
DeviceInfo: deviceInfo,
WebAuthnSession: l.AuthResponse.WebAuthnSession,
WebAuthnResponse: webauthnResp,
......
......@@ -7,7 +7,7 @@
"bootstrap": "5.3.3",
"css-loader": "7.1.2",
"extract-loader": "5.1.0",
"glob": "11.0.0",
"glob": "11.0.2",
"html-webpack-plugin": "5.6.3",
"mini-css-extract-plugin": "2.9.2",
"purgecss-webpack-plugin": "6.0.0",
......
......@@ -16,7 +16,7 @@ import (
type Request struct {
Service string
Username string
Password []byte
Password string
OTP string
WebAuthnSession *webauthn.SessionData
WebAuthnResponse *protocol.ParsedCredentialAssertionData
......@@ -26,7 +26,7 @@ type Request struct {
func (r *Request) EncodeToMap(m map[string]string, prefix string) {
m[prefix+"service"] = r.Service
m[prefix+"username"] = r.Username
m[prefix+"password"] = string(r.Password)
m[prefix+"password"] = r.Password
if r.OTP != "" {
m[prefix+"otp"] = r.OTP
......@@ -47,7 +47,7 @@ func (r *Request) EncodeToMap(m map[string]string, prefix string) {
func (r *Request) DecodeFromMap(m map[string]string, prefix string) {
r.Service = m[prefix+"service"]
r.Username = m[prefix+"username"]
r.Password = []byte(m[prefix+"password"])
r.Password = m[prefix+"password"]
r.OTP = m[prefix+"otp"]
if s := m[prefix+"webauthn_session"]; s != "" {
var sess webauthn.SessionData
......
package metadata
const (
// https://secure.globalsign.com/cacert/root-r3.crt
ProductionMDSRoot = "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpHWD9f"
// Production MDS URL
ProductionMDSURL = "https://mds.fidoalliance.org"
// https://mds3.fido.tools/pki/MDS3ROOT.crt
ConformanceMDSRoot = "MIICaDCCAe6gAwIBAgIPBCqih0DiJLW7+UHXx/o1MAoGCCqGSM49BAMDMGcxCzAJBgNVBAYTAlVTMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMScwJQYDVQQLDB5GQUtFIE1ldGFkYXRhIDMgQkxPQiBST09UIEZBS0UxFzAVBgNVBAMMDkZBS0UgUm9vdCBGQUtFMB4XDTE3MDIwMTAwMDAwMFoXDTQ1MDEzMTIzNTk1OVowZzELMAkGA1UEBhMCVVMxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxJzAlBgNVBAsMHkZBS0UgTWV0YWRhdGEgMyBCTE9CIFJPT1QgRkFLRTEXMBUGA1UEAwwORkFLRSBSb290IEZBS0UwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASKYiz3YltC6+lmxhPKwA1WFZlIqnX8yL5RybSLTKFAPEQeTD9O6mOz+tg8wcSdnVxHzwnXiQKJwhrav70rKc2ierQi/4QUrdsPes8TEirZOkCVJurpDFbXZOgs++pa4XmjYDBeMAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAfBgNVHSMEGDAWgBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAKBggqhkjOPQQDAwNoADBlAjEA/xFsgri0xubSa3y3v5ormpPqCwfqn9s0MLBAtzCIgxQ/zkzPKctkiwoPtDzI51KnAjAmeMygX2S5Ht8+e+EQnezLJBJXtnkRWY+Zt491wgt/AwSs5PHHMv5QgjELOuMxQBc="
// Example from https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html
ExampleMDSRoot = "MIIGGTCCBAGgAwIBAgIUdT9qLX0sVMRe8l0sLmHd3mZovQ0wDQYJKoZIhvcNAQELBQAwgZsxHzAdBgNVBAMMFkVYQU1QTEUgTURTMyBURVNUIFJPT1QxIjAgBgkqhkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xFDASBgNVBAoMC0V4YW1wbGUgT1JHMRAwDgYDVQQLDAdFeGFtcGxlMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0yMTA0MTkxMTM1MDdaFw00ODA5MDQxMTM1MDdaMIGbMR8wHQYDVQQDDBZFWEFNUExFIE1EUzMgVEVTVCBST09UMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUuY29tMRQwEgYDVQQKDAtFeGFtcGxlIE9SRzEQMA4GA1UECwwHRXhhbXBsZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDjF5wyEWuhwDHsZosGdGFTCcI677rW881vV+UfW38J+K2ioFFNeGVsxbcebK6AVOiCDPFj0974IpeD9SFOhwAHoDu/LCfXdQWp8ZgQ91ULYWoW8o7NNSp01nbN9zmaO6/xKNCa0bzjmXoGqglqnP1AtRcWYvXOSKZy1rcPeDv4Dhcpdp6W72fBw0eWIqOhsrItuY2/N8ItBPiG03EX72nACq4nZJ/nAIcUbER8STSFPPzvE97TvShsi1FD8aO6l1WkR/QkreAGjMI++GbB2Qc1nN9Y/VEDbMDhQtxXQRdpFwubTjejkN9hKOtF3B71YrwIrng3V9RoPMFdapWMzSlI+WWHog0oTj1PqwJDDg7+z1I6vSDeVWAMKr9mq1w1OGNzgBopIjd9lRWkRtt2kQSPX9XxqS4E1gDDr8MKbpM3JuubQtNCg9D7Ljvbz6vwvUrbPHH+oREvucsp0PZ5PpizloepGIcLFxDQqCulGY2n7Ahl0JOFXJqOFCaK3TWHwBvZsaY5DgBuUvdUrwtgZNg2eg2omWXEepiVFQn3Fvj43Wh2npPMgIe5P0rwncXvROxaczd4rtajKS1ucoB9b9iKqM2+M1y/FDIgVf1fWEHwK7YdzxMlgOeLdeV/kqRU5PEUlLU9a2EwdOErrPbPKZmIfbs/L4B3k4zejMDH3Y+ZwIDAQABo1MwUTAdBgNVHQ4EFgQU8sWwq1TrurK7xMTwO1dKfeJBbCMwHwYDVR0jBBgwFoAU8sWwq1TrurK7xMTwO1dKfeJBbCMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAFw6M1PiIfCPIBQ5EBUPNmRvRFuDpolOmDofnf/+mv63LqwQZAdo/W8tzZ9kOFhq24SiLw0H7fsdG/jeREXiIZMNoW/rA6Uac8sU+FYF7Q+qp6CQLlSQbDcpVMifTQjcBk2xh+aLK9SrrXBqnTAhwS+offGtAW8DpoLuH4tAcQmIjlgMlN65jnELCuqNR/wpA+zch8LZW8saQ2cwRCwdr8mAzZoLbsDSVCHxQF3/kQjPT7Nao1q2iWcY3OYcRmKrieHDP67yeLUbVmetfZis2d6ZlkqHLB4ZW1xX4otsEFkuTJA3HWDRsNyhTwx1YoCLsYut5Zp0myqPNBq28w6qGMyyoJN0Z4RzMEO3R6i/MQNfhK55/8O2HciM6xb5t/aBSuHPKlBDrFWhpRnKYkaNtlUo35qV5IbKGKau3SdZdSRciaXUd/p81YmoF01UlhhMz/Rqr1k2gyA0a9tF8+awCeanYt5izl8YO0FlrOU1SQ5UQw4szqqZqbrf4e8fRuU2TXNx4zk+ImE7WRB44f6mSD746ZCBRogZ/SA5jUBu+OPe4/sEtERWRcQD+fXgce9ZEN0+peyJIKAsl5Rm2Bmgyg5IoyWwSG5W+WekGyEokpslou2Yc6EjUj5ndZWz5EiHAiQ74hNfDoCZIxVVLU3Qbp8a0S1bmsoT2JOsspIbtZUg="
)
const (
HeaderX509URI = "x5u"
HeaderX509Certificate = "x5c"
)
var (
errIntermediateCertRevoked = &Error{
Type: "intermediate_revoked",
Details: "Intermediate certificate is on issuers revocation list",
}
errLeafCertRevoked = &Error{
Type: "leaf_revoked",
Details: "Leaf certificate is on issuers revocation list",
}
errCRLUnavailable = &Error{
Type: "crl_unavailable",
Details: "Certificate revocation list is unavailable",
}
)
package metadata
import (
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/go-webauthn/x/revoke"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
)
// NewDecoder returns a new metadata decoder.
func NewDecoder(opts ...DecoderOption) (decoder *Decoder, err error) {
decoder = &Decoder{
client: &http.Client{},
parser: jwt.NewParser(),
hook: mapstructure.ComposeDecodeHookFunc(),
}
for _, opt := range opts {
if err = opt(decoder); err != nil {
return nil, fmt.Errorf("failed to apply decoder option: %w", err)
}
}
if decoder.root == "" {
decoder.root = ProductionMDSRoot
}
return decoder, nil
}
// Decoder handles decoding and specialized parsing of the metadata blob.
type Decoder struct {
client *http.Client
parser *jwt.Parser
hook mapstructure.DecodeHookFunc
root string
ignoreEntryParsingErrors bool
}
// Parse handles parsing of the raw JSON values of the metadata blob. Should be used after using Decode or DecodeBytes.
func (d *Decoder) Parse(payload *PayloadJSON) (metadata *Metadata, err error) {
metadata = &Metadata{
Parsed: Parsed{
LegalHeader: payload.LegalHeader,
Number: payload.Number,
},
}
if metadata.Parsed.NextUpdate, err = time.Parse(time.DateOnly, payload.NextUpdate); err != nil {
return nil, fmt.Errorf("error occurred parsing next update value '%s': %w", payload.NextUpdate, err)
}
var parsed Entry
for _, entry := range payload.Entries {
if parsed, err = entry.Parse(); err != nil {
metadata.Unparsed = append(metadata.Unparsed, EntryError{
Error: err,
EntryJSON: entry,
})
continue
}
metadata.Parsed.Entries = append(metadata.Parsed.Entries, parsed)
}
if n := len(metadata.Unparsed); n != 0 && !d.ignoreEntryParsingErrors {
return metadata, fmt.Errorf("error occurred parsing metadata: %d entries had errors during parsing", n)
}
return metadata, nil
}
// Decode the blob from an io.Reader. This function will close the io.ReadCloser after completing.
func (d *Decoder) Decode(r io.Reader) (payload *PayloadJSON, err error) {
bytes, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return d.DecodeBytes(bytes)
}
// DecodeBytes handles decoding raw bytes. If you have a read closer it's suggested to use Decode.
func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) {
var token *jwt.Token
if token, err = d.parser.Parse(string(bytes), func(token *jwt.Token) (any, error) {
// 2. If the x5u attribute is present in the JWT Header, then
if _, ok := token.Header[HeaderX509URI].([]any); ok {
// never seen an x5u here, although it is in the spec
return nil, errors.New("x5u encountered in header of metadata TOC payload")
}
// 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute.
var (
x5c, chain []any
ok, valid bool
)
if x5c, ok = token.Header[HeaderX509Certificate].([]any); !ok {
// If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain.
chain = []any{d.root}
} else {
chain = x5c
}
// The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor.
if valid, err = validateChain(d.root, chain); !valid || err != nil {
return nil, err
}
// Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the
// certificate bytes.
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
var (
n int
cert *x509.Certificate
)
// Decode the base64 certificate into the buffer.
if n, err = base64.StdEncoding.Decode(o, []byte(chain[0].(string))); err != nil {
return nil, err
}
// Parse the certificate from the buffer.
if cert, err = x509.ParseCertificate(o[:n]); err != nil {
return nil, err
}
// 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain
// jwt.Parse() uses the TOC signing certificate public key internally to verify the signature.
return cert.PublicKey, err
}); err != nil {
return nil, err
}
var decoder *mapstructure.Decoder
payload = &PayloadJSON{}
if decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: nil,
Result: payload,
DecodeHook: d.hook,
TagName: "json",
}); err != nil {
return nil, err
}
if err = decoder.Decode(token.Claims); err != nil {
return payload, err
}
return payload, nil
}
// DecoderOption is a representation of a function that can set options within a decoder.
type DecoderOption func(decoder *Decoder) (err error)
// WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for
// these entries will exist as an unparsed entry.
func WithIgnoreEntryParsingErrors() DecoderOption {
return func(decoder *Decoder) (err error) {
decoder.ignoreEntryParsingErrors = true
return nil
}
}
// WithRootCertificate overrides the root certificate used to validate the authenticity of the metadata payload.
func WithRootCertificate(value string) DecoderOption {
return func(decoder *Decoder) (err error) {
decoder.root = value
return nil
}
}
func validateChain(root string, chain []any) (bool, error) {
oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(root)))
nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(root))
if err != nil {
return false, err
}
rootcert, err := x509.ParseCertificate(oRoot[:nRoot])
if err != nil {
return false, err
}
roots := x509.NewCertPool()
roots.AddCert(rootcert)
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[1].(string))))
n, err := base64.StdEncoding.Decode(o, []byte(chain[1].(string)))
if err != nil {
return false, err
}
intcert, err := x509.ParseCertificate(o[:n])
if err != nil {
return false, err
}
if revoked, ok := revoke.VerifyCertificate(intcert); !ok {
issuer := intcert.IssuingCertificateURL
if issuer != nil {
return false, errCRLUnavailable
}
} else if revoked {
return false, errIntermediateCertRevoked
}
ints := x509.NewCertPool()
ints.AddCert(intcert)
l := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
n, err = base64.StdEncoding.Decode(l, []byte(chain[0].(string)))
if err != nil {
return false, err
}
leafcert, err := x509.ParseCertificate(l[:n])
if err != nil {
return false, err
}
if revoked, ok := revoke.VerifyCertificate(leafcert); !ok {
return false, errCRLUnavailable
} else if revoked {
return false, errLeafCertRevoked
}
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: ints,
}
_, err = leafcert.Verify(opts)
return err == nil, err
}
func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err error) {
var n int
raw := make([]byte, base64.StdEncoding.DecodedLen(len(value)))
if n, err = base64.StdEncoding.Decode(raw, []byte(strings.TrimSpace(value))); err != nil {
return nil, fmt.Errorf("error occurred parsing *x509.certificate: error occurred decoding base64 data: %w", err)
}
if certificate, err = x509.ParseCertificate(raw[:n]); err != nil {
return nil, err
}
return certificate, nil
}
// Package metadata handles metadata validation instrumentation.
package metadata
package metadata
// PasskeyAuthenticator is a type that represents the schema from the Passkey Developer AAGUID listing.
//
// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids
type PasskeyAuthenticator map[string]PassKeyAuthenticatorAAGUID
// PassKeyAuthenticatorAAGUID is a type that represents the indivudal schema entry from the Passkey Developer AAGUID
// listing. Used with PasskeyAuthenticator.
//
// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids
type PassKeyAuthenticatorAAGUID struct {
Name string `json:"name"`
IconDark string `json:"icon_dark,omitempty"`
IconLight string `json:"icon_light,omitempty"`
}
package metadata
import (
"fmt"
"strings"
)
// ValidateStatusReports checks a list of StatusReport's against a list of desired and undesired AuthenticatorStatus
// values. If the reports contain all of the desired and none of the undesired status reports then no error is returned
// otherwise an error describing the issue is returned.
func ValidateStatusReports(reports []StatusReport, desired, undesired []AuthenticatorStatus) (err error) {
if len(desired) == 0 && (len(undesired) == 0 || len(reports) == 0) {
return nil
}
var present, absent []string
if len(undesired) != 0 {
for _, report := range reports {
for _, status := range undesired {
if report.Status == status {
present = append(present, string(status))
continue
}
}
}
}
if len(desired) != 0 {
desired:
for _, status := range desired {
for _, report := range reports {
if report.Status == status {
continue desired
}
}
absent = append(absent, string(status))
}
}
switch {
case len(present) == 0 && len(absent) == 0:
return nil
case len(present) != 0 && len(absent) == 0:
return &Error{
Type: "invalid_status",
Details: fmt.Sprintf("The following undesired status reports were present: %s", strings.Join(present, ", ")),
}
case len(present) == 0 && len(absent) != 0:
return &Error{
Type: "invalid_status",
Details: fmt.Sprintf("The following desired status reports were absent: %s", strings.Join(absent, ", ")),
}
default:
return &Error{
Type: "invalid_status",
Details: fmt.Sprintf("The following undesired status reports were present: %s; the following desired status reports were absent: %s", strings.Join(present, ", "), strings.Join(absent, ", ")),
}
}
}
package metadata
import (
"context"
"errors"
"reflect"
"time"
"github.com/google/uuid"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
// The Provider is an interface which describes the elements required to satisfy validation of metadata.
type Provider interface {
// GetEntry returns a MDS3 payload entry given a AAGUID. This
GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *Entry, err error)
// GetValidateEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation
// statement during registration.
GetValidateEntry(ctx context.Context) (validate bool)
// GetValidateEntryPermitZeroAAGUID returns true if attestation statements with zerod AAGUID should be permitted
// when considering the result from GetValidateEntry. i.e. if the AAGUID is zeroed, and GetValidateEntry returns
// true, and this implementation returns true, the attestation statement will pass validation.
GetValidateEntryPermitZeroAAGUID(ctx context.Context) (skip bool)
// GetValidateTrustAnchor returns true if trust anchor validation of attestation statements is enforced during
// registration.
GetValidateTrustAnchor(ctx context.Context) (validate bool)
// GetValidateStatus returns true if the status reports for an authenticator should be validated against desired and
// undesired statuses.
GetValidateStatus(ctx context.Context) (validate bool)
// GetValidateAttestationTypes if true will enforce checking that the provided attestation is possible with the
// given authenticator.
GetValidateAttestationTypes(ctx context.Context) (validate bool)
// ValidateStatusReports returns nil if the provided authenticator status reports are desired.
ValidateStatusReports(ctx context.Context, reports []StatusReport) (err error)
}
var (
ErrNotInitialized = errors.New("metadata: not initialized")
)
type PublicKeyCredentialParameters struct {
Type string `json:"type"`
Alg webauthncose.COSEAlgorithmIdentifier `json:"alg"`
}
type AuthenticatorAttestationTypes []AuthenticatorAttestationType
func (t AuthenticatorAttestationTypes) HasBasicFull() bool {
for _, a := range t {
if a == BasicFull || a == AttCA {
return true
}
}
return false
}
// AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports.
// Each constant has a case-sensitive string representation (in quotes), which is used in the authoritative metadata for FIDO authenticators.
type AuthenticatorAttestationType string
const (
// BasicFull - Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). Authenticators must provide its attestation signature during the registration process for the same reason. The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). This sharing process should be done according to [UAFMetadataService].
BasicFull AuthenticatorAttestationType = "basic_full"
// BasicSurrogate - Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. As a consequence it does not provide a cryptographic proof of the security characteristics. But it is the best thing we can do if the authenticator is not able to have an attestation private key.
BasicSurrogate AuthenticatorAttestationType = "basic_surrogate"
// Ecdaa - Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. Support for this attestation type is optional at this time. It might be required by FIDO Certification.
Ecdaa AuthenticatorAttestationType = "ecdaa"
// AttCA - Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. Support for this attestation type is optional at this time. It might be required by FIDO Certification.
AttCA AuthenticatorAttestationType = "attca"
// AnonCA In this case, the authenticator uses an Anonymization CA which dynamically generates per-credential attestation certificates such that the attestation statements presented to Relying Parties do not provide uniquely identifiable information, e.g., that might be used for tracking purposes. The applicable [WebAuthn] attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application Attestation "apple-appattest".
AnonCA AuthenticatorAttestationType = "anonca"
// None - Indicates absence of attestation
None AuthenticatorAttestationType = "none"
)
// AuthenticatorStatus - This enumeration describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key).
// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#authenticatorstatus-enum
type AuthenticatorStatus string
const (
// NotFidoCertified - This authenticator is not FIDO certified.
NotFidoCertified AuthenticatorStatus = "NOT_FIDO_CERTIFIED"
// FidoCertified - This authenticator has passed FIDO functional certification. This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1.
FidoCertified AuthenticatorStatus = "FIDO_CERTIFIED"
// UserVerificationBypass - Indicates that malware is able to bypass the user verification. This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge.
UserVerificationBypass AuthenticatorStatus = "USER_VERIFICATION_BYPASS"
// AttestationKeyCompromise - Indicates that an attestation key for this authenticator is known to be compromised. Additional data should be supplied, including the key identifier and the date of compromise, if known.
AttestationKeyCompromise AuthenticatorStatus = "ATTESTATION_KEY_COMPROMISE"
// UserKeyRemoteCompromise - This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted.
UserKeyRemoteCompromise AuthenticatorStatus = "USER_KEY_REMOTE_COMPROMISE"
// UserKeyPhysicalCompromise - This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device.
UserKeyPhysicalCompromise AuthenticatorStatus = "USER_KEY_PHYSICAL_COMPROMISE"
// UpdateAvailable - A software or firmware update is available for the device. Additional data should be supplied including a URL where users can obtain an update and the date the update was published.
UpdateAvailable AuthenticatorStatus = "UPDATE_AVAILABLE"
// Revoked - The FIDO Alliance has determined that this authenticator should not be trusted for any reason, for example if it is known to be a fraudulent product or contain a deliberate backdoor.
Revoked AuthenticatorStatus = "REVOKED"
// SelfAssertionSubmitted - The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in StatusReportJSON.url.
SelfAssertionSubmitted AuthenticatorStatus = "SELF_ASSERTION_SUBMITTED"
// FidoCertifiedL1 - The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED.
FidoCertifiedL1 AuthenticatorStatus = "FIDO_CERTIFIED_L1"
// FidoCertifiedL1plus - The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level 1.
FidoCertifiedL1plus AuthenticatorStatus = "FIDO_CERTIFIED_L1plus"
// FidoCertifiedL2 - The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level 1+.
FidoCertifiedL2 AuthenticatorStatus = "FIDO_CERTIFIED_L2"
// FidoCertifiedL2plus - The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level 2.
FidoCertifiedL2plus AuthenticatorStatus = "FIDO_CERTIFIED_L2plus"
// FidoCertifiedL3 - The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level 2+.
FidoCertifiedL3 AuthenticatorStatus = "FIDO_CERTIFIED_L3"
// FidoCertifiedL3plus - The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level 3.
FidoCertifiedL3plus AuthenticatorStatus = "FIDO_CERTIFIED_L3plus"
)
// defaultUndesiredAuthenticatorStatus is an array of undesirable authenticator statuses
var defaultUndesiredAuthenticatorStatus = [...]AuthenticatorStatus{
AttestationKeyCompromise,
UserVerificationBypass,
UserKeyRemoteCompromise,
UserKeyPhysicalCompromise,
Revoked,
}
// IsUndesiredAuthenticatorStatus returns whether the supplied authenticator status is desirable or not
func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool {
for _, s := range defaultUndesiredAuthenticatorStatus {
if s == status {
return true
}
}
return false
}
// IsUndesiredAuthenticatorStatusSlice returns whether the supplied authenticator status is desirable or not
func IsUndesiredAuthenticatorStatusSlice(status AuthenticatorStatus, values []AuthenticatorStatus) bool {
for _, s := range values {
if s == status {
return true
}
}
return false
}
// IsUndesiredAuthenticatorStatusMap returns whether the supplied authenticator status is desirable or not
func IsUndesiredAuthenticatorStatusMap(status AuthenticatorStatus, values map[AuthenticatorStatus]bool) bool {
_, ok := values[status]
return ok
}
type AuthenticationAlgorithm string
const (
// ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW is an ECDSA signature on the NIST secp256r1 curve which must have raw R and
// S buffers, encoded in big-endian order.
ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_raw"
// ALG_SIGN_SECP256R1_ECDSA_SHA256_DER is a DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the NIST secp256r1
// curve.
ALG_SIGN_SECP256R1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_der"
// ALG_SIGN_RSASSA_PSS_SHA256_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian
// order RFC4055 RFC4056.
ALG_SIGN_RSASSA_PSS_SHA256_RAW AuthenticationAlgorithm = "rsassa_pss_sha256_raw"
// ALG_SIGN_RSASSA_PSS_SHA256_DER is a DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the
// RSASSA-PSS RFC3447 signature RFC4055 RFC4056.
ALG_SIGN_RSASSA_PSS_SHA256_DER AuthenticationAlgorithm = "rsassa_pss_sha256_der"
// ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW is an ECDSA signature on the secp256k1 curve which must have raw R and S
// buffers, encoded in big-endian order.
ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_raw"
// ALG_SIGN_SECP256K1_ECDSA_SHA256_DER is a DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the secp256k1 curve.
ALG_SIGN_SECP256K1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_der"
// ALG_SIGN_SM2_SM3_RAW is a Chinese SM2 elliptic curve based signature algorithm combined with SM3 hash algorithm
// OSCCA-SM2 OSCCA-SM3.
ALG_SIGN_SM2_SM3_RAW AuthenticationAlgorithm = "sm2_sm3_raw"
// ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW is the EMSA-PKCS1-v1_5 signature as defined in RFC3447.
ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_raw"
// ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER is a DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the
// EMSA-PKCS1-v1_5 signature as defined in RFC3447.
ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_der"
// ALG_SIGN_RSASSA_PSS_SHA384_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian
// order RFC4055 RFC4056.
ALG_SIGN_RSASSA_PSS_SHA384_RAW AuthenticationAlgorithm = "rsassa_pss_sha384_raw"
// ALG_SIGN_RSASSA_PSS_SHA512_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian
// order RFC4055 RFC4056.
ALG_SIGN_RSASSA_PSS_SHA512_RAW AuthenticationAlgorithm = "rsassa_pss_sha512_raw"
// ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA256(aka RS256) signature must have raw
// S buffers, encoded in big-endian order RFC8017 RFC4056
ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha256_raw"
// RSASSA-PKCS1-v1_5 RFC3447 with SHA384(aka RS384) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056
ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha384_raw"
// ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA512(aka RS512) signature must have raw
// S buffers, encoded in big-endian order RFC8017 RFC4056
ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha512_raw"
// ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA1(aka RS1) signature must have raw S
// buffers, encoded in big-endian order RFC8017 RFC4056
ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha1_raw"
// ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW is an ECDSA signature on the NIST secp384r1 curve with SHA384(aka: ES384)
// which must have raw R and S buffers, encoded in big-endian order.
ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW AuthenticationAlgorithm = "secp384r1_ecdsa_sha384_raw"
// ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW is an ECDSA signature on the NIST secp512r1 curve with SHA512(aka: ES512)
// which must have raw R and S buffers, encoded in big-endian order.
ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW AuthenticationAlgorithm = "secp521r1_ecdsa_sha512_raw"
// ALG_SIGN_ED25519_EDDSA_SHA512_RAW is an EdDSA signature on the curve 25519, which must have raw R and S buffers,
// encoded in big-endian order.
ALG_SIGN_ED25519_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed25519_eddsa_sha512_raw"
// ALG_SIGN_ED448_EDDSA_SHA512_RAW is an EdDSA signature on the curve Ed448, which must have raw R and S buffers,
// encoded in big-endian order.
ALG_SIGN_ED448_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed448_eddsa_sha512_raw"
)
// TODO: this goes away after webauthncose.CredentialPublicKey gets implemented
type algKeyCose struct {
KeyType webauthncose.COSEKeyType
Algorithm webauthncose.COSEAlgorithmIdentifier
Curve webauthncose.COSEEllipticCurve
}
func algKeyCoseDictionary() func(AuthenticationAlgorithm) algKeyCose {
mapping := map[AuthenticationAlgorithm]algKeyCose{
ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256},
ALG_SIGN_SECP256R1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256},
ALG_SIGN_RSASSA_PSS_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256},
ALG_SIGN_RSASSA_PSS_SHA256_DER: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256},
ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1},
ALG_SIGN_SECP256K1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1},
ALG_SIGN_RSASSA_PSS_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS384},
ALG_SIGN_RSASSA_PSS_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS512},
ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS256},
ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS384},
ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS512},
ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS1},
ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES384, Curve: webauthncose.P384},
ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES512, Curve: webauthncose.P521},
ALG_SIGN_ED25519_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed25519},
ALG_SIGN_ED448_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed448},
}
return func(key AuthenticationAlgorithm) algKeyCose {
return mapping[key]
}
}
func AlgKeyMatch(key algKeyCose, algs []AuthenticationAlgorithm) bool {
for _, alg := range algs {
if reflect.DeepEqual(algKeyCoseDictionary()(alg), key) {
return true
}
}
return false
}
type PublicKeyAlgAndEncoding string
const (
// ALG_KEY_ECC_X962_RAW is a raw ANSI X9.62 formatted Elliptic Curve public key.
ALG_KEY_ECC_X962_RAW PublicKeyAlgAndEncoding = "ecc_x962_raw"
// ALG_KEY_ECC_X962_DER is a DER ITU-X690-2008 encoded ANSI X.9.62 formatted SubjectPublicKeyInfo RFC5480 specifying an elliptic curve public key.
ALG_KEY_ECC_X962_DER PublicKeyAlgAndEncoding = "ecc_x962_der"
// ALG_KEY_RSA_2048_RAW is a raw encoded 2048-bit RSA public key RFC3447.
ALG_KEY_RSA_2048_RAW PublicKeyAlgAndEncoding = "rsa_2048_raw"
// ALG_KEY_RSA_2048_DER is a ASN.1 DER [ITU-X690-2008] encoded 2048-bit RSA RFC3447 public key RFC4055.
ALG_KEY_RSA_2048_DER PublicKeyAlgAndEncoding = "rsa_2048_der"
// ALG_KEY_COSE is a COSE_Key format, as defined in Section 7 of RFC8152. This encoding includes its own field for indicating the public key algorithm.
ALG_KEY_COSE PublicKeyAlgAndEncoding = "cose"
)
type Error struct {
// Short name for the type of error that has occurred.
Type string `json:"type"`
// Additional details about the error.
Details string `json:"error"`
// Information to help debug the error.
DevInfo string `json:"debug"`
}
func (e *Error) Error() string {
return e.Details
}
// Clock is an interface used to implement clock functionality in various metadata areas.
type Clock interface {
// Now returns the current time.
Now() time.Time
}
// RealClock is just a real clock.
type RealClock struct{}
// Now returns the current time.
func (RealClock) Now() time.Time {
return time.Now()
}
......@@ -15,6 +15,7 @@ import (
// credential for login/assertion.
type CredentialAssertionResponse struct {
PublicKeyCredential
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
}
......@@ -22,6 +23,7 @@ type CredentialAssertionResponse struct {
// that allows us to verify the client and authenticator data inside the response.
type ParsedCredentialAssertionData struct {
ParsedPublicKeyCredential
Response ParsedAssertionResponse
Raw CredentialAssertionResponse
}
......@@ -30,6 +32,7 @@ type ParsedCredentialAssertionData struct {
// ParsedAssertionResponse.
type AuthenticatorAssertionResponse struct {
AuthenticatorResponse
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
Signature URLEncodedBase64 `json:"signature"`
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
......@@ -51,8 +54,10 @@ func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAs
return nil, ErrBadRequest.WithDetails("No response given")
}
defer response.Body.Close()
defer io.Copy(io.Discard, response.Body)
defer func(request *http.Request) {
_, _ = io.Copy(io.Discard, request.Body)
_ = request.Body.Close()
}(response)
return ParseCredentialRequestResponseBody(response.Body)
}
......@@ -64,7 +69,19 @@ func ParseCredentialRequestResponseBody(body io.Reader) (par *ParsedCredentialAs
var car CredentialAssertionResponse
if err = decodeBody(body, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error())
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
}
// ParseCredentialRequestResponseBytes is an alternative version of ParseCredentialRequestResponseBody that just takes
// a byte slice.
func ParseCredentialRequestResponseBytes(data []byte) (par *ParsedCredentialAssertionData, err error) {
var car CredentialAssertionResponse
if err = decodeBytes(data, &car); err != nil {
return nil, ErrBadRequest.WithDetails("Parse error for Assertion").WithInfo(err.Error()).WithError(err)
}
return car.Parse()
......@@ -80,20 +97,18 @@ func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionDa
}
if _, err = base64.RawURLEncoding.DecodeString(car.ID); err != nil {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded").WithError(err)
}
if car.Type != "public-key" {
if car.Type != string(PublicKeyCredentialType) {
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
}
var attachment AuthenticatorAttachment
switch car.AuthenticatorAttachment {
case "platform":
attachment = Platform
case "cross-platform":
attachment = CrossPlatform
switch att := AuthenticatorAttachment(car.AuthenticatorAttachment); att {
case Platform, CrossPlatform:
attachment = att
}
par = &ParsedCredentialAssertionData{
......@@ -114,7 +129,7 @@ func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionDa
}
if err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData); err != nil {
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data").WithError(err)
}
return par, nil
......@@ -124,14 +139,14 @@ func (car CredentialAssertionResponse) Parse() (par *ParsedCredentialAssertionDa
// documentation.
//
// Specification: §7.2 Verifying an Authentication Assertion (https://www.w3.org/TR/webauthn/#sctn-verifying-assertion)
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, relyingPartyOrigins []string, appID string, verifyUser bool, credentialBytes []byte) error {
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, appID string, verifyUser bool, credentialBytes []byte) error {
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
// "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
// "assertive" steps, i.e. "Let JSONtext be the result of running UTF-8 decode on the value of cData."
// We handle these steps in part as we verify but also beforehand
//
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
// returned by the authenticator
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigins)
// returned by the authenticator.
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify)
if validError != nil {
return validError
}
......@@ -161,7 +176,7 @@ func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPa
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
var (
key interface{}
key any
err error
)
......@@ -174,12 +189,12 @@ func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPa
}
if err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error parsing the assertion public key: %+v", err))
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error parsing the assertion public key: %+v", err)).WithError(err)
}
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
if !valid || err != nil {
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v", err))
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v", err)).WithError(err)
}
return nil
......
package protocol
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"fmt"
......@@ -22,6 +22,14 @@ type AuthenticatorAttestationResponse struct {
// The byte slice of clientDataJSON, which becomes CollectedClientData
AuthenticatorResponse
Transports []string `json:"transports,omitempty"`
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
PublicKey URLEncodedBase64 `json:"publicKey"`
PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"`
// AttestationObject is the byte slice version of attestationObject.
// This attribute contains an attestation object, which is opaque to, and
// cryptographically protected against tampering by, the client. The
......@@ -33,8 +41,6 @@ type AuthenticatorAttestationResponse struct {
// requires to validate the attestation statement, as well as to decode and
// validate the authenticator data along with the JSON-serialized client data.
AttestationObject URLEncodedBase64 `json:"attestationObject"`
Transports []string `json:"transports,omitempty"`
}
// ParsedAttestationResponse is the parsed version of AuthenticatorAttestationResponse.
......@@ -60,21 +66,24 @@ type ParsedAttestationResponse struct {
type AttestationObject struct {
// The authenticator data, including the newly created public key. See AuthenticatorData for more info
AuthData AuthenticatorData
// The byteform version of the authenticator data, used in part for signature validation
RawAuthData []byte `json:"authData"`
// The format of the Attestation data.
Format string `json:"fmt"`
// The attestation statement data sent back if attestation is requested.
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
AttStatement map[string]any `json:"attStmt,omitempty"`
}
type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error)
type attestationFormatValidationHandler func(AttestationObject, []byte, metadata.Provider) (string, []any, error)
var attestationRegistry = make(map[string]attestationFormatValidationHandler)
var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler)
// RegisterAttestationFormat is a method to register attestation formats with the library. Generally using one of the
// locally registered attestation formats is sufficient.
func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) {
func RegisterAttestationFormat(format AttestationFormat, handler attestationFormatValidationHandler) {
attestationRegistry[format] = handler
}
......@@ -85,18 +94,18 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon
p = &ParsedAttestationResponse{}
if err = json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData); err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
if err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject); err != nil {
return nil, ErrParsingData.WithInfo(err.Error())
return nil, ErrParsingData.WithInfo(err.Error()).WithError(err)
}
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
// structure to obtain the attestation statement format fmt, the authenticator data authData, and
// the attestation statement attStmt.
if err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData); err != nil {
return nil, fmt.Errorf("error decoding auth data: %v", err)
return nil, err
}
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
......@@ -114,82 +123,76 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon
//
// Steps 9 through 12 are verified against the auth data. These steps are identical to 11 through 14 for assertion so we
// handle them with AuthData.
func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error {
func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, mds metadata.Provider) (err error) {
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
// Begin Step 9 through 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired)
if authDataVerificationError != nil {
return authDataVerificationError
if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired); err != nil {
return err
}
return a.VerifyAttestation(clientDataHash, mds)
}
// VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the
// AuthData values you should use Verify.
func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadata.Provider) (err error) {
// Step 13. Determine the attestation statement format by performing a
// USASCII case-sensitive match on fmt against the set of supported
// WebAuthn Attestation Statement Format Identifier values. The up-to-date
// list of registered WebAuthn Attestation Statement Format Identifier
// values is maintained in the IANA registry of the same name
// [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries).
//
// Since there is not an active registry yet, we'll check it against our internal
// Supported types.
//
// But first let's make sure attestation is present. If it isn't, we don't need to handle
// any of the following steps
if attestationObject.Format == "none" {
if len(attestationObject.AttStatement) != 0 {
// any of the following steps.
if AttestationFormat(a.Format) == AttestationFormatNone {
if len(a.AttStatement) != 0 {
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
}
return nil
}
formatHandler, valid := attestationRegistry[attestationObject.Format]
if !valid {
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format))
var (
handler attestationFormatValidationHandler
valid bool
)
if handler, valid = attestationRegistry[AttestationFormat(a.Format)]; !valid {
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", a.Format))
}
var (
aaguid uuid.UUID
attestationType string
x5cs []any
)
// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
// the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized
// client data computed in step 7.
attestationType, x5c, err := formatHandler(*attestationObject, clientDataHash)
if err != nil {
if attestationType, x5cs, err = handler(*a, clientDataHash, mds); err != nil {
return err.(*Error).WithInfo(attestationType)
}
aaguid, err := uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID)
if err != nil {
return err
}
if meta, ok := metadata.Metadata[aaguid]; ok {
for _, s := range meta.StatusReports {
if metadata.IsUndesiredAuthenticatorStatus(s.Status) {
return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered")
if len(a.AuthData.AttData.AAGUID) != 0 {
if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil {
return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()).WithError(err)
}
}
if x5c != nil {
x5cAtt, err := x509.ParseCertificate(x5c[0].([]byte))
if err != nil {
return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c")
if mds == nil {
return nil
}
if x5cAtt.Subject.CommonName != x5cAtt.Issuer.CommonName {
var hasBasicFull = false
for _, a := range meta.MetadataStatement.AttestationTypes {
if a == metadata.BasicFull || a == metadata.AttCA {
hasBasicFull = true
}
}
var protoErr *Error
if !hasBasicFull {
return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation")
}
}
}
} else if metadata.Conformance {
return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String()))
if protoErr = ValidateMetadata(context.Background(), mds, aaguid, attestationType, x5cs); protoErr != nil {
return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", protoErr)).WithDetails(protoErr.DevInfo).WithError(protoErr)
}
return nil
......
......@@ -10,10 +10,8 @@ import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var androidAttestationKey = "android-key"
func init() {
RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat)
RegisterAttestationFormat(AttestationFormatAndroidKey, verifyAndroidKeyFormat)
}
// The android-key attestation statement looks like:
......@@ -31,26 +29,26 @@ func init() {
// }
//
// Specification: §8.4. Android Key Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-key-attestation)
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) {
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
// the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
alg, present := att.AttStatement[stmtAlgorithm].(int64)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
sig, present := att.AttStatement[stmtSignature].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
x5c, x509present := att.AttStatement[stmtX5C].([]any)
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value")
......@@ -67,27 +65,25 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (strin
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
if err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
if err = attCert.CheckSignature(webauthncose.SigAlgFromCOSEAlg(coseAlg), signatureData, sig); err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)).WithError(err)
}
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)).WithError(err)
}
e := pubKey.(webauthncose.EC2PublicKeyData)
valid, err = e.Verify(signatureData, sig)
if err != nil || !valid {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)).WithError(err)
}
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
......@@ -109,11 +105,11 @@ func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (strin
decoded := keyDescription{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions").WithError(err)
}
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) {
if bytes.Compare(decoded.AttestationChallenge, clientDataHash) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
}
......@@ -165,19 +161,19 @@ type authorizationList struct {
Padding []int `asn1:"tag:6,explicit,set,optional"`
EcCurve int `asn1:"tag:10,explicit,optional"`
RsaPublicExponent int `asn1:"tag:200,explicit,optional"`
RollbackResistance interface{} `asn1:"tag:303,explicit,optional"`
RollbackResistance any `asn1:"tag:303,explicit,optional"`
ActiveDateTime int `asn1:"tag:400,explicit,optional"`
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"`
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"`
NoAuthRequired interface{} `asn1:"tag:503,explicit,optional"`
NoAuthRequired any `asn1:"tag:503,explicit,optional"`
UserAuthType int `asn1:"tag:504,explicit,optional"`
AuthTimeout int `asn1:"tag:505,explicit,optional"`
AllowWhileOnBody interface{} `asn1:"tag:506,explicit,optional"`
TrustedUserPresenceRequired interface{} `asn1:"tag:507,explicit,optional"`
TrustedConfirmationRequired interface{} `asn1:"tag:508,explicit,optional"`
UnlockedDeviceRequired interface{} `asn1:"tag:509,explicit,optional"`
AllApplications interface{} `asn1:"tag:600,explicit,optional"`
ApplicationID interface{} `asn1:"tag:601,explicit,optional"`
AllowWhileOnBody any `asn1:"tag:506,explicit,optional"`
TrustedUserPresenceRequired any `asn1:"tag:507,explicit,optional"`
TrustedConfirmationRequired any `asn1:"tag:508,explicit,optional"`
UnlockedDeviceRequired any `asn1:"tag:509,explicit,optional"`
AllApplications any `asn1:"tag:600,explicit,optional"`
ApplicationID any `asn1:"tag:601,explicit,optional"`
CreationDateTime int `asn1:"tag:701,explicit,optional"`
Origin int `asn1:"tag:702,explicit,optional"`
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
......
......@@ -14,10 +14,8 @@ import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var appleAttestationKey = "apple"
func init() {
RegisterAttestationFormat(appleAttestationKey, verifyAppleFormat)
RegisterAttestationFormat(AttestationFormatApple, verifyAppleFormat)
}
// The apple attestation statement looks like:
......@@ -33,12 +31,11 @@ func init() {
// }
//
// Specification: §8.8. Apple Anonymous Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation)
func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifyAppleFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// If x5c is not present, return an error
x5c, x509present := att.AttStatement["x5c"].([]interface{})
// If x5c is not present, return an error.
x5c, x509present := att.AttStatement[stmtX5C].([]any)
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value")
......@@ -51,7 +48,7 @@ func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []
credCert, err := x509.ParseCertificate(credCertBytes)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
......@@ -76,10 +73,10 @@ func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []
decoded := AppleAnonymousAttestation{}
if _, err = asn1.Unmarshal(attExtBytes, &decoded); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions")
return "", nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions").WithError(err)
}
if !bytes.Equal(decoded.Nonce, nonce[:]) || err != nil {
if !bytes.Equal(decoded.Nonce, nonce[:]) {
return "", nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce")
}
......@@ -87,7 +84,7 @@ func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []
// TODO: Probably move this part to webauthncose.go
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err)).WithError(err)
}
credPK := pubKey.(webauthncose.EC2PublicKeyData)
......@@ -106,7 +103,8 @@ func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []
return string(metadata.AnonCA), x5c, nil
}
// Apple has not yet publish schema for the extension(as of JULY 2021.)
// AppleAnonymousAttestation represents the attestation format for Apple, who have not yet published a schema for the
// extension (as of JULY 2021.)
type AppleAnonymousAttestation struct {
Nonce []byte `asn1:"tag:1,explicit"`
}
......@@ -12,10 +12,8 @@ import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var packedAttestationKey = "packed"
func init() {
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat)
RegisterAttestationFormat(AttestationFormatPacked, verifyPackedFormat)
}
// The packed attestation statement looks like:
......@@ -36,26 +34,24 @@ func init() {
// }
//
// Specification: §8.2. Packed Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-packed-attestation)
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifyPackedFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) {
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields.
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
// used to generate the attestation signature.
alg, present := att.AttStatement["alg"].(int64)
alg, present := att.AttStatement[stmtAlgorithm].(int64)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
// Get the sig value - A byte string containing the attestation signature.
sig, present := att.AttStatement["sig"].([]byte)
sig, present := att.AttStatement[stmtSignature].([]byte)
if !present {
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
x5c, x509present := att.AttStatement["x5c"].([]interface{})
x5c, x509present := att.AttStatement[stmtX5C].([]any)
if x509present {
// Handle Basic Attestation steps for the x509 Certificate
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c)
......@@ -63,7 +59,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
// Also make sure the we did not have an x509 then
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte)
if ecdaaKeyPresent {
// Handle ECDAA Attestation steps for the x509 Certificate
return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID)
......@@ -74,7 +70,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [
}
// Handle the attestation steps laid out in
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) {
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []any) (string, []any, error) {
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
for _, c := range x5c {
......@@ -85,7 +81,7 @@ func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte,
ct, err := x509.ParseCertificate(cb)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) {
......@@ -102,14 +98,12 @@ func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte,
attCert, err := x509.ParseCertificate(attCertBytes)
if err != nil {
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
return "", x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err)).WithError(err)
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
if err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature); err != nil {
return "", x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
if err = attCert.CheckSignature(webauthncose.SigAlgFromCOSEAlg(coseAlg), signatureData, signature); err != nil {
return "", x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err)).WithError(err)
}
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
......@@ -201,15 +195,11 @@ func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte,
return string(metadata.BasicFull), x5c, nil
}
func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []any, error) {
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
}
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) {
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []any, error) {
verificationData := append(authData, clientDataHash...)
key, err := webauthncose.ParsePublicKey(pubKey)
......@@ -217,6 +207,7 @@ func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signatur
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err))
}
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
switch k := key.(type) {
case webauthncose.OKPPublicKeyData:
err = verifyKeyAlgorithm(k.Algorithm, alg)
......@@ -232,6 +223,8 @@ func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signatur
return "", nil, err
}
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
// clientDataHash using the credential public key with alg.
valid, err := webauthncose.VerifySignature(key, verificationData, signature)
if !valid && err == nil {
return "", nil, ErrInvalidAttestation.WithDetails("Unable to verify signature")
......
......@@ -2,6 +2,7 @@ package protocol
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
......@@ -14,10 +15,8 @@ import (
"github.com/go-webauthn/webauthn/metadata"
)
var safetyNetAttestationKey = "android-safetynet"
func init() {
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
RegisterAttestationFormat(AttestationFormatAndroidSafetyNet, verifySafetyNetFormat)
}
type SafetyNetResponse struct {
......@@ -26,7 +25,7 @@ type SafetyNetResponse struct {
ApkPackageName string `json:"apkPackageName"`
ApkDigestSha256 string `json:"apkDigestSha256"`
CtsProfileMatch bool `json:"ctsProfileMatch"`
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
ApkCertificateDigestSha256 []any `json:"apkCertificateDigestSha256"`
BasicIntegrity bool `json:"basicIntegrity"`
}
......@@ -42,7 +41,7 @@ type SafetyNetResponse struct {
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
//
// Specification: §8.5. Android SafetyNet Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation)
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
// The syntax of an Android Attestation statement is defined as follows:
// $$attStmtType //= (
// fmt: "android-safetynet",
......@@ -59,7 +58,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string
// We have done this
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
version, present := att.AttStatement["ver"].(string)
version, present := att.AttStatement[stmtVersion].(string)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
}
......@@ -75,8 +74,8 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string
return "", nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
}
token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
chain := token.Header["x5c"].([]interface{})
token, err := jwt.Parse(string(response), func(token *jwt.Token) (any, error) {
chain := token.Header[stmtX5C].([]any)
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
......@@ -86,18 +85,19 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string
}
cert, err := x509.ParseCertificate(o[:n])
return cert.PublicKey, err
})
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// marshall the JWT payload into the safetynet response json
var safetyNetResponse SafetyNetResponse
if err = mapstructure.Decode(token.Claims, &safetyNetResponse); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err)).WithError(err)
}
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
......@@ -106,27 +106,27 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
return "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
return "", nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response").WithError(err)
}
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
certChain := token.Header["x5c"].([]interface{})
certChain := token.Header[stmtX5C].([]any)
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
attestationCert, err := x509.ParseCertificate(l[:n])
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
err = attestationCert.VerifyHostname("attest.android.com")
if err != nil {
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
return "", nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err)).WithError(err)
}
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
......@@ -134,19 +134,13 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string
return "", nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
}
// Verify sanity of timestamp in the payload
now := time.Now()
oneMinuteAgo := now.Add(-time.Minute)
if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(now) {
// zero tolerance for post-dated timestamps
if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(time.Now()) {
// Zero tolerance for post-dated timestamps.
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
} else if t.Before(oneMinuteAgo) {
// allow old timestamp for testing purposes
// TODO: Make this user configurable
msg := "SafetyNet response with timestamp before one minute ago"
if metadata.Conformance {
return "", nil, ErrInvalidAttestation.WithDetails(msg)
} else if t.Before(time.Now().Add(-time.Minute)) {
// Small tolerance for pre-dated timestamps.
if mds != nil && mds.GetValidateEntry(context.Background()) {
return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago")
}
}
......
......@@ -15,20 +15,18 @@ import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var tpmAttestationKey = "tpm"
func init() {
RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat)
RegisterAttestationFormat(AttestationFormatTPM, verifyTPMFormat)
}
func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifyTPMFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) {
// Given the verification procedure inputs attStmt, authenticatorData
// and clientDataHash, the verification procedure is as follows
// Verify that attStmt is valid CBOR conforming to the syntax defined
// above and perform CBOR decoding on it to extract the contained fields
ver, present := att.AttStatement["ver"].(string)
ver, present := att.AttStatement[stmtVersion].(string)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving ver value")
}
......@@ -37,35 +35,35 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
return "", nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
}
alg, present := att.AttStatement["alg"].(int64)
alg, present := att.AttStatement[stmtAlgorithm].(int64)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value")
}
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
x5c, x509present := att.AttStatement["x5c"].([]interface{})
x5c, x509present := att.AttStatement[stmtX5C].([]any)
if !x509present {
// Handle Basic Attestation steps for the x509 Certificate
return "", nil, ErrNotImplemented
}
_, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
_, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte)
if ecdaaKeyPresent {
return "", nil, ErrNotImplemented
}
sigBytes, present := att.AttStatement["sig"].([]byte)
sigBytes, present := att.AttStatement[stmtSignature].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value")
}
certInfoBytes, present := att.AttStatement["certInfo"].([]byte)
certInfoBytes, present := att.AttStatement[stmtCertInfo].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving certInfo value")
}
pubAreaBytes, present := att.AttStatement["pubArea"].([]byte)
pubAreaBytes, present := att.AttStatement[stmtPubArea].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Error retrieving pubArea value")
}
......@@ -74,7 +72,7 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
// is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
pubArea, err := tpm2.DecodePublic(pubAreaBytes)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement")
return "", nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement").WithError(err)
}
key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
......@@ -115,10 +113,9 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
}
// 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
f := webauthncose.HasherFromCOSEAlg(coseAlg)
h := f()
h := webauthncose.HasherFromCOSEAlg(coseAlg)
h.Write(attToBeSigned)
if !bytes.Equal(certInfo.ExtraData, h.Sum(nil)) {
return "", nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
}
......@@ -149,15 +146,13 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
return "", nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
}
aikCert, err := x509.ParseCertificate(aikCertBytes)
if err != nil {
var aikCert *x509.Certificate
if aikCert, err = x509.ParseCertificate(aikCertBytes); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
}
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes)
if err != nil {
if err = aikCert.CheckSignature(webauthncose.SigAlgFromCOSEAlg(coseAlg), certInfoBytes, sigBytes); err != nil {
return "", nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
}
// Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements
......@@ -166,23 +161,41 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
if aikCert.Version != 3 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
}
// 2/6 Subject field MUST be set to empty.
if aikCert.Subject.String() != "" {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
}
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
var manufacturer, model, version string
var (
manufacturer, model, version string
ekuValid = false
eku []asn1.ObjectIdentifier
constraints tpmBasicConstraints
rest []byte
)
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 17}) {
manufacturer, model, version, err = parseSANExtension(ext.Value)
if err != nil {
if ext.Id.Equal(oidExtensionSubjectAltName) {
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return "", nil, err
}
} else if ext.Id.Equal(oidExtensionExtendedKeyUsage) {
if rest, err = asn1.Unmarshal(ext.Value, &eku); len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
}
ekuValid = true
} else if ext.Id.Equal(oidExtensionBasicConstraints) {
if rest, err = asn1.Unmarshal(ext.Value, &constraints); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
if manufacturer == "" || model == "" || version == "" {
return "", nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
}
......@@ -192,44 +205,10 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
}
// 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
var (
ekuValid = false
eku []asn1.ObjectIdentifier
)
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 37}) {
rest, err := asn1.Unmarshal(ext.Value, &eku)
if len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
}
ekuValid = true
}
}
if !ekuValid {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
}
// 5/6 The Basic Constraints extension MUST have the CA component set to false.
type basicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
}
var constraints basicConstraints
for _, ext := range aikCert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 19}) {
if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
} else if len(rest) != 0 {
return "", nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
}
}
}
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
// through metadata services. See, for example, the FIDO Metadata Service.
......@@ -241,9 +220,9 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []in
return string(metadata.AttCA), x5c, err
}
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
// forEachSAN loops through the TPM SAN extension.
//
// RFC 5280, 4.2.1.6
// SubjectAltName ::= GeneralNames
//
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
......@@ -258,6 +237,7 @@ func forEachSAN(extension []byte, callback func(tag int, data []byte) error) err
// uniformResourceIdentifier [6] IA5String,
// iPAddress [7] OCTET STRING,
// registeredID [8] OBJECT IDENTIFIER }
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
var seq asn1.RawValue
rest, err := asn1.Unmarshal(extension, &seq)
......@@ -306,13 +286,16 @@ func parseSANExtension(value []byte) (manufacturer string, model string, version
case nameTypeDN:
tpmDeviceAttributes := pkix.RDNSequence{}
_, err := asn1.Unmarshal(data, &tpmDeviceAttributes)
if err != nil {
return err
}
for _, rdn := range tpmDeviceAttributes {
if len(rdn) == 0 {
continue
}
for _, atv := range rdn {
value, ok := atv.Value.(string)
if !ok {
......@@ -322,15 +305,18 @@ func parseSANExtension(value []byte) (manufacturer string, model string, version
if atv.Type.Equal(tcgAtTpmManufacturer) {
manufacturer = strings.TrimPrefix(value, "id:")
}
if atv.Type.Equal(tcgAtTpmModel) {
model = value
}
if atv.Type.Equal(tcgAtTpmVersion) {
version = strings.TrimPrefix(value, "id:")
}
}
}
}
return nil
})
......@@ -343,23 +329,34 @@ var tpmManufacturers = []struct {
code string
}{
{"414D4400", "AMD", "AMD"},
{"414E5400", "Ant Group", "ANT"},
{"41544D4C", "Atmel", "ATML"},
{"4252434D", "Broadcom", "BRCM"},
{"4353434F", "Cisco", "CSCO"},
{"464C5953", "Flyslice Technologies", "FLYS"},
{"524F4343", "Fuzhou Rockchip", "ROCC"},
{"474F4F47", "Google", "GOOG"},
{"48504900", "HPI", "HPI"},
{"48504500", "HPE", "HPE"},
{"48495349", "Huawei", "HISI"},
{"49424d00", "IBM", "IBM"},
{"49424D00", "IBM", "IBM"},
{"49465800", "Infineon", "IFX"},
{"494E5443", "Intel", "INTC"},
{"4C454E00", "Lenovo", "LEN"},
{"4D534654", "Microsoft", "MSFT"},
{"4E534D20", "National Semiconductor", "NSM"},
{"4E545A00", "Nationz", "NTZ"},
{"4E544300", "Nuvoton Technology", "NTC"},
{"51434F4D", "Qualcomm", "QCOM"},
{"534D534E", "Samsung", "SECE"},
{"53454345", "SecEdge", "SecEdge"},
{"534E5300", "Sinosun", "SNS"},
{"534D5343", "SMSC", "SMSC"},
{"53544D20", "ST Microelectronics", "STM"},
{"534D534E", "Samsung", "SMSN"},
{"534E5300", "Sinosun", "SNS"},
{"54584E00", "Texas Instruments", "TXN"},
{"57454300", "Winbond", "WEC"},
{"524F4343", "Fuzhouk Rockchip", "ROCC"},
{"5345414C", "Wisekey", "SEAL"},
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
}
......@@ -372,3 +369,96 @@ func isValidTPMManufacturer(id string) bool {
return false
}
func tpmParseAIKAttCA(x5c *x509.Certificate, x5cis []*x509.Certificate) (err *Error) {
if err = tpmParseSANExtension(x5c); err != nil {
return err
}
if err = tpmRemoveEKU(x5c); err != nil {
return err
}
for _, parent := range x5cis {
if err = tpmRemoveEKU(parent); err != nil {
return err
}
}
return nil
}
func tpmParseSANExtension(attestation *x509.Certificate) (protoErr *Error) {
var (
manufacturer, model, version string
err error
)
for _, ext := range attestation.Extensions {
if ext.Id.Equal(oidExtensionSubjectAltName) {
if manufacturer, model, version, err = parseSANExtension(ext.Value); err != nil {
return ErrInvalidAttestation.WithDetails("Authenticator with invalid Authenticator Identity Key SAN data encountered during attestation validation.").WithInfo(fmt.Sprintf("Error occurred parsing SAN extension: %s", err.Error())).WithError(err)
}
}
}
if manufacturer == "" || model == "" || version == "" {
return ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate.")
}
var unhandled []asn1.ObjectIdentifier
for _, uce := range attestation.UnhandledCriticalExtensions {
if uce.Equal(oidExtensionSubjectAltName) {
continue
}
unhandled = append(unhandled, uce)
}
attestation.UnhandledCriticalExtensions = unhandled
return nil
}
var (
oidExtensionSubjectAltName = []int{2, 5, 29, 17}
oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37}
oidExtensionBasicConstraints = []int{2, 5, 29, 19}
oidKpPrivacyCA = []int{1, 3, 6, 1, 4, 1, 311, 21, 36}
)
type tpmBasicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
}
// Remove extension key usage to avoid ExtKeyUsage check failure.
func tpmRemoveEKU(x5c *x509.Certificate) *Error {
var (
unknown []asn1.ObjectIdentifier
hasAiK bool
)
for _, eku := range x5c.UnknownExtKeyUsage {
if eku.Equal(tcgKpAIKCertificate) {
hasAiK = true
continue
}
if eku.Equal(oidKpPrivacyCA) {
continue
}
unknown = append(unknown, eku)
}
if !hasAiK {
return ErrAttestationFormat.WithDetails("Attestation Identity Key certificate missing required Extended Key Usage.")
}
x5c.UnknownExtKeyUsage = unknown
return nil
}
......@@ -11,14 +11,12 @@ import (
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
var u2fAttestationKey = "fido-u2f"
func init() {
RegisterAttestationFormat(u2fAttestationKey, verifyU2FFormat)
RegisterAttestationFormat(AttestationFormatFIDOUniversalSecondFactor, verifyU2FFormat)
}
// verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation
func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
func verifyU2FFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) {
if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
return "", nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00")
}
......@@ -42,7 +40,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []in
// }
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
x5c, present := att.AttStatement["x5c"].([]interface{})
x5c, present := att.AttStatement[stmtX5C].([]any)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
}
......@@ -50,7 +48,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []in
// Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F
// registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]
// received by the client from the authenticator.
signature, present := att.AttStatement["sig"].([]byte)
signature, present := att.AttStatement[stmtSignature].([]byte)
if !present {
return "", nil, ErrAttestationFormat.WithDetails("Missing sig data")
}
......@@ -78,7 +76,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []in
attCert, err := x509.ParseCertificate(asn1Bytes)
if err != nil {
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate")
return "", nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate").WithError(err)
}
// Step 2.3
......